diff --git a/examples/starting-3D-platformer/assets/StartingWall.png b/examples/starting-3D-platformer/assets/StartingWall.png index b3a097887..06278d6e5 100644 Binary files a/examples/starting-3D-platformer/assets/StartingWall.png and b/examples/starting-3D-platformer/assets/StartingWall.png differ diff --git a/examples/starting-3D-platformer/starting-3D-platformer.json b/examples/starting-3D-platformer/starting-3D-platformer.json index dc710a171..3b068b660 100644 --- a/examples/starting-3D-platformer/starting-3D-platformer.json +++ b/examples/starting-3D-platformer/starting-3D-platformer.json @@ -1,7 +1,7 @@ { "firstLayout": "", "gdVersion": { - "build": 283, + "build": 284, "major": 5, "minor": 6, "revision": 0 @@ -240,6 +240,14 @@ "preloadAsSound": true, "preloadInCache": false, "userAdded": false + }, + { + "file": "assets/StartingGround.png", + "kind": "image", + "metadata": "", + "name": "assets/StartingGround.png", + "smoothed": true, + "userAdded": false } ] }, @@ -256,12 +264,13 @@ "mangledName": "Game_32Scene", "name": "Game Scene", "r": 255, + "renderer3DWorldScale": 100, "standardSortMethod": true, "stopSoundsOnStartup": true, "title": "", "v": 255, "uiSettings": { - "grid": false, + "grid": true, "gridType": "rectangular", "gridWidth": 64, "gridHeight": 64, @@ -271,7 +280,7 @@ "gridOffsetZ": 0, "gridColor": 10401023, "gridAlpha": 0.8, - "snap": false, + "snap": true, "zoomFactor": 0.5117784035210373, "windowMask": false, "selectedLayer": "", @@ -299,25 +308,6 @@ } ], "instances": [ - { - "angle": 0, - "customSize": true, - "depth": 30, - "height": 1728, - "keepRatio": true, - "layer": "", - "locked": true, - "name": "Ground", - "persistentUuid": "6a14e5ab-5c31-4692-8cd5-c50937fd60c6", - "width": 1920, - "x": -320, - "y": -576, - "z": -30, - "zOrder": 1, - "numberProperties": [], - "stringProperties": [], - "initialVariables": [] - }, { "angle": 0, "customSize": true, @@ -804,77 +794,25 @@ "numberProperties": [], "stringProperties": [], "initialVariables": [] + }, + { + "angle": 0, + "customSize": true, + "height": 1728, + "layer": "", + "locked": true, + "name": "Terrain", + "persistentUuid": "493eb8ee-edd0-4112-9bc6-c86db668f5e4", + "width": 1920, + "x": -320, + "y": -576, + "zOrder": 0, + "numberProperties": [], + "stringProperties": [], + "initialVariables": [] } ], "objects": [ - { - "assetStoreId": "", - "name": "Ground", - "persistentUuid": "b3b4fe2a-4ed5-45be-8d05-d1161e3cea5e", - "type": "Scene3D::Cube3DObject", - "variables": [], - "effects": [], - "behaviors": [ - { - "name": "Physics3D", - "type": "Physics3D::Physics3DBehavior", - "object3D": "Object3D", - "bodyType": "Static", - "bullet": false, - "fixedRotation": false, - "shape": "Box", - "shapeOrientation": "Z", - "shapeDimensionA": 0, - "shapeDimensionB": 0, - "shapeDimensionC": 0, - "density": 1, - "friction": 0.3, - "restitution": 0.1, - "linearDamping": 0.1, - "angularDamping": 0.1, - "gravityScale": 1, - "layers": 17, - "masks": 17, - "shapeOffsetX": 0, - "shapeOffsetY": 0, - "shapeOffsetZ": 0, - "massCenterOffsetX": 0, - "massCenterOffsetY": 0, - "massCenterOffsetZ": 0, - "massOverride": 0 - } - ], - "content": { - "width": 1000, - "height": 1000, - "depth": 40, - "enableTextureTransparency": false, - "facesOrientation": "Y", - "frontFaceResourceName": "assets\\StartingGround.png", - "backFaceResourceName": "", - "backFaceUpThroughWhichAxisRotation": "X", - "leftFaceResourceName": "assets\\StartingWall.png", - "rightFaceResourceName": "assets\\StartingWall.png", - "topFaceResourceName": "assets\\StartingWall.png", - "bottomFaceResourceName": "assets\\StartingWall.png", - "frontFaceVisible": true, - "backFaceVisible": false, - "leftFaceVisible": true, - "rightFaceVisible": true, - "topFaceVisible": true, - "bottomFaceVisible": true, - "frontFaceResourceRepeat": true, - "backFaceResourceRepeat": false, - "leftFaceResourceRepeat": false, - "rightFaceResourceRepeat": false, - "topFaceResourceRepeat": false, - "bottomFaceResourceRepeat": false, - "materialType": "StandardWithoutMetalness", - "isCastingShadow": true, - "isReceivingShadow": true, - "tileScale": 0.2 - } - }, { "assetStoreId": "bb9aa52170c0941b6a504a192b7d9e495e174642b99a19bd548939c37c27209e", "name": "Player", @@ -1353,6 +1291,59 @@ "content": { "IsChecked": false } + }, + { + "assetStoreId": "", + "name": "Terrain", + "persistentUuid": "ecc67faf-8454-4dab-ae90-cf993acd4c28", + "type": "Terrain3D::Terrain3D", + "variant": "", + "variables": [], + "effects": [], + "behaviors": [ + { + "name": "Physics3D", + "type": "Physics3D::Physics3DBehavior", + "object3D": "Object3D", + "bodyType": "Static", + "bullet": false, + "fixedRotation": false, + "shape": "Box", + "meshShapeResourceName": "", + "shapeOrientation": "Z", + "shapeDimensionA": 0, + "shapeDimensionB": 0, + "shapeDimensionC": 0, + "shapeOffsetX": 0, + "shapeOffsetY": 0, + "shapeOffsetZ": 0, + "massCenterOffsetX": 0, + "massCenterOffsetY": 0, + "massCenterOffsetZ": 0, + "massOverride": 0, + "density": 1, + "friction": 0.3, + "restitution": 0.1, + "linearDamping": 0.1, + "angularDamping": 0.1, + "gravityScale": 1, + "layers": 17, + "masks": 17 + } + ], + "content": { + "SculptData": "2;256;8ekBFx8QgAGmAsYDwgOuA5oDiAOCA/oDygTCBOQDugK2AZ4BkAGQAZwBpAGIAYIBmgHKAZgCpgPYA5ID3gFH2wOvBu0HhwjlBocEnQFc3gHiAXBLqQGVAQn4Ae4DzATUBIgE7gKkATQBBECsAawBkAF6cG7AAZwCnALAAQ7BAesB2QGTARuQAboCpAPAA5gDsALIAZYBZj4cAAEIIlKUAewBrAK4AoYCogEGrQHJAskDrwT9BLEFxwWtBdkEywONArUBhwFbLQNN6wHZAo8DkwPTAucBnwF7gQGfAUcWUF5IPlpqcnBOBgZM4AGeA8YC0AGcAawBggLuAqYDrAP+AqICxAHSAeYB9gH+AYwC+gKsA/wC7AEB0QGrAtUCywKLAqcBXykHBg3bAaMDmwTFBJcE+wOdBJsE9QOrA+0CuwP7A6EEqwSbBM8E+QT/BNcEiQSFA/kBhQEjJKwB4gOMBYwF8gPgAeQBsALMAsAChgK6AXxCDCFtgQLbAvUCywLtAZMCvQKrAtsBTwwcRo4B8gHaAooDjgPwAq4C2gHOAsoDgAT2A64DsAP8A4QEyAPQAq4BLjN/twHbAaECxQKxAuEBWVMGEBQOBAUHBQMBAg4YGhgIBAYGBgIFFx8hFwEKEBQQBgoYHiIYBA0ZHRMDBAQEAAEFDQ8TDQkCAQABAwEFBQUDAwACAgYIChAWHBgSAAEHExELBQQCAgIAAQQMEg4GDSc7OS8TAQYICAoOEhYUDgQHDQ0PCQABDBISDgoWIh4YAg8ZHRULAw0PDQcIGCAgFgQaLDQsFgENFRMLBxkjKSMTHyUnGwsQMDw8JgoJFRcPAwQICAgEChQaFhIGDhIUFAgEBQMHAAEMGiAWBhk3R0EvDwEAAQQCBgcbIx8TBig2OCgKAwcJCQABChYeGgoFCwsNCQENExMPBwsfJyERBB4oJhwEDx0hGwvJARIoLCQGFRURCQUCJD5ANhoKDBAMCA03UU81BxoqLiISGjxQTjwSIT1BLwsICggBDyMvLScRAQABAwMNDxEPCwUBBAgOFh4qPkQ+KAYZKSkhBwYGBgQAAQgeJCAGH2WRAZUBdTUCChQcHiAyODQkCA8fJSUZAAEaLCwiGD5OTDQMJT9HORUPHyUhDxA8UE4wCjpufmw4ASEvLR0RP19lVTtPZV9LHS50lgGQAWQaFS01KQcKEhQUDhY0PDwkEiQyMiweBgUNCwMePkYyCEONAa8BqQF1JQcAAQYKDBNBU00vEF6GAYgBYhgJFRcRBxo8RDoaDxsjHRUJHS8zJw0dS19TLRJEXl5EECdJUUUZ7QEULDQkBCUfFxMHBC5MVEIgDhYSEggNRWFjQwciODguFiJMZmZQGCNFTTcJBAoEBRcxPz0xGwIBBQ0RGRcXDw0DBhAaIjA+VlxSNAgfMTUnCQoICAYECCAqHAABM4UBuwHDAZsBRQMUICgsNkZQSDIMFSczMSUFIDQ6LCZMZGBCCitNVUcdDScxJxEWTGJePAhIiAGaAYYBSAIjNzMfGVF3gwF3U2+HAYMBYyc8lgHCAboBggEkETM7LQkMGB4aECA+UEgyGDA8RjwmEAEJCwEiREoyA1u3AeMB1QGbATUNAQYOEBVNZ2E3FnKmAagBeCANFRsVCSBEVEIeFScrJxsLIzs9Lw0jXXNlNxRYeHZUEjFXZVEjwQEOJCQWCSspIRcLBCxIUEAkEBIYEAoLPVlVOwUkMjYuFiJMYGhQIhM1PS0LBgIDDyUzQ0EzFQEBCxMXHx8bFwsFChgkMjpMXmRSNgYbKS0fBwoMCgYEBBYYChE9iwG5AcEBoQFHAAESKDQ8QFRaUDYOESk1MykJGCw0LihQXlo+DCdFTT8ZDSMnIQ8YRlhSMgZAeI4BfEgMFScpGRVPeYcBf2F5jwGJAWUrOpQBvgG2AYIBMgEhLSMFDBgaHBQiPkpGMBwuQkY+LhgKAwECGjIwGBtnuwHdAdUBmwE9EwUIDhAPQVdVMxJqmAGYAWwcBxMXEQMYPkQ4EhcnKycbCx81NScLG1FlWS8WUm5uTBIpTVtNH0UGBgILHTMvJxsPAiAyNi4cEBQSDgoFIS8xHQABGCYkHhQcOEpMRCoICxMTAwMJEx0nNzc1JxMCCQ8XHyMjHRkRAwwiMj5ITlpUSCoIDxcXDQIMCggIAgIBCxcpSXGPAZEBdTsDGig6REhWVEoyDg0dKSsnEwQWICQqOkI6KAoTJS0jDQkNEQ8DEig2LBYHIE5cVDgaBAULCxE9W3FvY3F7b1MdKGiCAYQBZjocBgMHAwoSGBYSHCw0MiQYLDQ6OC4kGBQKCAoGBRs9Z5cBrQGhAX05GwkAAQwQAAEbLSkbDD5YWEASAAEJBwkAARAeIBYAARshIx8VCREbGxEAAQkpMy8XEjRGQDIMFSsvKxGQAQsZJS81Nz05KxsBBgoMDhAQDgwKBggQGBoWDAURDwkAARQkLjY2MDAqIBQEDRslLS8nHxMHAQIPGyUnJyMhGREDDig6RExORDgqFgABAgQKCgoMDhIMCAkpQ1FVTUEzKx8XCQQaKDhMTEo8KhAGAAEFDxcdGQ8BEiQQAAEFBQYOEBAOAgYKDhAOCgoEAAEJFRMLAAEOHigoIBgIBRkrPU1ZV0c5Iw8EDBYgMkBAQDIiCAgMEhYUEhIQFBQWGiAiKCw0PjouFg8zTV1jZ3V5b1k3FQABEhQUEhQUEAgDDQ8PBwIOGBYWCgMVHyEdFxsXEw0FCBYWFhIOEBIQDggBCQUFAgwQDg4E3AIJHys1Nz1ZYVk3CQ0ZGQsCEAoGBgQUTG5wUhRDdX1jHxw4SExCNEJGPCoIEycxNzETFioqHgEfMzk5JSEdFw8FDB4sOEJEKhYIAAEBCBAUEg4UJCYiEhFbhwGXAYUBVQkuRj4QHy0rExZCTEhAKhQoOj4qBB8vKxkIFilTWTsBME5WQhwMICgkGAYKCAQJG09za0UDLjIyJhQAAQwKAyNJNRcBBgYjWWNJDT5ibGBAEA4cIiAcDAYEBAwUCggKFihcfIQBbDAdWX2HAXtxlwGrAaUBgQFDCDxOSCQWJi4qFhFNcWtJBy5MWEogGUldWz0bFxcVDQEsUl5QKBosMi4gATlTVTkDJj5GOhiYAwkdKS8xO2N1a0UNFysrHwEOBgQAAQQcbpoBnAFwGmGrAbsBjwE1IkRYWkQ0SFBIMAoVKTU1LQE0TkouASU9RTknGRcTCQMIEh4sNDYYAQ0NAwoUFhYOFiwyLBoXcaUBtwGdAVUcZogBciYtTVM/CTpERDoqFDxeYk4aGy0zHwQUR3t/WQNKcHZiKBAmMCwYBggKAgUfc6UBqQF3GSo2NCgWCCAqHgM7GwogIhI7jwGnAYcBMzxsfHRMEhQkKigcDgABAQEKEAIDAQokapwBogGEATwhZYsBlQGBAW+lAcUBwwGZAUsWVm5cKBAsNi4eGXObAZ0BbRFAbnxiLiVjfXVPFxcXExEAAUR0hAFsNB44Qj4oCVN7eVMLNFhkUCLEAgsVISUnL1dtY0MNGSsvHwMKBgIAAQIecpwBngFwGF+pAbkBkwEzIkJWUkIuQExELAoRIy0tIwo+UlAwASE3OTUZFQ8LCQEEChQaKCYICxUTBQgUFhIMFCguKhYTZ58BrQGTAUsmdpABeCYtVWFLGSY2NjAkFD5kblgmDSEjFwwWP3V5UQJOdH5gKA4kKiYUAwQEAAENIXGrAbEBgwElHiouIhIKJjIqCiMNGiwqFjuTAa8BlQFBMmJybEgSEiIqKB4KBAMAAQoSAgcDBCJikgGaAX44IVt/hwFzY5sBuwG/AZsBTRJOZFIaBiAsKhYZb6EBnwFtE0JuemQuJV99c0kTExMTCwFEeIIBbjQgOEI6KAVReXtVCTZaYlIiXAMNExcXGztHQy0HDx0fFQABCgQAAQQEGFJ0cFYWPXN/YyEeNj5AMCAyNDIgCAsVGRsRDC5AOiQEFR8nGxEHBwUBAQIEBhAUFgENEw8FBgwODAQOGiAaEA1JbXlpNRpSZlAWK0VNQRsSHCIeGBIyUlhMLAoDBQQYIhs9RSkSQl5cRhwKFBoSBAcJBw0PIVmBAYUBZycMFBYUCAYeKCIOFQABGCIiDillf20xHkBQRjIKEBogIBoQBgQIDhIIAgIKGkZkbFQmEz9bYVVNdZEBkwF9TQskMCYAAQsIEhQKEVFzcVELLlJWTB4VRVNRMQsNDQ0JAy5YYlAoGiwyLiIBNVFTOQMqQkg6GIMCAAEDAwEBAwUHBQEEAQsHBQIGBgYGBAocJCYeEgABCQkHBhQYHBgYEhASDgwEAgACAgIEDgwOCgYEAgIAAQQEBAQEBAMBAwMAAwEFAwEDBQUDAQEEBgQEAw8bHR8VBwYGAgsXGx0ZEQUBAQYGDhgiKCwqKCosMDQwIhoYHCQoJB4WBgACAwULFxkdHR8dJy0vKRsPCQUBAAEBBgoMBgEBAAECAgIDERMTBwQQEBAIBgQIDhISEhIQEhQWFhYWFhYYGhYUCgUXHyktLTdBRUtBOTUrKysjGw8LAwkTFxcPAQoQFBAKAAEDCQkFAQMHCQkFCBQaFhIOFBQYEg4AAQUFAAEKFBgYEgivAwABCAgIDBZKZGBIDj9pc1slBAYICAoKEBQWEgwuSE5GJhAUFhYUDgUREw8AARIcIB4QET1HPx8KJDI0LBYcJigcCg0nLysdARAWHBABJT1DOyELBwcBAQQUHhoMCQ8PDxERAAEgMi4WDSMnJRMEDR0bBxxOanZwVjg2NjIyKAoTHR0LBg4QBAklS11dSSEFCAoCFQ8GEhIKAAEMFBgWEClTYVEhHkZWTiYBAQYICAELEREHEBYKBgIMHkJSVEIcDSkxKw8BCREXHRsAARAMCTmFAbUBvQGfAWElCQQOBAYaKCYcAAEhOz8xDypgdmg4ByMvLSMPFR8dEwcIChASEBYuPEI+MjY8NigQ6QMEChASECx+rgGqAXgSb78B1QGnAUUBBAgKCAwWHBgQCEBwfmg4FBYYHBoQDyUnGwMaMDQsGCdngwFxPRA8UlZEJDA+PjIUG0FRSS0FGiwqIAABPWt5ZzkVEQkFAQgkNDIiAw0LCw8TFEpkXDQTM0M/JQEjRUUnGmaUAagBmgFwODQyNi4qBy0/NRcMHB4WBzFviwGHAWUnCB4iEA8RGCgoFgQWJigqFkOPAaUBiwE5MHiSAYABQAkCCgwKAhclJQ8SHAQFBwImXHx8YCYfSVlLHwABBw8VHRMiQj4aMbMBiQKfAu8BhwEjCCAmGAYyRkYuAUVrc10jSKgByAG0AV4TP1FPOxUjOT0vEwABCg4QEhxIZnJsWGBmYEYa8wMGDBQYGDKUAcABwAGCARR72QHrAcMBVQMCBAgKDBYYFgwEQnaEAXQ4FBgcHB4SDyUrHwEeMDoyHCl1jwF/QxBCXGRQLDZISDYWHUlXUzUJHCwwHgABRXeJAXtJIRcPCwMKKjo8JgMLCQsJERZWcGo4DzlLQykDJ09LKR5wqAG2AaYBcjQyNjAyKAk1Qz0ZDiAeEgU3d5cBkwFtJwoiJhAVExYsLBYGHCg0MB5FkwGzAZkBQTaEAZwBiAFCDQMGDggAARclIw8WJAgFBQIiYoYBggFmJiFPXU0fAwULFRkNLEpMKCu7AZ8CtwKLApUBKQwmKBoMNExMMAVPfYcBaylQuAHgAcABZBtJX11BGStDRzkdBQQMDhAiWHyMAYYBcHqCAXJUIMsDBhAUGB4yfqYBngFuEGWvAccBowFLCwMEBAYKEBIMAgUwXmpaMBIaHB4gFgUZHRUAARoqMCoWHVtvZzUQOlRYSC42Qj4wEBc5TUUxCw4iIBgBO2l7b0spIRkNBwYiMDAcAwkJCQsPEEJaUiwRLzs5IQIbMTEPKnKaAaQBkgFkMjAwMC4oAR8xKRMIEBQEDzVlgQF9XyUEEhACIRsIGhwQBBgqMC4iKW2JAXMzKGZ6ZCoTCQABAggAAQ8ZEQABIigYBgYGIFBqbFIiETdFOxUAAQUNDxcNHjo4FimfAfEBiwLrAY0BLwcUHBAGKDo4IAtHb3VbJUCWAbABlgFIHUVXUz0VKTs/Nx8JAQIKECBagAGUAZIBfoYBhAF2VCDxAgYOFhoaJkZWTDYMKU9ZTykNCwcBBAgEAQMJCwggKCYYFBYcIB4cDgYAAgIOFhgYEgUdKSETDCQyNjQmKiwoHAwNHSUpHREDAAEGBAEhO0tLPTEpIRcLBAwQDgYHCwkJDQ0DFBwYCA0bGxcJCAoIEiREYm5wYEguLCwwKiYWAgUHAwEDDRMdLT9JRzUdEREVHy0pGQsDAAECEhwkKCQEFyMjDw4gJBYCGxUPBQUBAggSHC4yKCQYFhQsNDYqHAYHDw0HAgUHDxERAQQAAQsrX4kBmQGNAWk/JxUNBwEKDgwBEy07PS8RFjhAMA4fMzc1JxEXIyclFxEJAwQIHkxyhAGMAYQBggF6ZkYaqwIGEhoeHhQPJSUbAi5MTjgMDQUDBAQDIzU5LxslMSsdAxgoMDAsHA4EAAIGIDQ8MhwKCAYGBggWHiQmIhYMBAADAgEDDREPCwkHAwkVHSkvOT87LRcCEBgYDgUtSU9DJQMKEhAEBwIKEhISJDhGUFJGNiwkJCYyNjQwKCYoJhgKCRUfISchHRcVFREdIykxMzk5NScTAwIKDhYiPExOPhoVP09LOxsXDQsDAhgwOkI8NjIsJhgMBQkHAhImNDYqFg8tPzsxEwUAAQUPIykpLTU/PSsfEQkRN09TQR8QNkA2FiVdc21PHwMIDgYHEx0hHRsTDwkDCBIsRFhqgAF+cFo6Fq0CCBogJiICUX+BAVUBdsIB0gGmAToJBA4MDAtPc3dZJ0FlaU0XIkJOTDgYBxcbEwREeIABbjIGAgABAQIKGiQoKCAGDRUVCQoQFAwAAQ8JBwMAAQEDCRMZKT9VV0knCDBGQjICW6EBswGXAUcONkhCJAABFiYqJBgqPk5STjIMBQMKJDZAQDYiLjYyJhAHFxsjHx0NBQEFDxMZHyUtPUtJOSMFCw0FCCBYhAGQAXQyMXudAZEBXx0JAwQCBChGWlZCLCwkHBQAAR0vLRkKPlpiUiQlZYUBhwFXDxYqKhIVDwMDES0rDwQMCBt3pQGtAYMBK1SgAb4BmgE+WdMBhQLvAZcBGSZKTDYCHy8zLyETEwkDBAgIDCZIdHpsVjwUwwIMGCQkIgdvpwGnAXMFmAH6AYwC1AFMAQoUFBARXYkBjQFnJU97f2MfJExcVjwSEyUrHQZWkAGiAYIBPAQBAwMAAQgYJCgmGAEZIxsLChocFAQJBQAEAQEFER87WV9PKwo+VlpABGu/Ad0BuQFTGE5kXDIIIjI4LhooQEpMRh4HHxsFIDZAPDIgLDY2KA4FERkbHRMDBgYECw0PFR0jOUtNQSEJFRcRARhilgGgAYYBODmPAbMBqQFrGQUGCAgELFBeWDomIBwUDgMtPz0lBkBmcFoqK3mfAZsBZw8mPkAmCwEQEgQbHQYYGhAhiwHJAc8BnQEpbNIB7gHCAUxtgQK/AqUCtwEXPmZsSgYlNTs1IxUPCwUCAhURAipgamJOMBTtAggYHiAaB2mdAZ0BaQOQAeoB+gHCAUoCDhYWEA1Td3tZH0NrdVkfIkRSTjYMEScnGwJUhAGSAXY0BgEFAwABCBYeIBwUBRsfHQkMFhwYCAcDAgQCAAECAgABCxMvSU9FJwg6UlI6DF+rAccBpwFLHlBiWjQQIjQ2LhwkNjxANBYTIR8JGiwyNCgYIiwsIg4HDREVEQsAAQoMBgUJCQ8RGS09QTcfCxEZEwkUUIABjAFwMDF5nQGRAV0TAQYKBgYmQlBILhgUEA4IBSk7OyUCNlhcTCIlZYsBiwFbCSQ8PiYDBBgaDBERChweDh17swG7AY0BKWbCAdQBrAFCY+cBoQKLAqsBETpiZEIEITE3Lx8TDwsFAwMbIRESRlRIOigOqQMGDBIUEAM7W1k/AlyQAZgBeDAEEBASDAMpQ0EvDSE9PzMNGCo0MCAMCxUVDwYyUlZIIgICAQACBg4SEhIMBQsVDQUIEBAOCAEAAQIEAgICAgABAwkXJy0nFQYiMDImBjFfbV0jFjZEOioQHiYqIhYcJComIA4HEQ8DEBogHBgOEBoYEgYDBQsJCQMECAgEAwMHBQkPGSUlHxUFDw0PBQguRkw+GhlDVVM1CwABAgQEBBIoKCYWDAgIBAQFGSUjGQEcLjIqEhM5TUs1AxYkIhYBBA4QCAkJBg4QCBFLaW1VGThqeFwiP4sBqwGhAWcRHjQ0JAMZISMhFw0NCQkBBxcZEwYkKCYgFga7AwABBAQEBAQECgYKBgcTEw8CDBAQEg4KDBAODAwIDAoMCgQAAQEAAQQECggGCgYKEhAMBgICAgABAgYICgoIAgMNCQsBBgwQDAgEAAEDBQEAAQICAgICAgQCBAIEBAoKCgwOEhYUEAwEAAEEBgoAAQMFBA4eJCQeFAgMDAoKCAgGBgYAAQEHCwcBAwsJBwMGFBgWDAIJDQ8NCwcFBwUDAwcJBQMCDBASDgYAAQcJBwUDCQsLBQMEAAECAAEDBQMDAQUDAQEBAwUDBQEBAQABBggKCgYCAAEBAQEECAYEAQkRFRULBwEFBQcJBwUFAwsJCQcLCwsLCwkJDRUbGxcLDRETEQ8DDBIUCgcPFxkRBaMCAgACAwUAASAyOCYAAWuzAb0BkQEvHCgqJBoYMEBAMBIgLjQqGgclMy0VDBQcGhIKEBQUDgABBwkHBQMAARQcHBABIzc7KxESKjAyIgwLGRcPAAEGCAQKDBQgJB4SAAEKDhQWGj5eZFouBy09OSETP1tVOQZAXmJKFgISGhoQBAQEBAACGy8zJxENIScfBx5GXlg+CB0vOTUfCwABBggCBxEXEwUOKDY4LhgBBwsPBwkdJScZBQEAAQUNERURCwsHBQYMDAQLEQ8PCwcMIjQ6Mh4KAwcFCBgmKCAKHT9NRy0BEh4cDgkTFxUXEwgqOjYYDRMTExMRLUNFNRUPKTkzJQJMbGAsJWV9d1sjvQEAAQEDCQ8JKkREMAOfAYUCmQLXAUcoNDo0JBpEWlpCFihARj4kCzlJQx8OHiQiGAwUGhgMAxEREQ8LAAEWIiIUBzVPV0UZHDpMRjYUDx0jEwQMCBAQFCQwNCwYBgwWHCIoWIIBjgF2PhFHXVs7I2WLAYkBVwABUnp+XBYFEBwcEgICAgICAydBRzkXESs1JQcwaIQBgAFUDidHT0kxDwEKCAYHFxsXAxw8VFRCIgQLERELCSU3MyUFAQULEyEjGxkREQkIEhACDxkXFREJDjRKVE4yGAQDAAEOKjo8LhIjV2thOwEcKioYCxkfHx0XDkJWTigNFRkXFRE5V1tFFxE3R0s1BmaOAX40RZsBvQG3AYkBNZ8BAwMLDxkNHjpAKgmhAYkCnwLdAUUoPEI2JB5GXFxCGihCSkIoCTNJPx0QICYkHAwQFhQIDRkZFxcRBxIgHA4NOVddRxkeQlJQPh4DGRkRCBASFBocKDg6MBoGEBoiKC5ihgGQAXo8E0lhX0MrbZEBiwFdA1B2dFAODwYUFA4CAgIAAgUlQUM5FxEnKR8COHKKAX5WDidFVU8zGQcECAIHERcLBiRIWlpIJgYFDQ0JCSUxMyMHAw0THSstKR8dFw0ECggBGR0dGRULEDZUXlpCJhYMDBYwPkAwFiNRa2E5Ax4qJhQPHyElHxkKPlJMJg8XFxkVETVTV0ERCzVJSzcFVnpkGl+3AdUBzQGbAT3NAQELERkdHQgcHhYNc78BywGZASsqNjY0IBY4REQ0FCQyPDYmBh0pIw8UHiAeGAoKCgQFEx0fIRkZCwQKDAIRM0lHORMaPExMQCYMAAEHAAEOFBocHiQqNDQqFgQSHCYqMlRuclwuCzNNSzcrV2tpQQI0TEYqBxkHAAEIBAIBAgABAQEbKzEnDwcTEQMUOmBuYkAIHzVDQTMfEQsDAAEFBwEEFixGUEw+IgwGAQEDAxklIxcHBxEfKTE3Ly0lHRMLAwkPHSEhGxMNDDBIWlxKPC4kHiAsNDAoEBM5R0MrARAYFAYRISUjIx0BIjQsEg8VGRURESc3NycFBR81OTMVHCwUI3mxAckBuQGLATfRAgcRGSEpIx0VDQ0NGyEfDQoiJCQeFg4SFhQSEBIcHCIgGA4MCBAWFhYUDAgBDRMbHyMjISEZFRMPERMXHyEjFQUSJDA2NjAqJB4aFh4mKi4qKCAcEgoGEB4kLDI2OjYsGgABEx8lIx8fHxUJCgYHERkjJR0VDwUAAQIAAQIBAQUFCQMBBBIaIi4yOjIoFgINGR8nKyUlHRcNAg4eKDA0Mi4qIBgYGBoSDAABBw0NCQMLGSMvNzs7NzElGxkfHSEjJyUfFw0GJDxMUlRQRkI0KB4aFg4IAAELDw8LAwABAgABCxkjKSslIRMJAwUJFRkZGRMPBwMCBgoKAAEJESEvR1ttf48BmwGRAYEBWR+JBQsjMTcxIwUOEgoDMmJ4ajwUDggICAgHDQ0FChggKCQkHBYOEhIaJCgkHAYrTV9TOR0RCwkRGRkbGRkbIykjHQMAAQMCDiI4Pjo2KiJUhAGUAYIBSgwZLy8bAAEYJC4uLB4MAgECAgcNFxsLMFhkUBhDgQGTAX9JJSchGQsECgoKBgQUIiogDBAkNjw4LhgKAAEDAQUHDxUfM0VJOx8KSGp4ZjoEHSsdATJmeGxCAAEnNzcnCQABAQsXK0NTWU85HRcTFx0pQ1VXRyMgZpIBngGIAVQ2JhocIg4NFRMDBgYEAQEIMERAKA9LbXNlOwsKFBQAASFTaWlPFx5EUEYiHjY6KAABQ50BzwHhAc0BowF/Z0ctD98GETFBQzsdGDQ6JgRcrgHIAagBUgoDBwcAAQITHx0PDCIuMjQqGg4KCBAeNDo2JgZFgQGRAX9NFQEIAgcbHRkbHR0rNTMjCwsfIw8UPEZGQDQohgHOAeIBvgFkB0VnXzsEGCosMiYIDxsZCQIFDRMZAmaoAbIBiAEeb8cB4wG/AV8jJyUZCQYOEg4OBCY+RDgYEDA6QjoiCAsRDwUAAgMNGTtXYVMvFm6eAagBhAE8G1NhTRNOmAG2AaoBYgM7WVk/CwwSDAMfRWFrZUcbEQkLEyVdf4UBbTU2ogHaAeQBrAFQHAEJAAEeAAEfLycLDAgGBgEOWH5+UAVnnwGxAZMBTQMkNi4SKX+vAasBfSE6cH5oLiZWZEoWT8UBjQKXAvUBpwFtQycPA4cHFTFFRzsbIkZILgRmwAHaAbQBUgINEQ8DAhUhHw0OJjY6Ni4cEAgIDiI2PjwoCE2JAZ8BiwFNFQIOCgkdHR0dGx8vNzknDQ0tMRsKOkZKRDYukgHmAfQBxgFmDVVxbUEBGiQsKCABHykjCwABBQcNEQZ6vAHGAZIBIHPTAe8ByQFhHSMfGQsGEBISDAgoRkw8GhIqOjoyHAMVGRUHBAICBxEzWWNVMRR2rAGqAYYBNCdfb1UVTqIBwgGoAWYBO1VZQQ0QFhYGFzlba2NHIQsHBw8lX4kBkwF5Oz6wAe4B6gGsAUQODxcFHAABIzEnBw4QCgQAARRejAGIAVwDaasBuQGdAVMDKjw2Fi+NAb0BvwGJASVAeIYBaiYgVmZSFE/LAY8CmwL1AZsBVy8PAQT5BRErNz01Fxw0OCABTpYBrAGMATwFDREPAQQPExMDFio0OjYsIBIQDhYeMjgyJAQ7cX9zRRUBBAINIR8fHR0dKzUvIwkNIycXCjRAQjwyLIABwAHKAaIBTgk/WVkzAAESGh4cFgcZJR8NAAIFBQsMZpwBoAF0HFehAbkBnQFLExkZEwkEDA4OCAIkNj4yFAwgKioiDgUTGREFAgYCAQ0lQ01FJxRehgGIAWYoHUlTQQ1GggGYAYQBTAQnPT8tCQwUEAQPLUVRUz8dEQsJFSFPc3tnMTKSAcABugGKATgKBw8AASAIERsXBBYSDgoCDExqbEIDV4kBmQGFAUkHGiokDC1znwGfAXUfLlhiShIMNkQyBkmrAd0B6wHHAX1FIQkEAqsDBxcfIyEXBQoGAQ8QMjowDgkLCQECCAgCChIcKi4wLiwkIBocGiAiJB4WCBsvPTsnFxMRExshIyEdHxsfIR8VAwABBwUEFCgsLCwoIkhgYFAsBBEfHRMCBgoKCgYBDQ0NAwICAAMKMEJINhAbO0U7GwcJCwcFAQABAgICAAEKFhgSCAYKEBAKBgcJCwkBAAECAgABAxEbHR0PBCo4PCwaAxETDQgoQEI6JAoDDQ8LAwICAAEDCxkjKSspHRsXGRUdLzs/MxkUQFRYQiQWDhIYJh4SDBAWGhgUCgQCGiQgFA0vRU1HLxcFAwEPHz9PSz0TBhQUBhMXBQMJGz9jeX1tTTEfFQcBVQoYGBIAASFRbWlTJwUMEg4BER8fGwUOFBgaFhQEAQEKHEBcZmJIIAEVGxMCGB4eEgUZHyMnJSUlJScfGw8BBgoIAgQIDBYiOEJCNB4FHyEZAiI4QjgiByc5Oy0VBwEAAgIECA4ODgwGBgQGDhYkJBwQAgIDBQMHDRENCwIEBAICAgEAAQEDAwMAAQEAAgEGCgwKCAABAwkFBQcdIR8LCCQ6PDokBg0VEQcMHCQgGAYLFRkZEQUICgQHITE5OS8dBQwWEgYRIyUdBxgyQEQ+LBwOCgoQGhYSEAQFDQ8NCwEYKCgaCStDSUUxGQ8DBQEPHzU/Pzk9R0tJQSsFDhQIFTE/Py8TVhxEVEYeL6EB3QHhAaMBNRhASjwWH0FPPyEMFhoSBA8zR0MlDGCsAcwBvAGEASgnUVk/BTZaXEYUFR8nKSclKy0pJx8AARwoJhIBDQ0FDCxedGxSHjtveVkXPHaKAX5IEV+NAY8BczUbCwUAAggUGh4aEgoCAAECDCxETEAgCAACBQcJHR0dCQgQEgwKAgIBBQUHBwMEBAIAAQ4YHh4QAgkNDQ0RQ1tVMwJGaHZiNAk1Rz8hDCo6PCoIFScrKRkGIjAsECNJX1tJHxg8TEYaJ11xYTEUSmZsXjIEGScjCQoOEgwEBREPCQIcWoYBigFmGj1xhQF7TxUGEBYKBzlZa2dRT2tzaVMfQHiCAV4SOWFtXSW6ASRWZlwuMb8BiQKLAscBOyhUZFIiIU1fUSkKEg4CDSlbc2dDAAF01AH6AeYBogEuMWNvTQdGbnheIA8dIyklJSstLykfCCY2MBgFExULCDBqhAF+XBpNjwGbAXMhSIoBpAGUAVYReasBuQGXAU0pGw0FAwYaJCgkGAwEAAEECjZSXFAqDAQBAwcLGyMbCRQaGBQOBgMDBQcLCwEAAQQCAhAiJiQWBgcPEQkXVXNpRQJMfIYBbjoTRVlRKwguQEIuChkpMy0dCDA+OhYfT2lpTyEkVGRUJi91jwGBAUcQTnR8ZDIHLT85HQMICAgCBwsJAAESMoABtgG6AYgBKEN/lwGLAVsRDhwcFAVBa395XV19hQF9XxNkqgG8AY4BLC9pe2Ur0AEiTl5SKiunAe8B8QG1ATkkUl5OIhdFUUshBgYFESVBaX91SQJuzgH0AeIBngE4H09bQwNAaHBYIgkVHyEjIScvLSkfBCIwLBYFDxEHCC5ienJQGEN9iQFnHUCAAZQBggFIEWulAbEBmwFbNykdDwkKGCYqKCASCAgEDjBQVkwsDggCAQMLFRkNAAEaIBwYEAQAAQUHCw0NBwEAAQIAARAeJCIaCAMHDQcTS2VjPwNEbHZgLg0/T0kpAiY2NigGEycrJxkIJjowFh1JWV9JGyJKWk4gK2uFAXlHCkZkblosCStBPSULBQEAAQEHAQQSJkKKAbYBsgGEASY7b4cBe08PDBoeEgc7aXt5X2N9hQF7WRdkqgG6AZYBOhlNX1UjoAESLDYsFhlljQGTAW8nDiw4MBoJISkjEwQLHS9BT2dtXTUEWpgBsAGoAYIBQg4THRkCKEBENhYDDRMXGx0jJSclHQkKFBQOAAEDAAEGFCZGTkw0GB0/RzMJLExYSiwNSW+DAXtZRz0tIQsEGCAoJiIcGA4ODCI0PjYiFA4IBAEFBQECFB4iIBgOBgMHCQ8PDwsHBQMAAQwUHBwUDAgAAgMLKT05JQEmPj44HgcfKysdAAEQHBwSBAsTGxcRAhQYFgQXLT05MRMMKjAoEhk/UUstBCY8QjokAAEdJy8jFRENCwkDChwqPE52hgF8WBQnRVFLMQsEDg4ICS9NX2VZWWtvY08ZOHB+bj4MESEhDYABBREVExEJCQsPERUNBQABBAoKEhAQDAABIz1PV1lRQy0REjBASlBOTkxCNiQMCAgGCAQCAgEJDxsrNTMvHxEFAAEGBAwSGBwcHhoaFhYQDgwMCg4MBAIBAAEFFSk3RU1DMSUVCwMFAQYWIhoUEAoIGCImJh4UEAoGAgQaLjQuJhwQCgYAAQUPGRUVEQ8PCwcBBAgMDg4UGBgUCgABAQABAgEBAAEBAgQGBgEJBwkJAQABBAIAAQUJCQsHBwkJCw8RFRUTEQ0LCwcFBwMBAQEDAwQMDhYYEAYJDxkdGxsVDwABGDBATFRKPCgSAxENCQsJBwEEBAABBxctOUFLTUlHQTUnCwoeKjY6ODAkDKQCI1FlXzUFFh4cDgsVFxMHAhosMi4eBS1NXWNZQScRAhgcEhQeNlBqcmRGEgMFBwUCDA4QCAMjWXd7WykKJjQsFhAgKCYiFAgBAQIMDAgICgwHJTErGwABBAIPJUEPIjw6HiNPX08dEgcbIxUGLkxUSioQBgIBAxBWeH5eKAABFRkXCRMrNTUlDw8LCQMBAwABAgQOHDI6NBwEEhoYEAEPEQ8LAAIPExcPAwYODAwAAQ0bHx0PBQsNDw8RExcXEw0VGx0ZCwgeKCQUAQQKEhQWFg4KAxEdHRcVCwMYKj5ISiwMBxUVBwwSFAgBFCIiFgABEx8rNT8/OS8rJycxLRsCLkpYVEAa6gIvcY8Bf00AASAyMiADGR0ZEQIgNkQ6IgEvTV9fTzETAAESGgoHCwIgTHSCAXZQFAMNCwsCDhgYFAQhb5kBmwF3JRxCUkYgEiQsLCIMBw8RBQgEBAICCBM3SUMnBBIWDA0xEFh6bi4teZMBgQE/DB89PysAAT5ibloyDAABBwkFGHKcAZoBcCQRJy8pERM3RT8rDwsJBQUBAQUDAAEGIj5KPiQIGiQgFAMRFxcRAwcTHRkTBQ4SGA4EEyMrIw8HBwsNDwsVGRkTDRcjJR8REDA8OBwBBBASFBIUFg4EDxcXFRENAAEQJDA6QhYPKSkbAAEWIiAQAiAyMiQCDRUhKzMzJSEZGSdBSzsTJk5iXkoexgItc40BgQFLAiY2NCIAARMbGQ8AAR44PjomAylBTU09HwcOFhgEFxkLEj5oeG5MEgULDwUBEBwaFggZZZEBlQFxHyJKVkgiECIoKBgIDRUVCwIEAwEAAQQTO0tFJQIaIBgBIyJqhgFyNi93lwGFAUMAASVDRy8DPGBqWioIBQkLBxhslgGOAWIYFSszJw8VMz89JwsFBwUDAAEFBQUDBh46RD4iBhgiIBIDExcZDwUJExkZEwEMFBQQAhEfJSEPAAEHCQkHCxETFRELEyEjHxEQMEA2HgEGChAODhAUEgYJFxMPDQkDDBYiLiwIHy0vHQIYIiAQBCAwNCICCQ8VHycjGRENER9DS0EdGERUWEIcsgEfT2FZNQIcJigaBA0PDwkCFiouKhoCFykvLSMNBA4UFAEPFQsGLEZSTDQQAQcHBQQOEhYUCg9DX2NHFSA4PjgYChgcGBACDRERDQACAQEBAAEPKzUvHQIWGhgGDx5QYlQmHVNlYTECHzExJQEoQkg6HAIDCQkHEEhiXDwOEyEhHQsNHysnGQcHAwMBAAEBBQMAAQIUKC4oFgYQFBIKAw0TDw8FBQ0REQkBBhAMCgABCRMVEwcAAgMFAwMHDQ0JBQ0XGRMLDCAqKBYDAgYGCgQKDAoEBw0NCwkHAwIMEBYaAxsnJRUAAQ4YFAoCFCAiFAABBQsNERcXDwcJBxMvOTEZDiw4OCwQPQcRFxMJAgwSFA4EAAEHBQEAAQgMEg4KBAIBAQEAAQIICAoIBgEDAQYMGBwaFAgAAQMDAQIGCAgGBgQBBQcDBgwQEgwICAoKCAQDBw8LBwMAAgEAAQEFBwcHBQIECgYIAgoOEhAGAQcNCQMCAQkJBwEGCgwGBAMAAgEBAAEEBgQCAwcFCQUBAQMDBQMBBQUHAwECBAABBAIGCAwIBgECAAECAwMHCQkFAwEDAAEBAQEAAgIAAQIAAQQGBAYGBAQEAgIAAQICAAIDAwcFAQABBAYCAQMFBQMFAQECAAIDAwADAQUHCQcFAQcNEQ8JAwIEAgABBQABAgACAwkFCQkJCQMDAwUHDxMRCQIKEBAOBsEBAQEBBQkIRmZqSg4vTVdLJwsDAAEGBgoYJCguKBYAAQcHAgUZHxsNEDhISjAOKUlRRR0NGRsVCRhQcnxiKgQNFREDFi42LBIVNT8/KwsGEBAIBQABGCIiFgELDwcAARIVO00/Fx5EWFY4FBYcFhABERkfGREIICokFBlRa2dJGQABBgwMBA4iKCQSDSs/NycHDhgcFgwGCgYIAgENERMPCw8REw0HBgwODgwGAwUHAwABHDQ8PiQMBgIAAQIKEBYUDgQFDw0NAwkZJSMbEyMrKyEPAQQODhAqTl5SLAszSUk7GxsjIxkNDCAmJBADCw8PCwcPGxsXDwEGCgwIByU3Mx8EKkBIOBblAQABBQkNDwpwrgGwAXwQVY8BowGLAU8fEQUAAQYMKDxMUk4oBAsPAAELLz0zGxZYfHRUDkmFAZUBez0fMTktEySMAcYB0AGmAUgKFSMhDSROXEgcJVtvZ0cVChgWDAsAASY4NiABERkRBiAjZ39rKzJ2lgGQAWAiKi4qGAABGy0zKxkONkRAHCGFAbUBqwF7KQMQFBIKFjZEOhwZT2NjQwsYJi4oFgoOEAoIAxUbIRkRGR8dFQcGFhoeFAgAAQcJBwIuVGZcOBYIAgECDBwgHhgEDRcbFQUPLT09LyM7S0k3GwABDhYiIlKQAaYBkgFSDVN3fWErLzk3Kw0cPEpCJgMRFRMRBxklKycVAAEOGBgQBztXUzUMUHh8ZCrNAQMHDRMbBHa6AbwBggEOZakBvwGnAWkvHw8HAAEQMlBkamg8GAEDAg01Qz0fGGKCAYABWBBTkwGpAZEBSyk/QTcXLqAB5gHwAb4BWhQPHx8NJlRaSBgtY39zUxsIEhQEFQUkNjggARERCwwqG2WBAXEpOoYBqAGgAW4qMjYuHAABITE1MSEKOEg8ICWRAcEBvwGJATcFCBQSBho6RDoaG1Vxb0sLGi40LhoOEhIQBgMXIyMhFR8lIxkJChggIBwOAgUHBwI0XGxmPhYQBAIGDhoiIBQGERkdFwcRM0VHOytJV1dFHwMSJCo0YqoBvAGmAV4PWYUBiwFvMzNBPysNIkhUTCoBDRUXDwkXKzEpFwISGh4YAz1bVS8WYogBkgFyMH0DDRMbHQtWiAGMAWICW5sBrwGhAW0/LSEPBQ44WnB8fFY0HhAMByk3MRsQTmpmSAxDfY8Bf0crOz0xES6UAcwB1gGwAVwkCAsNAxg8QiwGLVtrZ0kdBQQCCR0TECQkGAABBwcGHDgDO1VLGzZ0kgGKAWIqMjIqGgMZLTEtHwIiMCoSH3OdAaEBfTcRAQYIBBIqNCgQHUdhWz8JGiosKBoQEhIOBgMTHyUhGR8lIxsJChYgIBwSBgEDAQIsTFpQNhwSDAoIDhYYGBABDRcXFQUPLT9FNzFHV1VDIQUSJDY4ZJgBqgGMAU4LS295XzMzOzclAyI+SkAkAAELERENAxUjKSMTAAEOGBoYBic5ORUiWoABfmQmDAUPGR8nHQgeJBQJQWF1dV9NQTEhCw44XHKCAYYBdmBMMh4ECxMVDQYeKCgcCB87SUUvIyclFQEiWnqCAXZUPiweEAYGCgYDEyk7Pz0xGxUXHSEnIxEDAAECAgoUIjI+LhIEAAEIJEBQUDwqJiYeDAETGSEfGw0BBAQAARE5UVdLMSMXDwcBBAoKBgcVJy0tHQABDhYeGhIQEAwICAMNGRsbGx0dGREJAg4UGhYUDgoGAgIUJiwsIhoYGBIOCgwIBgIFCw8PCwUJGysvMy89QT81JQcQJjI8UGRiTigHKTtBPycnJx0RBhoiJh4UAAEDBwcDAwkRFRMLAwgMEBYSBgIKFC5GTEo2FLIBCx0rMzErJR0bFRs9ZXV1ZUElDwECDCpKZHqEAYYBeGJKJA4MCAQAAQEBAQABAgIDDRMZHQsMHiQiGhggJjRIPjIkFgYLISsvKyEVEQsRFyElKS0xO0lNOx8EJD5KTkpedHJgMggDBwESJjQ0KhoBDRUVGRcnP0E3HwMOEAgLJzs9OSsPAQUJDQ8RCwUBAgQWICQgGAITGxkPAwsRFxkZDQcAAQEDAAEEDAwSEg4OCAQCAgIIChQcIiIeGAgHDxMTCw0NEQsHAgQEBxkvNz07NSUHGC46PigKCREPAw4SDAIbJScfDQgaIBwcDgITGx8XAxAaHhYGAgQMEhQWEAwQHjBGTkg0FrACEzNDRz8pFw8JDxthnQGzAacBdy8SMjokBhwySGR+eG5aQCQSEA4KBAABAgQCBgQDCQ8VFQxEYGJGFgUPBRI6LhYGAQETMT8/NRsAAQwKAAEVHSEjJytRfYkBdUEIPFxqZE5+rAG0AZIBSgUtPzMPKkhUSiwEERcbGRk7a3tnOwo8TEYeHUlhXUUdAAECAAEDCQ0BBAgKCiQ+QjoeETtPRysDCxMZGRcAARQWFAQFAgQKDA4KBAQCAAEFAwMEDhwoKiwaBhEhJyEPERkZFQkMKC4gAi9DTU1BKwABIjhCPAQ/Y189AjZSUi4JLTkxIQIoNjoyIgErRUczCSpIVEQcBhIYHCASESMfAypgfnxiJvgCFTdLTUElEQcFCR1vrwHJAbkBfRsuVFQ0BAocNkxqalxKOCAUFhIOCgIGBggEBgABBQsLDx5efnxWFBMlHwIsIAYJCwcVN0VDNRkGGBQIFxsbHyEnU5EBoQGNAVMKRGh2ZkyEAbgBxAGiAU4LP1FHGyRMWlA0AhETGRkXPXeJAXVBDE5mXDITSWNlTx8CBgQAAQMHAgwSEAoqREo+HhdLX1kxBQ0RFRkTCBwkGggHAQABBAYIBgACAQEFCQkAAQoaJi4oHgQVJSslEREZGRcPEjRCNAorRVFRRy0DIDY6MBNpjQGHAVMCSnBwTAABKzk5IQIsQkpCKgABNVFVQQk2YGpYKg4YIiQiEh03MxUmaIwBjgFuLI4DEy9BRTchEQUFDR1nnQG1AacBaxcwUlAsAAECDh44UFBEPC4cFBQWEgoKBgoKBgYAAgEDASJeenRQFhMhHQUiGAABCw8HEy8/PTEZAhASARUbGRkdIUt9kwGFAUkGQF5mWjxwoAGmAYgBRgk1Rz0bIEBKRigCDRMTEw0zY3FnNwxGXFAuCzlNU0EbBAQGBgEDCg4SDgwmOjwwGhM/UU0tAwkPExMPCBogFgIFBwMAAQICAQEDAQEFCQcDBhYgJCQaBhEdJR8LDRMXEwsSMDguCiU/S01DLwsSICYaI2uPAYUBVQJIbG5MCB0rKRkILkJKQC4GJ0VNNQc0WmZYKhIgIigmFhcvLRMgXHx8XibyAgkdJysnGRMRDxMfR2l1aUsVEigoGAUDAhAeLi4sJhwYFhYUEhAKDAwICAQIBggICCJCUk46GAABCwcCGhAEAQUBDSErMScZCwUFCxkZGRkZGS9PWU8vBiY6PDgkRF5kUiwCEyEdBxIoKiIYAQULCQkLGTM/Nx8KJjIsGAUbKSkhDQABBAQCBAQGEBAOCBQeIhgMCR0pJRcDAwkLCwsAAQoKBgEJBwUFAwMFAwUBAAEFBQUBBg4UGBYOCAMNEQkDBQcLCQUIFhoSAyEtOTs7LRkJAQABBSVRX1s5AyxCRjYQBQsLAAEUKDY6NigQCx8hGwABIjxCPCYaHiYkJhoAAQkNAAEaPEpKNhb0AQgUGBQEFzdJTT8nFxENERUVDwkHCwUCDg4QDBYcIiAYEhAMDgwGAQkJAwQGDBIYHhwYFhQWGBgUEBAOFhoaFAgLGyUnJyEjISMdGxkVDw8PCwMBAAECAgQEDAgKBAIAAQQMGCQqJhoKAwkNDQMBAAMCBAoKCgQDCw8PCwMKFhYSCAMDAwECCAoICAYCAQULBQABCAwQDAgCBAQGAQUNExEPCwkHBwsHCwcJAwEBAQkFAwIEBAICBgQMDBAMCg4SFA4IBQsRFxcZGxshIykvLy8vJyUhHxcPBQIOEhYWDAgIDhoiIiAeHCIoMCogCAABAQIGEhgYHB4eJiwsKiIUAgcPCwUjMHaUAYgBVBefAeUB7wG1AT0QOEY+GAIEBgIBASAyMhwAASREUEYkBgIBCQkVO0tFKwUIDBYmOi4IDQ0CGBQOCgYEJEJIOhYZQVVVRTE1NS8pGwkBAgYGFjA+OiAAAQMHCwcHK0dHNQ02boYBfE4GL0dHMQsAAQYGDAwcMDguHA0zQ0EtCSpMXEogBxMTCwISCAEDAQIVJyUdCRIqMjAaBhYiGhIHGyUpHxMHBwcJDw8PBwcFAAEPFRsTBwEPERELBhQiJCIaLkRKPhwHHSUpIxUHAwMPITdJT0tDLx8RCwcFCBIcICAFJzEjBhwWEBIWMHCSAZIBaBolR005DQ4IBgwWLE5eWDgCP2dtVyPhAUasAdoBzAGCARPNAbcCwQLzAUsoYnhsNhIaFhAAAi5CPiIJIlhiVioCCxEXGSldc2dDCQoUIjZUQAoTGQcUEggCBQcqUFpKICVZb3FZP0FFOysZAwoWGhYuUGBWNAINDxMVFUdtdVUbRpgBuAGqAWYBQWNjQxEGCBAQFCpGTEIiD0dfWT8RNmx2YigJFRcPBhQKAgcFARsxNSkLGjZAOiIKHiYmFAUjLS0nFQcFCQsPFQ8NBwMDESEhHQkJFR0dEwIcLDQyIkJcZlQqBx8vMSURAgwKAR09U19jUzsjEwcFAwwcJCooEUFPOwcaDgwKFDaUAcgBxAGQASA5Z3NdIQIDBQIQLF5wZDwLZ50BowF/M7UCSrIB4AHUAYwBB78BqQK9Au8BTS5qiAF4RiAkIBQEAAEmODQUFRhIXE4kAQ8bHyMxZXdtRwcOHjJOZlQeAw0BFAgCBw8PIEZSRhgjWXd3Y0dLSUExFwABFiAiJDpealw2AAENFxsZH093e18dSJgBugGmAWQCQV9hRQ0EDhIWHC5ITkIgD0VdXUUVNmJwWiQHEREHChwOBAABAQABGy8zJQsaMj42IAYYIiAOCR0rLSMRBwkJCxMVEQ0JBQEPHSEbDQcVHR8TAhwuOjQqRmJoViYDHSstIw0CDg4BGTtXZ2VfRy0ZDwcDDh4sMC4LO0s9AxoMCAwUOpQBxgHGAYoBIjlneV8lAwkLAQ4sVGRYLBlzowGrAYcBNZ0CNogBqgGmAXIMdcUB1wGpAT0eVGhmQCoqIBYCCQ4YEAUlASI2LhgBER0jJTFTX1MxBRowSF56akAiFA4SBgcPFxcGJDAmEh9LY2tdSU9LPy8XAhYkLCo6VFhOKAIPFx0hIUdfYUkVOHSIAXhKBClBPzEJCBAUGhwqPDwwGA8xR0c3FRw+RjoWBQUDCBAgGBIKBgIRHyEXBxAkKiQUAg4OCgQJFx8fGw8HCQsPERURDQkFAQsTFREHAw8VEw0CGCo0Mio+UFBAIgMTHR8dDQEEAAEFGTdPX2dfTzstGxEDECAqMjQKGScZBBoUEBIWMnKSAY4BZholS1NHGwUJBQABDiQ4Pi4OKWGHAYcBaSmvARIuPkA6JgwHFx0bAxosMjAoIhwMAxEdJy0xNScXDwABBAABDxkhJScnJRsPBipMZHyEAX5uWkIqDgABDRcbIRcRCwcDDy1BSU1LRT81JxUBDhokKjQ4OCoaAw0bHyMhKSsnGQEWJCokGAwBCQsJAQoSHB4cHhoaEgYHExkfGxULBQADBgwUGCAeJB4eEgIBAwUBAAEGCggIBAEJBwsNCw0PEQ0HCQkPERMREQ0JAwEBAQMDAAICAQACBhIeIiooLCgkHA4EBw0RDw8RDxMXGys9S1VbWVNFNyEHDB4oMjQwKiQgIBwaGhgcIjI2NCYMBxMXFQsBAQABBhIaEgYLGy1DRT8vE7sBDR0dEQg+mAHCAbwBeAYPAQoYJB4GBxETJ2OHAYsBc0E5NykdAAEBER8jJRUKIiYeFEyCAaYBqAGSAXhoWkAmCgMLFRshKScjGQsVL0FLSz8jDwkFESUtIw0WQmJmWC4BIzM5NSUhIRkPAgQFCQcCCAEFCwcBJkJKQiYMAAEFCwkFCwsLERcrOzsrDxAqNjgqIiAkHhQGBgoMCAYBBQMFAwURGRcTCQ0NCQkJBQ8REREPCQUBAQABAgQEBAIGDBISDgoYIiwsKhADDw0DBQ8bGxkTFRsdHRsNCQ0hQWuFAYkBcUUNBhAeKjZcfIIBbjwMCRELBiI8RkIwDhElLycTCRENAwgiQkpAFiVFUU87Fa0BHUVTQRVO5gG4ArICzgEeEwsBChwODycpIzGPAcEBxwGbAUs9OzchBwABFyEnIQsuTFI+HF6oAcgBwAGSAWJSPC4aCAELDxkdJyspHQ0XM0dPSS8DFCAQCT9ZVzkEUIQBjgF8RgUtQ0s/JR8hGxEDAxMbFQkECxUVDwABOGRsViwCDxsZEQcFCQsLEz1VV0MZGjxKRjQcHiAaEgQKDhAKBAEHCwcFBRMbHRMJCQkNBwUHDREREQsDAAEEAgICBgYGAggUHBwUDBwqMC4kAR0tKQ8JGyUpIRUZHx0fGQYkJAwpcaMBswGfAV8RAQgUHjJ4rgG6AZoBTAMnNy0RIlJoZEYUIUNNQx8NGRsVASp2lAGGAUoXT2dlTR85I1VlVSFI9gHQAsgC4gEiExMHAAEOBCEvNSk1kwHNAdEBoQFHNz83IwkDFyElHwM6YGBIHGKmAcABsgF4SjoqHA4GAQUNERkfJyUbDRExP0M9IwwoLCAHQWVpRwlOggGUAXpEAy1BSTkfGR0ZEQABBx0hGw0AAQ0ZGxMAAT5mblgmBRkfHRUFBQUHCRE5V11JHRpAUEYwFBgYFhACCA4ODAQFBwkJBQURGRcPCQUJCQcDBQsNEQ0JAAEGBAQCBAQGBgQKFB4eFAwcJCoqHAcpNS8VCx8nKyMTGRsfHRcWNDogFWWjAbcBpQFjEQcAAQoYKnq0AcIBngFMCTNFOxkgVGxqTBQlR1NHHw0bIRcFLIABqgGaAV4JS2FlTSGcAR1HVUslOMIBiAKAAq4BHBMRDwUECSExMycreaUBpwGDATkvMS8hCwcTHRsZATROUjoYTH6SAYIBVC4gGA4KAgABAwcLDxcZGRMJDyErLykTDiYoGgMzUVU9CThkbloyAR8xMysTERERCwMHFR8ZCwEPExcPATBQVD4aCRMbGREHAwMDBQspRUs5GRgyPDQiDhAQEAgEBAwKBgIDBwUFBQMNDw8PAwMFAwUBBQcJCwkDAgIGBgABAgQIBAQGFBYWFAoQHB4cFAshLSkTBxkjIRsTERcXFxEQLjQiC019kQGDAVEPCQEEDB5ekAGaAX46Cys3MRcYRFhWPhAbN0E1FwcVGRMBIGyKAYQBUAMzS0s7F9QDCx8lIREOSGRiPgINDw8PCQ8bIyEbHTtNTz0hGyEZGQsLDxEPDQMUICAaCiA2ODIiDgwIBgABAgIBAAEBAwcLBwcBBQsPEQ0HCg4QCgABEyEhGQUWJCYkEgMJDxEPAwMFBQMAAQULDQ0HAwMLCQcBEB4cFAYFCw0NBwMDAQEDARMZGxkHBhQWFAoGBAgGBAABAgIEAgEAAQEDAwABAQMHBQEBAAEBAwABAQABAQMDAQEBBAABAgABAgQCBAQECgwMBgQKCgoIBAMPExEJBQsPEQ0JDQsNDQsEDhIKByM1PzslCwMFAAEECCg6QDYaAxETEwUOHiomHgwHERURAwABAwMDAAEUMDw4JAIRGR0XB9YGAAIBAAEBDTFHRzMRDREVFxsRCQEFCREJBQMNFSMtLyUdAiAuKBYNKzszIwMFCw0LBwgSGhgSAAELCw8FBAoKCAYCBgoKEAgAARMbHRUDFCQkGgYPExETBwIAAQIGCgoYIiIeCgMNERELBQMBAAIDDxsdFQ0FAAECBAABAQQKCAoGDhISDgYNHyUhEQMGBgoGAgICAgACBQMBAAEBAAEBAQEBAgADAQACAgIEAgABAgICAAECAgYICAQEAgQEBAABBQsNBwECCAgEAgABAgIEBAIFCw0JBx81PTUlAh4uLhoBBAgKBAABBAoMDAYPKTMrEwYLFRMNBjhebF40CgEHCQMDFR8fEwEIDA4IBPYIAgQGBAIXaZMBkwFnGQsXIScnEwgUFgINAgoGAxtBVVtPMRZeeGw6F2WFAX9VDw8fJR8VDi4+OCIEFyMfEQwUGBQOBggYHhoWAAEtRUMvBy5MVkAKHysrIREBAgYKEB44UFJAHAcbIyMbDQUBAAEBAyM3OS8dAwQKCgQCDhYcGBIgKCwkCh1HU0crBwgSEAwCBAgGBgUHBQcFAwEBAwUBAAIDAQMAAQQEBAQCBAQEBgQCBgwODAgEBAQCAgABERcZFQMMEhQUBgABChISEAoDDRMNBz9ziwF/RQ5WfHhEBhAcGBAFBhgaFgora4MBbzUGHzs/KwSAAd4B/gHaAXAQCx8fFQ09VVM3CQ4WGhYIggoCBggGBhl3qwGpAXcXCxsnLS8bDBwYBgsGDgoFI01rc2M7GHCUAYQBSB93oQGbAWcXFSkxLRsMOEpELgYbKSMPEiAeHBQIDBokJhwAATVRUzsLNl5iRgorNzcpFwABBAgSHChOYmpSJAshKyshDwkDAQEHKUFJPSEJBAoMBgIUICIgHCg0NCYSI1NlWTcJCBAUDgEECggCBQsLCQcDAAEDBQMDAAEBAQUAAgIEBgYEBAgGCAQAAQoMDhAGBAIAAQICARMfHxUDDBYcFAoEDBQaGhQAAQ0PDQNJjwGrAZsBXRBsmAGUAVgKFCAeDAcGGCAYCjeDAZ8BiQE/CidHSzUEoAGWAroCkAKMARoPIScXF09naUkNEB4eGAyACgIGCAgIEWWPAZEBYxEFGyUvNx8DDhAAAQ0AAQgCDSdRa3FjRQxefnI8GWuPAYsBXREVLTExIQQwPkAqCA8dGQUaJiQeFAoMGiQkHAQrRUk3CyxOUDYDLzk3KxUBCBIYJDJSaGRQJAcdKykhEwsFBQMDJ0FHPScPAAEGBAQEFh4mJBwqMjAkDB1HV1E3DwIMDAgBAgQEAwUNDQsHAwABAwMDAQACAwABAwIGBAQGBAYGCgYEAgYKDgoGAAEBAAEBAAEDERsbFQMMFhYUBAIOFhocGAYBBwMCQYEBoQGVAVsIXIYBgAFOBBIaFAgLAAEOGBQIMXONAXcvFBc5QS0GmgGEAqgC/AGKASAAARUbFxdHY19BDQwYHBgI/ggCBgQKBgUvSUkvAAEDESMvMysdEQ8NDwsJDRstRVVbVUMPIjw2HhE9U00zCREhKykhCRAgIh4OAgABBA4kJiIeFAYIFhwaGgoRIycdDRIeHAoTLzEtIxECDhgkLjhIUk46HAMTGSEbEw0LBwUHGy8zMyMVDwUBAwIQHiAeICIoIhgIESk3NScVBwMCAQMHAwMFDQ0NCwcBAAIBAwABBAECAQIEAgYGBAgCCAYGBAABAgYGAgIDAQUDAwUNERELAQgKDggAAQEKEBYcGBIKCggKIU1lY0UNJD5AIgIEBgIFEwkAAQQGAAEdO0MzCSIGDRUPCGSqAcABqgFoKhYGAwkRLzs7JwsIDhAOBowHAgQKCAoMEhgcGhQKAw8bKz1DRzsnGRslLS0zKyEjKTNBPzkpGwUIEBAMBAcTGx8dGRMLAQoUGh4iJiYkHhYOBggYHiIaDgoGAQMHFyErLSsrLycdDwQcLDo8OjQsIhgMBAkRERUPBQIGBAMVIysnIR0VEw0FAQ4UHB4aHBYUDgQDBwkPERURDw0JCREZGRcRCwcHAwABAgIAAQQAAQIEAgQEBAgICggIBgIEAAQDBQMHBw0NDw0JAAEEBAYCAwMHBwkHAwIKEhooMi4qFgIPGSElHx0VEQsJFyEjIRsfJSEbDw4mOkI8MigiHBIMFiYuNDY8PDgmDg0fKSUdBRAeJB4MjgUGEhQWEBZAWFhIIiAiGggXR215aUUbM0NJSTUEKjIaHW+pAbUBmQFVBDhYWEASBw8XGRsbGRMHBhQWGh4iJh4UCgQAAR5EVFA0EAABBQcNCylDS0c1N0FFNR0KNlReWjwcAgkJAAEBCxUXEwUiODoqAilBSUMrFRELBwUCEhweHhwYGBAKBAQKCAQJEQ0HBwULKT1DNRsHAgoMCAQBAAEDAgABBAYGBgYMFhoaDgQAAQMDAQABBwsLDQcRHSsrIQkQICgcBgsVHRsVDxcVDwEeWHaAAWImBAkXHx8XEQ0HBRFBXV9LJ0VhaVElOJoByAHEAYoBMCAYEAwGAwcDDCJKaGxcLBtfgQF/WRFIgAGSAXgy6AMKFB4cFh5ScnhaKCw4NB4FQ3uJAX1NHztRWVM5GFBePBWHAd8B9wHTAXkAAVR6fFYUAwsPExUXFxEHBhQWFBYeIhgKBAIAASRacGQ+CAUNEQ8PL0tXUTc1T1NDJQxEaHJiOggTIRsHAg8XGRMCOlZaOggxU1tLLRMJBwMBBBIcHh4YFBQQCgIGEBIKAQ8JAwEDCzNPV0UpAwwWFg4CAAEDAwEEBAYGCAYSHiYeFAQDBQUFAQcNEQ8LEys5PS0LGjY4KgoTIycpFxkhJR0LIG6eAaYBgAEwCAsVGxUPBQICBBVVfX9jK1eFAY0BcTNS1gGUAoQCqAEsFgoGBAYPHx0NFkh6hAFuOCODAbEBrQF9G2S4AcoBpAFGlAMIFh4eGB5UcnJaKjA+OigGNWl/cUcdOVFZVTcUTlg2E4sB4QH9AdsBgwEFUHh2VhYBBQkLCQsPCwMKFhASFBggEggCAwMmVmpcNAYHERUPDytHT0kvL0tNQyMMRGJqWDACGyUhBQELERMPCDxaVj4GLUtVSScNCQEBAQIOGBoYDhIMDAgCCA4QDgELBQEAAQUJL01VTScDEBoYEAQDAwEBAgYICAgIECIiIBIEAwUHAwABBw0REQsRLTs/MwkeNjwsBBMlKykbGSMpIQ8ecJ4BogGAAS4KAw0RDwMICAwEE1V3fV8tVYMBjwF1M1LYAY4C/gGgASIMAgECAhMlJRUGQGp6ZDIlf7EBsQGBARlksgHGAaQBQIwDCBIWGhYcQFZWRiQoMjQmDBtBUU0zFy89S0c7AyAoDiN1swHFAa8BbxEqTFA6EAICAgABAgACAgYOFhQSEhYWDggAAxg6QjwiAQsNDxELHzE5MSEjMTcvGwouRko+HgIRGxMDBgMJCwUKLEA+KAQdMzkzHQkFAwEBAQgMEAoKCAgGAgIECgwEAAEJBQEDAwsfOz87IQEKEhIOAgABAQECAgYGCAgGEBgeGA4CAAEFAQMBBQkLCwcRHy8xJQsWJigcBBMbJR8ZER8fGQkYUHZ6XCgMBgEDAAEICg4MBg07V1lFHz9faVcpOJYBwAGuAWoUBAEBAAEEDR0fFQABJEJMPh4fX4EBgwFfGUR6igFuMNIDAgoMEBISGBoeGhoeIiAcFAQHERUTERMbIy03NTU1OUFLV1VPPSkPBQYIBgYGCA4QFBYWGhgWFhYUEhIMBgIAAQEECAoGAAEHBwkJCQkPERMRDQsPDwsFAAEOEBQQDgQCBAQGCggICgQGDg4MBgEJDQ8NCQcDAQMFBQMDAwEBAAYBAQEFBQcFBQUHERMbFQ8HAAECBgIEAAEEAgIGBAYGBggIDgoKCAQAAQIBAAIBAQMDAwULDREPBwECAgEHCQ0NDQ0NCwkHAAEMHCIkIBgUFBQUFBAQDgYEAw0TFxMNER0ZGQ8EGB4cEgABBQEBAAEEAQcLDQkAAQYEBAcRHyUlHRECEhYSCLYEAAECBgQGBzFJRSkKMkZORi4SAQ0REwABLEI+Hh9rmwGrAZcBZSkDCggRLzMrIREBEyMhEQwwSFBEMBomLCweDAsbHRsJBQcNERMPCwMBAwUNDw0NBwMAAQQEAgEPEREFBA4MDAoOFiwyLB4EEx8fFwUFCQkHBQgQFBIGCxshHxMFBAoKCgQJDRcTDwMGCAoCBQsNDw8JBwUCAAEEAAEEAAEEBAgGAAECAAEGDA4QDAgBCQ0NCQEIDg4OBAQIDhAIByEtMSkbAwgQDgINDw8FBgwRHyMTBi5IWFQ4IBQIBAEGHiwoFgUGHCIcBiFHUUszFSEpJxkAAQsXHRkRCw0NDw8FKEBAKAUxSUk3GdQEAAEBAwMDGWeNAYsBWQABRmx4akYYARUbFwpchgGEAVYJgwHTAesBzQF7FShCNAY3PzkvGQcnQ0UtBEZudmhAHjA6OCgGGzM7LxMFDxMbHR0LBQIAAQkLExMPDQEGCgoGBRkhIREGDA4QEAoiQlBGLgQjNTclCQkLDQsDDiYqIBANKzcvIQMMFBQOBgkdISMVARAYGA4FCxEREQ8FAQABBAIEAAECAggMBAABBQABBA4UFhIGAxMdGxMDEBogGg4IGB4eFgkxTVVJKwcUJBwIDxURCwoSH0NJMwJEcooBhAFaLBoMBAEKOExKKgkSNj42EDNvjQF/UyU9S0UtBxUpLysVCw0NEREGUnx+VANRdX1jJ6gEAQUJCQ0jeaMBnQFnA0x2hAF6UCIAAREXFRBmlgGcAWgCfdMB8QHTAXsPNE5ACjlDPzEfBS1NTTEGTniEAW5EGjI8NiYGIzk/MxcHER0fJyUVBwMBBQ8TExUJAQgKDAgFHSchFQYOEg4QDCZIVk4yBCM5OSkLBQ8NCwEUKi4oEA0tNzcjBQ4WGBIGCx8nJRcBFh4cEAEPDxETDwkBAAEEBgICAgYKDAgBAAEBAg4WFBIIBxkjIxcDFCAmIhAOGiYkGgc3WWVbOQ0UICAIDxcTAxAaIUdNNwFKhgGeAZgBajgiFAYEDjxSSioNEDRGNhQ3fZcBkQFfK0lXUTUJFy81LRkLCw8PDwhcjgGMAVoBV4cBjQFvK7IDAwcNEREpZYsBhQFXAAFEbHhuUCgQAQsNClR+fFgMX58BuwGtAWkTIDIoAzc/PS8ZByE3OR8OSGxwXjgYJjAsHgIbLTMpEQkXHykrLR8TCwcHDw8TDw0BBgoIBAMTHxsNCBAOEgwMIEBKRCoGFyktHwUHBwsFARIkKB4OCyMvKx0FDBQSDgYJFyEfFQABEBoYDgULDxURDQkDAAECBAIEBggMEgoGAAIEDBIQCgQHFx0fFwUSIiIcEA4aHiIUBTFPXVtBGwEQDgABEQ0LBBwkCSs3JwZGeJIBjAFqQjAgEggMLDo2FhMAASQqKAwtZ4MBe1ctQ09FLQkTJS0nFQkNDQ8PBEpsbEQHTXF3XyX4AQMNERcXHTtDPSMIMEZQTkIyJBgKAgIiNjYoBh9DUVFDJRcNEyE3NyshFQEFCQUIIDZAQDYgEhYWFAwCCRETDwcLGyUrMzErIxsTCQkNDwsJAwECBAIBBwcFAgwODg4MChYkKiQcDgELCwkDAQEBAAEEChASDgYHDxUTCwMEBggIAgMLDQ8JAQYKBgIFCQ8PEQ0JBQMAAQIEBgoOEBQSCggKBgYEBgIBBw0REQsAAQoOFBAMChASEgoFHTVDRUEzIxsREQ0FChYoMh4OCAYMMk5iZlhIQDIkEAgKCgABDR0VCQIAAQMZNUVFOScvMSsZAwUPFRMNCwkNCw8HEiAgDBMtPTstEYIBBREZGx0ZDQcCCA4GAggWKjgyKh4KBQcLAwMGKEJAKglHZ3lxVy0PAQoOBhgsNjQsDBEfGwkOGB4YEAYIDA4KAhUzQ0U/NS0nHRMLAQICAgELCw8NBQABCAwODhAgKiwkFgMRFQ0BDggIAgICGCQsJBAAAQsPDQcAAQgMCgYBCQsLCQEBAwMDBQMJCw0JBwUAAQIAAQcFAQYEBAIICg4QFBIUEAoIBQsTEQkFAAECBAIAAQEBAQIEBgQIAAIDFyUvPT09NS8lGQcQJDI4Oj5AOCgaGiYyPEZSUk4+JAc7XWNVLw0EDAwCBw0PGRsdCQgMDgYJExUTDxk3QT8pCQYQCAITEwsFAwF8CRUdIR8VAwoQFAobNTcfEjI2LCAOBxcZGw0KYqABrgGCASBbpwHJAbcBfSMYOkIuECpGUkwwGWN5aTESLjg2JAoQHB4YCiNRZ2dRLyUZEwsDBhIYEAQNHR8bDwIKEhQUFDZWWkogF0FPQR0KBAMDBQY2XGRQIg8fJyETBBggIBIBFR0bFQkBBQcJAwcRGxsVCQgUHBYEAQoQEAgCAgYIDg4UEhIMBAsjJSEVAQgOEg4CCQ0NCwABBAIBAAEDCRknNz9BMysdGRUAASA6RkQ6QkY+MBYSFh4sOlJkYlIwE327AcUBlwE5EjxKQBgBAgEFExEaNkAuDBUpMSsTNXWPAYEBUQUuQkQmBwkCBgoGagcXHSEdEQQSGBgKLVNXOwIuLioeEAkZJR8VCnjIAdgBqAEyWbkB3wHRAYUBGTBWWEASLlBcTiotgwGjAYkBRRI4REIqChQiJB4II1lxb1MtGw8HBQMOHCAaCA8hJyMVAg4UGBQQQmZsViYjV2dbKQQAAQcLBQZEcnpiJg8pLykXBBwsJhYBFyMjGQkDBQsJBwUZHx8bCxAiJiQKAAESFBQMAgACBAgMDhIQCgQTJS8nFwEOFBgQBAsRFw0FBAABAQUFCRkpMzk7LRsVCw8EKkBKQjQ+QjwuEgoMDhwuSmBiVDIXlQHZAecBrwE9JFhqViIAAQoIAgkJLkxSPg4bMzkvGTuLAa0BoQFfAAE+Xlo4AgMKEhAETgcRGRkVCwQSGhQILVFXPQUiJCIYCgUZHyERCmy2AcIBmAE2S50BwQGzAXUNNFBUPBAoRE5CIi19lQF/Qw4yQDgoChIcIhgKIUthX0chEQcDAgMQHB4aCA8dJR8RAAEMEhQQEDpcYEwgH09lUysEAwkJBwRCam5UIg8lKSUTAh4iIhYBFR8dFwcBBwkJBQUTHx8XBw4iKiAMBAwWEgoAAQEAAQIECAwMCggAAQ0jJyMVAQwYFBAECxMRDwMAAQEDAwUHEyEpLS0fEwcHBwYkOEA2JjIyMCISAgIIDh44TlBEKBV/wwHLAZsBMShWYlAgBAoMCAUDLEpOOAwZLTMrETV9mwGPAVkCQFZWOAICDhAQCCgDCQ0NDQUGDAwMBBsxMyUHFBISDAgDDRMTCwRAanJYHidXa2VBBR40MiIKFiYqJBIbSVVLJwgcIiIWBgwSEBAGEyk3NScRBwMCAAIIEBYOBAURFRMLAggKCAwGIjY2LBITLzczGwIBBQUFBCQ+QjASCRMZEw0EEBYSDAELDxENAwMDBQMDAQ0REQ0FDBQYFAgAAQwKDAQAAwECBAYEBgQCCRMVEw0AAQgMDAoAAQMJDQcDAAIDAwEDCxEVGRcPBwEBAwIYICIeFBgcGhQGAgACBhAcKCwkFgtHbXVbGRgyPDISAgYKBAABARwqLiAGDRkbGQsdR1lTMQIkNjIiBAIGDAgENwYKDAwIAgABAgABAgABBAYGBAYAAQICAgABAQUJBwUEChAQDAYDCQsNBwULDwsJAAEMGBYQAgkNEQ8FAAEEBgYEAgYMDgoCAAEFAwUBAAEDCQkHAAEECAgGBAEBAAEBAAICBgYCAgEDAAEDAQEFCQcFAQIIBAYAAQMJBwUAAQQEBgIEAAEDAQEDAAECAgIEAQQCBAQCAAECAAQBAAEBAAIBCw0LAwIGCAoGAgABAgEABgIAAQQEBgQAAQMHBwkDAgQMCggAAwEAAgcNDw0HBggODAYAAQUJCwcBEBgcGgwFGR0fDwISICAWBAsRFREFAQICAAEBCRcZGxEDBAYGBgIMHCIiEgMdKScbBwYODAoGkwIUMEA8JgoEAQIBBhwoLigSBgADAgEXHx0NChogIBwQAg8VFRMTPVFPMQUyUFQ4BiMvLSMVBAgKCAoMIDAyJg4FBwMAAQoKEyUrIQUQHB4aCgEABAQOEBIKAxEbHRUNBw8RFRMNDhoeGAABIzk7JwkSICAcEAENFRUNBQgSEhAIBA4UEA4CBAgIBAIDCwkJAxMxPTUdBh4qKh4KAgMDAwABBAgKCggKDhAQCAIPHR8fEwIcKi4gCAYEBAYGGTtHOxsKJi4sGAcnOTcnATxwgAFsOid5nQGPAVcSdqoBsgGAARQ9ZW1bLQECAgEPLW2NAYkBXRUQJCgiEkCUAboBqAFoFYsBxwHFAYsBHRwwOCwSwQMcSlpWOhIGBAABAgomQEQ6HAYGAAEEAgMbKSMREiwyMioaBA8fIRcfWXVvSQlGbm5KBDlFRzkbAgwQEhAQLkZINhQHBQUEEBQXMzkrCRYoLCIMAAEBAAMEEhoWDgMbKSsjDw0XHyElEwwiKBwEM1FROwkiLjIqFgEVHx8ZBwoYHBYICBYaGhACCggKCgIJDRELARtJWVErCi5APi4SAgEDAwEGDA4QDgwWGhQSAAEVJzEtHwImOkAsDAYICAgMJU9jVycUNEQ8JAs5T083A16gAbYBngFSM6MB2wHLAXsYqgH0AfgBsAEaV5EBnwGFAUENAQELHUulAdMBywGPASEcMjw0Gl7YAYoC8gGQARvFAZkCmwLJASkoSE5CGLcEIExiWkAYDAgECAwsQkg8IgwGCAQEAhchHwUeMjw8MCIGDRkfGSFdeXFPCUBoZEALQ1FPPR0CDhIWFBQ0SEg2EgMFAAEMGBoPKTMlCRYmKiIMAwEBAgABAhIYFAoHGysrJRUPHSctKyMGGiQaAAEvT1E1BSY2NiwaAAEXIyMbDwwWHBYKCBQaGg4GBgwKBgABBw8PDQMZSV9VLww2RkQ0FgYAAQUBAAEIDhISEBQYGBgOBBcnMzMjAAEkODwqDggICA4OIU1hVSUWNEY+JAs5TU85AAFkqAG8AaIBViedAdEByQF3HKgB9AH0AaYBFFuTAaUBiwFJFQ8PGy1dtwHlAd0BmwElGjQ+OB5k4AGQAvgBkgEZwQGbAqECywErLExSQhztBBg+TEw4GhIODA4OKDY8MiAQDgoKDAYJCwMMJjo6OjAkEgMNFRUdRVtXOQkkOjIUHUVNRzkbBBAUFhYUKjg2KAwBBAoUHiIIDxcVAw4aGhQGAQEAAQMAAQQKDAwEBxshJR0TEyEpMTUpEQQODAIfMTMfBCYyMCgWAhMdIRsPAAEOEg4ICAwUEAwCBAgGBgABBwsNCQABETVFQSMMLDw8LBYIBAIBBAYOEhIUEBYYFAwAAQ0fKSkhBRIkJhwKCAgKDgwPLz83FRAqMC4WByU1Nx8GToABkgF+Sg1dhQGBAU8WeKYBogFsBElzf29BHyMnMUFhowHBAbcBgQEjEigwKhxOqgHKAbQBbA2FAcUBywGTAR0kNj4wFuEECBggJCAcHBgYFBYWICAgGhQSEA4ODBgcJCwyMjIsKiIYEgYAAQcNGx8dFQUNGyExOT09MyMRBAoQEhIUGBYUDAICEBwgKCgkHhQMAgIBAgEBAwEBAAUDBwsPFRcVDxMfKzM1MycfEwkBAAIGDhggIB4WDgABBw8TFxENBQEAAQECBAABBAACAgICAAEDAwMDAQABAQkLCwUGEhgaGBISDAoIAgYKDhIQEhIODAYAAQMLDRUVEQkHAAECBgYIDAoQCgICAAEGDAoKCAYBAwcBBBAgLjIwLBwQBAACChIYDgIRIy0zMy0vPUtTV1tjY1U7EwABDBAUECA0PDgkBhcpKRsAAQoSEg4EqQQHGRUPBh4oLi4mFhwgIB4cEAIDAwQaQFpkWDwaBgMCFioyMiYSAwYKCgYJM1tpY083Jx8TBwEXGxcFDgoCAwUDDiY0OjQuNjw2IgYNExcRCwABBAgKBAIKDAoCCyk3PTEdDw8XHS0xIxEHAQIQHiIkIBYMCAQGAgcRExUVGyEdEwMDAwUHAwABAQUDBQMECA4OCgweJCQYAxsjHQsQIioqHgoBAQECChIYGBAMBAABBgIAAQkVHRsZCQIMEBAWFCI2OjYgBBUfHxEIHCgwKBobP0cxAjxcaF48ATtbYU8nARIWCBNJeZEBlQGBAVENIDAkAAECChQUEgIHCwsBDhYcGhIKAAEFCQkDmwQXMT0tDR40PDoyHiAmKCYeDAkVDwUiXoIBigF0RAYbIxcMNkpQRCYGFCAeEgdJdYkBe1EvHQ0FAQ0vPzkfCgILDw8FFDRGSDwqPkRCLAgTHyMdDQABDhAOCAYUGBYKCzdZXU8nCQUDDyEtEQIKDAQUJjAqHg4CBQMDAgsPFxUVJS0vHw8AAQkJBwUDBwUJCQMIGB4YDBg0RD4kCTtPRyUMMEQ+MBQFDw0JBBIeHBwOBAgMDAoAARMhJyEPCBAUGBgYNkxaUDIBIzc1GQ4wRkhCHj+FAZEBbxdKgAGWAYoBVgddlQGZAXsxEjY+KAlbnQHFAcEBnQFHLHSIAWQOAhQYHBQDHykhDRAiKCoiDgMRFQ8HyQQZPUc7FR40QEIyIiQqLCoiDA0XFQMkZpABkgF8PgQjKx0MOlRcUC4OICgmFAVHeYUBd00hEQUAAQIPOU9FKQABAw8XEwsWNkhINCQ4RDwqBhEhJR8PBA4QEAwGFB4YCgs9XWVXLQcCAAEHFycJDBQQBhYoLCocCAMHCQMAAQcRExMRIzUxJw0FBwsLBwEFDQsJBQ4eIiAOGjxKRCoLQ19XMwg6SEg0FAcTFQ0AARIeIBgSAggOEg4EDx0lHxEKEBYcGho2VGBWNAIlOTMbFDhOVEQgRY8BpQF7IUyEAaABkAFYA2mbAakBgwEzFD5GMAldqQHLAc0BoQE/RJABpAF2EgISHBoSByUxKw8QJjAwJg4FERUVB7kFEzM7Lw8cLjg4MiIkKi4oJhADCwsCIlx6gAFoPgoRHQsSOk5USjAWIiYgFAc3WWdZORcJAQIAAQ0zQT8lAwcTFxMJDCo2NCYYKDAuIAQRGx0ZCQIKDg4IBBAUEgYLMU9XSSkLAAIFFR8FChIQBBAgJCAWBgEFBQECBQkNCwsdJyshDQEHCwkFAwcHCwkDDBgeGgwUND46IAc5TUsvCC4+PiwSBw8TCwMQGBwWDAYEDA4KBAcXGRkHCBAWGBoWMkZOSC4GFyMjDRY0REg+Ii1rfWMVPnCAAXRKA019hQFvLwooMBwTV48BrQGrAYsBOzR0ggFcCAABDBQUDgcdJR8NEiIqKiISAQ0RDQXpBgcREw0CFh4kJCYiJCgoKiQaEgoMECA4SExGNCASFBQkMjo6NioaHBoUCAMZJSknFwkDAQEDCRsfHRcHCQ8NDwkAAQwQEgwGDBAQCAABBwsNCQcCBAYGAgIEBAABAw0dJy0pHQ0NCQ0RFwkBBAQAAQgOEhIMCgQCBAQEAgEBAwMJDw8NBQMFAwUFAwEFBQMDBAoOCgQKFBwYEgEVIx8RBBggHBYKAwUFBwABCgwQDggAAQQCAgQAAQEFBwUAAQoOEhISFhwoLCgaDgQBAgoYJC4sKiABGR8VBCY4PjgmBh0xPTUhDwcJESNBV2VlVTUHGiAWBwkAAQYICAABBwkFBBAWGhoWEAYCAQMB1QcAAQQGDhYaGBQWGB4SBAABBhIyTFpSOhoSEhQWHiAiIiYsMjg8PjgsEAMNDQMGEBISBgMHCwkLBQ4SDgIRDwkJBQkNExcVDQcFAwMBAwMFAwMEBAIEAAECAwsTFRMNAwYGAgkTERERFRUrLSsdBwQICgwMAgUHBwQMDhIQDAgEAgQAAgQICAgGAAEECgoEAQkPFREJAwMDAAEBCBAWFBAGBwkJBQEIDA4OCAEJCwsFAAEECg4MCAADBAYKEBAYFhQIBQ0JBhQcHh4WEhoeIiAeICAiHh4kMjQyIhAYFA4BGTdNU089JxsTExkhLTMtJxcXGxsRBwkZHRcHFDBAQjYWDScvJw/RBwACBhIgLB4WFhYeDS05LQtGkgGuAaIBaBoBDxEJBgYIDhokPE5eYGBGChkrIwkaKjQsGAABCxMXDwABJjQqDB8dDQUDBxcnKycdCQcDAwEDBwkHAQgODggIAgMbJSUhEQ4iKh4CDw8REQ8hTWdhQw0IDhISEAUfJxsNECImJBwMCAoEBAIIFh4aDgYOFhgQBBkpLykXCw0NCQABCiQyMiIGFSMjGQcQHiQeEgchKSUXAxAgJCQUAgABBQMCDBQeICQcCSk1LwsWJCgkFAQUICgoHiAmJigkPFpoWjwcKjQsEhtXfYsBe00ZBhwgFAUlOTsvGyk3OS8VI09bTyUcYIgBjAFqJDFrf2cr1QcBBAwaLDoqHhYcGhdJWUkbSKoB1AHAAXoaDSEnIQ8JCQEOJEBicHx4WhQbKycLIDpCOCAECxUXEwABKjguCCsnFQkHBxktMzEfDwcFAwUBBwkHBA4WEhAKBAcbKy8lFRYuNigIEQ0PDxEjXXt7UxMIEhgWEAspNS8PEigyMiQQDAgIBgABDCAmIhQCFB4cFgIbNT03HRETEQ8FEDA+PCoIGy8vIwkSKi4mFAsrPTUjBRwsNi4cBAMDBQABDBogKCgiCzNFOxMYJiwgCgUOHiYkICImKi4qRmx4bEQcMjwyFhthkQGdAYkBUxEaNDwyDBctOS8bMUNHPR0zZXdnNx54pAGoAYIBLEGFAZkBgQE14wcECBgkPEY2KiIgIBlHV00lOpoBvAGuAXIYDycxKR0ZEwcIIkZmfogBiAFsLAEbGwUaODw2IgoHDRETAxokFgk1Lx8TCw0XJy8tHw8LBwcDAwUFAAEIFhoYEgwIBRspKyMRDCouJAQPDw0PEyFXd3FREQgSFBYQByszKRMSKDIwJhIMDAgGAgwaJCAQBBIaGhICGTE7NSMRFxUNAw4qPDooCBsrKyEJFCYqJBILKzk3JwUcMDgwGggAAQMDAg4aJCgsJAMlOS8LGCAgFAQRBBIcIBokKi4uMEJmcGI8GiowJgwhXYMBkQF/Tw8eOEQ8HgMbJSUZLUFHOSMxYXNhMRpsmAGcAXguM3WJAXEv+QcIGCY2RlBIOjYsIgcrOTUdGlRsakoYBx8rKyEfFwsKIkRmgAGMAYoBfFAsEgIFEiAqJhoOBgEHDQ0FCRMnPTcrHxcNERshIRsPCw0HBwUBBgoQGh4cFhAIAxMZHR0TBAwUDAMNERETER87S0s1DQYKEBAOAxMbGQkOHCIiHBIMCgoEAgQSEhIKAAEKDhAKAhMhKSkbFRMRCwUKHiYkGgYLFxsRAwwYGhYKBxslJRkDFCIoJBQIBgQGBg4WICYmKBIDCwkGFBIMBA0VCwABChIcICowMDA8SEpALBQSEgYJJ0ljaV0/ExIqODYqFAYHDxUjMTMtHSdBRzshEkRgZlAmETNDOxnDCBI0SlpaWFJORDgmFAYHDw8VGQ8HDBoAARMdJSMdFQcKJEx4kAGcAZYBggFuWD4eAAEDBAYMEBIWEAgDFy87Q0NBQT01IxMHCwkJDxERDwsLAwQQFh4gIh4gFgwBDRMXExEVERURDw8NDQ8VFRUPDQcFBwcFAwgOEhIQDAYQFBYSDAgIBAQBAwcHBwUBBwcHAwMBAQMHDRUXFxEDCBASEg4GAgADAgIDAwMDAAEDAAEBAAEBBAoICAoODhAQEAwOEBgeJCw0NDIiDgUXHR8bGREHBBYqMjo6MiggGhIOCAUZISsrLy8vLScbDQQUJCwuJhoICwsJBQsRGyEfGwsEDBIWGhwiJiQcCuUJKGiKAZABfFhUTkQ2KBwSCAINNWdzWyEWAAEVISUhGxMFDCRqugHiAeABuAF6XEAoEgMNDwsFCBYcHhIAARkzQ0lHP0tRSTkZAQwMCAERExETCwcMGCQmICYuLiYUBR8vLykVGyUnIxUHAgYAAQcPAhAWDgEhNTUjBSI+RjwkDB4oKB4OBAMFBQMNHSMfFQkbJSUbBRgoLiIEHTE1LRUMLDo4JAoBAwUDAQkZHxkPAg4UFA4EBQsJAwQMGBwcFggFBwkGHEJeaFo6AAEpP0k1GxcVCwYWOFheVjwcBgUHAAEEGzE9OzEXBwIDFScvKRkEKjAuIg4JFjhALgItRVFFJwUNDwsAARZIanBYJKMKNoQBrAGsAYgBUkxIPjYmIBoUCAABQ40BowGFATkWAAERHyEbFw8BECJ23gGIAvwBvgFmRCoWBgcTGRkNAhQeIBYEFzNBR0E5SVdTQR0EEhoUBA8TFRENBwoeKCYiIjQ0KhgFKT0/MRcdLS8rGQMOEg4AAQ8QIigcAS9PUT0NMFZkVjAOJDQwJgwDBwkNBQ8pMS8ZDyU3NScJJERKOhAhPUc9IRI+Uk40DAMJBwcAAQ8lLykVBBogIBgEDRETDwEQGiIgGAYNGxkJGEpsfHA+AzdVVT0XFQ0HAhZGZG5iPhIHFxMHAAEhOUNDKwcWHBQFK0FFNQ0iLioiDgkqWGJKEDVfbV83DRkjHw0QWIYBkAF2MOMJNIIBqAGmAXxGQD44LiIiIhwSBDeHAZ0BgwE5FggLExUPDQUEDiJy0AH2AeQBpAFOLBoGAQUTHxkRAw4aGBIEFSk5OzUvQU9POx0EFhoWBg8TERMNCQwaIiIYICwuKhQHJzs9LxMdKy8nGQABDhYUAAEJEiYqHgABM1FXPxEuWmZWLg4kMjAgCgMJDQkHESczLR8LKTc7KQkoSFBAFB89Rz0fEkBWUDYOBQcHBQENJy0rFwYaJCYWBAsVFxEDDBwgIBgEER8fDxRGanZmPAExS081DwkJAQgWQGBoWjYKDRcXBwABGzE9Nx8EHCggAiVBRzsXGCAiGAgLKlhiShAzX21hPQ8hJyUXCk6CAYwBbi6dCCZgeHhcNDAsLiYiIiAgGhIbUWVRHxwQBgEDAAEGBgoWHFCUAaoBmgFoMBgIAgUHDxUXEQcGDA4IAw8hJScnHy03Ny8XAAEOEg4AAQsPEQ0PBwQQFBYOEhwgGhIFHSkrIQ8VHSEfEQEMEAwAAQkMGhwUASU9Qy8NJEJIPiQIHCIkFgYDCQkJAw8dJSMTCx0rKx8FHDY8MA4XKzEtFw40Qj4sCgIDBQECCRkhHQ8GFhwaFAQJERELBQwSFhoQBg0VEQkQNE5WSiwEGysrGwACBAYMFjJGSj4mCAcRDQMEDR0jHQ8KHCAaBhspNS0VCBAOCAELGDY+LAQrRVNLLxMdJSETAAE0VGBMINEFDBokJiIcHiIeICAeHh4eHhYIBggUIh4aGhoYFhgYFhgcICQcFAgEAAEBAwUJCQkLBwUDAwMJDQ0REQ8NERETDw0HBQMDBQsHCQkHBwUDAwEBAAECBAQAAgUFBwMDBwkLCwcDAQMBBwcDAAMDDQ8PDQMIEBQSCAIGCAgGAgEDAwMBAwkLCQcDBwsLBwMICg4KAgUJDQkAAQgQFBIQDAYGBgQEAgEDAQABAgoICgQAAQEBAwEAAQIECAYKBgQGBAoOGBweHhgODAgIDhAQDhAQFBYaGBYMCAQEAgQMBgQEBAgKDg4KAAIFBwUJCQ0RERUTCwMFBw8VGRcZFREVFxcRCQYSFBIGlwMLFxcVBxIsOj42IgwEAg4eMDg2NCokMDo4LiImLCwkFh9bdWlBDQkFBQMBAAECAgIAAQQMDgoAAQsJBQUFCwECBAABAw0RExELAQYQDggFGSUpJxUbJyslEwYqOjosDgABBwkHBQMPDxMHAQUJCQkFCg4UDAABCQcFBQEJHyMlFQUGCggIAAEEFBoWDAIIDhIKAgcLDQkCEBocHBAFMUM9JQESFhgSCg4UFhYOBAcFCwUAAQIEBgQEAw0REQcMKjxCNBYIBAQGDhokJigcEAUPEQcKBAUHCwMCBgwODAgEBAQMEhwcGhYKGD5MRCQXU3N1WSUZHRsZDw46SkQiEzE/PzMXDCQqJhDPARMxPTUdDDhUWEgiAg8RAAEiPEhIPjImPEhIPCQuODYsGEGnAdEBuwF1IxMPBwUBBgwMDAoSHiIYBg0DAwEHCQEKDgoDDRcXFQ0AARYeIhQDJT9HQSkrSU9DJQ5IaG5QHgQHCwcAAQURGREHBgABBwsJBRAiIhYDExERCwkRN0tDLwUKEhQOBA4mMCwYBBQeHhYGCxUVCQYgMjgyHBNdgQF7TwsUIiIeDg4iKiQWAQsVEQ0DBAgKCAoHGSUfERJIZGpSHgIDBQEOHiwyMCQGGSstHQABBRUdGRUHCAwOEAYBAwABChwqLi4iEixmggF0QB+BAbkBvQGNAS0dHx8VDSxykgGCAUYRR2NlTyEaQE5AHG0XO0tBKQQ4VFpKJgABEw8AASZGTlBGMiQ+TEo+JC46OCwUR70B6QHXAY0BLx8VEQUBCA4UEg4YKCYcCA0JAQMFDQABCg4KAAEPFRkVDQYaJiIWAStHU0s1OVVhUS0QVn5+YigIAAEHAwIAAQ8TEQESBgEHCQMSIiIUBxkbFRMNFUFXUzULDhQWEAYSLDo0HAYWICQaCA0TEwkQLDxEOCITbZUBkQFjFRYiJB4MECYsKBgAAREXFw8DBgoKDgoDHSUlERhOcnZYHgIHBwEMICo2LiAEITU3IwMNHScnHw0BCg4KBgABAwIOJDI6OCoWMHCKAX5GI4sBxQHNAZkBNRcfGxMHOIYBogGWAVQPS2tvWyUcRFJGHHkVMz8/JwMqQEo+IggBAAEQLkpOTEQuJDZCQjQkKjAwJhI7nQHFAb0BgwE5KyEVDwABCBAUFhAYICIWBA8JCQULDQUGCAYDDRMTDwkEFh4eEgUnQU9JOTtVWU8rEFBydFoqEgYEBAoEAwUDDBgOBgIFAwwUFAgRIRsdEw8VO0tJMQkKFBIOBAwmMCoaAhAeHBYGBQsFBBgsPkA2HBFXfX1bFwwWHBQIDBwmIBIAAQ0TFQ0BBgoODg4AARMdFwsYSGBgShgCAQMCDBwmKCYYAAEbKy0fAw8hJysjGQcBBAgGAgYMGCg4PDguFCxYbmAwHXOfAakBfysXFxMLATRwjAF4Rgs9V2FNJwwyPDIW7QELHSMjHQkKGCAkHhweJC4+RkhCNiwcKCwsKB4gIiAYEBtLaW1bPTUtIREFBg4QEhASEg4GBxERDRENEQ0HBQUHCQkJBwEAAQoMCgQHHS83PTE1PT8xGQYuQkg+JhoYFBASEg4OFhoeHBIMBAUFAQsRGyEhGRcRDx8lIRkHAgQIBAEGDhISBgABBgoMDAQEChAYIC4uLiQWBSc7PS8VBwICAgMECg4OBgEFBwcFAQYIDg4OCgQAAQEIFCgwLiQQCgQGCg4WGhYSDAABCxETCwULGyUnJR8ZDQcBBg4UHiYyNjgwJBgUJiggChc3TU1BGw8JBQEIGjg+OBwFHS8zMyELAgwKBoMEAAIFBQkRFxUNAAEgTG56dlhKUExCLBYGBQEIFhYSDAwMEhIIByNBRUE1IQsFCQcBCBAYFAwFExcXExUTFxsbFQ0LCwkJBQICCAQBCR0vOz01Jx0PCQMCChAYHiQkHhoaFCA2PkA0KCAeFAgDCRMVHSMtMzMrGw8LCwkHBQUFBwUDAgoMDAYHERsXEQESICgsKiYmHBYKBAYEAAEJDQkDAQABAw8TGRMJAhAWFg4ECQsNBQgWHCQkHA4HERMLBgoODhASGCAkIBIEDA4SCgIRHycrKyMdGQ8BBgYKEh4uLB4YEgwCCxUZFw0GFBYMCRERCwMGCggGBAIECAoGAxknJyMZCYUGBAwQCAIRLTEpDR6AAcIB1gG6AXJQYmRUOgwdMS8ZEBAGBgQKKk5UNgI/U1FJLw8ZJSUbAxgsLCAIFxcXFxUTISspJRMNFRkVCwABDA4OBgkrRVNRPx0BDhQMAgMBBhQiIBwYFBgyWGxmUCgkIBYMAwcHDRcfOVNVSS8PDw8PCQcJDQsJBwogJiAUCy89QSsHGio0NjAkGhgSCgYUGBYIDQABEBYOAAEbMzUxGwYmODYmCBstLyEFGjI2OCgKIz0/KQEMDBAQDiQ+SEAmChooKBoEGSs1NSshHRMLAQINEw0GJBoIAwcAAQMfKS0lBy5OUjgAARshIRUAAQoCAQEAAQooODAaDys3OScP8QYKFBYWCA8tPzMVIpIB5AH4AdIBekxmaFo4CitDQyUKDgQBAgY2anJWFDdNVUczDyE1NScNGC40JAgTGRcXFRUhLzEpFw8bHxsRAQ4SEggJLU9bWT8VDiAkGAELCwEMIiAYGBYWOmp+dlQqJB4UCgEDBQcPHz1XY1U1FxERDw8JCw8RCwcOJjIuFgk3U087BxowODgqHhoSCggKGiQcDgkIGhoUAh87Rz8jCDJERDAGITlBLwsgNEI+LgYvTVE3BQ4SDhIQJEpYTiwKIi4uIAQZLTc1Jx0XEQcAAhUhHQkeEAcTDwMJITMzJQFAZmxMBB8pKRsDBgABAQUAAQ44SEgqBSk9OS0TvQYKFhoYDgcnMS0RIIoB0AHgAb4BaEJWXFA0CCc7OSEMCgQAAQIINGBsUhonPUM9KQ8hMTUrEw4kJh4EFxcZFRMRISsvJxMRGR0bFQEIEA4ICytFVU83DxIkKBYAAQ0JBQoiHhoWFhw6YnRsTCYeGhQIBQEBCQ0bM1FZTzMTExURDwsPDxEPBQwmLioYCzNJTTUHGioyMCQYEgwKBAgaHhwOBwgWHBAAAR05QzkjBi5APioIHzc5LwscMjw6KgYpQ0cxAAEOEhIQECZCTkQqChwoKBwAARclLSkhFREJAwABAhUfHwkWCgkREQMFGykpIQJAYmRGBBspIxsFBgABBQEAARA0SkQuAAEhLzEnD+EEBhAWFA4GDxMPARxchAGMAXZGMDw8NiYMCxsXCwwQCAYECiA8PjISFR8jHxcLFyMnIRUDCAoEBxUVFRMTExkfIRkRCxUVFQ8HAAEGBAEJHzE1NSUJChQWEAEBAQYQHB4aGBwcLkhOSjQcGBQMBAEHBQcPFSU3OTUpExUREw8PDRENCwcGFh4aEAUfKysfAxIcIBwYDgoKBAYGDhIOBAUCCgwKBRUlLScZAAEcKCYaBBEhIxsDFiIoKB4MDyEhFQgSEhQSDhoqMiwWBhAWFgwBDxkZGxMLBwcBBgIFDw0BEAoAAQcDBAQLDxMLBiY8PCYAARUXGQ8DBAACAgIQIi4uIAQNFxkVB7cCAQIGBgoSKDI0LhoECQsDDBQMCgoOGiIoKCYeFBAMCgoMDhAKAgABAgYGAAEFBQkNExUdJyMhGxUVExMPDQkHAwUFCw0REQ8NDREPDwsNCw8NDQcDAgICAgQGDhQaICQoIiIaEAwMEhggHhgQAwsRFRcVFREREREVExMTEQ0LBwUDBQUJCQkDAQEBAwIEBAgKCggODAwKBgQEBAIBCQ0LDw0JCQUDBwUJDQsHBQEEBggMCggGCAYMEBQUFBYUFhgWFhIODggKCAQDCw8LBwMBAwEDBwsFBQEKEhocGhIQEhQSFAoGAAIEBggGAgEHDRMRDwkFBQEBBgoWHh4cEgIHDQsF4QEFDxUPBxh2pgGqAXwiXacBtQGNATELJy8lBSZefIABbkQoFgoIBhIgHhYIAxYkIhYAAgQBBxs5T1dLMRcTEQ8LAxIgIhwIDx8nJRkTIysrIREBBAYECQkEDBAIAQsNBwgaNEBGQCwAASc1LQ0oTlpWNgYZKTEvIxMBAgABBxMTExEPDQMICggCFzE9NycJAwABBggEAgIEChQeKCYiFAYSFBAGCSExLScVBBgiGgABI0FNRS0FGC40MB4FFyMdCRYkMDQqGiAmKiQYDAgKDBYQFSs1Kw0GDhIOBBEhKR8PEDxWXkYcGiYuKB4GDx8dDwQCAwMFCSE1Qz8vJSMdEwUSPFBcUDgAASk7MxWjAQkdISMZHJwB4gHgAaIBIo0B+QGTAtkBVR1HU0EVMoABrgG4AZwBZDogEAgGFiYmHAQFGi4uIAABBAYGCR1FZ29fPRURDwkDBiI4PC4QFSUvMSMXLTs5LRMCEBIKCwsIEhQOBRMZEwMePFRYTi4JQ1lJFTJoggF0TAofNz89LxMBDAgBExETEQ8LBA4WDgQfS1tVORUHAAEGBgQCAAEGECAwOjwuGggWIBoMDTFBQzUdDCoyJAIzZ3ltSQ0oRlJIJgctOTMVEjJCQjQcIjI0KBwKDg4WICAVO0M1EwwaHBYGFy83MRkUUnh8YB4gMDQ0IgYfLS8bBAEDBQcHLVFfYU9BOzUjDRpQeoQBelQGM01FH20NHSspIRKUAdgB3AGaASCRAf8BlwLlAWEjUVtHGzSMAcAByAGwAXZKLBwOCBIeIBIAAQ0WJioaAgABBgABCx9FZW1hPRMPCQMGDio+QDQODycvLyMXLzs9LREEDBIGDREEEhIMAxcZEwEcQFZWTDANQVNFEzxwgAF4TAohN0FBMxkBBggFDxMRDw0LBBIUEgQjS2FdRR0NAwABAgQCCA4aJjpEQjYaChYaGggRL0NHORsKJiweB0FzhwF/Uw8sTlpMKgktPzcZGDRCQjYaJjAyLBYQDhggLiwJKz0xDwwaHhYGFS85MRkWUnZ6XB4aLDQwJgQbLS0bAgABAwcDBTFZb3FhU1FBLxMYXoYBlgGMAWgaIT09GT8LHScnIwABXpABlAFsGGO3Ac8BqwFNJUFHNxEueqgBtAGkAXpYRCoYCgYMCgINEwISFA4DAwMJEyM5UVdLLw8JAQQKFig0NiYODxslIx0VIy8vIw8BBgIFExUFAggGAQsPBwgeNkRIPCYCIS0hBDpeaFY4CBsrOTkxHxEHAwkREQ0NDwcCCg4KAhs/TVE7IRkPBwECCBAaJjA+Qj4uGgQMEAoAAREpNTctGQABEBAAARlDaXtvSwskQEg+JAMfKycPFCw2OCwYHiYoIhYOGiQuODYQCRcXCQgQEhAEESMnIQ8SPFRWQBYUICQkHAoNGxsPAgIDAQMFKVFncWlfWU0zFRpUfI4BjAFwOgwNFQsXBREXHx8XAAEQFhgOESk3MSEZGxkNAhxIZHR6cGhWRCgMBxEXGx8dFxMNCQcLFRkfISknJSEZDQEEDBIWHB4aEAQFDQ0REQ8VFRMPCQkPERUbHRcNCwMAAQQKEBYcJCQkIBwWGiAmLDIwKB4QAw0ZISUrJyMhGxMRCwkJBQkFAwUBBQsbKSkpIx8bFQkAARAgKjI4ODQsIA4BAQUJDRMZHRsXDw8XISs1Q0tLPykJDhoeHhQKAgACCBQYHBoYEhISEA4KFCg2PkJAOiwiDgIDAQABAQEFBwcDAggWGBoSBgYIDA4SDgoEAgIEBAgEAgIfP1dhZWFZRzMTEj5abnZyZFQ+KA4oBggGAAEPIzU3KxkIMEZELAITBQIICgQTEwImYnZ4ZEQUEyErKyUlO0E9KQsPFR0hIwkQHh4MDRcVDwESGhgWDgIHBw0NDQ8REREPBwEEAAEHFRsVEwkHAAIEBhAYDAQAAQQMMlxwblQiDy81KwkBAQURHS81My8fBxQiJBgBFx8fGw0JERcdIyENAwQEBA4YJiw0KiAOCgACBQcJDw8dJSUdEw8fKzM9QT83LRkDChAWFBQWGh4gGBQkLCwkEBUxNysNGjpKUE5ITEo+KgYNFRkTCwABBAYGCA4WHhwYCAcVEw0EDgYCAQIKKjg6LggpVXF5a1U9KxUDECY8UFxufHpsTB5kDB4gFAABJ0VRSSkCUH6EAWASDwABEBAME1FrURNIfoQBdFAWFSktLyknS11VOw8NFRsfGwY6UEYkDScxJxMOGBwUDgQFCQ8PDwsTFRUPBwoSFgwLFRELBwEBCQkDBBAAARUXDwRAgAGaAZIBZBY/ZWtLFQgODAABFS07PzcjAS5GSDAEHzE1LRMHDxUbHxkBFhwYBAYQFiIqIgoBCQUDAAEDBQcTITExKRMPFyMvNz85MSMVBQwWGBoUGCguLCAYMkJALhI1Y3FdISBCWFxSRk5SSC4KEyMjHwsAAQYKCgoOJDAwIgwRIykbAQgBBwcFEkJgZEoUNWeFAYUBbUElCQQMDBYmNkpgeoYBeFgikgEOJCoeBCFHVU0xAlaMAZQBbhwPBhISCBtrhwF3MzJsem5KFBUlLSshJUtfWT8NCRUXFxkUSF5ULA0pNzEbCBgUFA4EBQsLDwsLERUTDQcOHB4UAxELBwMBAwkRCwABCgsbIRkDPoYBngGQAV4ISXV5VxUKFBIICyc5PTUlBDZSUDgEHzU5LxcJDREXGRUKHCYcCAIEDhYgGAENEQsAAgIDBwsjLzUrFQkTHSMvNzElHREAAQoUGhgSGiwyMCAYNERCNAw9dYUBaykeRFZaSjxKTEQuCBEhKR8LBAgKDAoQJjI0KAwTIy0hBQgDCQsFEkpqblIWL2N5fV0vCwgSFAoMFCI0TGx2cFIgsAEOHiIaCBk3RT0pA0ZweFoWCwYMDgYZW3lrNR5KWFA0DhMdHx8ZGztHSS8NCQsREQ8QQE5GJAkjKysXBg4QDggCAwcLCQkHCw8PDQEKGBoSAgkJAwEAAQELDQsDBgsbIRcDMGZ6bkQAAT1fY0kRChASCAkdKy8rGQIsQkQsBBsrMScTBQsNERURChogFgQDAgYKFgwDDw8JAQABAgABAwkbJykjEQcNERkhJyMdFQkBChIUFAwUJComHBIsODgoDDNhb1sjGDhGRjouNjw0JAgNGRsXCQYKCAwKDiAsKiAODR0fGQMGAQcHARJAWFxCFiFFWVU/GQABEBgSCAYIEiAySlZSPBa+AQYMDgwAAQkZGx0TARosLiIGBQICAgENKTMvHQYaHhoSAgcNEQ0JCxchGxUHAQcHBwcGGiAcDgMPExELAAEGBAQEAAEBAwMFAwMHBQcDAQQKCgoAAQUBAQACAQMFBQMAAQULDQsDEigwKhgAARsrKR8JAgQGAgUNExcRDwESHBgQAAENFRcTCwcFCQsNCwABCAgGAwMBAAEEBgYDBQcFAAECAAMDDRETEQcDBQcLERMRDwkHAAEGCAwKCAoOFBIMChYeHBQIEyktJw0QHCQiHBYcGhgSCgMFBwUAAQgGCggKCBQWFhIIAAEHBwUCCAQEBAYOHiwoJA4FExsbDwEGDhAKBgQEBgoSHCIcGAh+DiYyMCYQBxkbFw0LDw0PCxktLy0XCCw6OB4NGyEdFQkHBQUBAgwYHhoSAgcHBwcFDx8jIQ8DAQMFAwMCCAgEAgocJCYaBAsVFQ8DCAoKDAgKDAoGBAMZJSkjEwoeJiAKDRkdGQ0DCw8RCwUDAgABAgQGCAwIBAMHCwkNCQMCBgQBCRMVEQkEEhoaEAABEyMnIRUPEREPBQABChASDggIBgYEAAICBAQEAg4SEg4AAR0tNSkPChYYGBIGBAAEDBgYGA4CAAEDAQIGCAgIBgQKCgoIBgEHBwcBBgUHCQEECAgGCAoQHiQkHhQMBggEDixIUkwwASc7OR8AAQ4QCgIRKTc1Kw8SJmJ+fmQuCy07NRkdHyMhHTlndWE3Fm6UAYYBShlFST8vEwUJBQABECRATkgsBgsPExEJJUtVRykDBQcJCwMGEhISBhxGXlpCDhUvMSMAARQYIh4cGhwaFgoLP11pXTsSSl5KGiM9RTsdBRchIx0JAgYMDhAYJCQcEAMPFRkZFQMOEhAGER8nHQsaPE5MNgYvUWFXOy0xMSUVBhwqLiQWGBoWEAICDBASEBYsOjgmBEl9iQFrKxYyPDokBAMHCwsJFjQ+NhoBExcVCQEECAoIBgoQDgwAARMbIx0LARMhHRUECgYICAoeOkxQPiISCAABBA5gngG6AaQBZA1nlwGRAVsFFiAUBy9rhwGHAWcnMzSAAaYBqgGKAUgAAS1FOyUlLTMvJ02DAZMBf0UahgG0AZwBUClZX1c7FwMFAAEMGjZaZFo8Bg0TFxMRK1trWzEDBwsNCwcIFhoUCiJadnZSGhcxNyUGGCgsMComKCQaEBFRe4sBgQFXCE5mVBgnTVVFJQMbKy0hBwQMEhgcKDIyKhQBEx8hIx8FDBgUBBElJyEBJlZqZEYMOWl/eVVDS0U3HwYoOjw2IiQkIhYEAg4YHhwiPExKNARXnQGrAY0BNRo+UEYqCAcRExcPGDpMQiIHGSEfFwMCBgYIAgoQEAYHGS0vJRUBGyknGQQKDAoMDCZMYmRULhoKBgQUcsQB4gHMAXoHebEBsQFvDRIYChVLjQG1AbEBhQE1TzaCAawBsgGUAVwYFy01KSs3NzkxT4EBjwF5RRZwkAF+MjdjZVc9FwEGDBoqQF5mWDoIDRUTFQ8rU2FVLQEHCw0NCQYSFhQGJFhwcFQcBx8lFwoiLjQ0Mi4uKBwOD09/kwGPAW0TLEY8EilFTT8fAxclJx8JCBIaICYsODgsGAMVHycnJw8GDA4EERsfDwY0XGxiRBA3a4EBgQFjUVtTQSMGKDxCPCwoKiQYBgIQHCAiJkJQSDAET4sBnQGBATMaPEZEKgYLFRkbFxAyPDocBRcjHxUJAQABBAICBggGAAELHy8xKxMBFyElEQgQDhAQEixOZGhaOiQYDAwWbrgB0gHAAXgGW48BkwFlFQEBES9dmQG3AbEBhwE1QyhsjgGYAYwBaDwYBRklMTc7OTdHX2VVLwQsNCAJR19ZTTERCBQiLjZEUFJEKAYJDRMPDR01OTMdAAEBCQsLCwABBgwKBBg8Tk5CJBACAAEEFCYuNDg2MCwkGgwLP2V/hwF3RRcBBAkdKy0jDwABCRUXEQMIFCAoKjIyMCYWAxMdJysnGxEFAwMHCQEIHDZMVEwyCidRaW9lW1tVQSMAASA0PDYsLCgiFAACEBogJCg4OjYkBC1NXUkfEiIuKBoGCRMbHRsDDh4aEAEPFRcTCwcDAwEBAQMFDRMhJSkfDQABBw8LARAUFhYYGChEVFxQQjQoIBgWTH6QAYYBYiQXOUU3HSEtP1FriQGVAYsBZSdSDCA6TGBqWD4eASMrKy0vMzMrIx0RESc5R09XVVE/Kw0OJDQ8QkI6MiYUAgEBAQcJCQUFAQABBAEDBwkJBQMBAQIGChIaICw2QDw0JCgwOjo2Kg4CBwUFESk/XXWDAX9rUSkPCwMBBAgOEhAKAAEMGiIqLioqIhgMAxcpLy0tLSklHQ0GFCQuNDY2NCgcChU1SVlbUUExJRMFAgoSHioqLCIUAgcCCBAaJCQiGhAGBAYMBgYCCAgGCgINGyEhHxUNCQABBgETGRsVCQcDAQEBBw8TFR0ZGxELAwYKChASGCAkJiYiICoyOEBESEI4KhgcICoyOkJGOiYGI1NxfYEBc11HMR8J9gMlVVs3EFg2EAkdKRMFAw8nKRMFAAEDDyk7R1FTaW1fQxcMGiYyPEI2KBoQBA4WFhACAQwSEhAEAgEBBwUDBAYIBAsdHxMINGaCAYYBbDIyREpKQAg9YVs5CCw4JA1jqwHNAcMBlQFHEwcBBAYQNEpINAoMHCYqKigcFg4ICzVPVU8xNT87KxcMMkZORjY4NCweCh07VV9bMQwuMhwVPU0/Gxo+RkIwDBkpKxsBHhwcFAwEIjxEOhgGBAYIBgEjNz01IQUMGBYOF0dbVTMJEBoaEgIDBQkRFRcHAQYICAQIDBAcMEBGPjIaDgwULEpYXlY+HAkfHw0cTnJ4YCgxgwG1AcMBqwF3IRw8PhqcBkObAbMBiQEjPBITKTMrCxASAh8hBwgMBA0hMT9FTWt7cU8dBgwYJjY+LiAWDAYYJCYcBgYWHiAUBgQCAQABBwQIDgwIEzE7Kwc4gAGmAaYBggE4OEpSTEAJY40BiQFRDFJuWh5LsQHlAekBtwFZEQsDAAECFEhmZkgQDBoiJigeFgwGAgtDYWtVMzlDQzkZFD5WXEoyNjQuHAobP1VdVxNAaGY4HWOBAXNDDkJUUjoOI0FJOxUWGBQQCgQwVGBSJgIGCAgIAy1DSz0bBCIsKBYjZYEBd0kDHC4wHgIAAgEHFQ8AAQwSDggEBAYSHjpOVlA4GAABCQQgRmJoXkgeHT9JLwZQfIoBdDIzmQHPAdsBvQFvAlZ8cDCgB0enAccBowFBIAkpPT8tCQ4UBBsfBQwOBAsbJzE3P19xa08dAAEEDBgoNCQYDgYIGCoqIAoKGiIiFggEBAQBAAEGDA4OCBMzPS8LOHyiAaQBejo0REpINgtdhwGFAU8OVnBmLjWbAc8B2QGxAVUZDQcFAwxIZGRGEAoUHB4eFgwGBAINO1tjUykvP0ExGRQ8UlRGKi4sJBwKFzNHTUUDUHZwOBtriQGBAVEEOExKNAwlR1FFHw4QDAwEBjJWYlAiBggICggDJz9DNxcSJjAuGCVlf3dJAyAyMCIEAQYAAQUNCwYSFBIIAgYIECA8UlZSOBoAAQcAARpCWmBaQiAdRU07Bz5sfGIsMY0BwQHJAa0BYxRmhAF0MpAHNX+ZAYUBRwEbNz89MxUDAAEFHSMJAQYAAQsTGx8lLUNPTT0XAwMCDBgeFg4GBgQWHCIaDAgWHhgUCAgGAgQCBhAOCggJIycdASxkenheLiw4ODQsAAE1V1MzCjpORCAhY48BmQF/SRcVEQ0LAAEsQkIwCAYMEhQQDggEAAIFKT1BNRsfKSklEQoqOjwuHB4gHBQICx8pLysEPlZMKBtPaWVBCR4wLCAEHzlBOR0CCAQCBAIkPkQ8HAYICggMAhkpKR8LECIoIhYXQ11VMwEYJCIWAgEAAQIBCwUIDhIOCAgGDhQeNkRMQjgeDgoMHjpGSkI2Gg8tOSsLIEBIPBItZ4sBkQF5RwpEXlAi+AUNJzU3MSczNzk5MykhHR8jHxkTDwkLCw8RExETFxURDQkHBQMCBAQAAQICAgQICAYGCA4OEAwIBggIBgYIAgQGBAQGCAwUHiYuLCogIiAgIBwWCgQBAQIGCgYDDx8rLzEjHR0dGRcPBQQIBgABAgABBAQEBAIAAQQCAAEFCQkHAwMFCQUHAggQEA4KCAQGBAgEBAEBAQABDA4IAREdJSUdDwcHBQcLExUXEw8FBQEBAQIGDBIOCggKCAwICAQAAQMCBgoQEg4KAgsPEQsDBAIEAgUDAwMDAwEECAoKCAoQEBoeJCwsMCosKCgqKigmIh4aFg4BBQsLAwACBw8hKzExLyEVBwABAgScBQgOCA8xXXF1b1c3EwABEBIMAgMHCQ0NGyMhGQkgPkIyChchIyMdEw0HBQEDGSktIQsSKjIuHgoMDBAQDhs3PzMRHkZWUDIGER0XBRYQCAgMGDBESEAkAAEDCwkJCQcLDxUZJS0zMSsbCwEEAgIHBw0JBwUHAwUAAQYQFhoWFBoiJBoIAAEKDA4KBzNHRy0DHiw0MBoKBQ8REw0JBQkTITM9OzEdAAESIh4SAAEIDAwKAAEPGx8XCQIEBggKCgwMEg4SCAQBBAQOEhQMAREVERENBQsPDwcEGCw2MCAGCxEJAAEcHh4eICgwPDw4LBIPIR8RDh4oIhYAAQsFBwkTEwgYGg4TS291WyWGBRIgFgtDhwGnAa8BnwF5NwIwREo+KhoQBAUTKTMxIwtCdnxeFiE1PzszJRkTCQkDLU1TQRseQlJKLAwQFBYaEi9ldV0jMHCGAXhCDTtHPyMOCgUHBh5EYnBeNgQHBwkFAAEFAwsPFyc9RUU3IwkGDg4AAQsRExcRDw8NBwMIGiYsKiIyQDwwEgYOFhIMD12BAX1VDSxIWE4yDgMPFRUPAQMJFy9RYWFLKQY0QkQsDhQcHBICHzE7LxMAAQECAgYODhIUFhgMAQMFBBIcGhIJHyUhHRMJDxMRBwoyUlxUNAIbKScNFB4YGhwkMkBEOiYAATVNTS0GKjo4JgQHAgYAAQsLIkBEKglrrQG7AZUBPZ4FDBgMG1mjAcUBzQG3AYcBOxRIaG5gSDomEAIVLTc1JwdKgAGMAWQaKT1JST8vJRkRBwczV19NHx5KWlAyDhAYHh4WMW1/aSk2eJABeDoZR1tRKwwGBQMGJFBwdmo6AgMFBQECAgEFCxUnP0tJQScNBg4KAAEJFx0bGRcVEw0FCCAwNjYsPEpMNhQGEBQWDBVjjwGNAWENLlZiWj4UAg0TFQ0HBQ8hO19xb1svDjpUUDYUICIgFgIfO0E3GQUDAQIGDBIWGBwcEAIFAQISGhgIDS8tLyEZCQ0VDQABGD5kbl44Ah0xKRUYHBgWGiIyOj42HAtBW1k5Ai48PCoICQQIBgkJLExMNgdttwHLAaMBQdgFAgEdP3evAc0BzQG3AYMBNxhQcn50ZE46JgYTJy8rIQk6ZmhOEiU9R0tBNysfFQsHKUdPQxsYPEpEKgwUGh4gHh9PX1MdLFxqViQhRVFJJQwOBggWLk5obFgyBAMDAAIGCAIBBxEjOUVJPS0VBQIEAg0XHSEdGxsVDwUIIC44ODI+SkIyFAIOEAwKEU9xc08LKkhYVjggCAMLEREPER0rQ19va1UtCDRISDIYHiQgFgIbMTkxFwcFAwIGDBIaGiIgFAoGAAEIChAIBR0zNzEpGQkJBQABDCBGXmZWNAQZHx0JGB4YGBwiKjIwJBIPO09JLwgmMDAgAgcAAQQAAQkHIDo8KANXkQGfAYMBN7oGETdXe5cBswG7AbMBmwFxMRJIaHp6cmZQNBAPGRkZEwkQIiQUAx0xOz09NS0lGw0DEx8nHw8IHCIiFgwSGiAiIggNFxMFEhwcEAcjLzEjDxIeICQuOERIRjYcAgABAgABBggKCAIFCx0rMzk3MSMbDwcDCxMdHSEdGxcRBQgYKC40MDo2MCQKAAIEBgABBx8vLSEFGi44OjAiFgwBCREdJzE9RVNVTz8lAxYiKCIWGhwWDgINGRsbEQUFAQABCAwUGiAiIh4YEg4KBAUTHy03OTEpGQcEDBQiMDxGRDgiBgIBAg4cHiAeICAiHhgMAg0dIyELChgYEAoDDQkHCQ0NBA4SCgUlO0M1FdIGIVmBAZsBqwGnAZMBfWVJJww+XnJ6gAGCAXZSJBkzQT0rEREVFxURFx0nLTMvIRUNBQABAwcHBwUFBQMBBAoOEBQeIjQ8OjAYEztNTTshCQYSGBwwQk5OQiwaDAQAAQEJCwkFCBYgIhQAARcjLS81MTk3LxsHBQkNFRkXCQABBgQEChAaIDA0NjAeDA8fJyMVAhQgIBgGAgoQFiIgGg4EBRUdKzU/RzsvJR0TDwkCBg4QCAEDAwACAgABAQUPGRkTAxAkLDQwKBwUDgwKBAkPHy09RUc9JwsUKDg8OC4iFg4IDBgiKCggIiImICQiJiAYCAcIEBoYFg0lMS0ZAwgKCAIRHSMjGQcKGiAaCvwFHVV9lQGdAZMBaUkvIx8QPFxqcIQBmgGSAW4uI2uHAYMBXRsXIyUfFQsRFx0nKQ0EDgoAAQEHCQkFCRUTDQMGAgQIECJIYGhSKCt1mQGNAWEZFDQ+OCA+XmZcQhgLHSETBxUhHxMEJkBGOA4VIy81LTNHT0UvCwYGAgUVCRYkJBgCCQcBEiY8Qj4uEB9JW1MvBDROTjgKCw8LAAEUGhAGAwsPFR0nMz0pEwUBCRMRDwUGCg8fIxkFChIQDgABHTU/MxUUOExQQCgSAgMDAgwQCAUZO1lnWz8LID5MTkAgBAkPBw4kODo0IiIkJCQeLEBEOhoEIDY6MhgtYXFjLQ4qNjAYDS8/Py0JJEJOPhygBRtLa3+HAXdFIQ8LEw4yTFpedJIBkAFuLit5owGdAW8hGyUpIxUJBwkTHR8BEhoUBgcHBwcFDRcZFQcGAwUBCiBMcHReKjOJAa0BpwFvFShOVEYiQF5oWjoKHTExHQkdKSUZATBKUkQWDyEpLSspSVNNNQkKDg4CDQEkNDQeAQ8VDwIgOEQ+LhAjV29nQQZCXmBCCBMdGw8IEgYAAQcNDxETGyc3GwEOCgMTGRUNAQQdLzElBxAWGhIGH0NNQR8WQFhaSiYIBw0NAAEUGhgODTNbbWVHCyRGVlI+GAsfHQ8MLjxCOCAeJCIiHC5IUkYmCipCSDwYM3OHAXExFjpIRCYHMUdFMwcyVFxOILoEFTVRX2NVKQkEAAENBCQ0QDxUcG5WJCdvjwGNAWMfGSUlIQ8HAwMLExcEGBoUAgMFBwUDCRUXEwUEAwcDBhZGYmRSJClzmQGRAWELLEpUPh4yUFRIKgEfLysdBxkjIxcAASZGSjoWCRkdIR8fOUVBLQkKEA4ICQQkMjAcAxEZEQUWLDQyJg4hT2VdOQI4VFI2BhUhIRUDBgABBwsNEQ0PFR8rFQYODgMVFxcRBQABHzExIwUKGBoUBBs7RzsbFD5QUEAgBgcRCQABEh4gEgMjS1tXOwkmQEpINBAPHyEPDig2PDAcGh4eHBooREpCJgosQEI2GC1dcV8lGjpGQCgAASU5OScBLE5WRh7KAwsdKTE1KxEBBAINAQoSFhYkNjQkDBlDV1VBFRMZGRUNBQMFBQsNBAwQDAIBAwMBAQMJDQcAAQIAAQEAAQQSKDo6MhgTPVFPMQMgMDIoEh4qLiQUAAETGRsPAw0TFQsCGCYsJBAHBw8PCxEdJSMZAwQKCgQDAhQeGg4BDQ0NAwgUGhwQCBMtOzslARwqKBoDFRkbFwsDCQ0NEQ8PDRMZHw8DBAQJERcTDwcFFR8hFwMIDBAMBAshJyENEig0NCoWBgMDBQQQGBYSBhElLzEdARoqMCwiCgcRDQcKGiYkIBYSFhYWEhwsMiwcECAqLCYSDyk1KwsYKDIsHgYLFxkPBCIwNCoQvAMNISklFwQYJCIUAxklJykfHyMjGxcLBQABAQEHAAEEAgIFCw0NDQsJDxEPCwEKEBQQDgQBAwEAAQoOFBIQBAQAAQIAAQYKCAwMDgwKCAgEAgIDAwUDBQEAAwIAAQIEBAYGBAIEAggMEhAKBgIDAwABAQUJDQkDAwcJBwUAAQQEBgQBAQEDAAEDAQMFBwkLDw0NDw8HBQMHCxETERMRERMVFRUTExMREw8TFRsVEQsLCwsFAQYICAoKChgaGBIIAAEDAwABCAwOEBAOBgQEBAYOFBgcFAwGBAICBggICgYICA4OEA4KBAUDAQYSGBoaFAwCAQECDhoeJCAYEA4KDBASEhQSFBIMCAQCAAHKBTWBAZ8BiQE/LIQBrgGqAXIQQ3mLAYUBZWWBAX1pOQEmODwuEiY6PCoGGSktKR8ZO0tFLQUqSFZOLAoLFxULEDI+PCgCGyUnGwUSJjQ0Lh4MAgcHAREdIx8bDwcFAgIAAQoODgwIAAEDAwMAARo2QEQuEgYEAQMBGzE1Kw8JGycjEwIYJCAUAAEJBwUDAQABBAQAAQkTHSclIRMKIiYcARcZGxcVExcfHRsXGR8dGQ8bN0NBLRUZGxMPARQkLCoaNmJyZjoJNU1JMwkYKjAqFAABDxMLCCxYbmxSHgshJSENBAwOEgoKEhoaFggjQUk1DxgsODAkAi1HRysEOFxkWCwIBwkFBhgmLjIyJg4JERMJ9AZJsQHVAbcBVUrIAf4B9AGoARxfpwHLAcEBmQGZAbsBuwGVAVMGPlpcSCJAXFo8DCE3QT0rKVdrZ0MFPm5+ckoQER0fDRJCVFIwAyk9PSsNGjhETEAqEgABBwkJHSs1NSsbEQcFAAEEDBIUEgoAAQUHBQQgSmBaQhwOBAEAAQMlQUc5EwspMS8bAiIuMBwDCwkFBQMEBgYAAQkbKzEzKRcSMjwuBBkfHRsXFR0fJR8VHyElGQ0dSV1bQSEjJSETAR46RD4uTI4BpgGOAUwTV3N3URUaOD42HAMZGw8QRIABoAGcAXImEy87MxsCChAQDgoYICAYBDlhaVUfHjhCQCoDR2tpSQJIdoYBcDYBDxENAhoyPkhCOBIHGRkLmgdDpQHDAacBQVrSAYgC+gGqAR5jswHbAdsBtQG1AdcB0wGpAVkEQmBoUChGYFxACic9RUMzK111a0kHRniKAX5SHgUZGQsWPFJIKgcxPz0vDRw4TFJKMBoIBQkLITU/OzcnGw8JAwIMFBQSCgIDAwECJk5eXkggEgoGAAEBIT1DMxEJJTErHQYeLCoaBwsLCwUBAgQEAwsdLzc1LR0OMDgqAh0dIR0VExslISEXGyMfGQUbSV9hRyUrKScXAyRATkgyVJoBqgGQAUgXXX97XxsYMjw0HgMREwMeUJABrAGiAXgoDy89NRsFCAwOCgoWHh4UAjdhbVUfGDZAPCYFR2lrRQNKdIABbDABDRULBB42SExOQCIEDxEJygYpX3FVD16uAc4BvAGAARhTnwHHAdEBvQG7Ac8BwwGdAVUCMk5SQig8SEQoAiU1PzsvL01dVzkFPGR2blAqDgIFAAEOKjQqFBEnMzEjCRY2RkpEOCISBgUNITU9QTkxIxsRBwABDAwSEAoGBAACCiA8UEw6JBoSCggCFSUnIQUDFR8fEQABEhwWCgcRDwkJAQICAwcTHyszMy0fAAEWHhIHHR8bGRcRHR0hGxUXGxcNAAEPNU1NPyUpLSEZASA2QkQySniEAWg0F0tlZ0shCCAqJhQEAgYWMFiEAZQBiAFgKAUfKScZBQEGBAgGEhQUDgABJUVJPxcQJC4oHAEtR0cxBDhSXEoiAAEJBwUKIjZGTkxILhoMAAL+BAYSIDJCVGBcSjAKNWuRAakBrQGrAacBkwFvPwUQHCQmHhwcEAYPHSktLS0lJyUdEQEaND5EQjYyJBwQCAIDCQ8TGRcRCQABEiQwODo2LCASBAsfLzk9OzEpIRULAgABBAYGDAoKDg4MFCAmKigkHhgUCgIEBAQIBgYEAgEBBQMHCQsPExERCQMAAQMHDxcfIyUlJSEbGRcZGxsbFxcRExUXFxUPEQkFAAEIBBEdJSUjIRsbDQEOICYoKi4wKhwGEyElKSUZDwkBBAoUHjA+SFhgXlI+HgwDCxENCwcDAAIGBAYCAgABBQsPCQUCBggICAQBBQMCDBIYFBIMBgQGDBQiMjxGSEhEPDIgDpoCLHKYAZwBhAFQOiocEgYbQ2eBAZcBmQGFAW1PLQsBCAwSFAgFDxUXKUFJSTkXGDY8LAoFCQIULj42MiQWAAEdLzMrGQYaIiAUCgwYHiouKh4SBA0pQU1LPR0KICYaBx0pKRkCGiYmJhgMCgoOGBwIBQ8LAAEcNDoyGhYkLCYSByczNSsXGSMjGQsGDAwGDRkVDQ8TIUFbYVU1HxcXExMRBwUDBwsAAQwUGBYQCAEJExUEFBwUBAEDBAwaATVLSzUJKEBAIg87S0szDRYuOkpUWGhoWkIiBR8rJxcEFBwaFAABDxsZEwMYKC4iDhEjKSMRChooKCIQAxEVEQMEDAwUGCIwPkRIRlhiXEYaIEKeAdIB0gGaAUosHBAICAsnQ2F9gQFpUzkfDwcDAgYKBxcfIx8zVWdfRw1GdHhYEB0pJQscODYsJBYDK0dFORccPEo+HAIAAQIMGiYgFgwAAQ0zT1dVQQc4VlY2BzVLTTMBJDg+NB4IAQMCDhYLJScfAyxOWEgkGjpCOCANM01NOxkhLS8lCwoaHhICGQcBAAEJH1mFAZEBf0cfGxcREw8BCgwIBwweKioiFhYQBgUHJj5EMgwPFxcLCiN3oQGTAWEDXIgBiAFWB1d7fWEdGCo+SFBecnZsTiQVN0k/HRAuOjggAR8vMSEFJkhMPhYdPUs/IwgqOD4wFA8jJyEJCgwSFBoiMj5CQkJedHJYIjFAqAHWAdIBmAE6JA4KBAoBEyc9X19JNycZDw0LCQMAAREjKyshNV1pZ0kJVIgBjAFgEiU3NRsSLi4mIBIDL0NJNxEkSlJGIAABBwkAAQwcFA4EBQ8vS1dRPQRIamQ6BTlTUTkBJjpCOCIGBQsDDA4TKTMlAzBQWk4gGjhCOhwLM0lNPRUfLS8jEQweIBwAARMFBAQHH2GPAaEBiwFRIRsXFRMNBBAUDgABFCo0NCQeIBwSBgY0UlY8DBUjIxcBNZUBvQGzAXUBaKABngFmB12JAZEBayUYKDhCSlRyeGxQIhM/TT8bGDxEPiYBIzM3JwcsSlREFh9HUUknBiw+QDQSDyUvIQUOEBQYHCAwODw8OFpsbFIgGDaEAaoBpAFyLBYMBAYICAEPITU7KR0VEQ8REw8PCx0pLykjM0tbVT0JRGxuTgwdMS0bCiIeIBYOAAEhMzUnCSI+RjgaAQkLBwQMCgIDBw8nO0U/LwQ8WFIuBSs/QysAASIyODAeCAMFAwgMDyElHQMkQEg2GhIqLigWCSc1OSsRFSMjHQsIFhoQARMHAQULH1N9iwF9TSMdFxUVDwQOFBAGGCo0MigiIiIcEBA2Tkg2ChMfJx0NNYcBqQGhAWcLVH58TgdPd3ddHRIkLjg8SF5oWkYkDSk1Lw0cNkA2IAABHS0rIQciPEIwEhs5RT0jBiA0NCgSCxkdEwESFhgaHCQqLjAuLEJWUD4ahgIYOEhIMhgMCAYIDgwKBAMLDQcHBwkRExUZGRkjKSkpISEvMy8lDxImKBoEDRMRBwQQEgwMDAYHDw8JAAEUHiAYDAIDBwcHAQcJCQ0NFx0jIRkBFh4eFAERFRULBhYcHh4SDgYGBggMAAEHCQkAARAYGhQGBgwODAQDDRMVDwUJDw0NAwQEBAABCxcTExEZIzlLVU81JR8bFxMRBQYKCgwWIigoJCIkJCAYGiYqKhoGCRcZGxUpS11XQxkQKCQUES87OysJEBgeJCwwPj4+MCAOAQcDChoiJB4SAAEJERURBQgWFhIAAQ8bIRsPAAEOFBoWDAQBAAEEEhgaHBwgHiIiIBwaIiYkGgqAAxAoNDAiCAcRDwcGCAIECBIUDAYBCQ0RExchLSsrJyEfEQEIBgMXLTUxHQkEBggEBAEDAwABAgoQFhQUDAELDQkAAQ4gJBwMEScxMSMRBhYcFgYTHyEfDwEICAwMDg4QEAwSDhIODAgIFiAeGAQNExsRCQsfIR8RAhYeIBgEAgQGBAQAAgEHCRUjKzEtJxkTERUhMTs7NSUNCBQeHBIBDQ0BFCgyODg2JBAAAQkJAQIEAwsZIRsZHR8lKyslIRMEGiIgFgwUGBgWChMdGwUYNEZKQCYGERkZDQokMjIiBAUNDQsJBwUFAwMFERUTCwgcLjg0LCYiJCQeGgwCAQABBAMHCwcFngImXHBqQggjNTUfAgEJBwYeJBYMAAEJDQkNGSc9OzMrIxsDJDIuEhtRa2NFDQYMCgYAAQ0TEw8BDh4kJhwSDR0nHQkkTFpOJhdHX11FFSJGVEYeGT1NPycAAQgQFBQOEBAQEg4QEhAKAgQkNjwsCBszMysPGTlLQSUGMEpMNgwGDAwMBgQKCgYBDS9BQz0lCQ4SChVBY21fPQkkQEQ6FiFDTTcBNE5cYFY6BBspHwkQFhQBHSUZExEZJztBOysNMFxsXCgQFBYSBhlpiQF7Rw5agAGOAXY6E0VZTykUWHx8WBYHDxMVCQkFAwMDCyk3NyMCNlhkZEgwMiokIhQLGyUfExslIx8JygEsbowBfFIIKUM/KQEDCQMMJjQiEgQFDQsRHTFJRzktJR0GMEI+IB9de3lRDwYKCgYHER8bEwUQIiwsKBINKS0jCypecF4wFVFvb1EXMFxqWCoXR1dRKwEMEhYWEBAUEBAQEA4OBAEBJDxAMAofOUExDRtJV08vCj5aXkAOBg4QDgYIDBAOCA0tQ0tBJwIaJBQRSXF/c0kHLlBaRhovX2lNCThecnhsSgofLykFEhwUAiEtGxEPFys/R0ErCUZ8jAF2NAoQEAgHM5UBvQGtAWkKap4BqAGIAUAfW3VlOxpwngGcAW4gBQ8VEQ8FBwMDAw8xR0UtAAFAan52VDw0MioeDhMvNTMhKTUzJQ1+KmZ6dkgMITU3IwQCAAEIGDY8LhwOAQ0TGSk9UU1BNycdBCo+NhwXU21pSw8ECAQBCRkbHxUDDiQqLiQYCx8lIQkoWGRUKhFFYWNNFSxWYlQmETdLRSkAAQoUFhQSEBASEA4MDAQAAQsHGjAyJggbMTMnDRM9T0crCDpSUjoKBgwMDgYEDhAQBgclN0E3JQEWHA4TR217b0UHMEpSQhYpVWFFCTxieIABeFYcCx8bBQ4SDgkrMyMXFRslOUE5JQABRHqEAWwwBggCBxU/kQG7AakBZwhkkgGaAXw4HVdrYTUYbJIBkAFqIAEJDw8LBwUDBQMNLz8/KQABPmh4clg+PDAqHg4ZLTs1JzM3NSUROhpAUEg2DgkVFwsGEBggMEBENiwYBA0dLTlHV1VJOy0dAxIeGg4RL0E9KQkAAQMHCRETGxUNAQ4aJCIgFgQLEQsDGjI8MBgJKTc9LxEYNjwyHAMbJSMXAgoMEhQQEBAQDAwKAgMJEQ8CEBYQBA0XGxMBBx8pKRcGIC4uIAQAAQYIBAIGCAoKBgEVIycnHQ0DAQkhO1NXTTMJGC4yKhQTKy0hBjhYcHh2YDwgDAIBAgMPHTM7MScjHyEpJx8RAjBMVEQcAAEHDxkjOWt7bT8KRl5eTCIPMT05HQ5EXl5EGggBAwcHBQUHAwUJHSclFwQuTl5cTEI4NCggCg8jLTErLTErIQkSAAEECg4QFA4IBggMJjxKUExIRDgoDBEjM0NPVWNlV0MhCwIECAABBQkJCQUBBQkNExMVEQ8JAAEOEhgaGhYWGBQOCAEBBwUDAQEDAwULAwMEBgoKCAgEBAABDBIUFBAOCgoKCgABDRUbGRcNCwcDAAECBggICgoQDgoGAwkLDQsDBwkHBwEAAQgICAYCAAEDBw8VIycvMy0tIx8ZEw0NDQcBChYeIiYiKkBQXGRqal5OMAgXKztBQT01MysjGxUPBwIIDAoOCAgFFyMpLSsdDwMKGBAMBgQEBgYGBAQEAwMBBg4UFBAKAQkNDw0LAQgMDg4IFCAsNjw+OjAmGAwFGR8nJx8VDQUDEgsbIRcBDAcVEw0KQGp8dFpGRj4sEgkRGyU1T4cBpQGfAXk5Cio4NBoBCQsJBwACAwUJDxshHRUFDB4kJiAYJi4wJA4RLzUtGQQQFhQKAxcbGw8AAQoODgoGBh4sMigUBgIDAgIJIzUzKRUDCA4KBAYOEhIKGi40MhoJKz8/Kw8RGx8XCwYSGhYQBAwODAABEStDSUk5GQABDhQIFT1NTS8BOGBuYDYYHCY0SmqOAZYBhAFUEDNba2tPLSEVERMXHRkVCQYAAQcLBwMFFSMrLRsiSFZGGg0nLyUNECQoJhYPR2VdOQQkMjAiDBMlKykXBDROUj4SBQUCEiw2LiYcFgYPIS0rJQIiLioSFBExMysPCBclKxcITHyOAYQBVj5CPCwSBQEFDyVBjwG/AcEBkwFBFkRYTiwBCw0NBwEEBgABBQsZJycdCxAgKCwiGCo6OiwUG0FNQyECGiAiFgMbKSkdBw4MDAwEDCY+PjQSAgUHBwIPM0FDMQ0IFhoUBAwQFBIOHjxIPiQLO1NXPxEXJSkjEQoYIiISCBAaFAwPLUdXTzkPGCwsGhdTd3FNB05+kgF8QgwCBBg0YJYBqAGUAWQSO22DAXlPIwsBAgUVHyMbDwQFERcRAwUVHScpEUZ8gAFiHiE/STsXFDA6NB4Vb5cBjwFdBTBAQDAQFTE7NyMMTHR2VhQRGxcDHiwkGhQOBBUlLS8hFkJSQh4SEy87KxMCGy0pHQRIdoQBckowNjImEAMEBgMRM3uvAbMBjwE7GEpaUDABCQ0LBwEEBgYAAQkXISUdCwoeKCYgFCQ4NioQGz9NQyUGGiQiFgABGystHwUIDgoICAgoOj4uEgEJCQcAAQ8xQT8rDQwcHhgGBhIUEAwcOEY6IAk3UVM9ERcnKSMPCBokHhYEFBgYDAcpQU1JMQUgNjQcEVd1dVMJSoIBkAF4OgQLCwQeUIIBlgGKAVoOOWV3aUUVAwoKBBEbIRsPAgkTFxMFBQ0bHSEHUH6GAWAaI0NLPRcSLjo0HhdvmQGZAWMHLEBCMhAXLz03IQxSdHpUFBUlIQ8SJBgSDAgCEyMrJRsgRlZIHAwPIScjDwITISMTAjJQWE4wHiAeFgwBBggECRtRdXthKxY0QjweAAEFBwkFAAEEBgYAAQUPFxkTCQgUGhoWChwkJBwMES81LRsEEhgaEgABFR0dFwUGBggIAAEIHCgsHgwBBQcFAQ0fLSkfBwoWFhAECAoMDgQUKC4oGAclNzkrDQ8ZHxcNBhQYGAwGDhISCAMZLTMxHwABHCgmFg07U1U9BzZcYlYoAQ0NBRIwVmRYPAojQ1FJKQ0EDAwCCREXEwsAAQUNEQ8FAQkPERUBOl5eRBAZMTMrExAgJiQUD01va0kHHi4uIg4PISsnGQo6VlQ8DA8bGw0IEhAIBgQCDxUbGw0WODwyFhYFCQsJBQABBQUHBQIIEhQQBggICggCAAIBAwEFDxUXEwcCCAgICAICAgQCAAECAAMBAQMAAQEBAAECBAIAAQICBAQEAAEDBwsJAQABBAgGBAABAQUFAwEEAgICAgABBgYGBgACAQEBAAEBCQcJBwEEAgYCAgIGBAQEBAgKCgQBBwkJBwEDCQcHAQIEAgQEAAIGAgIAAQUFBwcHAAECBAYCAwkRDwkAAQ4YHBQKAQMFBQABChISFAoEBw0NDwkAAQICBAIBCQkJBQABAgMBAAEBAgABBAEAAQEICg4IAgEDBQMBAgYIBAIBDxUTDQEGBAQEAgEDAwUBAgwOEAwEAwMDAwIEAwABAQABAQABAwEDAQQIDAgC5AEHGSEdEQcAAgYEAAEFDxMRDQEWHhwQByErKRkEHiouJBIZQUtBGxo8TEguCgkNDw8JCiYuKBINJTExIxclNTUnDQgSFBIIAAEIDA4KCAwWFhQMBg4ODAgDGycnGwUDAwUFAQIECAQEAAELDQ8HARAcIBgMAQMFAwMCFBweFgYHDxMPBQULDRENCQsNCQUEFh4mIhYNMUNBJQABGigsKBgmMjIkCg0bIR8bExUTEwkBDBYYEAABAwgQDgwHM0tJLQIYHhwUChY0QDgcF1l5dUsNNFZgTCILHyUhEwYmODYoCCVBRzkXGDZEPCALHyUjGQUOFhYQBBUpMykVBhwoKiAMGTM7KxGYAxMrNzUjCwEEBgQEDRkhIx0BIjAuGg83S0MpCDZOUEIcK2uBAWcpMm6EAXxUDA8XHRkVEDxKQBwbQ1VTRSlHX11HHQ4eJh4QBAwWFhQKHCQqIhQQFhoWDAktQUMxCwMJBwcDAQoIDAgDERcZEwEeMjYsEAEDCQkHBB4wNCYKERsfGQsFEx8dGREVFxMHCiI+QkAmE1dxbUMELkpQRjJEWlQ+EBsxOz0zJykpHxUBFiIkGgABCQ4WGhINWXt5TQgqODQmECRcamAuJ50B0QHNAY0BFViaAaYBhgE6EzNDOyEMQl5gRA4/b3lhJyZcdGQyEzdDQysFFiQoHAQlS1VJKQwySEo8FCtXYVEf2AMTMT87Kw0FAgYIBA8hKSsnBx4wMBgPO01JJxBAWl5KJCtvhQFrI0KCAZwBjAFaEBEfISEZDDpKPBghS2FjUzNVb3FXHxAiKCISAhAYGhgQHi4wKhgSHB4YDgsvRUszDQUJCwkHAgYMCggBExsbEwEgOD4wFAMHCQsHAh40NigIDx0jHQkJGSElHRcdHRUFDjBEUkoyEVd7d0kEOFhkWDxWZGRCDiE9SU1DMzkzKxcFFiYkGAkLBhYaFA1bhQGBAUsQOkJAMBAqYHRiMjGtAe0B5wGhARtmqgG6AZYBQBM5R0ElDkxoakwSQ3mFAWstLGZ6bDAbP09JMQkaKCgcAilTYVMtDD5SWEQcK1trWSWsAxEpNTUlEQcAAQIGBA0fKS8rEwweHhALLTczFxhAUlREIBtLWUMJSnyKAXhMChMZISEdASQuJgYhRVldTzlRaWdNHQwcIB4MAgwUGBYQIiwuKBgWGhoUDAUlOTkrCwUHCwkHAQYICAYBDRcVDwABGi4yKBABBwkJCQMaJCggBA0ZHRUJBRkfIyEbGxkRAxQwRkxGNAU7W1s3BjZSYFhCUGBSNggnO01PRz09OSsbAw4YFgYPFQEKEBIHQWFbLxxARDwsEiBKWEgeK48BxQHFAY0BGVSOAZoBejYNKTkzHQw8WFhAEDFbaVcjIlBcTCAdPUdBLQUSIB4UAAElR1FJJRA0Tk5AIBlFUUUdlgIHFx0fGRELBQEEBgcZJSktIxEHAwEDCQ0BCiAwOjYqGAQFAAEMKkhWUkIoAAELFRkbHxEJAwcNHzM9Q0E1P0c/LxMEDAoIBgMGCA4QFBoiIhwYEhQUDggDDRUXEwMDBwUHBwMBAAEEAgEDBwcFAAEOFBYSBgABAwUHCwUCCgwIAAEHDQsLAwUPGxsbGRcTCQYaLDI4OCoYBRUXEQgmPEZGPkI8LhgFIzVBRUdBOzUnGQMAAQMJDx0fEwkBBgYJEQcMLjo4MCAKCBYaEgABIUlfXUMTID5COBoAAQ0REQ0EHigqIg4LHyUfCwoWGg4FISkvJxcDBgoGBAUXIykhDwoiLjIuIAYLFxMHrAEHFRkbFwsCChAODAsdJy0tM0dFNxsEIDQ6OCgkHhoUECA8UFhOQjQiFgYFCQsPFRkrNTc1Jx0bIyUtMykbFQsDBRMTEQ0EDhwcHBYIAQABAg4QBgABAQECDhYUEAIDBQcHBwcDAAMCBAgIBgIDCQsHAwADAwUPFRkZFQcIEhgUDAsdJSslGRsXCwIeNkhOSjgqMCwiFgQIEBgmMiIOAw8ZGR0fKzM/Oy0lEQUTIysxKRsLAAEKDBYqQEZGODAmGg4CDRslJx8TAwYGAgkpOzspCQwOEA4IAgQKCg4OJDY6LBQJGyUnJR0dFRMJARUtLSkVAAESHBoUChokKCYgICIiFAiYAQ0hLSkdBRoqLiYQESczNS1Fa3dlNwxAYGZWMBwUEA4MKlZqblw4JBQGAQUHBwkRHTFNV0k5GRMRGR8vGwkECAQRIy0nFQgsPEI2Gg0fKRcCCgcREQ0IIjI2KAQFCQ0LCwUBAgICAgoQDg4CDxsbGQcAAQQEAAEFEyk1NycPGDY+NhgTM0VDMxslIx0DIFJ6igF4TjZAQjggAg8VDQYiCBcnLScRAQEJIzs3Kx8TBR03Q0ExDRYqLiIcQlxmWjwkFgwCARc1QT0vDRYuMB4LXZMBlQFxKRAaHhgQAgQKDAwUSnJ8ZCgPKTc5Kx0VEwsFAzddaVUnGD5QTCwSJjI2LiIoLCokDG4PJzEvHQImOj4uEA8nMzMnRXmJAXdDDlBwdmAuGA4ICg4uWnBuVjAYCAEHBwUDCREXN1NdVzkbDwsNGykVBBASCBMvNS0XEDpQUEAeETE1JQEIDxkZDwgsQEIsCAkLDw0LBQACBAACDBASDgQTIScdDwIEBgABBRUzRUM1EyBEUEYcFTtPTzUVHycZAyJmlAGgAZABVjxISkAmAAEbKSUNEgktPz0rDwoOBBc1LykbDQUhPUtFLQABKD5ALh5MaGxcNh4KAgMDGz1LRzMJJDxAKgt1tQHBAZUBNRAeIB4QAggGDA4UVowBlAF4MhMtOzktFxELBQMDQXWBAWkrIFZqYjwSKDY4MBwmMC4gDiQNISclFQYoOjouEAsdJycbOWV5az8KRmhqUioSCggIDCpQXlxEJA4CBQkHAwcJDRkxTVVNORsPCQ0ZJREEEhAIESctJw8SPFBSPh4NKS0jAAEICxcXDQgoOjooBgkPCw8LBwMABAgOEAoEER8lHQ8BBAACBxczQUU3Exw+Sj4aEzNDQScJExUPBipokAGcAYgBVjhGRj4kAh8rKxkGFzVDPy8NBBAEES8pHxcNAx0zPTcjCCxCQC4gQlpgUCwSCAMFBxc3QUErCSA4OCILaakBswGLATkKGB4aDgICCAoKDlCAAYQBaioNJTExIw8JBwMAAQM5aXVfJyRSZlw4ECIwMCYYHiQmHgpBBQ8VDwcKHiooIhAAAQsPEQ0ZO0U/JQQoOj40GA4MCgoMHjI8NioSCAABBQcFBQkLERclNz07LRsVERUXIRMFBAgAAQcTFw8AARowPjwyHAINEQkIDAEHCwcEFiAgFgEJCQ8LCwkFAwUDAwIGCAgCCRUVFwsFAwMFCxUnMzMrEwwiKiIQCx0hHQsMCAgQHDJWbnJiRjY6OCweAhEfHxkNGzE1NSkXCQUHFSEfGQ8LAQ8ZHxsLDCQsMCQYLjw6MBwOBAEDAxEhKScdCw4aGg4NQ2ltWyUCCA4KCAABAgQEBAwuSFA+GAcVGxcTBwUAAwIjOUE1ExY2QDgiDBYeGhgMEBgSEAqpAQQMEhISDAwIDAoODAwMDAgEAAEDBQcJAwIGBgoEBAYGDhIaGBYMAAEJDxEJBwsNExcXFRERExcfHSEfHxsXEw8HAwIKEBQWGBgWFhYaHBwYFhQSEAwKBAIGCAQAAQULDQ0PCwcHBQMFAwkLCQcAAQQEAgMLERUTFRMRExMTDw8NCwcFAQQMFBwgICouNDY6NjIwLi4oIBoUCgYGBAEJFR8jJychIyMjIyEdGRMPBQEQGiQiGAgFCwsAARQYFhoQCAABBQcHBQUFCwkNBQIIBgABCwsHBwcJDxUXEw0BAQABAQEDAwEDAwIABAQECAYGBgQIDAoMCAoMDgwKAgkTDwsAAQgOEAwEzwEULjw8KBAFDQ8HCBYcHBwUDgwIAgMLEREPBwELFxkNAAEgMDQmDBk7R0EtCw8bHx8bARggGgEbJysnIxkXDw8JAQocJiooFAUTEQEYJi4sKhoWGhgUCggUHh4QAxMdHRkPBQQKDg4AARknKx0DGCIcDg0tOT01Jw8DBggGAREfIRkLFDRITEIuNj4+RDogCgABBhgaAAELEwkKJjIsGgcfKS8rJSs5QT81JSMfFQ8EQnaMAYIBUglXe3E5DiIkHhAFHystIw8BAwUDAwYwQkQoARIsMiYKJUdXTSsFAQcJDRUlMS8hDQIECAoQFhweHBQIIjZANBwOFBIUCg1HZWNHERwuNiwQ1QEYRFRQOBALGxsPBhgiJiQcFhIMBgMPFR0XEQUZKSkdASA8QCwGLVtrX0EPESEpJRcIMD4wEh8pLS8nFxERCQcDECY2Ni4WFyklERQwNjowHhgeHhgOCCAsKhgDGSMlHxMBDhYaGgwdNTcnBRwuKhIXPVNXSzERBBQcGAgRIykjDRpMYmpSNDhCSEI6EgsVDQwOESUjFQ42SEosAh0rMS0lK0VRT0EvKyUfEQJgqgHKAbYBdA19rQGjAVkOJCoeDBM3RUU1FQEBAAEBARBMaGRCBiBCTDwSLWF1Zz8HBwkRGSU9TUc1EwIIDhQcKC4yKBwOLk5WSiYQGhoWDhVpkwGTAWkdIEJKPBrHARxEVlI6EgsXGREGFiQqJiAYFhIIAw8bGxsVCxstLR8FHjQyIAM5Y3NpRRETISclGQ42QjYQHSkvKyMXDQ0JBwEQKjg6MhYTKysRFi46Oi4gGhweGA4IIComGgcbISchEQABEhwiJBITLTMlAxwoIAofS11hUzkRChoiIhILHycfDSBOaGpUMjxCREA2EA8dEQoGEyUnFw42TEg0BBUlKyshK0NTU0U1MSshEwRisgHQAcABeAABa6MBmwFVCh4iFgABIT9RTzsXAAMCBBZMbmhCBiBCTDwSK1tzaTsJCRUbIy1JV1E9FQYMFh4qNDw4NCIMME5aTCQUGBwUDhVplQGZAXMjHEBIOhidARYyREAyFAABCw0HBBYeJCYgGhoQCAENFxsZEQ0ZISMXAxQWFgIZO1thWTkNDRshHRcIJC4kBhslJycdEQ8HBwUDDCYwNC4cAxUVBRYqMC4qHBIaGBQKBBYaGAoHFx8fGRECEB4mJB4AAREbEQEODgYPK0tZW1EzEQgWIBwYAgsTEQcaPlRURDA2Ojw8MBQBCQkKCgsXFw0KKDg2JgYNGR0hGyU7RUtDNTEtIRMAAVCMAaQBmgFwGjNXWTMKEgoAARErP0tDNRUAAgYEBhI8TEouAAESLjIoDB1BTUktBxEbJS81SU9JNRMIFiAsNDxAPjQiDCI+QjgiEBYUEggPSW1zWyUMIi4kDmsKGB4iHhYSDggIBA4YGhwcHhoYEAABDxEVExMLCwkHAwIDDyEnMzk/PTEhCQUFCQsNCwUHCxEbGxcVEw0JCwcFAwYSHiQmJh4aGhoYGhwaFhISEAwIBgIBAQMHDxMXFRMNAg4cICgkIhwYDAYLGykzPUdHRTkpDwMIEBoWFhIMCAQMGCIoKigsMjIuLiAYFBAQEgoMBgYGDhAQDAQBBQsNEx0pMTU3My8lHREAAR46TFJOQjImHBIICRklLTE1MS0dDQMCAgQIChQWEAQHBQEAAQIBBQsNCQcJFycxMzs9NzEfCwweKjY+QD46LBoKEBYcGhIODAwGBAABFSMpJR0VCwcDAAHZAQ4mNDQmGA4KCAQECAgQEhguSFA+HhMpMTUlEQwcJBwOETFBSUE9S01FKwckOkQwDilPX1c/GQABDhIMBRsnKR8PAAEIDhYgMFZqZEgeBgMHAQYSFBYSCgUTGyEbEyMxMysXAAEUICgoNFRiXkQOKVFfX1E9OSslGQ8AAQwUGB4cHBoSCgcVGQ8GJCoqLiwoIhwUFBAcNDg4IggCAgQAAQYMEhIIBRUfJy0vKx8VCQMBCw8HEDBcggGIAXhIAUNncWM9HxEHAQMLGR0VCQgOEgwECyk9PzMTGEBQSCoDHSk1Ozk3NSkdBwocLDQ6PDQkHBIEGCguJhgGAAEHAwMAAQMHDRUZKTUzIw2dAhg8SEQyEggAAQEBAAIEAgwQOGh2ZjATO09JORMWNj40Ehs9T1FHP1VjWTsJQHR6YiA7gQGbAY8BXxMWLDIkASc9RTkXAwEEDBg4gAGcAZIBZiIJHRsVARIeHhoOBR0vLScVL0lRQx8EGCIsLDp2kgGKAV4WO2l/d1c3JR8RDwkGEhoeHCAcGhQKETU/MxEcJiwoKiAeFBQMDCRIXFQ0CAIBAQIEGCQkGAIRGSElKSMPAAEGCAElOzkdGmqgAbYBngFeC2WZAaMBhQE9EwYUEAYVLzctFwoWGBYMCT9jaVUjLnCOAYABSAEbKTE3NTUxKxkLChggLjQ2JBYMBAgkPkA6GgILDREHAAEBAwcNGTVFRTUT6QEaPExILgwAAQUHBwMDBQEAAQw0bHhoNBU/T1E9FR46QjQSGz1NTz85VWFbPwlMfowBbig9jQGrAZ8BZw8eOj4qAitHTUEbBQcDBhA4hgGqAZwBahwRISUdBRIcIBwOAyEvMScXMU1bSyEEGCQqKjp8mgGQAWQUOWd9eVEpGQ0HBQUIFB4eGhocGBQMFz1NQxsWICQmIB4UFAoMCCRQYFo4BgABAQEBBhwmKB4GCRMZHx0ZAwgODgABL01LLwxengG0AZwBWg1pnQGnAYUBOwUSGhoIGTM9NxsGGBwYDAdDbXdfJTZ+nAGOAVABFyUpLSstKyEZBwYQHCIqLBgMAgABBChASDgcAQ0VDwkBAgABBQkTMUdJNRc3EjA4NCAEBQkNCQULCQkDAAEmTl5OJBMzQ0MxExIsMiYKFzE7OzEpQU9JNQc+anBYHjNtjQGFAVUPHjIyJAABJTtFNxkFCQcAAQwqbogBgAFUFg8dIxcDDhYaFA4FGSUnHxEnQ0lBHQgSICAiLmZ+dE4QJ1FhWTsbDQMAAwoSFhgYFhQUEAgPMUE5FxAaGhoaFBAKDAYGHD5QSCwIAQABAwIEFCAgGggHCQ8PFQ0AAQoQCgABJz9BLQABRHqGAXZEC1N5gwFpKQESGhYGEy0zLRsGEBQUCgc1W2FPHyhqgAFyQAERGR8dHx0dGw8FBgwQFh4cDgYBAQQgNDguFgENDQ8FAgIAAQEFCyU3Ny0R7gEGEBIOBAMJCw0NCwkPCwsHBhYcGAYPGyEjGw8CCgwIAQ8ZHR0XEx8jIxcFFCouIAoXMz09JwcGEBIKBRMfIRsPBwUDAwQSMD42KAYDDQsJAQgMDAwEAQsPEQ0HEx8hGwsCDA4UDhgwOjYkCg8fIyUXBwECBAQEBgwODgwMDgwICgMRFRMHCgwODgwKCAgEBgIQHCAgFAQEAQIAAQIIDg4KBgABAwMFAwUCBgYEAAENGx0TARosNi4YBx8zNSkTAQYKBgINERkVCwEEBgQCAxknKyERECoyLBgBCQkLDQsLCwcHAAECBAgKDAwGAAECAAEEDhgaEgoCAQUBAwABAgICAAEDDRUVEQesBBMzPTsnExcbGxcPExUVEwkAAQgIBAELCw8LDw0VGxsXCwEIDAoEAAEGCAoCBRMfIR0RBB4mJhILGyclGQEUJCYeCAUHCQUFCyErJRcAAQ4YGBIGCRUXFQsCDBIQDAIKFBoWCgEJDQ0DBREXFQ0CDBgWGBIKCAoICAYMCAgEAAEBAwECBBQiKiweDAIDAwUCBgwMCgYFCw0NBQYICgQAAQkrQUU1FwYSGBgSCAQCAQABAQMFBQUFBA4QDggEDBQUDAIAAgIAAQMDBwUDAgQCAQMFAwQKCgYFERsbFw0FBQcDAgYKCggGAgEHBwcHAQwSEhIEDRsdFwsHCQkJAQoWICAYDAoQDgwGzAUtb4kBf1EdKTUzJRUbIR8TAxQoLCQOCREPEw0PIzEzJREQJCwoHAwaICAWAy1FT0MnFk5sYjgLP1FNLwpEZnBWIAkPDw8NG1lzZ0UHIjg6KgofOUM5IQQgLi4eDCA6QjwaCx8pJR0VP0tFKQMgMj48KhwWEhAMEBAOCAABCxEREQsCIkxgXkIWAAEPFQ8HDBIYEAIPISkhEQYOEgoFH22jAa0BiQE5DCg6OioWCAQAAQEAAQcLEQ8JDCIsKhYKIjAyJAYECggIAAEHCQcAAQwYDAQBBQMQHh4QCSk9PzchBwkLBQgcHhwWDgQHERsbFwsWKCoeBidHT0MjFx8hGQkQOE5OPBYaJCQcDOAFN4cBpwGbAWMhNz89MRcdIx8TAiQ4PjAWCQ8TEREPJzs7Kw0WNDw8JhYmMioaATVbY1UzGGKEAXxGEUdfWTEUYooBjgFwKgsPExMRJW+RAYcBVQ8sREQyBitNWU0rCCg6PCwOKkxWRiAPKTU3JyVPZ109BShIVlI8KCIeGBgSEhAGBRUdHxkPASpeeHJSHgEPFxcLCBQWEAIZKzEtFQYQDgQNL40B0wHdAbUBTxI4SE48HBQIBAACCRMVEQ0MLDgyIAwqQDwuCgYMDggCBQkBCBgmFgwCAQESIiAQETlNU0UnBwsFAAESKi4oIBYGCxsjKSUPECwwJAYzW2lbNSMxLSMPFEhkZk4eIi4uJAzoBC95mQGPAV0nM0E9LxsbHRsLCiY8QDAWCREREREPJTM1JwkcNkI8LBwqLiwcATVXYVc3EFZyajwRO09HISRmigGMAWokCQ8TExEhZ4UBf1MLJDo6JgIvTVdPKwQqOjgqEChGTkIcDyk3NysnU2VdPQcwSlpaSDQuKiIcGBAKAgsbIyEfEwEoXG5uTiAICRMTDQABDgwIAxkrLSkVBAoCBxs3kQHLAdsBsQFNEDZOUEAmGBIMBgEJERUVDQomMi4cDig4PCYKCAwMCAABAwABCBImLiQWCgIDDhYWAhk7UVNFJwcDAhAeNDgyKBoIDR8pKysdChwkHgQzWWddPSs3NScRFEhgYkwgJC4uIg70Ah1LX11DIy8xMykZExEHAhAiLiwkDAsNEREPCxchHRcBGio0MiYeJCQgFAUjO0dBMQkkNjIYCx0jFQQwVmhkSBgJDQ8PDRk/UUk1CxAaGAgLJzk9NR8EHCQmHAoaKjAoEAsfKy8nIz1HPycBJEJOUEY+OjQuJBoOBAsRHSMjHREBIDxQTD4gEgYDCw0HBQEFDRUdIRkLAgcPHSs/b5cBmQF9NwoqPEA8KiQeEgwCBw0PEw0AARQaHBIIGiIgGgYCBggEAAIOGCQwOC4gFggBAAIHESc5QUM1HwUKFCIwPDw4LBwIDRsnLSslDwIKCAIjP09LNzEzLyEPDDBCRDYeHiQiGAouBQ0VGx0dGRcVFxcNAQoQFBYMBgABBQcEDAoGBQ8RDwUEEhwgICAcHh4YCgMTHSUpJSkvKyUZCQwiMjo4PDgsGggJCw0NDQcAAQQCAgMRGyEfHRcXFw8HAAECBAQEBAYEAgIAAQcTGR0dHxkVDwUGFigyPEJAPjYwJBoOAwsXHRsXDwUCEBwiJCQkIBwSBA8dJSklGREFAAEEBgEdNUFFQTUnHQ8JBhIgKCwsIBYOBgIECAQEBRMbHRcLBgoMDggCAAEBAwMDChwuODw4LiAUCgMRFyErLy8nHxcNAhYoND5ARkA2JAoPJTE3NScjGREJAQ8jLTMxKR0VDQMEBAYMDhYYGhgQBo8BAQABBQsVFwQQEAIREwcAAQgSAAEZJycbBkJiZEIIIzM3JQUWJCoqIiIwOC4eAAEbLTEzK0Fre21HCS5QYFpANC4iFgILFxkXDwQuREIsAAErRUtDKREJAAECBAMNEQ8LAAEFCw0PBQcRGR0fFwsBBggIFBwqMj44JhgWGBwUDgIJFQsCEA4ODhogJCAgKiwiDg1DZW9bNQEeLDAcB0FncWlHDlBuYiwEEhokKiAHJSkjAyQ6QjIOMWt5azkEICgoHgYFCw0JBw4qOkRAMA4HExMHCxMfIy0fCQgKCAIYLDY6QFBcVD4WI1FnZ00pEwkAAQYAARkxPT03FQgcHhQFIS0pFQwkKCYeDNUBBAQAAQURERIkJBITFw8JAAEKDTNBQScOZpQBlAFoECtHSzcLHCgsLCYiOEQ+KAEdMT03KUuLAaMBkwFfBzxmdGY8LCQeEAABDxkfHRMMSmRgPgQ3V19RLQ0CCgwGARUbHREFCRUZEwkJDxkbHRMBDBASCg4YIi44MBQKAhAeIBgSAAENBBggIBQQGBweHBwqLiYUEVWFAZEBfUEEMEZGLg1RfYsBd0M4lgG+AaIBRgYQGB4iFiNDTzcJMlhiTBo/jQGtAZkBUwgqPDoqCgcREREHDiw+RD4kCSMrIQsHERMfJRUMHCISAhgkMDY0VGJgRBgrY397WyMHBg4KBB03RUM1CSY4OiAJOUtFKQYkMjAiDqEBAgQCAxEPEiYkEBMZGQ8JAAEVPU1DLQ5qmgGaAWwUKUNHNQkaKC4oIB44PjwiAh8tNTMlR4cBoQGXAWEHOGJsWjAeHBIMAAEPGx8bEQ5KaGI+BDFTW00pCQgMEAwFFR8fFQMNFxkXCwURExcXDwQSGBIMDBQcJjQoEAABBAwgJCQYDgEOJCgoFA4UFhYSEiIkIAwRUYMBkQF9QwYySEYsB091gwFxNUioAcoBrAFKBgoQFhwOK01ROwk0WmBOHjuLAakBlwFTCC46PCoMCQ8TEQcMJjY4MBoTLTMlDQUJDxUfDRQmJhQEDh4kKChEVlQ8FCddd3VTGwEMEhACGzE9PSsBLEI6JAs5T08vAiQsLCQMBgIAAQEHERMEEhICFx8bGRENGTM/OyMGTHBwThQVKy0dAxoiIiIaFiYuJhgBFSEnIx0zY3dvSQ0mPkY6GA4MCAIDCxMXFQsIOEpILgQjOT81HQEEDAwEAQ8XFREFDREVEQsFCw0PDQcGEBQSCgwSGiAqJBIICBQiJiYiFgwYJCggFAwKDggGBA4SDAIRP19rXzMCHjIyIAUzT1dLIzqAAZIBfDYCCAoOEAYhNzsrByY+SDgWJV9zZzkKHjAsHggDCw8LBwYYICIeCBMhJx8JAwcJDxEHEBwaDgABCA4WEhgoNjImDB0/U1E3EQEMDgwAAQ8jKSkdAiIwLhgLKTs3JQABFiAgFgqUAgEDBw8TFxURFRcfIR8dHRkbHRsXEwUKFhgWDAoKCgwSEg4MCgoOEA4KBgUJDQ0RDxUfJycbDwUCBAEDBQUDBQMHBQUDAwIKEg4MAgkNEw0HAQICAAEEAQMFBQcDBQcHBQEDAQMBAAEBBAQGCAgMEBQaHiAcIB4gJiQkIiIcHBoaFA4GAgMFCQcLCQsPExkdHx8VDQECBAgCBQ0RCwMOHCAaDAIBAAEBAgIFDQsJAAEIFBAQCgMNEQ0HBhAOEAwGAQEFBQUBAAICAAEDBw8NCQMFAQUBBQMABAMBAgECAwADAQMJDQ8PDQUAAgICAgMDCQUFAAEECAYEAw0PDwsAAQQGBgYEogMEBgQDESEXGRcbHScrMS8xHQUKCgQVO0k/HRROcnxmMBVJV0slCBYYGAoBAQICAw8VFRMTExEfJSMdDwkJBQcHDio2MBYPKTUtHQUJFRcVCQIKEA4IAAEBAQEAAhIcIBoIChggHhQAAR0nKRkCAAEFAwQMIjxERjokJiYoKiocDAACBgYCAwUJCwkNDw8PCiAqIAQXJScfDwYICAgGAhEfJRsLCyEpIxEGDhIODAIKDA4IAwYeKigcChIYGhoSAgsPDwsJDRMTFxENBwUDAwABAggMEAYNFxcRARAUEgIZRVtdSSEWPlBOMgQFCQkHAAESHiYgGAETHRsTBQUHBQUDAwMFAwP8AwgQDAIPIR0TExUfKzU9Q0MpBBoeDB1ffW07HoYBwgHKAaABSDWHAaUBjwFLCh4qIhYAAQQIBgETHx0bFxMVKzM3JxUJBwcFBRxQYlImG01dVzkJEyUpJREEFhgcDgIAAQEBBAgkOj4yFA4uOjgkAS9LSzEHCRcZEwEmTmBeRCgoLC4yMhoEBwkAAQgCAAEFAwUFBwkLDSBETkISHTU9MxkGCgoKBgIfOT0zExc3RT0fDhweHhIEDBQSBgsCLEBAKAwcKC4oHAQNFxkNCxkbJSMhFQsHBQMCDhQcJBINISUZAAEeJh4AASd1oQGjAX81KnKUAY4BWA4HDREJAR42RD4sBh0vLx0FCQkLAwEHCQsJAdgEBhAOBBUlHRcRFx0rPUlPUzEDGBwQH2WFAXU7Kp4B3AHgAa4BSDuVAbUBoQFXBiIqKBYAAQIKBgcZJyMhGxcXKzs7LRMLBwcDByBYalwqIVtvYz8NFyszLRkGFCIcEgIBAAEEBhAuSEw8FhYyRD4sAAE1U1M3Bw8fJR8JIE5kYkgkKjA0ODoeBAUJAgYGAAIBAAEDAwUJCSJKWEgUHzc/NRsGCgoICAEhP0U3FRU7TT8bEiAmIhYIDA4MAAEXBShAPigQIC40NCQMCxUVDQ8ZKSsvJx8VCQcFCBQgKDQgBxkhFwIaJBQFN4cBuwG9AZUBQTKGAaoBoAFmEgENDQ0AASRATk42DhctLR8JBwsHBwEFDQsJA7IFBgYGBxcpIRsZFx0vP0tXWUMXAAEKBBlNX08dNpIBwAHAAZABPC11kQF/RwQaHh4SAQEBBQ0hKyslIRcVKTMxJxULBwcHBxpEVEQaIU1fWTcJEykxKxkBEhYYDgABBAYKDBQwQkQ2FhAuODomBik9QS0DDR8lHREYPFJOPCgsMjg6PigSBAICCgQEAgIAAQIBAwcJHDhAMg4bLTEtFQgKCAgEAAEdMTctDxEtMy8PGCIkIBYGAgYDDR8TEiYsHgweMDQyLBQECQsLDR8pMTMxJRsVCQMKGig0PC4QBQ8NAAEMCgABGUF/qQGrAYcBOypwkgGIAVgUBAMJBwIgPkpIPhgFGR0VCQcFBQMBBQkLCQOQBgMFDxUhLScjIR8bK0FPWV1ROysZEQ0TDQEcQmR2ak4gDyk1MxsAAQgKBgQFCw8VHystLSsjGxMZHx8ZDw0LBwcFBBQWDgEdKzMpGwULGR8dEwkDAgQEAggQEBYWJCwqHgwIFiIgHAoFExENBAUPFxcPAhgiKigiMDY8QD40KB4UDgYIBgQEAgIAAQEHBwIOEAoDExkZEwcGCAYEAgELFxcRBQABBwsBChgcHBgOAgcRFyMpJREFAAEGBhwkLjAsIhYQBAENHyczMzUtJxsTBRAgMjxCPCweEgYHERsnNUVha2tPIxA0REQwFgwIBgIAARYsOD40KhYMAgMFAwEAAwMHBwUBvAYHGSUrKyUjHxkVFyMxPUlVXV9RPyMBHjZGSEQeARERAx46RD4gAw8TExELFSUvMy8lFQkFBQ0LCQkNDRslJyERGTU9OysTAAEMEA4KDhoWDgMTGRkXCwACBAwOGBoYFA4CAgYIDBASDgwICggGAQUHCwsDBAoWIiQsMDY8QkQ8LhYIAAEBAAECBAULDQkJERsbHRMNBwECBgoOEhAKAhEbHRkHFjRGQjIaGhQQCgABCxkjKS0zPzktFwQGEBYeKjg+NiYOCw8ZIS81OzcvHQcOJjI8QkA4Lh4KCR8zP0VFMRsLBwUTKSsjCxQaHBYQBBIkMjQ2LBwQBgACFCAgGAQXJykfDZYGDSUvNS8fEQcFCQ8TFyMxRVtjXUsrAAEqSFRUOB1fb1kfPIQBmgGKAUoHHScnHQsfNT8/NxMWKiwYBQMBAwMPMU1TQSEvaYEBdUsLIjo8LhAyTlhCEBspLycXBQsRDQABFBgYFAwGAAIECAwQDAYICAgKCAYDBwkHAgoUGhQSHCQ2TFROPhwCBQkHAwIPFxkVCxklMSkbCQIKDAwKHiYoGgQlP0k3FypmhAGAAVQcFBQOCgIHERkfJT1RWUknAwkLBQwkSlxcRBgBAAEDDx01Q0c9KwsQHi44OjYqHBIGCR8tOT09CxowLhIxa4MBbzcQJCYiFgYYLjg8NBwGCRENCDJISjQINVNbSR26BQ8lMTUpGQUEBAILBwkNHzVPXVtJKwABLkhUSiw/iQGbAXsrSJ4BvAGiAVgHIy8tHw8hN0FDMwcoREImAQECBAMNOV1hUyU5f50BjwFZBTRSUjoWQGpyWhwbLzUvGwcVHRcJDhgUEgwEAAMGCg4ICAIGCgwOCgYFCQEAAQoSFAgGCBouSlZSPiAAAQkNCwEAAREbHRcHGS0zLRsDCBASDg4iMi4kBClNVUkbMnycAZIBXhgSEgwIAAEDCw8XHzVVX08tBRUdEwEeSmZmTBgECAgAARMtQ0dBLQkIGiQuMigcEgYAAQcZJS81MQY6TkQaOYcBowGTAU0MJComGAYaLDY4LBIJFxkTCjxYWD4GO2dvVSOqBAsdKScjEQABBgwCBQUCBQ8jPUdJPSUBJjpCOBpBhQGXAXctQI4BqAGOAUoHHyknHw0dLTs3KwUqQEAkAwECAAEBCTVRXUkjNXWPAYMBUwM0UFA4FDxmbFIcFykxKRkDFxsbCwwQEA4KBgECAAECCg4IAgYECA4SDggAAQMBBggOEAQAAQIOJjxKRjQaAgUJCQECDRcZEQURJSclFQABDBIUEA4iLC4iCCNDSz8XLnKKAYABThQOCgoGAgMFCQ0TK0VNRScJFR0ZCRJAVFQ+GAABDgoGDSE1OTclCwYQGhwgHBAGAgEJER0jJSUQPExAGjV5lQGJAUkIHCIgFAYQJCwqIgoPHRsPCDZOTDQINVlhTx/wAgcPFxUTBwABBAQCBQACAQcTHycrIxcBDh4eGggtUV1LHyJQXkwoBxUZGRMJEx8jJRsFFCIgEAUDAwEFCSMzNy8XIUdXUy8DIDAyJAomPEAyEA0ZGxsNBQsTDwUEDAwICAQCAAEEBggKCAQEBgoODA4MBAIGBgwODAYAAQYKGCYqKiQSBAEBAAIEAwcLBQIHDRMRBQYOEBIMDBgeIBgKDx8nHQsgQlBKLAoGCAQCAgEBBQcHFSMpJRUHDxMTBwYcLCoiCAABBgYAAQMVGyEfFQkCBggMDAoEAAEDBQMNEREVEQgiLCYMHUVZTy8CDBIQCgEKEhQUEAILERENBh4sKh4CHTM3LRGyAQYMEgoGBxMXGREFBAoQDgYAAQsNDwsHAQMDBw0PExEPCwkFBwMHBw0NDQkJAwEDAwcJCwsLCwkLCQkHCw0PEQ8LAQgODgQFFRsXEQEKDhAMCAICAgQCAAICAgABAQcNCwkBChAUEhIMDA4KDAoIBgYEBggICAgKDhAUFBIMCAgGBgYGAQMDAggKCgwMCBAYGBoUCgoICgoICAYICAoSFhgWDgIJDxELAwABAgYEAQcNDQcBDBIUEAgDCw0PDQcFBQUFAwUJBwkHAQYGCgYDCQ0LCwcDAAECAgABBQMDBQMFCQ0PDQUEDBQQCgURFxcPBQcJCQcHAwABAgADBggIBgABAQADAXoaOkhAIBdLZWFDDyJCTkw6FAkbHxkFAgQDESMhHxkXExkjJyEZER0fHxkHChggGgoHExsZEwkJCQ0NCxUfHRsPIlpyaj4TaZEBjQFfERgsMiwcGCImHhIECAwGAAEPM0tPORMaNEBAMCAgJCAYDgMHDwsDAQUFAQQSKjg0KBABDxELAAEJHyslEQYYHiIWEC5MWk4uEgYCAQQIAwcLAwouSFBEJBlRaWdHGQEIDAwAAR0zMyUDLlJeVC4FHy8tJw8NDQ0LBQ0ZGxkPBCg+PCgGGSctJQ8GEBYUCgEDAAIBASM9RTcXJl52cD4LQ11ZQRETIyUjFwMKEhIMAhQaHBICCg4QDghQIFRiViohaY0BiQFhDzZkeHhcLAABHSUZBwQCCRs3NysnIx0jNTUxIxsnMS0hCxQqNC4YBRUfHxUFBwsLCw8dJyshETCKAaoBnAFaGZUB0QHPAY8BGyZCSkYuKjg6MhoGDAoIBRlRc3VdGyZOYF5ILDI0LiIMAxUXGQ0NDw0JAAEUOEhINhIJGRkTAw8zPzshBh4oKCIQPmh4bDwUCAEBAggJEREJDDhealguJXOZAZUBaysJBg4MAAEpQ0UvAkR0iAF2QgMtPUExGQ8RDwsHDx8pIRcMPFZWOggfNzstFQgeIhwQAQACAgQDLVVfSx84iAGqAZoBWhFZfX1XGR0xOTUjCxAYGgwIHComGAYKFhgUCCgiTl5QIiNrjQGNAWERQnCKAYgBcD4UCRcVBwMHFStFRT01LSMtOT03KR8vNTMlCxYuODIeAhEZGw8DBQkLDw8dKSslFTiMAbIBngFeFZUB0wHVAZUBIShKVlA4NEJEOCACCAgAAQ0lWX1/Xx8uWGxqVDg8OjYmDgkXHRsVDxUTDQMSOExINBQJFRsTAREzRTsjBBoqKB4SPmp2aEIWCgQAAQQGBQ8RBwg6WmJUKiVxmwGdAXU3GQUGCAABJT0/JQxOfowBekYHKT1BNRcTEw8NBw8hIyMTCjxUVDoKHzM3LxMKGiQeDgEAAQIEAgABLVFZTRs8igGoAZgBWA1Xe3tXFx81QT0tEQYUFA4GHiYoGgYKFBgUBgQWMjwuEiNVa2dJCThqggGCAXRUMBgEBQURHSk/T1FJQzctLTc3NSsjLzMtIQsQJCwsHAYDCwkHAAEDBwsLERklJR0PJmqCAXhGDWWZAZsBcRkiQE5KPDZAPDQaBAEFDxcpT2lnSQ8qUmRiUj4+PDQkDgUTGxkVFRUTDQMOLDY4KhIBCw0LAAEHIy8vGwISGh4YDjBOWlAwHBAMCAoKAQcFAwooPkQ8HBlVd39rPykXCwUBEx8dCRpKbHJgMgEdKzEpFxMREwsJCxcbGQ0GKjw6KAQVIScfDQgUFBYKAQEAAQIGAAEbNT0vDy5ieGg6BztTUz8RHS87OTMbCQIGAgQSHhwSAAEIDhAOBhsAAgUHERshJSERAAEmRlpoamZYSjIUCyM5SVFZV1FJPzEtMTMvJyUjHx0TCwABCAwQEBAODgoIBAABBQkNCxEREQ8JCB4qJhoEEyMnHQsQJDI2NDIwKhwSAw0ZHykrMzEpFwQmOkJKREA8OC4eDgABCQ0VExETDQkAAQYQFBQUEAoKBgYGAgMJBwcDAAEEBgYKFBocIB4cHhwYEgoIBgQIDA4UFBAMByE3Q0dDQTcpGQMGEhwmLjw8OCgYAgULCw8RFxcTEQcFAQMBAwIEBgYCAQEDAQMBAgQGBAABBQMFAwEEAAEBAwABBhAaGhgMAwkTEw0JESEpMS0pIRsRBwMCBAQEAAICBAICKREpNzktDRg0PC4MCxEDHEp4jAGEAWg2DztXZ2ldOSEVFSVFZW1hSyEGGiAWASc7PS0LEBIODgwEAwkNDQkMICQeCAABCAwODAwMDgoCCQgaJi4wIAYFCwkDFyMtLysRBhYgIB4kMDI8RFRUTDQQAAEFDxETDQEAAQYEAREVEwcKEg4QDgoQFh4WCgUJCwkBBBctLSECKkZMSi4KAQcHAwQQEhQSDAYGARMnS217cUkPHjZERj4oGAgCAAEKKjo2IAstQUM1GRAoNDIcCS1DQSsJHjY6MBQEDhYQCAktOTklARQkKiQYAAELFRMJAgIGBAABAREbJSktKSEZEQsFBQkHBwEIEBQOBhkZQ1NPNwdGbnZWFjNTTSMugAGgAaQBhAFEDUVpdXNbIQgaDBlbjwGjAZcBZR0kSFI4AAE/b3djHw4MDgoKAgMJDQ0HJkZSQBoBBgoMDAoUGBQIBQIUICgqEBcrLR0HFyUtLycGLEJCLhweICwySGRuYEYUAAEJCw8RBwYQEAgFJTEpGQQQFBIODBIqLigWBQ0TDwUAAS9XYUcRMmB0aEIKCxMXDwQSHB4YDhIaGgwTR4cBqQGdAWsVKERWUkIcAAEPEw0USmZiQAFBYWdPIxpKXFIyD09tbU0NNFpkUh4IHiAgEBFFY11BByI4PjgeARslIRECCgoGBgMNGyMnKSMZDwsHBQ0NDwkDEB4gHAoQHUVVUzsEUoQBigFkFEFtaz8YcJwBnAGCAUIPRWN1bVMVGCYYFWOfAbUBpwF1Hy5cYEQET3+RAXErCgoKDAgIAwcJBwEwWGJQHAABBAoKBgYUGBQKBwABDhgeIAQnPT0hBxcjJycdEkJYUjYcGhwgMERkcGZEFgABAwsLDQMOFBQMCSk1Mx0CEBASDgoYLDIuGAMNEQ8HAjVjbVcZNmR6bEQICxkZEQQUHCAaDhIkJhoFOYUBpQGlAXEVKEhUUDgUCxsdDxRScm5GAkFla1clIFBiWjINVXV3VQ86ZG5UIAwcJB4SEUllZ0UHJjpEPCADGyMjEwYKDAoGAw0VGyEjHRMLBQUHDxEPCwMQICQcDlATNUVBJwRMbnJSFDVfXz0GUnJ2XDINNVNfXUcVDBgIHVuPAaMBlwFtIyJGTjgAAUNxf2klCAgMCgwKBAEBAQYuUFhEGgECBAQEAgoODAIJAwYOEhYDJzs1IwUNFxsbDx5AUlA2HhocIio6Vl5UOhQCAQMJBwQMFBIKBx8rKRcEDA4OEAgUJCwkFgILCQcDAAEnTVtFFSxUYFg0CgkPEQ0CEhgYFgoQICQaAidfgQF7Ww8eOEA8KA4NGRcNEERaWDwCMU9VQx8WPlBEKA1BX19DCyxQVkQYBhQcGAoLOVFNNQMgNDoyHgITGxcLBgoMDAQBCRETGRcTDQkFBQkNDw8NBQ4YGhgIrgEJERkVDQgiMjIoCBMhKRsCGigoIAwJHy81OzcnFxcbL0ldZ2NPKwcMFA4FJTs/MxMECggODA4ODAoMDCAsKiAKAQACAQMHBQUFCQ8HAQIGBAkXHx0RAQMFBQABAhguNDYoJCIiJiYuNDQwIhAKBAABAgABAgoKCggAAQkPDQUCCgwKCAgOFBYWDAQEAAEBBAYLGx0ZAxgmLCgaCAIBAQIECA4MCAgIEBAMBgsjLTEhBwwWHBgSBAMHCQMKHCYkFgABEx8hHw0EFhwYDgUZJScbBRIeIBgIAQYGBgQHFRsbDwYUHiAcFAgCAwMECAoKCAIDBQUJCw0LCQcHBQkLDQ0HBQABBgoGBNYBAQMBAgQGDRcZDwQaJigeAg0RDw0LBw0PGR0jO1NXV0MpDwMDFTE5OzMlDQIQFhgSEBIYFhgYIiYoHhANJSsjERUvOzcnCRQkJhYHEREPDwkDAwADAQ0NBwIYJCwuLCooLjAoIBIEAQMCCgwODgwMAREXFQkKFBoaDgYBAgIGCA4YHBgWCAcNDQkGGiguKBQHExkXBwgQFBYUDggEAAECAgQICggCAAEMDhAMAgkNDwcCCAgIBgQBBwsPCwESJCYeCBUxOy0ZCig2NiQGBxETFRMbKS0lFQABEiAiIhYQCAgIChIWGBYSDhwcGhIAAQMBAQABAwEAAQIDBQ8tOzcvEwMAAQQGArABAwkJBQYCJ0FDLwM4WFxACB0bGxcNBwcJCQ0XSXWHAX1ZDTZWTBovWWVdRRUgQFZQNiIqKiggIDhGQiwIPW17ZSkxc48BhQFbC0p6elIIHSEjGxMDCA4ODAchKyMFJkJKTEIuLDY0KBIJJS8rGQgSGBgaFgUxQzkfEC44Mh4FCwcDAAEIGC44NCgMGS0vIQI0WGBOIhk/Sz8jCBwmKCYaDgQCAAIIDg4MBAEOHh4YAhEjIRcCEBAMCgYBFyEjGQcuVF5MHDFrf2szImKAAYABWBAVIS0vL0FhZVczAi5GUEYqEgYBAAEIFCQoJhoYLjw4JAQBAQICBgoMDAgBHVl9f18pCwoODgaGAQMNCwUIBjFRUzsDQGhqSgYnKSEbEQcBAAEBAwlJf5UBjQFfAVZ+cDQtYXd1TxUqXnJwSjI2ODAoJD5MSC4AAVWNAZsBgQE3PZMBtwGvAXcPYJoBoAFsCh0rKyUXAwwWFgwJJzMnAzZUXl5OMiwyLh4GHz1JQSUCGhwiIiIHO1FLIxA2QjgeCxMTCQEGHjxGRDAOHTc7KQABQGpyXCIjT2FVLQogLjYuIhQKBAABAggOEAoCAxAeJBoEFycnFwIYFhAOCAUbLSsjCThkclggN32RAX83MH6iAZgBahYZLz1BPVl9hwFxQQY6XGZYOBoIAAEBBBokLiweGjZGQCoGAQIECggMFBIMAAEfaZMBmQF5NwsEEBAGVAMHBwEMCiVDSTMDOFhWNAUtKykdEQcAAQIIBgABO2uFAX9bA1JyajApWW9pTRcuXnRyTjg+PDgoHjpAOCYJUYcBlQF3MzeNAbMBrQF5G1CKAYwBYAofJysjFwUIEhAMBR8lFwxCYmhiUjYiKB4OCStFUUUnBhgiKCgmAi1FQSEQLjgsEBUdFw8HBCI6SEY2FhctNSEAATxeZE4cJUtZTykIIjA0NCYYEgoGAggKDAgDCQwWHBYEFR0fDwwcGBYOCAMdJysjDS5YYEocLWd9Zyk2fJgBjAFeEh0zQ0dJX3+HAXVBBDxcZlw8JBIIBggUJigmHhYyPDgmAgABBggKDhASEAoAARtdgQGLAXM7GQMGBgYaAAECBAoSEgkdHxcBGCYeChsxLycdFQUAAQoKDgYZPVFVRRUcODIQIT9JRzMPHEBUVkg8PjwyJBwgJBoKDTtZX00fJ119e2MnGDxCLgETHR0dEwsBAgYCAQMBECxSYmZeSi4eEAQLGys/PzUbBhoiJiwoFgsXGQ0IFBIKCR8jHRUJBhoyOjouHAQPEw0GJDQ2KAgZMTcxGQgcKC4wJiAYEgwIBAQCAQkLAQgICAIFCQUEEh4aFhAIAREbHxsNEiouJA4ZMTUnATRWZFg2CBkzP0tLV2trWTMCLkRSTj4sJBoSDg4YHhoUFh4mIBICAAEIDhAODg4KBAETN1FbUzkjGQ0DAQwCChIWGBoSDgYEBAsbIysxMzUrJRUJAggMDhAODAINJzMzMS0jHRcVDQ8JChwsNDg2LigiGhAOCgIJExkZGRMHBxchLTM3NS0jGQ0XHR0bFQsDAAEEAgQYLkBOWFxSRjgoCg8jKy8rHRMHAhAaJi4sLiomIBgQAAEVJy0tKychGQ0CDhYcHiQoLjIuIA4HDRUVDwsFAwIEChIcIiQmJBwUDggDCxMXEw0AAQwQDAQCCg4SHBwYEg4GAQMHCQsRExUVDQsIHCYwLiogFAwGAxcrO0NJST81JxcBChYiLDY6Ni4iEgoKCgwODBAKCgIAAQYOFBQQCgEFCQkFBQsRGyszMysfCXgEChAUGBICCw8NAw8hKzExPUVJOSMHAQQKDhAwRkYqB0lvfW1LGwMICAYFECg6QDoiCgkLARIkJiIQDRUTDwkDCBQQAhU5SU9DLRUzUVlJJQgqNDAgCBowQE5YQi4iGhoDNU9RQRsWND4yGiY6PkAyLjY0KhQHOVFXSy8rKyETAQILCwEUNm6IAYQBXhglT1VJJwIYJiYcDBIaICIkFgoCAQINKzs9LQU6XGZIEAcNCQABEBQOBgIAAgQGBAMPJzc1KxMYPlpaSB4DFRsXBxMlMz9DNR0JAAECCR0hGQQqOjo2JhIIBggKCg4SFA4MAQ4cIB4WBR0pJRcAASI0LhIbQUlJNRWwAQQIDA4UDgsdIRcHER8pLy07T1FDKwsBAAEGChBCYmZKCE+JAZ8BkQFfFQQUGBADFjBAQjgUFScpFxIwPjYeCREPCQcBECgsIAUzS1VNNRdBb3lhLxRCWFIuChgqOkRMLhgGCA4NSWFjRQ84YGhQIC5ASkQ0Ljg4LhwNRWVrUy0pKyUXAAEFISUVBkCSAbYBrgF8HDlld2MxCiw8PCYMEhYaIiAMAwkHAxE7T083BFiMAZIBbBgRGxkPBhACAAEFBQIIDAgCCy9DRTcXHFJoakgUEy0vIQsPHyszOyUDEBgQCzVDOxcgNjgwKBICCAYGCAwSGBYKAhAgJiISDSk3MyEINExKLA87UVE7GaYBAgQIDA4IEx8lHQcPGyMlJTNJS0EnCwcBAgQOPGRmTgxFhQGdAZEBXRUIGhoQARIsODosBh81Mx0MLkA6IgUPCwkDARYsNiYIJ0NLRTEPQ215Yy0cSl5WMAgUIiw4OiIEAwUMEUNdWT8FRm5yViIoQEZALCg0MiwYB0FbX00jISMfEwABCyUrHwABPJIBtgGsAXIaM2FxYysQMEA8KAwMFBgYGgoHDQ0AARM1S0szBmCQAZIBaBYPHR8TAAEIAAEHBwcBCgwKAgspP0M1FxxKYlo+ChstMyMLDRcfJSsbCB4gFAs3TUEjFiosKh4OBAABBAQEChQUEgwCEBogHBANKzUxGwY2TEwwBy9HRTcVXAEEBAYEBA8bGxUJCxMXGRkjMTUxHwkHAwUBBihCSDIINV9zaUUTBg4SCAMIHCQiGgEhKysXBCIsLBgDCwcFBQEOJigiBhUpMS0dCy1NU0UdFjhGPiYGChQcJCgSAgUDCAUrPTklBjpYWkYaJC40LiIeJiggFgMlPT0tFQ8TEwkCBRkhFwABLGqEAXZSEh9BSUEdECYwMBwKDg4QEhIEAwkFAQcjLy0hCkZmaEoODRMXDwEAAQMHCQUDBAgGAgkdLS8lERAyPDwkAhkjIx0HBw0TFxsLChYaEAsnNzMbCBgcGhIGAAECAAIEAgwMDgYCCBIWEgYJHSUhFQgkNjQgAAEdKS8lDSsABQEFCwsJBQUJCQsJCw0PDQkLCQ0JCQcCBggCBxMdIR8ZCwkFBQUJBwMDAQEFCQ8NCQEEBgYCAQEBAAECAgICBAICBQcJCQMAAQEHCwcAAQYOEA4IAAEEBgoMDA4KCgwMCAABAwECDhQaHBYSFBgYGBIQEhQSEAoAAQEBAAEEAgICAgYEAgECBBAaHBoSDAEBBwECCAwODAoICgwODAgIBAIEBgYAAQEBAgoSFhQMAgIBAAEDAwMFBQUFAwMDAQUDCw0NCQcAAQQGBAABBwsPDQkFBQUFBQUBBgYGBAEJBwsHAAECAgICAQEHBQUFAAEBAAEDAAEBAgQEAgQDAwMFAwABCAwKCgABBwkLBwWVAQYICgYCBwUBBQMHDxUVEQkGFhwYCAsdJScjEw0JCQcJBh4qIg4PFxsZEw0hNzkxHQABGCIgEgkpMTEdBDRQWkgcH01hWTcHDRELCQg0WGJQJgchKycXAhIaGhQIHjI4LBYGCAoKDgE1TU0tBCAmKB4ODhgcHBQSJC4uKBwIBwsHARAWHBQMCy05MR0AASI0PDYcBx0rJw8MJDIyJg4KDBAMDAYGBggOChMlJx0DHjY4LgwLCQsHBwMLDw0JAwABBAIBAAEPHScrJx0bGRUNCQsPDQcGFiAmIBAGIiwuHAcLCQkFAxcvPTUjERsfGxMDBAgICgQSJiooFgEAAQMAAQQIBxcdGwnDAQYQEA4CCQcFAQcJGR8jGQ0SLDYuFg0rPUEzHw0LBQUHFEBUSCQTISUjHRE1V2NXLwguRD4iDz9bUy8QYJgBogGEATg5iwGtAaEBXRUZHx8RCFyaAaoBjAFCDztNRysEICwqHgYsTlRGHAIAAQQICg1llwGRAV0DKjo2KhAKHCQmHBo2Sk5CLAoNFxUDEiIkIAoZUWdfOwE2VmRSLBE9S0cnEDpSUjoUDhQUEA4EBAQIEBAhPUUxBTRaZEgUCxENCwUHDRMVCwMGCAgIARUzRUlBNTMtJRsNExcTBxAsQEQ6IA42UkwsBxURCwsDI1NlYT8dMTk1IQUKEBIQDCREUEYmAAEFAAECCBINJzMtE8MBBhAUDgENCQcHCQsbJSUfCRY0PDYaDTFFST8fFQsHBQMcTlxUJhMlKykdEz1jcWM3BjJIRigPR11XLRh4tAG8AZgBQD2bAccBtQFzFSMnIxUIarIBwgGeAUwPP1dPLQQiLjAaBipSWkgaBQEEBAwPdakBpQFvBSw8PCgKBBwmJh4aQlZeUjgQBxUTBRIiJhwIH1t5a0UBPmJsXC4TQVdPKxBEWFZCFg4UFBYKCgQEDBgUHT9FNQc8YGZOFhMPDw0FAw8TEQsAAQgICgYCFz1RW1VJQz8xJRMXFxECGjxSVkgkED5SUCwLGRUTCwMnW3VvTSU9Rz8rBwwUGBYSKlJeTiwAAgEIEBYFKTcvE5EBBgwKBgURDwsNCw8XIx8XCRIsNCwWDS09RTkjFQ0LAwMaPk5AGhUjKScdDzVZZ1k3ASY2Nh4LNUU7GSZ2oAGqAYQBNjOBAaUBnQFhGR0jIREGXpYBpgGMAUIHLz8/JQQYJCASAx4+RDQQBQECCAoHW4kBhwFZASguKh4AAQEQHB4YHD5YWlY+IAgDBwEQGBoQAhtRYV05AjZUXE4mCzNDPyEONkxKMhIQEBQODggKDhQgHAsnMSMFLEpKNgwPEQ0JBwEHCw0HAgYKCAQAARc5U11fVVNJPS0ZEw8BDihGWFZEJgosQDQaERsZEQsFH09lZUsrPUU/KQUMFBYWEipKVEYkAgABCg4WHAIVIyMPKwABAQEJDRMTFxERERMREQkDCBIYEAYLGyktKR8ZEwsHAAEMGBoSARcdHx0VDyEzPTkpEwABCgwGAwsHAhYwVGRgSB4VPUtLMxMVFQ8LCC5OVEgsCgcTEwsAAQgGBAMNAg4UDgABBwABBAoQBh0vMRkGEhQMAAEJDQMGChAWNEZOTEQyJhgQCgQIAgEHFSctJxUEHi4sJhYEDRURCwocICIaDggOCgwIDBQcIiYmFAQBAwABChQUCgcNDQsFAQABAQECAAECBgICAgMRL0NVW1tZU0cxHQkGFCY2RkpENBoCDAoGBxcdFxMNBQ8rOz83Ky8zKRkDBg4SEhAYLCwiEgABCBAaICQWCgIBAToNIS8tJQ8WKioYBxcdHRUJCx8lIxcHDBAQAhMVEQ0DAgUbKSsjHzU/PS8RDBoeEgstQ0k/JQEiNEJCPCwiFAwEFDRANhwHAgoODgQnQ0UxBSpESkImAAEJDREVFR8lJx8TERUTCQIcSFxcRBYnTVlNLREJAwQKFCo0QkREVFpUPhoDFRsbFQ0BBAgMDAIHBQUIFCowKhwCExkZDQICAwEDBBo4Sk5CLComHhICITtDNx0JBQEBAggaJCQcCA8dIR0JBxMjNUldYV1TPR0MMkhQSDgoHhAEAxcfIyMfFQ0JAwABAQcPFR8nNzcxIQMQGh4aEgwEAgIDAwMAAQYSJCwwLiIOaBtHXVk/AVSIAYoBXgYtOz0xFSNNYVc1AjRSUDQFDwsDAAEKET1VVTknWXl7WRc4bHhcGEV/lQGHAU0CNFJcVjwaAAENDwc8kAGwAaABWAYYKi4iAnHFAdMBoQExSIoBogGQAVIECxEXFxszRUc9Ix83OzERLpQBzgHKAY4BHmW7AcsBqQFPDQEEDBAWJDI4QEpyigGGAWgqDScxLSEFChYcFg4NHyMZBShOYFg2AzNJRS0FAxETDQEoXn5+Xi4qKCIUAUV1f2ktBwIGCAgQOE5KNA4nQ009GwIGAAEVMVVnaV1JISZaeHJUJAgLEQsHHS8xKx0JAAEICAYGCggDDy1JWVU3CR40ODIYAAEHDQ8JDR0nHwcaOkRGNhZuI1dtbUkEdK4BrgF6CDFJTT0dK2V3b0UGSG5wSggLAwIGDhNLY2FBJWeRAZEBbRtQkgGmAYABKEuXAbUBowFhBDpcZlg4ChEdHQ1MtgHiAdABdAgmODosApEB+QGRAtEBSVKoAcYBrgFkBAkTGRkbPVNZRykpSVFDHzK4AfgB9gGsASKBAd8B/QHRAV0HAg4QEhYgKjI6QnicAZoBdC4RKzUzHwMWICQeDBEpMSMHLF5yZj4HO1lXNwcLExsRBS5sjAGKAWAoJCQeEgFJhQGRAXU1AAEGCgwKFEJYVj4KK09XRx8IEhAAASNLX2VfSSMsaoABeEoWDSEhFwshLTUrGQUMEhIIChISDAcnUWNhQQsmPkY2GgMTFxUPDzE7NRkUOExOQBhIH01lYUUCap4BngFoCDFDST0bLV1zZ0ECSGhqSgoAAQIGDBAPP1dRNRtbf4UBYxdKjAGcAXooQYcBowGVAVkCNlBaTCoEFyMfDUiqAdQBvgFqDiQ2NigAAYEB5QH3AccBRUyYAbQBngFYBg0RFxkbN1FRSScrR1FDIyimAeYB3AGcAR5xywHjAbsBVQEIDhISFBgiKC44aooBiAFkLA0jLScXAhgiIiAOESUnIQUqVGRcNgU3S0szBQcVFRMDLGB2dFAeHBwWDgE/bXtnKQIKDAwKFDxMSjAMJUFLPxsIFBQGEztNVVNDIyBYal42AhklJxUJGyspIxMAAQ4UEgoIFBYOAR1DWVU9CyQ6PDAWBRMXFxERL0E7IQ4yQkQ0FgURLz1BLQE2VlQ0AyMzMSsbHz1LQykBKEBCLg4CCAwQEAABHSkrFQsvSUs7ESxSWkgYJU9hVzMCIDAyLBoCDxMTBSpmfnBCChYiIhoCSYEBkQFxJyxaalo2AgcRERcXKzc7MyMlNTkxGxJihgGEAV4YPXGDAW0tAggOEBAOFBYaHiJEVFJAHAERExMFChQcHBYOAw0VCwEeNjw2HgABGyknGwEDBwsFAAEaOEhCLBAQEAwIAAEjOUMzFQQICgwIDCIuKBoGESEnIQ8ECgoCDyUvNzkxIwgiMCYSCRcfGxMFERUXFQkECg4OBAgMDAoAAQ8nMzMjBRQgJBwKBQ0RDwsPIS0pGwIaJCYeDHECBgQCBxMXGxsbFQ0HBwkLExMREQ0HDQkHAAEEEhYYGBISFBIUEA4MCAIAAQUPFRcTCwEAAQYEBAQMFBAMAAEJCw8LAwACAQABBgoKCgoICAwUEhAEAQkJBwUBBwkRExUZHyEfGRkZGRMNBQQQFBQODgoMDAwMDgoMDAgMCgwKCAQEAgQIChISFBAMCggKCgwODA4MDAoMDAgGBggKCAgECAQGBgYEAAEBAQQIDgwMCgACAQEBAAEIDhAOCgQDBwkBAgoODgwEAwkNDQ0JCQkLExktMzUrIxcXEQ0HAwABAgIGAgYGCAYGAAIBAQABAgYICAQCAAIBAwcPExcVDQcJBQcFAwIKCgoGwwEaOEg+HB9RaWlLIRIsNCgGDw8NCQkTN01HKwQiNDgwHhwwOjgsICguKBoDQXF9aTMILjo6JA4oLi4QFTdHRzMVEzM7KQkiHhYQCgpIcn5eHC9TXVEtBQkPExMTJS8zKyMVFxMRCwMEChAQFjJOWFA4IhoWDggIAgABBQkRKzs1JQkcLjg0IAYLFRMHDBYcHBgUCgYAAgQIHCgmHAQCCgwMCA0jLyUPFigyLBwIAQUFBQMMIi4oGgMjMy0bBCQ4PDAUCxkfGw8KJDAuGBNflQGlAY8BVzEnHRcLAAEMFhoUDhIaGBQKAQkLBQQWNEJEMAgJERcbGzNRWUsvBQwUFgwBFC40LBLhASJYaFoqJW2RAY0BbSEgSlZEEBENCwkHF09tZ0UEMkZKPiYiPkxMPi48QDwmAAFbnwGzAZcBTQw+VlQ0EDBCMhApWW9rVR0fRU85AzYyJBoQEGCiAawBgAEeS3uNAXdDBwkTExMVJTs9OSEZFRMPCQEIDhIUFkh0ggF8VDIqHBYKBAULER0lS19dPxEiQkxCKAABGyUlEQocIiQiGAwCAAEBAAEMKjI0IAQAAQoKDAoTN0E3ER48RkIsCgUJCw0LDi44NCAHM0M7IwY0SlBAGgsdIx8JGDxQTDAJdccB4QHJAYEBQzkvHREBGCImIhgcJCYcDgMLCwUIKlBoZEYODx0nKy1PeYUBc0UFGCYkFgMiRE5AGs8BJFRmXCYncZUBkQFrJSROWEISEQ0LCQcXT2tpQQY2Sk5CJiZCUFJENEBKQCoCXZ8BtwGdAVEKOFZSMBAsMiQCO2l9fVsjGT9DLQ5KQjQkGA5gnAGiAXYSU4cBlQF/RwULDxMTESc3PzUlFRUTDQkCCBASFBZQfJIBigFkQjIqGA4BCxchKTVZb2VLFSJESkImAR8rJxUIHiYoJBgOBgADDCY2LhwAAQEECggIETM/MQ8oRk5ELAgDCxEPDwgoNjIcBS09OR8MNEpOPhoJFx0TASJGVFA0BXHDAecB2QGRAVdJOSsXAAEYKi4oHCIsKCAOAQcFAAEWMF5yakoQFSUvNztZiQGVAYEBSwcaJiYWBSBGUkAciQEWOkQ0FCdZcXNTHxQ0OiwIDw8LCwcRNUtFJw4yQEI4HiI4SEpANEA+OCYAAT91hwF5QQUiNDQiChAQAAEdR2l5bVEhDRsXAAEsWlBEMhwQPmJkPgVPc3dlNwcHDQ8PDx8rLy0fFRMRCwUBCAwSEhRCboIBfmRKQDAiEAMTIy03PVdhXT8RHDA4LhoDGSEdDwwaIiQgGhAIBgQCChoiHhAHBQEEBAQLHSEZBiw+QjomCAcLDxMPAAEWICASARkjHw8QLDo6KhIDBwkDDiQ8RDwmBVGVAbkBuQGJAWFVSTUbAAEWJCooHiQoJhwMBAABCBIgNlRgWDoIEyk3PT9ZeYEBcUMHDhgWCAkSLjowEhkCAgABBREhKzEvJxUHAQABAQsPDw8LCQULBQMMGCImJBwYGCgyNjYyMjAoFgQVJS0rIxMJAwIGBRkvP0tVWU9FMQ8MHjJGWGJYSDggCAYCCRUrPUNDNRsBAwUJCQsNERMVExUTDw0FAQgMDg4SJkJQWFhSRjgmEgUfLzs/QUE9MR8FDA4MDAQBBQcDBAwQEhYUFBQUDgwIAgIBAwcNCwUFAAIECAwYICosKB4SAgEDBw0PDQkBAQQGBAQIEBQaGhgSBgIIEBYaJCQiGgwDJ0NbaW1hX085HwMMFBocGiAeGhQICBQeJi40ODQsHAQXKzc/RUlNTT8lCQUDBQ0TCwEBBAICBREVHR0dHyEdGQ8TFxUVERMVGRcPAxAYJCIcDAMHAwQePEhMPjRARjwqCAMDCQ0VEQMICgYRZ5sBqwGXAWEZFjIyFhY8VmRmUhoJHRkBCAoEDSVLZ2tXMQMAAgEFAxIoKiAAARcfHxkPBB4oKCAOEBYmNkZGNCQUCAsxTVdRPx8FDBAMBggKBAIAARAcIBgMCxsdEQIYGhwUDAEbKSslEQQSGBQKCBAaICgmEAIFBQIeMjgsBiM1PS0VBAgKDA4WIigkGAwBAwABBBIeHh4UCgMFCxktS1tPQS8XBxcbFwMWIiIgFgwMIiw2NDAmIBQKAAEfN0VLRz05KyMVAAEaJCAUEUNnZ08hAQcRGxsbGyEdHRkPExcbFRETHyEbEwEWKCwqGgUdJR0HJEpeXkgyTFZSNg4EAAIHDwUOGhwUHZkB3wHtAcUBZRhsjgF+NhhAWGZiPB1VZUsPHiwoEBdViQGVAYMBSQMEBAQAAQMuUlpCEhknKyUVCixAQiwMAQABCBo6OCAKAgENQV1lWzsDJDQyGAgODAgEAiIwMiYMIT9FMwkYICAaEgctQ0M1FRIoNCoUBgwWHiIcAxUbFQE4YmpUFi1TW00nAgABAgQMFCo4MigOBREVDQIcIBoaDAMKFAwLM0c9Lx8PCy9BOSEOJCgmHAwQJDI4NCoeEAwCASE/TU9DNycdEw0INlJOLgdrqQGzAY8BNwUDERcZGxUZGxkTDREXFxcPER0fHxMAARgqLigYESkxKQ0iTmBaRipIVlI4DAQIBAABCwEYIiIWH5sB6QH3AckBVzKOAbABlgFAGDpQVlIsOXWBAVsRJDg2IAtRhwGfAYkBTQUGCAYCAjReaFIYFycrJRcKMkZCMAwLDwcKKCoMAAEJBw0/W2NTMwo4SDwcCg4OCgIGJDg2KAgrSVE9ERQeIBwOBzFHTTsTGDY8MhQEDBIUHBQLIyUdAAFAbnhcHitVY1UrAAIFAAEGEiw4NigQCRUbFwUUGhwUDAIQHiAKHzcpHREHDTdNSSkIICYkHAoOJC40Lh4SDAICAyE5R0c7KRsRCQMKRFxcPAVxtwHJAaUBQwMFCxETDw8RExMLCQsTEREJDRUZFw0AARQgJCAODSctIwsaOkpIMiA0RDwuCgQGBgABBQIUHB4UGXu3Ab8BmQFBMH6WAX42Eig6PjoWN2VtTw0cMjAeAztreW09AwQGBgICLlBWQhYRHSEfDwYqNjYmBgkTDQABGBoGBwsFDS1HSz8jEjI+OBYGCgwKAAEEHiwsIAYjPUM3DxAYGBQMByc7Py0PFiw0KBIEBgoQFAwPHyMXATZaYkoaIUFNQyUBAwEDBAwiLCoeDAUTFxUHDhIUEAgCEBweEA0jFw8HAwktPzsnBBYeHBQIDBgiJB4WDAICAQMXKTUxKxkPCQEBDDZQSjIBV5MBpwGHATkBAQUFBwUFBQcFBQEHBQcFAwMJCQkHAggODgwICQ0TDwcMGB4cEgwWGhoQBAQCAgIBAAEKDgwGCTFJTz0XGDRANhYGEhQYFAgZKysjBQ4UFA4AARcpMSsbAAECAgICAhIgJBwMBwsNCwcEEBgWDgQFBwkAAQgIAAEDAwUDExkdGwsIFhwWCgIEBAQEAAEOEBQMAAENGRsVBwYICggEAREXGRMFChQUEggAAQQCBgYEBwsPCQABFiQqHgoLGx8bDQABAQMAAgQOEhIMBAEHCwkBBAgGBgQAAQgODAoFCQcDAwABAxMZGw0AAQoKCggGAgoODgwIAgIBAQABBxETFQ0JBQADAhggIhIAASE9QzkVqQESCBAKCgoICggKBAYGBgYEBgACAgICAgICAgABAgABAgIABAEAAQEAAgECAQABAQIAAQMDAgEFAwUBAQkDBwEFAQkJAwcFAQENDQABAwQEAgICHwYBAAIIAggBAgYCBgABAgYCAAIEAAEEBAIEAgIAAQQEAQQAAQQAAQICBAEGAQYBBAQBBAABBgACBAABAgABBgABAgQCAAECBAABAQIIAgACBAICAQABBAEEAgBVAQEDAwUFCQcHCQcJDQ8UBQYFAgEAAwEBAgABAQACAwMCAgACAgADAQIBAAIBAAYCAAIEAwIBAAEDAAEEBAMAAQYAAgMBCgUCAwYDAgYFAgABAwABDAABCwECAQABAQEmHwIBAAEBAgMEAQICBQEEAgMAAQIAAQIDAAECAAEBAAQCAQIBAQQBAQQBAgECAQEEAwACBAEDBgEAAQECAQABAQIBAgIAAQEAAQEAAQIBAAICAQEEAQEAVQIAAQIAAQIBAgIDAgIAAQIAARAAAQEAAQECAQABAQIAAQECAQEFBAACAgABAQADAQABAQACAQAFAgAEAQIAAQICAgEDAgIAAQECBgABAwABCAMAAQMCBAMGAwACAQEMAQMHAAICAAEBGhkEAwIABAECAQYDAQAEAQIABAEGAwABAgMCAAECAwIAAgEAAgQFAgQAAQEDAgAEAgMEAAEDAgIAAQUCAggHAAIBAgEAAgQBAQEAVwICAAEBAgIAAgQFAgIAAQ4AAQEAAwEAAQQDAAIFAQICAAECAAEBAQQDAQEAFgIEAQADAgEGAgcCCAEBAAEBBgECAAECBQACDAEJAAEDAgACAR4ZAgUCAgEBAAEBAgADAgECAQQAAQUEAQAEAQABBAMCAAEBAAMBBAEBBgMAAgIAAQMEAQEEAwIBAgABAgMAAQQDAgAFAwIAAgEAXAQAAwEGAwQBAAEEDgECAQABAwQCBQQHAQIGAwIBAQADAwAdAgIAAgEGBgcAAQYBAAEBAAECAgABAQABBAUAAQoDBQEAAwIAAQEEAQABAwIAAQIBAAMBAgACAgMAAQIBAgABAQEEAAQBAgMEAQIAAgEAAwECAgABAQACAQABBAMAAQIAAQMCAgEABgIAAQEBAF8EAgACAQABAgACAgwAAgECAgMAAQEFAgIAAwEAAgEBACICAgABAgQFAgYFBAEAAgIAAQIDAAIEAgMBAwABAgACHBsBAgACAgMEAAIDAAECAAEBAgACAgMAAQICAQACAwIBBgMEAwQFAgACAQABAgEEAQABAQABBgMBAgIDAAECAAEBAgECAAEBAAMBAGICAAIEAgEBAgACDgABBQQBAQQHAQQAAQIAAQMCAQEBACgECAUAAQYDAAEDCAMAAQQDCAUDDAEFAAEDAQIBAhgVAwQBAgEBAAICAAEBAgEAAQIBAAICAQIBAAICAwIBBAMAAQIAAgECAAECAgEDAgEEAwABAgEBBAEABwQFAGcCAgECAAECAAEBAgoBBAABAwIFAAECAAIBAAIDAC0GAQABCAUGBwQCAQYJBAEAAQwCDRAFBQUCBAEBBAwCBAkJAQQCBgcBAgMCAw4PAgECAQICAAIDBgABAQMCAQABAgABAQABAgECAgMCAAEBAgEAAQQDBgMCBQIBAGkCAAECAQQBAAEEAQgEAQABAQMAAQIMDQIAAQMBAC8EAgICAgYJBBQbCAABDg8IFAEkPygBKzoHKwMOHQEEJBcGAw02AysIEhECAgMCEBIjHAgFHx4GHxgBFRgbHBUOERYVEA0KDxQDDQwJCA0KAQMAAQQFAGkEAQIAAgIAAQIMAwIFAQQAAgUIAwMAMgIBBgUIAwABBQoUGQUKBgkDAgQWChkAARliRwUAAQoFEBEFEAsRDhYGIQwLNCMFEgcOCwcGHCMBAAEGAgMAAQIFAQACAQIBBAIKDwIKCwACBAMAAQQAAQMAawIAAQIBAAMIAgUBBAABAQABAwA3AgYHEgABBxQRHgMIDwABFDE+OQNAOzw3QhMaDQgpKAMREhsHDgsOCAUrJgQjCxIRAAICAQABIB8AARoZFhUWERATEA8MCwIBAAEEAwgHAgEAAQIBAGwEAQECAgECCgUBBAQFAgEDAPIBBAACAQIAAQQBBAIFAgABAQEA8gECAgIAAQEAAQIEAgMAAgMA9QECAAICAAEGAAEDAgABAwD2AQIAAQECAgYBAAIDAPgBBAEAAgICAgUA+gEEAAEDBAEBAPoBAgACAgQAAQMA+wECAAICAQD8AQIBAgIBAPwBAgIBAP8BAgCBAgIA/wECAQD/AQIAgQICAIACAgCBBAIAjBoBAIIGAQCABAECAP8BAgCCBAEA/wEBAgD/AQIDAIACAQD/AQMEAP8BAQD/AQEAggICAP0BAQEEAQCAAgEA/gECAQUA/gEBAwgA/AEBAAEDBgD9AQIDAgEBAPwBAQICAwD9AQMEAwYA/AEBBAMCAAECAPsBAgMCAgABBwD7AQECAgIHBgD7AQICAwUEAQD8AQEDBgEBAP0BBAkEBAD7AQEAAQEIAwD8AQMAAgICAgD6AQUEAAEEAwABAQD6AQIBAAEDAgEBAPoBAgIDAAEDAAEEAPoBAwABBAABAQYDAPoBAgMDAggBAwD6AQMAAQYCBwEGAPoBAgQDBwYBAPsBBAUCAgECAwD6AQECAQYCBQEA+gEDBAQDBwgGAPoBBAEFAAEIAQD8AQMEAgEBAPsBAwYAAQEAAgMA/gEBAAEEAPoBBAEDAAECBAEA+gEDAAEEAgIFAQD9AQEBAgD+AQEBAgQDAgIA+AEBAwIEAAIHAgYA9wEBAAEEAAMCAAEBAAECAPYBAQQAAgIBAQECAwQA9gEEAAEBAgEAAgECAQMGAPUBAgECAAEBBAMAAQECAAEDBAD0AQIAAQIDBAMEAwIAAQIBAAECAPQBAgECAQIAAQQDAQIAAQEAAQIA8gECAAIBAgEAAQIBAAEBAgD4AQIDAgEAAQECAwYBAPQBAgEBAgAEAwQAAQMEAAEBAgDzAQIAAQEAAwQDAAEBAgD2AQIDBAABAgABAgEAAQIDAgD0AQEAAQIAAQICAgABAQIBAPcBAQICAQEA+wECAAIDAgIBAgIAAQECAQD1AQIAAQIBAgACAgEEAQEA9AEEBQICAgMAAQIBAgABAQIAAQEA8gEBBgABAQECAAECAAIBAAEBAgDzAQICAwABAgEAAQMAAQYBAAECAQD1AQICAQECAAIFBAIBAPYBAgMCAgIAAwEBAgD3AQIAAgIBAgABAQIBAPYBAgADBAEAAQEA+wECAAICAQABAgEA+AECAQQCAAEBAgEA+QECAAEBAAICAQD7AQIDAgABAQD9AQIAgAIBAIACAgCAAgIAgAICAP8BAgMAgAICAIEEAgCDCAIA5hoDAAICAQABAgIA+QEBAAMCAgEBAAECAAEDBAD0AQQAAgMBAwACAgABAgIBAAICAPUBAgABAQABAwEBAgABBgABAQIA8AEBAgECAQQCAQACAQACBwABAgQAAQIBAgDtAQMCAgIFAAECBAMCAAECAgEDAwABBAQBAOwBAwABBAUEBgEAAQEEAAEBAQABAgABAwABBQABCADrAQcCBAEEAQUEAAICAQYDAgECAgABAQMFAgYCAOgBAwIBAQABAQYCAAIBAgMEAwQCAQACAgMBAwIGAOgBAgEAAwEBAAEBAgQAAQMCAAEBBAEEAAECAQMDAQYCAOYBBAACAQACAQABAgEFAAEEAAEBAAICAAECAAECAAEBAQMGAAEBAgDjAQECAAEEAQAFAgEBAAQBAQABAgABAgIDAQMCBgECAOYBAgABAQAEAgEABAIBAQACAgABAgEDAgMGAQIA4QEBAAECAwICAAYCAQACAQICAQAFAgEBAgcIAQIA4gEBBAEAAQIABAECAgABAQABAQABBAMABAICAwEGCQgBAOABAgACAwADAgABAgECAQACAgEAAgECAAMCAQADAwQHCADfAQIAAQEAAQIBAAIDAgQEAQIAAgICAAECAAMBAAEBAAICAAIBAAEHAN8BCgABAgACAgEAAQIBBQECAQACAQACAQDinQEBAIACAQD/AQUA/wEDAAEBAP0BAQMAAQMA/AEBAwACAgECAPkBAwABAwQAAgECAPkBAwECAQABAQMEAPcBAQMCAQIBAwQDAQYA9QEBAQEAAQIDBAEBAwABBAD1AQUAAgIDBAMBAAEDAAECAgDyAQEFBAACAQABAwMEBQUEAQQA8QEDAQQAAQEBAAEHAgADAwEBAgYA7wEDAgABAwEDAAIEAQIBAAEJBAICAgDtAQEBAAEBAAEBAQIAAQUGAQEAAQIFAAQGAOwBAQMBAAEBAQQBAgIHAgIBAwcCAAEDBgICAOgBAQEDAAEBAgABAQYDAgcCBAEBAQEJAAEEAgMCBAIA5gEBAwEAAgIAAQEEAwEDBgEAAgUAAQoA6aECBAACAQIAAQEBAPkBAgADAQECAgABAQABBAMA9AEDAAIEAgQAAgEAAQEBAgACAQD1AQEAAQIAAQQCAgEAAQUAAQIBAPABAgECAQIDAQIAAgIAAggAAQEDAAEBAgEA7QEEAQEBBgABAQMEAQABAQECBAQAAQMDAgDsAQQAAQMGAwUCAAECAwABAgIAAQEAAQQAAQYAAQcA6wEIAQMCAwIGAwACAQIFBAECAQEAAQIEBgEFAQDoAQQBAgIAAQIFAQACAgEEAwQDAQIAAgEEAgQBBQDoAQECAAMCAgABAgEDAAEEAQABAgMCAwABAQIEBAIFAQDmAQMAAgIAAgIAAQECBgABAwABAgACAQABAQABAQABAgIEBQABAgEA4wECAQABAwIABQECAgAEAgIAAQEAAQEBBAIEAQUCAQDmAQEAAQIABAECAAQBAgIAAgEAAQECBAEEBQIBAOEBAgABAQQBAQAGAQIAAgIBAQIABQECAgEIBwIBAOIBAgMCAAEBAAQCAQEAAQIAAQIAAQMEAAQBAQQCBQoHAgDgAQEAAgQAAwEAAQECAQIAAgECAAICAQADAQIAAwQDCAcA3wEBAAECAAEBAgACBAEDAwIBAAIBAQABAQADAgABAgACAQACAgABCADfAQkAAQEAAgECAAEBAgYCAQIAAgIAAgIA9L8BqgERBw8JCQkHCQcJAwUFBQUDBQACAQEBAQEBAQABAQABAQEABAIAAQIAAgIBAgABAgEAAQQEAQIGBAYCAgoECAIGAgoKBAgGAgIODgABBAMDAQEBIAUCAAIHAQcCAQUBBQABAQUBAAIDAAEDAwEDAQEAAQMDAgMAAQMAAQEBAwIFAgUCAwMCAwABBQACAwABAQABBQABAQMBAAEBAwABAgEHAQACAwEBAgABAwIDAQBVAgIEBAYGCggICggKDhATBgUGAQIAAwICAQABAgACBAQBAQACAQADAgECAAICAAYBAAIDBAECAAEEAAEDAwQAAQUAAgQCCQYBBAUEAQUGAQABBAABCwABDAIBAgABAgIlIAECAAECAQQDAgEBBgIDAQQAAQEAAQEEAAEBAAECAAQBAgECAgMCAgMCAQIBAgIDBAACAwIEBQIAAQIBAgABAgECAQEAAQIAAQIAAQECAAIBAgIDAgIAVQEAAQEAAQECAQEEAQEAAQEAAQ8AAQIAAQIBAgABAgEAAQIBAgIGAwACAQABAgADAgABAgACAgAFAQAEAgEAAQEBAQIEAQEAAQIBBQABBAABBwQAAQQBAwQFBAACAgILAgQIAAIBAAECGRoDBAEABAIBAgUEAgAEAgEABAIFBAABAQQBAAEBBAEAAgIAAgMGAQMAAQIEAQAEAQQDAAEEAQEAAQYBAQcIAAICAQIAAgMCAgIAVwEBAAECAQEAAgMGAQEAAQ0AAQIAAwIAAQMEAAIGAgEBAAEBAAECAgMEAgIAFgEDAgADAQIFAQgBBwICAAECBQIBAAEBBgACCwIKAAEEAQACAh0aAQYBAQICAAECAQADAQIBAgMAAQYDAgAEAgABAwQBAAECAAMCAwICBQQAAgEAAQQDAgIDBAECAQABAQQAAQMEAQAFBAEAAgIAXAMAAwIFBAMCAAEDDQIBAgABBAMBBgMIAgEFBAECAgADBAAdAQEAAgIFBQgAAQUCAAECAAEBAQABAgABAwYAAQkEBgIAAwEAAQIDAgABBAEAAQECAAMCAQACAQQAAQECAQABAgIDAAQCAQQDAgEAAgIAAwIBAQABAgACAgABAwQAAQEAAQQBAQIABgEAAQICAF8DAQACAgABAQACAQsAAgIBAQQAAQIGAQEAAwIAAgICACIBAQABAQMGAQUGAwIAAgEAAQEEAAIDAQQCBAABAQACGxwCAQACAQQDAAIEAAEBAAECAQACAQQAAQEBAgACBAECBQQDBAMGAQACAgABAQIDAgABAgABBQQCAQEEAAEBAAECAQIBAAECAAMCAGIBAAIDAQICAQACDQABBgMCAgMIAgMAAQEAAQQBAgICACgDBwYAAQUEAAEEBwQAAQMEBwYECwIGAAEEAgECARcWBAMCAQICAAIBAAECAQIAAQECAAIBAgECAAIBBAECAwQAAQEAAgIBAAEBAQIEAQIDBAABAQICAwIABwMGAGcBAQIBAAEBAAECAQkCAwABBAEGAAEBAAICAAIEAC0FAgABBwYFCAMBAgUKAwIAAQsBDg8GBgYBAwICAwsBAwoKAgMBBQgCAQQBBA0QAQIBAgEBAAIEBQABAgQBAgABAQABAgABAQIBAQQBAAECAQIAAQMEBQQBBgECAGkBAAEBAgMCAAEDAgcDAgABAgQAAQELDgEAAQQCAC8DAQEBAQUKAxMcBwABDRAHEwIjQCcCLDkILAQNHgIDIxgFBA41BCwHERIBAQQBDxEkGwcGIB0FIBcCFhccGxYNEhUWDw4JEBMEDgsKBw4JAgQAAQMGAGkDAgEAAgEAAQELBAEGAgMAAgYHBAQAMgECBQYHBAABBgkTGgYJBQoEAQMVCRoAARphSAYAAQkGDxIGDwwSDRUFIgsMMyQGEQgNDAgFGyQCAAEFAQQAAQEGAgACAgECAwEJEAEJDAACAwQAAQMAAQQAawEAAQECAAMHAQYCAwABAgABBAA3AQUIEQABCBMSHQQHEAABEzI9OgQ/PDs4QRQZDgcqJwQSERwIDQwNBwYsJQMkDBESAAIBAgABHyAAARkaFRYVEg8UDxALDAECAAEDBAcIAQIAAQECAGwDAgIBAQIBCQYCAwMGAQIEAPIBAwACAgEAAQMCAwEGAQABAgIA8gEBAQEAAQIAAQEDAQQAAgQA9QEBAAIBAAEFAAEEAQABBAD2AQEAAQIBAQUCAAIEAPgBAwIAAgEBAQYA+gEDAAEEAwICAPoBAQACAQMAAQQA+wEBAAIBAgD8AQECAQECAPwBAQECAP8BAQCBAgEA/wEBAgD/AQEAgQIBAIACAQCBBAEAjBoCAIIGAgCABAIBAP8BAQCCBAIA/wECAQD/AQEEAIACAgD/AQQDAP8BAgD/AQIAggIBAP0BAgIDAgCAAgIA/gEBAgYA/gECBAcA/AECAAEEBQD9AQEEAQICAPwBAgEBBAD9AQQDBAUA/AECAwQBAAEBAPsBAQQBAQABCAD7AQIBAQEIBQD7AQEBBAYDAgD8AQIEBQICAP0BAwoDAwD7AQIAAQIHBAD8AQQAAgEBAQD6AQYDAAEDBAABAgD6AQECAAEEAQICAPoBAQEEAAEEAAEDAPoBBAABAwABAgUEAPoBAQQEAQcCBAD6AQQAAQUBCAIFAPoBAQMECAUCAPsBAwYBAQIBBAD6AQIBAgUBBgIA+gEEAwMECAcFAPoBAwIGAAEHAgD8AQQDAQICAPsBBAUAAQIAAgQA/gECAAEDAPoBAwIEAAEBAwIA+gEEAAEDAQEGAgD9AQICAQD+AQICAQMEAQEA+AECBAEDAAIIAQUA9wECAAEDAAMBAAECAAEBAPYBAgMAAgECAgIBBAMA9gEDAAECAQIAAgIBAgQFAPUBAQIBAAECAwQAAQIBAAEEAwD0AQEAAQEEAwQDBAEAAQECAAEBAPQBAQIBAgEAAQMEAgEAAQIAAQEA8gEBAAICAQIAAQECAAECAQD4AQEEAQIAAQIBBAUCAPQBAQICAQAEBAMAAQQDAAECAQDzAQEAAQIAAwMEAAECAQD2AQEEAwABAQABAQIAAQEEAQD0AQIAAQEAAQEBAQABAgECAPcBAgEBAgIA+wEBAAIEAQECAQEAAQIBAgD1AQEAAQECAQACAQIDAgIA9AEDBgEBAQQAAQECAQABAgEAAQIA8gECBQABAgIBAAEBAAICAAECAQDzAQEBBAABAQIAAQQAAQUCAAEBAgD1AQEBAgIBAAIGAwECAPYBAQQBAQEAAwICAQD3AQEAAgECAQABAgECAPYBAQADAwIAAQIA+wEBAAIBAgABAQIA+AEBAgMBAAECAQIA+QEBAAECAAIBAgD7AQEEAQABAgD9AQEAgAICAIACAQCAAgEAgAIBAP8BAQQAgAIBAIEEAQCDCAEA7NoBAgCAAgIA/wEGAP8BBAABAgD9AQIEAAEEAPwBAgQAAgECAQD5AQQAAQQDAAICAQD5AQQCAQIAAQIEAwD3AQIEAQIBAgQDBAIFAPUBAgICAAEBBAMCAgQAAQMA9QEGAAIBBAMEAgABBAABAQEA8gECBgMAAgIAAQQEAwYGAwIDAPEBBAIDAAECAgABCAEAAwQCAgEFAO8BBAEAAQQCBAACAwIBAgABCgMBAQEA7QECAgABAgABAgIBAAEGBQICAAEBBgAEBQDsAQIEAgABAgIDAgEBCAEBAgQIAQABBAUBAQDoAQICBAABAgEAAQIFBAEIAQMCAgICCgABAwEEAQMBAOYBAgQCAAIBAAECAwQCBAUCAAIGAAEJAIGEBACBhAQAgYQEAIGEBACBhAQ=", + "Layer1Texture": "assets/StartingGround.png", + "Layer1Color": "255;255;255", + "HeightBlending": 0.5, + "TextureVariation": 0, + "TextureSize": 128, + "CliffLayer": "3", + "Layer2Texture": "", + "Layer3Texture": "", + "CliffAngle": 45, + "CastShadow": false + } } ], "objectsFolderStructure": { @@ -1371,7 +1362,7 @@ "objectName": "Obstacle" }, { - "objectName": "Ground" + "objectName": "Terrain" }, { "objectName": "ControlsToggle" @@ -11230,7 +11221,6 @@ "name": "PlatformerMultitouchMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -11484,7 +11474,6 @@ "name": "Platformer3DMultitouchMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -11659,7 +11648,6 @@ "name": "Shooter3DMultitouchMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -11823,7 +11811,6 @@ "name": "FirstPersonMultitouchMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -13028,7 +13015,6 @@ "name": "PhysicsCar3DMultitouchMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -13241,7 +13227,6 @@ "name": "TopDownMultitouchMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -27661,6 +27646,7802 @@ } ], "eventsBasedObjects": [] + }, + { + "author": "", + "category": "General", + "dimension": "3D", + "extensionNamespace": "", + "fullName": "3D terrain", + "gdevelopVersion": ">=5.5.222", + "helpPath": "", + "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLXRlcnJhaW4iIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTQsNkwxMC4yNSwxMUwxMy4xLDE0LjhMMTEuNSwxNkM5LjgxLDEzLjc1IDcsMTAgNywxMEwxLDE4SDIzTDE0LDZaIiAvPjwvc3ZnPg==", + "name": "Terrain3D", + "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", + "shortDescription": "A ground with hills and valleys that can be sculpted and painted in the editor, with collisions.", + "version": "0.1.0", + "description": [ + "A 3D terrain object for open worlds, adventure or racing games: hills, mountains, islands or valleys, with painted layers of grass, dirt, rock or sand.", + "", + "- **Sculpt and paint in the scene editor**: select a terrain to show the tools (raise, lower, smooth, flatten and paint 4 layers).", + "- **Start from a relief** (hills, mountains, island) or **from a heightmap image**.", + "- **Steep slopes automatically show rock** (or any layer).", + "- **Natural borders between layers**: stones and tufts of a texture cover the gaps of the other where layers meet (height blending), and the repetition of textures is made less visible (texture variation).", + "- **Physics**: add the **3D physics** behavior to the terrain. Characters and objects collide with the ground exactly. The terrain is always static.", + "- **Pathfinding**: add the **floor/obstacle for pathfinding (navmesh based)** behavior to the terrain, so that characters with the navmesh pathfinding behavior walk on its hills and around its cliffs (in recent GDevelop versions).", + "- **Shape it with the Edits property**: a list of raise, flatten, smooth and paint strokes written in JSON, shown in the scene editor. It's readable and can be changed by hand or by an AI agent, like the relief, the heightmap and the other properties. Actions only change the terrain during the game.", + "- **Change it during the game** with actions (raise, lower, flatten, smooth, paint, along a line for paths and rivers) and read the ground height, slope or layer with expressions.", + "- **Grass**: add a **terrain grass** object over the terrain. Thousands of blades sway in the wind, grow where a layer (like grass) is painted and where the ground is not too steep, and are pushed aside by characters. Sculpt or paint the terrain and the grass follows.", + "- **Forests, rocks and bushes**: add a **scattered models** object over the terrain. It places hundreds or thousands of copies of a 3D model (trees, rocks, bushes, flowers...) on the ground and draws them all at once, which is much faster than an object for each. Copies grow where a layer is painted, away from steep slopes, and follow the ground when it's sculpted. Add or erase them with the brushes of the scene editor or with the Edits property, give trees trunk collisions, and cut them down with actions. Ready-made forests, rocks and bushes are in the asset store.", + "- **Tunnels and caves**: make holes in the ground (and fill them again) with the brushes, the Edits property or actions, and put a 3D model of the tunnel or the cave (with a mesh collision shape) under the ground. Nothing is drawn in holes, and characters, cars and rays go through them.", + "- **Made for large worlds**: the terrain is split in chunks that are hidden when off-screen and simplified when far away, and collisions are updated only where the ground changes." + ], + "origin": { + "identifier": "Terrain3D", + "name": "gdevelop-extension-store" + }, + "tags": [ + "3d", + "terrain", + "ground", + "heightmap", + "landscape", + "open world", + "grass", + "foliage", + "forest", + "trees", + "tunnel", + "cave" + ], + "authorIds": [], + "dependencies": [], + "globalVariables": [], + "sceneVariables": [], + "eventsFunctions": [ + { + "description": "Define helper classes JavaScript code.", + "fullName": "Define helper classes", + "functionType": "Action", + "name": "DefineHelperClasses", + "private": true, + "sentence": "Define helper classes JavaScript code", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "if (gdjs.__terrain3DExtension) {", + " return;", + "}", + "", + "// Tested by the gameplay tests of the \"3D island terrain\" example (GDevelop-examples repository).", + "", + "const groundsByScene = new WeakMap();", + "/**", + " * The grounds of a scene (the terrains), followed by the terrain grass.", + " *", + " * A ground has: `getVersion()` (changing when the ground changes),", + " * `containsPoint(x, y)`, `getHeightAt(x, y)` (Z position of the ground),", + " * `getSlopeAt(x, y)` (in degrees), `getLayerWeightAt(x, y, layer)` (how", + " * much a painted layer, from 1 to 4, is there, from 0 to 1) and optionally", + " * `getTintAt(x, y)` (the painted tint, as red, green and blue from 0 to 255).", + " * @param {gdjs.RuntimeScene} runtimeScene", + " * @returns {Set}", + " */", + "const getGrounds = (runtimeScene) => {", + " let grounds = groundsByScene.get(runtimeScene);", + " if (!grounds) {", + " grounds = new Set();", + " groundsByScene.set(runtimeScene, grounds);", + " }", + " return grounds;", + "};", + "", + "/**", + " * @param {number} seed", + " * @returns {() => number} Random numbers between 0 (included) and 1 (excluded), the same for the same seed.", + " */", + "const createRandom = (seed) => {", + " let state = seed >>> 0 || 1;", + " return () => {", + " state ^= state << 13;", + " state ^= state >>> 17;", + " state ^= state << 5;", + " return (state >>> 0) / 4294967296;", + " };", + "};", + "", + "/**", + " * Find the highest ground at a position: grounds can overlap (like an island over a sea floor).", + " * @param {Iterable} grounds", + " * @param {{ground: object | null, z: number}} result Set to the ground (null if there is none) and its Z position.", + " */", + "const findHighestGround = (grounds, x, y, result) => {", + " result.ground = null;", + " for (const ground of grounds) {", + " if (!ground.containsPoint(x, y)) continue;", + " const z = ground.getHeightAt(x, y);", + " if (!result.ground || z > result.z) {", + " result.ground = ground;", + " result.z = z;", + " }", + " }", + " return result;", + "};", + "", + "// The grounds or the objects placed on them can change every frame (while", + "// sculpting or moving them in the editor): things are placed again at most this often.", + "const PLACEMENT_DELAY = 0.3;", + "", + "/**", + " * Tells when things placed on the grounds of a scene (grass blades, scattered", + " * models) must be placed again: after the grounds or the given properties", + " * changed, at most every `PLACEMENT_DELAY` seconds.", + " */", + "class GroundPlacement {", + " /** @param {gdjs.RuntimeScene} runtimeScene */", + " constructor(runtimeScene) {", + " this.grounds = getGrounds(runtimeScene);", + " this.placedFrom = '';", + " this.timeSincePlacement = 0;", + " }", + "", + " /**", + " * @param {number} elapsedSeconds", + " * @param {string} propertiesKey Changes when the things must be placed again.", + " */", + " shouldPlaceAgain(elapsedSeconds, propertiesKey) {", + " this.timeSincePlacement += elapsedSeconds;", + " if (this.placedFrom && this.timeSincePlacement < PLACEMENT_DELAY) return false;", + " let placementKey = propertiesKey;", + " for (const ground of this.grounds) placementKey += '|' + ground.getVersion();", + " if (placementKey === this.placedFrom) return false;", + " this.placedFrom = placementKey;", + " this.timeSincePlacement = 0;", + " return true;", + " }", + "", + " placeAgain() {", + " this.placedFrom = '';", + " }", + "}", + "", + "/**", + " * Replace parts of the shader code of a Three.js material, with an error if", + " * a part is missing (for example after an update of Three.js).", + " * @param {string} shaderCode", + " * @param {Array<[string, string]>} replacements The searched code and its replacement.", + " * @param {string} materialName", + " */", + "const patchShaderCode = (shaderCode, replacements, materialName) => {", + " for (const [searchedCode, newCode] of replacements) {", + " if (!shaderCode.includes(searchedCode)) {", + " console.error(`${materialName}: \"${searchedCode}\" was not found in the shader, which won't look as expected.`);", + " }", + " shaderCode = shaderCode.replace(searchedCode, newCode);", + " }", + " return shaderCode;", + "};", + "", + "// Size of the object inner area. Instances are scaled from it, so these only", + "// define the default size of a new terrain.", + "const AREA_SIZE = 4096;", + "const AREA_DEPTH = 600;", + "", + "// Terrains have at most 8 x 8 chunks (so at most 64 draw calls), of at least", + "// 32 x 32 cells.", + "const MIN_CHUNK_CELLS = 32;", + "const MAX_CHUNKS_PER_SIDE = 8;", + "/** @param {number} resolution */", + "const getChunkCells = (resolution) => Math.max(MIN_CHUNK_CELLS, resolution / MAX_CHUNKS_PER_SIDE);", + "// Grid steps of each level of detail, and the distance (in chunk sizes)", + "// from which each one is used.", + "const LOD_STEPS = [1, 2, 4];", + "const LOD_DISTANCES = [0, 3, 6];", + "", + "const LAYER_COUNT = 4;", + "const RESOLUTIONS = [64, 128, 256, 512, 1024];", + "const DEFAULT_RESOLUTION = 256;", + "// Each undo step is a copy of the data: fewer are kept for big terrains.", + "const UNDO_STEPS_SAMPLES = 20 * 257 * 257;", + "", + "const clamp01 = (value) => (value < 0 ? 0 : value > 1 ? 1 : value);", + "const clampInteger = (value, min, max) =>", + " value < min ? min : value > max ? max : Math.floor(value);", + "", + "/**", + " * A rectangle of grid samples, inclusive. Empty when `minI > maxI`.", + " */", + "class SampleRectangle {", + " constructor() {", + " this.clear();", + " }", + "", + " clear() {", + " this.minI = Number.MAX_SAFE_INTEGER;", + " this.minJ = Number.MAX_SAFE_INTEGER;", + " this.maxI = -1;", + " this.maxJ = -1;", + " }", + "", + " isEmpty() {", + " return this.minI > this.maxI;", + " }", + "", + " /** @param {SampleRectangle} other */", + " add(other) {", + " if (other.isEmpty()) return;", + " this.minI = Math.min(this.minI, other.minI);", + " this.minJ = Math.min(this.minJ, other.minJ);", + " this.maxI = Math.max(this.maxI, other.maxI);", + " this.maxJ = Math.max(this.maxJ, other.maxJ);", + " }", + "", + " set(minI, minJ, maxI, maxJ) {", + " this.minI = minI;", + " this.minJ = minJ;", + " this.maxI = maxI;", + " this.maxJ = maxJ;", + " return this;", + " }", + "}", + "", + "/**", + " * The heights (between 0 and 1) and the painted layers weights of a square", + " * grid of `size * size` samples.", + " */", + "class TerrainData {", + " /** @param {number} resolution The number of cells on each side. */", + " constructor(resolution) {", + " this.resolution = resolution;", + " this.size = resolution + 1;", + " this.heights = new Float32Array(this.size * this.size);", + " /** 4 weights (0-255) per sample, one for each layer. */", + " this.splat = new Uint8Array(this.size * this.size * LAYER_COUNT);", + " this.fillLayer(0);", + " /**", + " * 4 bytes per sample: a tint multiplying the layers (red, green and", + " * blue, white for none) and the wetness (0 for dry).", + " */", + " this.colors = new Uint8Array(this.size * this.size * 4);", + " this.clearColors();", + " }", + "", + " clearColors() {", + " this.colors.fill(255);", + " for (let index = 3; index < this.colors.length; index += 4) {", + " this.colors[index] = 0;", + " }", + " }", + "", + " /** @param {number} layer */", + " fillLayer(layer) {", + " this.splat.fill(0);", + " for (let index = layer; index < this.splat.length; index += LAYER_COUNT) {", + " this.splat[index] = 255;", + " }", + " }", + "", + " getHeight(i, j) {", + " const last = this.resolution;", + " const clampedI = i < 0 ? 0 : i > last ? last : i;", + " const clampedJ = j < 0 ? 0 : j > last ? last : j;", + " return this.heights[clampedJ * this.size + clampedI];", + " }", + "", + " /**", + " * @param {number} gridX", + " * @param {number} gridY", + " * @returns {number} The height interpolated on the triangles that are drawn.", + " */", + " sampleHeight(gridX, gridY) {", + " const x = Math.min(Math.max(gridX, 0), this.resolution);", + " const y = Math.min(Math.max(gridY, 0), this.resolution);", + " const i = Math.min(Math.floor(x), this.resolution - 1);", + " const j = Math.min(Math.floor(y), this.resolution - 1);", + " const fx = x - i;", + " const fy = y - j;", + " const topRight = this.getHeight(i + 1, j);", + " const bottomLeft = this.getHeight(i, j + 1);", + " if (fx + fy <= 1) {", + " const topLeft = this.getHeight(i, j);", + " return topLeft + fx * (topRight - topLeft) + fy * (bottomLeft - topLeft);", + " }", + " const bottomRight = this.getHeight(i + 1, j + 1);", + " return (", + " bottomRight +", + " (1 - fx) * (bottomLeft - bottomRight) +", + " (1 - fy) * (topRight - bottomRight)", + " );", + " }", + "", + " /** @returns {number} The weight (0-1) of a layer, interpolated between samples. */", + " sampleLayerWeight(gridX, gridY, layer) {", + " const x = Math.min(Math.max(gridX, 0), this.resolution);", + " const y = Math.min(Math.max(gridY, 0), this.resolution);", + " const i = Math.min(Math.floor(x), this.resolution - 1);", + " const j = Math.min(Math.floor(y), this.resolution - 1);", + " const fx = x - i;", + " const fy = y - j;", + " const weight = (i, j) => this.splat[(j * this.size + i) * LAYER_COUNT + layer];", + " const top = weight(i, j) + fx * (weight(i + 1, j) - weight(i, j));", + " const bottom = weight(i, j + 1) + fx * (weight(i + 1, j + 1) - weight(i, j + 1));", + " return (top + fy * (bottom - top)) / 255;", + " }", + "", + " /**", + " * @param {number} channel 0, 1 and 2 for the tint, 3 for the wetness.", + " * @returns {number} The channel (0-1), interpolated between samples.", + " */", + " sampleColor(gridX, gridY, channel) {", + " const x = Math.min(Math.max(gridX, 0), this.resolution);", + " const y = Math.min(Math.max(gridY, 0), this.resolution);", + " const i = Math.min(Math.floor(x), this.resolution - 1);", + " const j = Math.min(Math.floor(y), this.resolution - 1);", + " const fx = x - i;", + " const fy = y - j;", + " const value = (i, j) => this.colors[(j * this.size + i) * 4 + channel];", + " const top = value(i, j) + fx * (value(i + 1, j) - value(i, j));", + " const bottom = value(i, j + 1) + fx * (value(i + 1, j + 1) - value(i, j + 1));", + " return (top + fy * (bottom - top)) / 255;", + " }", + "", + " /**", + " * @param {number} index The index of a sample.", + " * @returns {boolean} true if no layer is painted on the sample: it's a hole.", + " */", + " isHole(index) {", + " const offset = index * LAYER_COUNT;", + " return (this.splat[offset] | this.splat[offset + 1] | this.splat[offset + 2] | this.splat[offset + 3]) === 0;", + " }", + "", + " /**", + " * @returns {number} The sum of the layer weights (0-1), interpolated between samples: there is a", + " * hole where it's under 0.5, like what is drawn.", + " */", + " sampleSolidity(gridX, gridY) {", + " let solidity = 0;", + " for (let layer = 0; layer < LAYER_COUNT; layer++) solidity += this.sampleLayerWeight(gridX, gridY, layer);", + " return solidity;", + " }", + "", + " /** @returns {number} The index (from 0) of the layer painted the most. */", + " getDominantLayer(gridX, gridY) {", + " const i = clampInteger(Math.round(gridX), 0, this.resolution);", + " const j = clampInteger(Math.round(gridY), 0, this.resolution);", + " const offset = (j * this.size + i) * LAYER_COUNT;", + " let dominantLayer = 0;", + " for (let layer = 1; layer < LAYER_COUNT; layer++) {", + " if (this.splat[offset + layer] > this.splat[offset + dominantLayer]) {", + " dominantLayer = layer;", + " }", + " }", + " return dominantLayer;", + " }", + "", + " /** @param {TerrainData} other */", + " copyFrom(other) {", + " this.heights.set(other.heights);", + " this.splat.set(other.splat);", + " this.colors.set(other.colors);", + " }", + "", + " /**", + " * @param {number} resolution", + " * @returns {TerrainData} A copy of this data with another resolution.", + " */", + " resampled(resolution) {", + " const data = new TerrainData(resolution);", + " const ratio = this.resolution / resolution;", + " for (let j = 0; j < data.size; j++) {", + " for (let i = 0; i < data.size; i++) {", + " const index = j * data.size + i;", + " data.heights[index] = this.sampleHeight(i * ratio, j * ratio);", + " const sourceOffset =", + " (Math.round(j * ratio) * this.size + Math.round(i * ratio)) * LAYER_COUNT;", + " for (let layer = 0; layer < LAYER_COUNT; layer++) {", + " data.splat[index * LAYER_COUNT + layer] = this.splat[sourceOffset + layer];", + " }", + " for (let channel = 0; channel < 4; channel++) {", + " data.colors[index * 4 + channel] = this.colors[sourceOffset + channel];", + " }", + " }", + " }", + " return data;", + " }", + "}", + "", + "/**", + " * Brushes change the samples around a segment, with an effect going smoothly", + " * from full on the segment to none at `radius`. A point is a segment of", + " * length 0. Positions are in scene units relative to the terrain origin.", + " */", + "const brushes = {", + " /**", + " * @param {TerrainData} data", + " * @param {{cellWidth: number, cellHeight: number, ax: number, ay: number, bx: number, by: number, radius: number}} stroke", + " * @param {(sampleIndex: number, weight: number, t: number) => void} apply Called with the weight of", + " * the brush and the position (from 0 to 1) of the nearest point of the segment.", + " * @param {SampleRectangle} changedSamples", + " */", + " forEachSample(data, stroke, apply, changedSamples) {", + " const { cellWidth, cellHeight, ax, ay, bx, by, radius } = stroke;", + " const last = data.resolution;", + " const minI = clampInteger(Math.floor((Math.min(ax, bx) - radius) / cellWidth), 0, last);", + " const maxI = clampInteger(Math.ceil((Math.max(ax, bx) + radius) / cellWidth), 0, last);", + " const minJ = clampInteger(Math.floor((Math.min(ay, by) - radius) / cellHeight), 0, last);", + " const maxJ = clampInteger(Math.ceil((Math.max(ay, by) + radius) / cellHeight), 0, last);", + " changedSamples.set(minI, minJ, maxI, maxJ);", + " if (radius <= 0) return;", + "", + " const abX = bx - ax;", + " const abY = by - ay;", + " const abLengthSquared = abX * abX + abY * abY;", + " const radiusSquared = radius * radius;", + " for (let j = minJ; j <= maxJ; j++) {", + " const y = j * cellHeight;", + " for (let i = minI; i <= maxI; i++) {", + " const x = i * cellWidth;", + " const t =", + " abLengthSquared > 0", + " ? clamp01(((x - ax) * abX + (y - ay) * abY) / abLengthSquared)", + " : 0;", + " const dx = x - (ax + t * abX);", + " const dy = y - (ay + t * abY);", + " const distanceSquared = dx * dx + dy * dy;", + " if (distanceSquared >= radiusSquared) continue;", + " const falloff = 1 - distanceSquared / radiusSquared;", + " apply(j * data.size + i, falloff * falloff, t);", + " }", + " }", + " },", + "", + " raise(data, stroke, amount, changedSamples) {", + " const { heights } = data;", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index, weight) => {", + " heights[index] = clamp01(heights[index] + amount * weight);", + " },", + " changedSamples", + " );", + " },", + "", + " /**", + " * @param {number} targetHeight", + " * @param {number} [endTargetHeight] The target at the end of the segment, for a slope (like a road", + " * going up). The same target as at its start by default.", + " */", + " flatten(data, stroke, targetHeight, strength, changedSamples, endTargetHeight = targetHeight) {", + " const { heights } = data;", + " const startTarget = clamp01(targetHeight);", + " const endTarget = clamp01(endTargetHeight);", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index, weight, t) => {", + " const target = startTarget + (endTarget - startTarget) * t;", + " heights[index] += (target - heights[index]) * clamp01(weight * strength);", + " },", + " changedSamples", + " );", + " },", + "", + " smooth(data, stroke, strength, changedSamples) {", + " const { heights, size } = data;", + " const changes = [];", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index, weight) => {", + " const i = index % size;", + " const j = (index - i) / size;", + " let sum = 0;", + " for (let dj = -1; dj <= 1; dj++) {", + " for (let di = -1; di <= 1; di++) {", + " sum += data.getHeight(i + di, j + dj);", + " }", + " }", + " changes.push(index, heights[index] + (sum / 9 - heights[index]) * clamp01(weight * strength));", + " },", + " changedSamples", + " );", + " // Written after all averages are computed, so that the result", + " // doesn't depend on the order in which samples are visited.", + " for (let index = 0; index < changes.length; index += 2) {", + " heights[changes[index]] = changes[index + 1];", + " }", + " },", + "", + " paint(data, stroke, layer, strength, changedSamples) {", + " const { splat } = data;", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index, weight) => {", + " const amount = clamp01(weight * strength);", + " const offset = index * LAYER_COUNT;", + " for (let otherLayer = 0; otherLayer < LAYER_COUNT; otherLayer++) {", + " const target = otherLayer === layer ? 255 : 0;", + " const value = splat[offset + otherLayer];", + " // Rounded towards the target, so that light strokes still paint.", + " const change = (target - value) * amount;", + " splat[offset + otherLayer] = value + (change > 0 ? Math.ceil(change) : Math.floor(change));", + " }", + " },", + " changedSamples", + " );", + " },", + "", + " /** Remove all the layers, making a hole (`fill` or painting a layer fills it again). */", + " hole(data, stroke, changedSamples) {", + " const { splat } = data;", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index) => {", + " splat.fill(0, index * LAYER_COUNT, (index + 1) * LAYER_COUNT);", + " },", + " changedSamples", + " );", + " },", + "", + " /**", + " * Fill the holes in the radius with the layers of the ground around them:", + " * each pass fills the holes next to the ground with the average of their", + " * neighbors, from the edges of the holes inward.", + " */", + " fill(data, stroke, changedSamples) {", + " const { splat, size, resolution } = data;", + " /** @type {number[]} */", + " let holes = [];", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index) => {", + " if (data.isHole(index)) holes.push(index);", + " },", + " changedSamples", + " );", + " const filledWeights = new Float32Array(holes.length * LAYER_COUNT);", + " while (holes.length > 0) {", + " /** @type {number[]} */", + " const remainingHoles = [];", + " /** @type {number[]} */", + " const filledHoles = [];", + " for (const index of holes) {", + " const i = index % size;", + " const j = (index - i) / size;", + " const weights = filledWeights.subarray(filledHoles.length * LAYER_COUNT, (filledHoles.length + 1) * LAYER_COUNT);", + " weights.fill(0);", + " for (let neighborJ = Math.max(j - 1, 0); neighborJ <= Math.min(j + 1, resolution); neighborJ++) {", + " for (let neighborI = Math.max(i - 1, 0); neighborI <= Math.min(i + 1, resolution); neighborI++) {", + " const neighbor = neighborJ * size + neighborI;", + " if (data.isHole(neighbor)) continue;", + " for (let layer = 0; layer < LAYER_COUNT; layer++) weights[layer] += splat[neighbor * LAYER_COUNT + layer];", + " }", + " }", + " if (weights[0] + weights[1] + weights[2] + weights[3] > 0) filledHoles.push(index);", + " else remainingHoles.push(index);", + " }", + " if (filledHoles.length === 0) {", + " // No ground around (a hole bigger than the brush): the first layer is used.", + " for (const index of remainingHoles) splat[index * LAYER_COUNT] = 255;", + " break;", + " }", + " // Filled after the pass, so that the holes are filled from the edges inward.", + " filledHoles.forEach((index, filledIndex) => {", + " const weights = filledWeights.subarray(filledIndex * LAYER_COUNT, (filledIndex + 1) * LAYER_COUNT);", + " const total = weights[0] + weights[1] + weights[2] + weights[3];", + " for (let layer = 0; layer < LAYER_COUNT; layer++) {", + " splat[index * LAYER_COUNT + layer] = Math.round((weights[layer] / total) * 255);", + " }", + " });", + " holes = remainingHoles;", + " }", + " },", + "", + " /** @param {number[]} color The red, green and blue (0-255) of the tint: white removes it. */", + " tint(data, stroke, color, strength, changedSamples) {", + " brushes.paintColors(data, stroke, 0, color, strength, changedSamples);", + " },", + "", + " /** @param {number} wetness From 0 (dry) to 1. */", + " wet(data, stroke, wetness, strength, changedSamples) {", + " brushes.paintColors(data, stroke, 3, [Math.round(clamp01(wetness) * 255)], strength, changedSamples);", + " },", + "", + " /**", + " * Bring channels of the colors (see `TerrainData.colors`) towards targets.", + " * @param {number} firstChannel", + " * @param {number[]} targets From 0 to 255, for the channels from the first one.", + " */", + " paintColors(data, stroke, firstChannel, targets, strength, changedSamples) {", + " const { colors } = data;", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index, weight) => {", + " const amount = clamp01(weight * strength);", + " for (let channel = 0; channel < targets.length; channel++) {", + " const offset = index * 4 + firstChannel + channel;", + " // Rounded towards the target, so that light strokes still paint.", + " const change = (targets[channel] - colors[offset]) * amount;", + " colors[offset] += change > 0 ? Math.ceil(change) : Math.floor(change);", + " }", + " },", + " changedSamples", + " );", + " },", + "};", + "", + "const relief = (() => {", + " const hash = (x, y, seed) => {", + " let h = Math.imul(x, 374761393) + Math.imul(y, 668265263) + Math.imul(seed, 982451653);", + " h = Math.imul(h ^ (h >>> 13), 1274126177);", + " return ((h ^ (h >>> 16)) >>> 0) / 4294967296;", + " };", + " const valueNoise = (x, y, seed) => {", + " const x0 = Math.floor(x);", + " const y0 = Math.floor(y);", + " const fx = x - x0;", + " const fy = y - y0;", + " const sx = fx * fx * (3 - 2 * fx);", + " const sy = fy * fy * (3 - 2 * fy);", + " const top = hash(x0, y0, seed) + sx * (hash(x0 + 1, y0, seed) - hash(x0, y0, seed));", + " const bottom =", + " hash(x0, y0 + 1, seed) + sx * (hash(x0 + 1, y0 + 1, seed) - hash(x0, y0 + 1, seed));", + " return top + sy * (bottom - top);", + " };", + " /** Fractal noise between 0 and 1. */", + " const fractalNoise = (x, y, seed) => {", + " let value = 0;", + " let amplitude = 0.5;", + " let frequency = 1;", + " for (let octave = 0; octave < 5; octave++) {", + " value += amplitude * valueNoise(x * frequency, y * frequency, seed + octave);", + " amplitude *= 0.5;", + " frequency *= 2;", + " }", + " return value / 0.96875;", + " };", + " const smoothstep = (edge0, edge1, x) => {", + " const t = clamp01((x - edge0) / (edge1 - edge0));", + " return t * t * (3 - 2 * t);", + " };", + " const reliefHeights = {", + " Flat: () => 0,", + " Hills: (u, v, seed) => 0.05 + 0.5 * Math.pow(fractalNoise(u * 3, v * 3, seed), 2),", + " Mountains: (u, v, seed) => {", + " const ridge = 1 - Math.abs(2 * fractalNoise(u * 2.5, v * 2.5, seed) - 1);", + " return 0.9 * Math.pow(ridge, 2.5);", + " },", + " Island: (u, v, seed) => {", + " const distanceToCenter = 2 * Math.hypot(u - 0.5, v - 0.5);", + " const coast = 0.2 * (fractalNoise(u * 4, v * 4, seed + 7) - 0.5);", + " const land = smoothstep(0.95, 0.55, distanceToCenter + coast);", + " return land * (0.12 + 0.6 * Math.pow(fractalNoise(u * 3, v * 3, seed), 1.5));", + " },", + " };", + " const beachLayer = 3;", + "", + " return {", + " /**", + " * @param {TerrainData} data", + " * @param {string} reliefName Flat, Hills, Mountains or Island.", + " * @param {number} seed", + " */", + " generate(data, reliefName, seed) {", + " const getHeight = reliefHeights[reliefName] || reliefHeights.Flat;", + " const integerSeed = Math.floor(seed) | 0;", + " data.fillLayer(0);", + " for (let j = 0; j < data.size; j++) {", + " for (let i = 0; i < data.size; i++) {", + " const index = j * data.size + i;", + " const height = clamp01(", + " getHeight(i / data.resolution, j / data.resolution, integerSeed)", + " );", + " data.heights[index] = height;", + " if (reliefName === 'Island') {", + " const sand = Math.round(255 * smoothstep(0.16, 0.1, height));", + " const offset = index * LAYER_COUNT;", + " data.splat[offset] = 255 - sand;", + " data.splat[offset + beachLayer] = sand;", + " }", + " }", + " }", + " },", + " };", + "})();", + "", + "/**", + " * Edits written in the Edits property: a JSON list of brush strokes, like", + " * `[{\"tool\": \"raise\", \"x\": 0.5, \"y\": 0.5, \"radius\": 0.2, \"height\": 0.3}]`,", + " * applied on the relief or the heightmap. Positions and radius are fractions", + " * of the terrain size, heights and Z positions fractions of its depth: they", + " * are the same for all instances, whatever their size. They are made to be", + " * read and written by people and by AI agents.", + " */", + "const edits = {", + " /**", + " * @param {TerrainData} data", + " * @param {string} text", + " */", + " apply(data, text) {", + " if (!text.trim()) return;", + " let editList;", + " try {", + " editList = JSON.parse(text);", + " } catch (error) {", + " console.warn('The edits of a terrain are not valid JSON: ' + error.message);", + " return;", + " }", + " if (!Array.isArray(editList)) {", + " console.warn('The edits of a terrain must be a JSON list (like [{\"tool\": \"raise\", ...}]).');", + " return;", + " }", + " const numberOr = (value, defaultValue) => (typeof value === 'number' && isFinite(value) ? value : defaultValue);", + " const changedSamples = new SampleRectangle();", + " for (const edit of editList) {", + " if (!edit || typeof edit !== 'object') continue;", + " const x = numberOr(edit.x, 0.5);", + " const y = numberOr(edit.y, 0.5);", + " const stroke = {", + " cellWidth: 1 / data.resolution,", + " cellHeight: 1 / data.resolution,", + " ax: x,", + " ay: y,", + " bx: numberOr(edit.toX, x),", + " by: numberOr(edit.toY, y),", + " radius: numberOr(edit.radius, 0.1),", + " };", + " const strength = clamp01(numberOr(edit.strength, 1));", + " if (edit.tool === 'raise') {", + " brushes.raise(data, stroke, numberOr(edit.height, 0.1), changedSamples);", + " } else if (edit.tool === 'flatten') {", + " const z = numberOr(edit.z, 0);", + " brushes.flatten(data, stroke, z, strength, changedSamples, numberOr(edit.toZ, z));", + " } else if (edit.tool === 'smooth') {", + " // Smoothing the same fraction of the terrain at any resolution.", + " const passes = Math.max(Math.round(((1 + 9 * strength) * data.resolution) / 256), 1);", + " for (let pass = 0; pass < passes; pass++) brushes.smooth(data, stroke, 1, changedSamples);", + " } else if (edit.tool === 'paint') {", + " const layer = clampInteger(numberOr(edit.layer, 1) - 1, 0, LAYER_COUNT - 1);", + " brushes.paint(data, stroke, layer, strength, changedSamples);", + " } else if (edit.tool === 'tint') {", + " const color = gdjs.rgbOrHexToRGBColor(typeof edit.color === 'string' ? edit.color : '255;255;255');", + " brushes.tint(data, stroke, color, strength, changedSamples);", + " } else if (edit.tool === 'wetness') {", + " brushes.wet(data, stroke, numberOr(edit.amount, 1), strength, changedSamples);", + " } else if (edit.tool === 'hole') {", + " brushes.hole(data, stroke, changedSamples);", + " } else if (edit.tool === 'fill') {", + " brushes.fill(data, stroke, changedSamples);", + " } else {", + " console.warn('Unknown tool in the edits of a terrain: \"' + edit.tool + '\" (use raise, flatten, smooth, paint, tint, wetness, hole or fill).');", + " }", + " }", + " },", + "};", + "", + "/**", + " * @param {gdjs.RuntimeGame} game", + " * @param {string} imageResourceName", + " * @returns {CanvasImageSource | null} The image, or null if it's not loaded (yet).", + " */", + "const getLoadedImage = (game, imageResourceName) => {", + " const imageManager = game.getImageManager();", + " if (typeof imageManager.getImageSource === 'function') return imageManager.getImageSource(imageResourceName);", + " // Older GDevelop versions: a 192 x 192 placeholder is given for images that are not loaded.", + " const texture = imageManager.getPIXITexture(imageResourceName);", + " return texture && texture !== imageManager.getInvalidPIXITexture() ? texture.baseTexture.getDrawableSource() : null;", + "};", + "", + "/**", + " * Loads heights from an image: black is the lowest, white the highest.", + " * Heights have 16 bits of precision when the red channel holds the most", + " * significant byte and the green one the least significant (a gray image", + " * works as expected too).", + " * @param {TerrainData} data", + " * @param {gdjs.RuntimeGame} game", + " * @param {string} imageResourceName", + " * @returns {boolean} true if the image could be read.", + " */", + "const loadHeightsFromImage = (data, game, imageResourceName) => {", + " const source = getLoadedImage(game, imageResourceName);", + " if (!source) return false;", + "", + " const canvas = document.createElement('canvas');", + " canvas.width = source.width;", + " canvas.height = source.height;", + " const context = canvas.getContext('2d', { willReadFrequently: true });", + " if (!context) return false;", + " context.drawImage(source, 0, 0);", + " const pixels = context.getImageData(0, 0, source.width, source.height).data;", + " // Pixels are decoded before being interpolated, to keep the 16 bits.", + " const pixelHeight = (x, y) => {", + " const offset = (y * source.width + x) * 4;", + " return (256 * pixels[offset] + pixels[offset + 1]) / 65535;", + " };", + " for (let j = 0; j < data.size; j++) {", + " const y = (j / data.resolution) * (source.height - 1);", + " const y0 = Math.floor(y);", + " const y1 = Math.min(y0 + 1, source.height - 1);", + " for (let i = 0; i < data.size; i++) {", + " const x = (i / data.resolution) * (source.width - 1);", + " const x0 = Math.floor(x);", + " const x1 = Math.min(x0 + 1, source.width - 1);", + " const top = pixelHeight(x0, y0) + (x - x0) * (pixelHeight(x1, y0) - pixelHeight(x0, y0));", + " const bottom = pixelHeight(x0, y1) + (x - x0) * (pixelHeight(x1, y1) - pixelHeight(x0, y1));", + " data.heights[j * data.size + i] = top + (y - y0) * (bottom - top);", + " }", + " }", + " return true;", + "};", + "", + "/**", + " * Stores the data in a string. Only the differences with the base data (from", + " * the relief or the heightmap) are stored: heights on 16 bits, layer weights", + " * on 8 bits, then (since format 2) the tint and wetness on 8 bits. Each value is predicted from its neighbors and the", + " * prediction errors are written as variable length integers, with runs of", + " * zeros collapsed. Untouched or smoothly changed areas take almost no space.", + " */", + "const codec = (() => {", + " const formatVersion = '2';", + " // Format 1 has no tint and wetness.", + " const readableFormatVersions = ['1', '2'];", + "", + " class ByteWriter {", + " constructor() {", + " this.bytes = new Uint8Array(1024);", + " this.length = 0;", + " }", + " writeVarUint(value) {", + " if (this.length + 5 > this.bytes.length) {", + " const bytes = new Uint8Array(this.bytes.length * 2);", + " bytes.set(this.bytes);", + " this.bytes = bytes;", + " }", + " while (value >= 0x80) {", + " this.bytes[this.length++] = (value & 0x7f) | 0x80;", + " value >>>= 7;", + " }", + " this.bytes[this.length++] = value;", + " }", + " }", + "", + " class ByteReader {", + " /** @param {Uint8Array} bytes */", + " constructor(bytes) {", + " this.bytes = bytes;", + " this.position = 0;", + " }", + " readVarUint() {", + " let value = 0;", + " let shift = 0;", + " let byte;", + " do {", + " if (this.position >= this.bytes.length) throw new Error('Truncated terrain data.');", + " byte = this.bytes[this.position++];", + " value += (byte & 0x7f) * Math.pow(2, shift);", + " shift += 7;", + " } while (byte & 0x80);", + " return value;", + " }", + " }", + "", + " /** Predicts a value from the left, top and top-left ones. */", + " const predict = (values, size, index) => {", + " const i = index % size;", + " if (index < size) return i === 0 ? 0 : values[index - 1];", + " if (i === 0) return values[index - size];", + " return values[index - 1] + values[index - size] - values[index - size - 1];", + " };", + "", + " /**", + " * @param {ByteWriter} writer", + " * @param {Int32Array} values A grid of `size * size` values.", + " */", + " const writeGrid = (writer, values, size) => {", + " let zeroRun = 0;", + " for (let index = 0; index < values.length; index++) {", + " const error = values[index] - predict(values, size, index);", + " if (error === 0) {", + " zeroRun++;", + " continue;", + " }", + " if (zeroRun > 0) {", + " writer.writeVarUint(0);", + " writer.writeVarUint(zeroRun);", + " zeroRun = 0;", + " }", + " // Zigzag: small negative and positive numbers get small codes.", + " writer.writeVarUint(error > 0 ? error * 2 : -error * 2 - 1);", + " }", + " if (zeroRun > 0) {", + " writer.writeVarUint(0);", + " writer.writeVarUint(zeroRun);", + " }", + " };", + "", + " /** @param {ByteReader} reader */", + " const readGrid = (reader, size) => {", + " const values = new Int32Array(size * size);", + " let zeroRun = 0;", + " for (let index = 0; index < values.length; index++) {", + " let error = 0;", + " if (zeroRun > 0) {", + " zeroRun--;", + " } else {", + " const code = reader.readVarUint();", + " if (code === 0) {", + " zeroRun = reader.readVarUint() - 1;", + " } else {", + " error = code % 2 === 0 ? code / 2 : -(code + 1) / 2;", + " }", + " }", + " values[index] = predict(values, size, index) + error;", + " }", + " return values;", + " };", + "", + " const toBase64 = (bytes, length) => {", + " let binary = '';", + " for (let offset = 0; offset < length; offset += 0x8000) {", + " binary += String.fromCharCode.apply(null, bytes.subarray(offset, Math.min(offset + 0x8000, length)));", + " }", + " return btoa(binary);", + " };", + "", + " const fromBase64 = (base64) => {", + " const binary = atob(base64);", + " const bytes = new Uint8Array(binary.length);", + " for (let index = 0; index < binary.length; index++) {", + " bytes[index] = binary.charCodeAt(index);", + " }", + " return bytes;", + " };", + "", + " return {", + " /**", + " * @param {TerrainData} data", + " * @param {TerrainData} baseData The data without changes, with the same resolution.", + " */", + " encode(data, baseData) {", + " const writer = new ByteWriter();", + " const values = new Int32Array(data.heights.length);", + " for (let index = 0; index < values.length; index++) {", + " values[index] = Math.round((data.heights[index] - baseData.heights[index]) * 65535);", + " }", + " writeGrid(writer, values, data.size);", + " for (let layer = 0; layer < LAYER_COUNT; layer++) {", + " for (let index = 0; index < values.length; index++) {", + " const offset = index * LAYER_COUNT + layer;", + " values[index] = data.splat[offset] - baseData.splat[offset];", + " }", + " writeGrid(writer, values, data.size);", + " }", + " // Not painted, these grids are zeros: a few bytes each.", + " for (let channel = 0; channel < 4; channel++) {", + " for (let index = 0; index < values.length; index++) {", + " const offset = index * 4 + channel;", + " values[index] = data.colors[offset] - baseData.colors[offset];", + " }", + " writeGrid(writer, values, data.size);", + " }", + " return formatVersion + ';' + data.resolution + ';' + toBase64(writer.bytes, writer.length);", + " },", + "", + " /**", + " * Store grids of integers in a string, like the terrain data. Used by other objects (the scattered models).", + " * @param {Int32Array[]} grids Grids of `size * size` values.", + " * @param {number} size", + " */", + " encodeGrids(grids, size) {", + " const writer = new ByteWriter();", + " for (const grid of grids) writeGrid(writer, grid, size);", + " return toBase64(writer.bytes, writer.length);", + " },", + "", + " /**", + " * @param {string} base64 A string from `encodeGrids`.", + " * @param {number} size", + " * @param {number} count The number of grids.", + " * @returns {Int32Array[]} The grids (an error is thrown if the string is not valid).", + " */", + " decodeGrids(base64, size, count) {", + " const reader = new ByteReader(fromBase64(base64));", + " return Array.from({ length: count }, () => readGrid(reader, size));", + " },", + "", + " /**", + " * @param {string} text", + " * @param {(resolution: number) => TerrainData} createBaseData", + " * @returns {TerrainData | null} null if the text is empty or not valid.", + " */", + " decode(text, createBaseData) {", + " if (!text) return null;", + " const parts = text.split(';');", + " const resolution = parseInt(parts[1], 10);", + " if (readableFormatVersions.indexOf(parts[0]) === -1 || RESOLUTIONS.indexOf(resolution) === -1) {", + " return null;", + " }", + " try {", + " const reader = new ByteReader(fromBase64(parts[2]));", + " const data = createBaseData(resolution);", + " const heightChanges = readGrid(reader, data.size);", + " for (let index = 0; index < heightChanges.length; index++) {", + " data.heights[index] = clamp01(data.heights[index] + heightChanges[index] / 65535);", + " }", + " for (let layer = 0; layer < LAYER_COUNT; layer++) {", + " const weightChanges = readGrid(reader, data.size);", + " for (let index = 0; index < weightChanges.length; index++) {", + " const offset = index * LAYER_COUNT + layer;", + " // The base may have changed since the data was saved.", + " data.splat[offset] = Math.min(Math.max(data.splat[offset] + weightChanges[index], 0), 255);", + " }", + " }", + " if (parts[0] !== '1') {", + " for (let channel = 0; channel < 4; channel++) {", + " const colorChanges = readGrid(reader, data.size);", + " for (let index = 0; index < colorChanges.length; index++) {", + " const offset = index * 4 + channel;", + " data.colors[offset] = Math.min(Math.max(data.colors[offset] + colorChanges[index], 0), 255);", + " }", + " }", + " }", + " return data;", + " } catch (error) {", + " console.error('Terrain data could not be read:', error);", + " return null;", + " }", + " },", + " };", + "})();", + "", + "const lodIndices = new Map();", + "/**", + " * The triangles of a chunk, shared by all chunks of the same size. Chunk", + " * borders have a skirt going down, hiding the gaps between chunks drawn", + " * with different levels of detail.", + " * @param {number} chunkCells", + " * @param {number} step", + " */", + "const getLodIndex = (chunkCells, step) => {", + " const key = chunkCells + ';' + step;", + " const existingIndex = lodIndices.get(key);", + " if (existingIndex) return existingIndex;", + "", + " const sideVertices = chunkCells + 1;", + " const gridVertices = sideVertices * sideVertices;", + " const indices = [];", + " const grid = (i, j) => j * sideVertices + i;", + " for (let j = 0; j < chunkCells; j += step) {", + " for (let i = 0; i < chunkCells; i += step) {", + " const topLeft = grid(i, j);", + " const topRight = grid(i + step, j);", + " const bottomLeft = grid(i, j + step);", + " const bottomRight = grid(i + step, j + step);", + " indices.push(topLeft, topRight, bottomLeft, topRight, bottomRight, bottomLeft);", + " }", + " }", + " const borders = [", + " (k) => grid(k, 0),", + " (k) => grid(k, chunkCells),", + " (k) => grid(0, k),", + " (k) => grid(chunkCells, k),", + " ];", + " borders.forEach((borderVertex, border) => {", + " const skirtVertex = (k) => gridVertices + border * sideVertices + k;", + " for (let k = 0; k < chunkCells; k += step) {", + " const a = borderVertex(k);", + " const b = borderVertex(k + step);", + " const skirtA = skirtVertex(k);", + " const skirtB = skirtVertex(k + step);", + " // Both faces, as a skirt can be seen from either side.", + " indices.push(a, b, skirtB, a, skirtB, skirtA, a, skirtB, b, a, skirtA, skirtB);", + " }", + " });", + " const index = new THREE.BufferAttribute(new Uint16Array(indices), 1);", + " lodIndices.set(key, index);", + " return index;", + "};", + "", + "const vertexShaderDeclarations = `", + "varying vec2 vTerrainLocalXY;", + "varying float vTerrainLocalZ;", + "uniform float terrainWorldScale;", + "varying vec2 vTerrainWorldXY;", + "varying float vTerrainUp;", + "`;", + "const vertexShaderCode = `", + "vTerrainLocalXY = position.xy;", + "vTerrainLocalZ = position.z;", + "vTerrainWorldXY = (modelMatrix * vec4(transformed, 1.0)).xy * terrainWorldScale;", + "vTerrainUp = normalize(transpose(inverse(mat3(modelMatrix))) * objectNormal).z;", + "`;", + "const fragmentShaderDeclarations = `", + "uniform sampler2D terrainSplatMap;", + "uniform vec2 terrainSplatUvScaleAndOffset;", + "uniform vec3 terrainLayerColors[4];", + "uniform sampler2D terrainLayerMap0;", + "uniform sampler2D terrainLayerMap1;", + "uniform sampler2D terrainLayerMap2;", + "uniform sampler2D terrainLayerMap3;", + "uniform sampler2D terrainLayerNormalMap0;", + "uniform sampler2D terrainLayerNormalMap1;", + "uniform sampler2D terrainLayerNormalMap2;", + "uniform sampler2D terrainLayerNormalMap3;", + "uniform float terrainHasNormalMaps;", + "uniform float terrainNormalMapStrength;", + "uniform float terrainTextureSize;", + "uniform float terrainHeightBlending;", + "uniform float terrainTextureVariation;", + "uniform vec4 terrainCliffLayer;", + "uniform vec2 terrainCliffCosines;", + "uniform vec4 terrainBrush;", + "uniform sampler2D terrainColorMap;", + "uniform vec3 terrainWetShore;", + "varying vec2 vTerrainLocalXY;", + "varying float vTerrainLocalZ;", + "varying vec2 vTerrainWorldXY;", + "varying float vTerrainUp;", + "", + "float terrainHash(vec2 p) {", + " return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);", + "}", + "// The rotation of the copies of textures used to make repetition less visible.", + "const mat2 terrainCopyRotation = mat2(0.8, -0.6, 0.6, 0.8);", + "/** A bump of a normal map (from -1 to 1), mixed with its rotated copy in patches. */", + "vec3 terrainSampleNormal(sampler2D normalMap, vec2 uv, vec2 uv2, float patches) {", + " vec3 bump = texture2D(normalMap, uv).xyz * 2.0 - 1.0;", + " if (patches <= 0.0) return bump;", + " vec3 copyBump = texture2D(normalMap, uv2).xyz * 2.0 - 1.0;", + " // The bumps of the copy are rotated back with the inverse rotation.", + " copyBump.xy = transpose(terrainCopyRotation) * copyBump.xy;", + " return mix(bump, copyBump, patches);", + "}", + "float terrainNoise(vec2 p) {", + " vec2 i = floor(p);", + " vec2 f = fract(p);", + " vec2 u = f * f * (3.0 - 2.0 * f);", + " return mix(", + " mix(terrainHash(i), terrainHash(i + vec2(1.0, 0.0)), u.x),", + " mix(terrainHash(i + vec2(0.0, 1.0)), terrainHash(i + vec2(1.0, 1.0)), u.x),", + " u.y", + " );", + "}", + "`;", + "// Holes are samples without any painted layer: the interpolated sum of the", + "// weights gives smooth edges. Only compiled for terrains having holes, as", + "// discard makes drawing slower on some GPUs.", + "const holeShaderCode = `", + "#ifdef TERRAIN_HOLES", + "if (dot(texture2D(terrainSplatMap, vTerrainLocalXY * terrainSplatUvScaleAndOffset.x + terrainSplatUvScaleAndOffset.y), vec4(1.0)) < 0.5) discard;", + "#endif", + "`;", + "// For the shadows: the depth of holes is not drawn.", + "const holeDepthShaderDeclarations = `", + "uniform sampler2D terrainSplatMap;", + "uniform vec2 terrainSplatUvScaleAndOffset;", + "varying vec2 vTerrainLocalXY;", + "`;", + "", + "const fragmentShaderCode = holeShaderCode + `", + "// Borders between layers are made irregular and sharper, to look painted.", + "vec2 terrainNoisePosition = vTerrainWorldXY / 24.0;", + "vec4 terrainJitter = vec4(", + " terrainNoise(terrainNoisePosition),", + " terrainNoise(terrainNoisePosition + 19.1),", + " terrainNoise(terrainNoisePosition + 37.7),", + " terrainNoise(terrainNoisePosition + 53.3)", + ");", + "// Layer weights are in a texture, so that painted details stay the same", + "// whatever the level of detail of the ground.", + "vec2 terrainSplatUv = vTerrainLocalXY * terrainSplatUvScaleAndOffset.x + terrainSplatUvScaleAndOffset.y;", + "vec4 terrainWeights = texture2D(terrainSplatMap, terrainSplatUv) * (0.7 + 0.6 * terrainJitter);", + "terrainWeights = terrainWeights * terrainWeights;", + "terrainWeights /= max(dot(terrainWeights, vec4(1.0)), 0.00001);", + "float terrainCliff = 1.0 - smoothstep(terrainCliffCosines.x, terrainCliffCosines.y, vTerrainUp);", + "terrainWeights = mix(terrainWeights, terrainCliffLayer, terrainCliff * dot(terrainCliffLayer, vec4(1.0)));", + "", + "vec2 terrainUv = vTerrainWorldXY / terrainTextureSize;", + "vec3 terrainTexture0 = texture2D(terrainLayerMap0, terrainUv).rgb;", + "vec3 terrainTexture1 = texture2D(terrainLayerMap1, terrainUv).rgb;", + "vec3 terrainTexture2 = texture2D(terrainLayerMap2, terrainUv).rgb;", + "vec3 terrainTexture3 = texture2D(terrainLayerMap3, terrainUv).rgb;", + "", + "// Less visible repetition: in irregular patches, textures are replaced by a", + "// rotated and shifted copy of themselves, and colors slowly vary.", + "float terrainPatches = 0.0;", + "vec2 terrainUv2 = terrainUv;", + "float terrainColorVariation = 1.0;", + "if (terrainTextureVariation > 0.0) {", + " // Up to half of the ground is covered by the copies, each patch showing", + " // only one of them (mixing them would make them blurry).", + " float terrainPatchThreshold = 1.0 - 0.5 * terrainTextureVariation;", + " terrainPatches = smoothstep(terrainPatchThreshold - 0.05, terrainPatchThreshold + 0.05, terrainNoise(terrainUv * 0.23 + 7.3));", + " terrainUv2 = terrainCopyRotation * terrainUv + vec2(0.31, 0.73);", + " terrainTexture0 = mix(terrainTexture0, texture2D(terrainLayerMap0, terrainUv2).rgb, terrainPatches);", + " terrainTexture1 = mix(terrainTexture1, texture2D(terrainLayerMap1, terrainUv2).rgb, terrainPatches);", + " terrainTexture2 = mix(terrainTexture2, texture2D(terrainLayerMap2, terrainUv2).rgb, terrainPatches);", + " terrainTexture3 = mix(terrainTexture3, texture2D(terrainLayerMap3, terrainUv2).rgb, terrainPatches);", + " terrainColorVariation += terrainTextureVariation * 0.3 *", + " (terrainNoise(terrainUv * 0.11) + 0.5 * terrainNoise(terrainUv * 0.37 + 3.1) - 0.75);", + "}", + "", + "// Height-based blending: the brightness of a texture, compared to its", + "// average, is used as its height. Where layers meet, the high parts (stones,", + "// tufts) cover the low parts (sand, gaps) instead of fading into each other.", + "if (terrainHeightBlending > 0.0) {", + " const vec3 terrainLuminance = vec3(0.299, 0.587, 0.114);", + " vec4 terrainHeights = vec4(", + " dot(terrainTexture0 - textureLod(terrainLayerMap0, vec2(0.5), 16.0).rgb, terrainLuminance),", + " dot(terrainTexture1 - textureLod(terrainLayerMap1, vec2(0.5), 16.0).rgb, terrainLuminance),", + " dot(terrainTexture2 - textureLod(terrainLayerMap2, vec2(0.5), 16.0).rgb, terrainLuminance),", + " dot(terrainTexture3 - textureLod(terrainLayerMap3, vec2(0.5), 16.0).rgb, terrainLuminance)", + " );", + " vec4 terrainHeightWeights = terrainWeights + terrainHeights;", + " float terrainTop = max(max(terrainHeightWeights.x, terrainHeightWeights.y), max(terrainHeightWeights.z, terrainHeightWeights.w));", + " // Only painted layers (and the cliff layer) can show.", + " vec4 terrainHeightBlend = max(terrainHeightWeights - terrainTop + 0.1, 0.0) * step(0.004, terrainWeights);", + " terrainHeightBlend /= max(dot(terrainHeightBlend, vec4(1.0)), 0.00001);", + " terrainWeights = mix(terrainWeights, terrainHeightBlend, terrainHeightBlending);", + "}", + "", + "vec3 terrainColor = terrainColorVariation * (", + " terrainWeights.x * terrainLayerColors[0] * terrainTexture0 +", + " terrainWeights.y * terrainLayerColors[1] * terrainTexture1 +", + " terrainWeights.z * terrainLayerColors[2] * terrainTexture2 +", + " terrainWeights.w * terrainLayerColors[3] * terrainTexture3);", + "", + "// The painted tint multiplies all layers. Wet ground is darker (and", + "// shinier, see the roughness).", + "vec4 terrainTintAndWetness = texture2D(terrainColorMap, terrainSplatUv);", + "float terrainWetness = max(", + " terrainTintAndWetness.a,", + " terrainWetShore.z * (1.0 - smoothstep(terrainWetShore.x, terrainWetShore.x + terrainWetShore.y, vTerrainLocalZ))", + ");", + "terrainColor *= terrainTintAndWetness.rgb * (1.0 - 0.4 * terrainWetness);", + "diffuseColor.rgb *= terrainColor;", + "", + "if (terrainBrush.w > 0.5) {", + " float brushDistance = distance(vTerrainWorldXY, terrainBrush.xy);", + " float ringWidth = max(terrainBrush.z * 0.04, 1.5);", + " float ring = 1.0 - smoothstep(0.0, ringWidth, abs(brushDistance - terrainBrush.z));", + " float disc = brushDistance < terrainBrush.z ? 0.12 : 0.0;", + " diffuseColor.rgb = mix(diffuseColor.rgb, vec3(1.0), max(ring * 0.85, disc));", + "}", + "`;", + "", + "const fragmentShaderRoughnessCode = `", + "roughnessFactor = mix(roughnessFactor, 0.3, terrainWetness);", + "`;", + "", + "const fragmentShaderNormalCode = `", + "if (terrainHasNormalMaps > 0.5) {", + " vec3 terrainMapNormal =", + " terrainWeights.x * terrainSampleNormal(terrainLayerNormalMap0, terrainUv, terrainUv2, terrainPatches) +", + " terrainWeights.y * terrainSampleNormal(terrainLayerNormalMap1, terrainUv, terrainUv2, terrainPatches) +", + " terrainWeights.z * terrainSampleNormal(terrainLayerNormalMap2, terrainUv, terrainUv2, terrainPatches) +", + " terrainWeights.w * terrainSampleNormal(terrainLayerNormalMap3, terrainUv, terrainUv2, terrainPatches);", + " terrainMapNormal.xy *= terrainNormalMapStrength;", + " // The X and Y of textures follow the X and Y of the 3D world, projected on", + " // the ground (the camera space of the normal is used).", + " vec3 terrainTangent = (viewMatrix * vec4(1.0, 0.0, 0.0, 0.0)).xyz;", + " terrainTangent -= normal * dot(normal, terrainTangent);", + " // A ground facing the X axis (a rotated terrain) uses the Y axis instead.", + " if (dot(terrainTangent, terrainTangent) < 0.000001) {", + " terrainTangent = (viewMatrix * vec4(0.0, 1.0, 0.0, 0.0)).xyz;", + " terrainTangent -= normal * dot(normal, terrainTangent);", + " }", + " terrainTangent = normalize(terrainTangent);", + " vec3 terrainBitangent = cross(normal, terrainTangent);", + " normal = normalize(mat3(terrainTangent, terrainBitangent, normal) * terrainMapNormal);", + "}", + "`;", + "", + "let whiteTexture = null;", + "const getWhiteTexture = () => {", + " if (!whiteTexture) {", + " whiteTexture = new THREE.DataTexture(new Uint8Array([255, 255, 255, 255]), 1, 1);", + " whiteTexture.needsUpdate = true;", + " }", + " return whiteTexture;", + "};", + "", + "let flatNormalTexture = null;", + "const getFlatNormalTexture = () => {", + " if (!flatNormalTexture) {", + " flatNormalTexture = new THREE.DataTexture(new Uint8Array([128, 128, 255, 255]), 1, 1);", + " flatNormalTexture.needsUpdate = true;", + " }", + " return flatNormalTexture;", + "};", + "", + "const layerTextures = new WeakMap();", + "const layerNormalMaps = new WeakMap();", + "/**", + " * Textures of the image manager have no mipmaps: they would shimmer when", + " * repeated in the distance.", + " * @param {gdjs.RuntimeGame} game", + " * @param {string} resourceName", + " * @param {boolean} isNormalMap Normal maps are directions, not colors.", + " */", + "const getLayerTexture = (game, resourceName, isNormalMap) => {", + " if (!resourceName) return isNormalMap ? getFlatNormalTexture() : getWhiteTexture();", + " const imageTexture = game.getImageManager().getThreeTexture(resourceName);", + " const textures = isNormalMap ? layerNormalMaps : layerTextures;", + " let texture = textures.get(imageTexture);", + " if (!texture) {", + " texture = imageTexture.clone();", + " if (isNormalMap) texture.colorSpace = THREE.NoColorSpace;", + " texture.wrapS = THREE.RepeatWrapping;", + " texture.wrapT = THREE.RepeatWrapping;", + " texture.minFilter = THREE.LinearMipmapLinearFilter;", + " texture.generateMipmaps = true;", + " texture.anisotropy = 4;", + " texture.needsUpdate = true;", + " textures.set(imageTexture, texture);", + " }", + " return texture;", + "};", + "", + "/**", + " * A texture of 4 bytes per sample of a grid of `size * size` samples (layer", + " * weights, tint and wetness...), interpolated between samples.", + " * @param {Uint8Array} bytes", + " * @param {number} size", + " */", + "const createGridTexture = (bytes, size) => {", + " const texture = new THREE.DataTexture(bytes, size, size);", + " texture.magFilter = THREE.LinearFilter;", + " texture.minFilter = THREE.LinearFilter;", + " texture.userData.isUploaded = false;", + " texture.onUpdate = () => {", + " texture.userData.isUploaded = true;", + " };", + " return texture;", + "};", + "", + "/**", + " * Send the rows of the changed samples to the GPU, or the whole texture", + " * before its first upload (and in older GDevelop versions).", + " * @param {THREE.DataTexture} texture", + " * @param {SampleRectangle} samples", + " */", + "const uploadGridTextureRows = (texture, samples) => {", + " if (texture.userData.isUploaded && typeof texture.addUpdateRange === 'function') {", + " const size = texture.image.width;", + " for (let j = samples.minJ; j <= samples.maxJ; j++) {", + " texture.addUpdateRange((j * size + samples.minI) * 4, (samples.maxI - samples.minI + 1) * 4);", + " }", + " }", + " texture.needsUpdate = true;", + "};", + "", + "class TerrainRenderer {", + " /** @param {Terrain} terrain */", + " constructor(terrain) {", + " this.terrain = terrain;", + " this.group = new THREE.Group();", + " terrain.object.get3DRendererObject().add(this.group);", + " /** @type {Array<{mesh: THREE.Mesh, geometry: THREE.BufferGeometry, firstI: number, firstJ: number, center: THREE.Vector3, lod: number}>} */", + " this.chunks = [];", + " this.chunksPerSide = 0;", + " this.chunkCells = 0;", + " this.chunkSideVertices = 0;", + " this.chunkGridVertices = 0;", + " this.chunkVertices = 0;", + " /** @type {THREE.DataTexture | null} */", + " this.splatTexture = null;", + " /** @type {THREE.DataTexture | null} */", + " this.colorTexture = null;", + " this.uniforms = {", + " terrainSplatMap: { value: getWhiteTexture() },", + " terrainSplatUvScaleAndOffset: { value: new THREE.Vector2() },", + " terrainLayerColors: { value: [new THREE.Color(), new THREE.Color(), new THREE.Color(), new THREE.Color()] },", + " terrainLayerMap0: { value: getWhiteTexture() },", + " terrainLayerMap1: { value: getWhiteTexture() },", + " terrainLayerMap2: { value: getWhiteTexture() },", + " terrainLayerMap3: { value: getWhiteTexture() },", + " terrainLayerNormalMap0: { value: getFlatNormalTexture() },", + " terrainLayerNormalMap1: { value: getFlatNormalTexture() },", + " terrainLayerNormalMap2: { value: getFlatNormalTexture() },", + " terrainLayerNormalMap3: { value: getFlatNormalTexture() },", + " terrainHasNormalMaps: { value: 0 },", + " terrainNormalMapStrength: { value: 1 },", + " terrainTextureSize: { value: 256 },", + " terrainHeightBlending: { value: 0 },", + " terrainTextureVariation: { value: 0 },", + " terrainCliffLayer: { value: new THREE.Vector4() },", + " terrainCliffCosines: { value: new THREE.Vector2() },", + " terrainBrush: { value: new THREE.Vector4() },", + " terrainColorMap: { value: getWhiteTexture() },", + " // Z (in the terrain group) under which the ground is wet, fade height, and 1 if it's used.", + " terrainWetShore: { value: new THREE.Vector3() },", + " // Scene units by 3D world unit.", + " terrainWorldScale: { value: 1 },", + " };", + " this.material = new THREE.MeshStandardMaterial({ roughness: 1, metalness: 0 });", + " this.material.onBeforeCompile = (shader) => {", + " Object.assign(shader.uniforms, this.uniforms);", + " shader.vertexShader = patchShaderCode(shader.vertexShader, [", + " ['#include ', '#include \\n' + vertexShaderDeclarations],", + " ['#include ', '#include \\n' + vertexShaderCode],", + " ], 'Terrain3D');", + " shader.fragmentShader = patchShaderCode(shader.fragmentShader, [", + " ['#include ', '#include \\n' + fragmentShaderDeclarations],", + " ['#include ', fragmentShaderCode],", + " ['#include ', '#include \\n' + fragmentShaderRoughnessCode],", + " ['#include ', '#include \\n' + fragmentShaderNormalCode],", + " ], 'Terrain3D');", + " };", + " this.material.customProgramCacheKey = () => (this.hasHoles ? 'Terrain3DWithHoles' : 'Terrain3D');", + " this.hasHoles = false;", + " // Without them, holes would cast shadows.", + " this.depthMaterial = this._createHoleDepthMaterial(new THREE.MeshDepthMaterial());", + " this.distanceMaterial = this._createHoleDepthMaterial(new THREE.MeshDistanceMaterial());", + " this._cameraPosition = new THREE.Vector3();", + " this._chunkPosition = new THREE.Vector3();", + " this._scale = new THREE.Vector3();", + " }", + "", + " /**", + " * @param {THREE.MeshDepthMaterial | THREE.MeshDistanceMaterial} material", + " * @returns The material, drawing nothing in holes.", + " */", + " _createHoleDepthMaterial(material) {", + " material.defines = { TERRAIN_HOLES: '' };", + " material.onBeforeCompile = (shader) => {", + " Object.assign(shader.uniforms, this.uniforms);", + " shader.vertexShader = patchShaderCode(shader.vertexShader, [", + " ['#include ', '#include \\nvarying vec2 vTerrainLocalXY;'],", + " ['#include ', '#include \\nvTerrainLocalXY = position.xy;'],", + " ], 'Terrain3D shadows');", + " shader.fragmentShader = patchShaderCode(shader.fragmentShader, [", + " ['#include ', '#include \\n' + holeDepthShaderDeclarations],", + " ['#include ', holeShaderCode + '#include '],", + " ], 'Terrain3D shadows');", + " };", + " material.customProgramCacheKey = () => 'Terrain3DHoles';", + " return material;", + " }", + "", + " /**", + " * Use the shaders drawing holes, or the faster ones without them.", + " * @param {boolean} hasHoles", + " */", + " setHasHoles(hasHoles) {", + " if (hasHoles === this.hasHoles) return;", + " this.hasHoles = hasHoles;", + " // The material has its own defines (STANDARD).", + " if (hasHoles) this.material.defines.TERRAIN_HOLES = '';", + " else delete this.material.defines.TERRAIN_HOLES;", + " this.material.needsUpdate = true;", + " for (const chunk of this.chunks) this._setChunkShadowMaterials(chunk.mesh);", + " }", + "", + " /** @param {THREE.Mesh} mesh */", + " _setChunkShadowMaterials(mesh) {", + " mesh.customDepthMaterial = this.hasHoles ? this.depthMaterial : undefined;", + " mesh.customDistanceMaterial = this.hasHoles ? this.distanceMaterial : undefined;", + " }", + "", + " /** Recreate all the chunks, for example after the resolution changed. */", + " rebuild() {", + " this.disposeChunks();", + " const { data } = this.terrain;", + " this.chunkCells = getChunkCells(data.resolution);", + " this.chunkSideVertices = this.chunkCells + 1;", + " this.chunkGridVertices = this.chunkSideVertices * this.chunkSideVertices;", + " this.chunkVertices = this.chunkGridVertices + 4 * this.chunkSideVertices;", + " this.chunksPerSide = data.resolution / this.chunkCells;", + " const cellSize = AREA_SIZE / data.resolution;", + " // Sample centers are at the texel centers.", + " this.splatTexture = createGridTexture(data.splat, data.size);", + " this.uniforms.terrainSplatMap.value = this.splatTexture;", + " this.colorTexture = createGridTexture(data.colors, data.size);", + " // Tints are colors chosen like the layer colors.", + " this.colorTexture.colorSpace = THREE.SRGBColorSpace;", + " this.uniforms.terrainColorMap.value = this.colorTexture;", + " this.uniforms.terrainSplatUvScaleAndOffset.value.set(1 / (cellSize * data.size), 0.5 / data.size);", + " for (let chunkJ = 0; chunkJ < this.chunksPerSide; chunkJ++) {", + " for (let chunkI = 0; chunkI < this.chunksPerSide; chunkI++) {", + " const firstI = chunkI * this.chunkCells;", + " const firstJ = chunkJ * this.chunkCells;", + " const positions = new Float32Array(this.chunkVertices * 3);", + " for (let j = 0; j < this.chunkSideVertices; j++) {", + " for (let i = 0; i < this.chunkSideVertices; i++) {", + " const vertex = j * this.chunkSideVertices + i;", + " positions[vertex * 3] = (firstI + i) * cellSize;", + " positions[vertex * 3 + 1] = (firstJ + j) * cellSize;", + " }", + " }", + " // Skirt vertices are below border vertices.", + " this._forEachSkirtVertex((skirtVertex, borderVertex) => {", + " positions[skirtVertex * 3] = positions[borderVertex * 3];", + " positions[skirtVertex * 3 + 1] = positions[borderVertex * 3 + 1];", + " });", + " const geometry = new THREE.BufferGeometry();", + " geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));", + " geometry.setAttribute('normal', new THREE.BufferAttribute(new Float32Array(this.chunkVertices * 3), 3));", + " geometry.setIndex(getLodIndex(this.chunkCells, LOD_STEPS[0]));", + " geometry.boundingBox = new THREE.Box3();", + " geometry.boundingSphere = new THREE.Sphere();", + " const mesh = new THREE.Mesh(geometry, this.material);", + " mesh.matrixAutoUpdate = false;", + " // Rays hit the ground at its full resolution, whatever the level of", + " // detail drawn, and go through holes.", + " const chunkCells = this.chunkCells;", + " const terrainData = () => this.terrain.data;", + " const localPoint = new THREE.Vector3();", + " mesh.raycast = function (raycaster, intersects) {", + " const drawnIndex = geometry.index;", + " const firstIntersection = intersects.length;", + " geometry.setIndex(getLodIndex(chunkCells, LOD_STEPS[0]));", + " THREE.Mesh.prototype.raycast.call(this, raycaster, intersects);", + " geometry.setIndex(drawnIndex);", + " const data = terrainData();", + " const cellSize = AREA_SIZE / data.resolution;", + " for (let index = intersects.length - 1; index >= firstIntersection; index--) {", + " this.worldToLocal(localPoint.copy(intersects[index].point));", + " if (data.sampleSolidity(localPoint.x / cellSize, localPoint.y / cellSize) < 0.5) {", + " intersects.splice(index, 1);", + " }", + " }", + " };", + " this._setChunkShadowMaterials(mesh);", + " this.group.add(mesh);", + " this.chunks.push({", + " mesh,", + " geometry,", + " firstI,", + " firstJ,", + " center: new THREE.Vector3(", + " (firstI + this.chunkCells / 2) * cellSize,", + " (firstJ + this.chunkCells / 2) * cellSize,", + " 0", + " ),", + " lod: 0,", + " });", + " }", + " }", + " this.updateSamples(new SampleRectangle().set(0, 0, data.resolution, data.resolution), true);", + " this.updateMaterial();", + " }", + "", + " _forEachSkirtVertex(callback) {", + " for (let k = 0; k < this.chunkSideVertices; k++) {", + " callback(this.chunkGridVertices + k, k);", + " callback(this.chunkGridVertices + this.chunkSideVertices + k, this.chunkCells * this.chunkSideVertices + k);", + " callback(this.chunkGridVertices + 2 * this.chunkSideVertices + k, k * this.chunkSideVertices);", + " callback(", + " this.chunkGridVertices + 3 * this.chunkSideVertices + k,", + " k * this.chunkSideVertices + this.chunkCells", + " );", + " }", + " }", + "", + " /**", + " * Skirts go down only to the lowest point a coarser neighbor chunk can", + " * draw along the border, so that they can't be seen through holes.", + " * @param {Float32Array} positions", + " */", + " _updateSkirtHeights(positions) {", + " const coarsestStep = LOD_STEPS[LOD_STEPS.length - 1];", + " const borderVertices = [", + " (k) => k,", + " (k) => this.chunkCells * this.chunkSideVertices + k,", + " (k) => k * this.chunkSideVertices,", + " (k) => k * this.chunkSideVertices + this.chunkCells,", + " ];", + " borderVertices.forEach((borderVertex, border) => {", + " for (let k = 0; k < this.chunkSideVertices; k++) {", + " let minZ = Infinity;", + " const from = Math.max(k - coarsestStep, 0);", + " const to = Math.min(k + coarsestStep, this.chunkCells);", + " for (let other = from; other <= to; other++) {", + " minZ = Math.min(minZ, positions[borderVertex(other) * 3 + 2]);", + " }", + " const skirtVertex = this.chunkGridVertices + border * this.chunkSideVertices + k;", + " positions[skirtVertex * 3 + 2] = minZ;", + " }", + " });", + " }", + "", + " /**", + " * Update the vertices of the chunks sharing the given samples.", + " * @param {SampleRectangle} samples", + " * @param {boolean} hasPaintChanged", + " */", + " updateSamples(samples, hasPaintChanged) {", + " if (samples.isEmpty()) return;", + " const { data } = this.terrain;", + " const cellSize = AREA_SIZE / data.resolution;", + " const slopeFactor = AREA_DEPTH / cellSize;", + " // Normals of samples next to changed ones change too.", + " const minI = Math.max(samples.minI - 1, 0);", + " const minJ = Math.max(samples.minJ - 1, 0);", + " const maxI = Math.min(samples.maxI + 1, data.resolution);", + " const maxJ = Math.min(samples.maxJ + 1, data.resolution);", + " const firstChunkI = Math.max(Math.ceil(minI / this.chunkCells) - 1, 0);", + " const firstChunkJ = Math.max(Math.ceil(minJ / this.chunkCells) - 1, 0);", + " const lastChunkI = Math.min(Math.floor(maxI / this.chunkCells), this.chunksPerSide - 1);", + " const lastChunkJ = Math.min(Math.floor(maxJ / this.chunkCells), this.chunksPerSide - 1);", + "", + " for (let chunkJ = firstChunkJ; chunkJ <= lastChunkJ; chunkJ++) {", + " for (let chunkI = firstChunkI; chunkI <= lastChunkI; chunkI++) {", + " const chunk = this.chunks[chunkJ * this.chunksPerSide + chunkI];", + " const positions = chunk.geometry.attributes.position.array;", + " const normals = chunk.geometry.attributes.normal.array;", + " const fromI = Math.max(minI - chunk.firstI, 0);", + " const toI = Math.min(maxI - chunk.firstI, this.chunkCells);", + " const fromJ = Math.max(minJ - chunk.firstJ, 0);", + " const toJ = Math.min(maxJ - chunk.firstJ, this.chunkCells);", + " for (let j = fromJ; j <= toJ; j++) {", + " const gridJ = chunk.firstJ + j;", + " for (let i = fromI; i <= toI; i++) {", + " const gridI = chunk.firstI + i;", + " const vertex = j * this.chunkSideVertices + i;", + " const sample = gridJ * data.size + gridI;", + " positions[vertex * 3 + 2] = data.heights[sample] * AREA_DEPTH;", + "", + " const leftI = Math.max(gridI - 1, 0);", + " const rightI = Math.min(gridI + 1, data.resolution);", + " const upJ = Math.max(gridJ - 1, 0);", + " const downJ = Math.min(gridJ + 1, data.resolution);", + " const nx =", + " ((data.getHeight(leftI, gridJ) - data.getHeight(rightI, gridJ)) * slopeFactor) /", + " (rightI - leftI);", + " const ny =", + " ((data.getHeight(gridI, upJ) - data.getHeight(gridI, downJ)) * slopeFactor) /", + " (downJ - upJ);", + " const inverseLength = 1 / Math.sqrt(nx * nx + ny * ny + 1);", + " normals[vertex * 3] = nx * inverseLength;", + " normals[vertex * 3 + 1] = ny * inverseLength;", + " normals[vertex * 3 + 2] = inverseLength;", + " }", + " }", + " this._forEachSkirtVertex((skirtVertex, borderVertex) => {", + " for (let axis = 0; axis < 3; axis++) {", + " normals[skirtVertex * 3 + axis] = normals[borderVertex * 3 + axis];", + " }", + " });", + " this._updateSkirtHeights(positions);", + " let maxZ = 0;", + " for (let vertex = 0; vertex < this.chunkGridVertices; vertex++) {", + " maxZ = Math.max(maxZ, positions[vertex * 3 + 2]);", + " }", + " const chunkSize = this.chunkCells * cellSize;", + " chunk.geometry.boundingBox.min.set(chunk.firstI * cellSize, chunk.firstJ * cellSize, 0);", + " chunk.geometry.boundingBox.max.set(", + " chunk.firstI * cellSize + chunkSize,", + " chunk.firstJ * cellSize + chunkSize,", + " maxZ", + " );", + " chunk.geometry.boundingBox.getBoundingSphere(chunk.geometry.boundingSphere);", + " chunk.center.z = maxZ / 2;", + " chunk.geometry.attributes.position.needsUpdate = true;", + " chunk.geometry.attributes.normal.needsUpdate = true;", + " }", + " }", + " if (hasPaintChanged) {", + " // The data may have been replaced by one with the same resolution.", + " this.splatTexture.image.data = data.splat;", + " uploadGridTextureRows(this.splatTexture, samples);", + " this.colorTexture.image.data = data.colors;", + " uploadGridTextureRows(this.colorTexture, samples);", + " }", + " }", + "", + " updateMaterial() {", + " const { object } = this.terrain;", + " const game = object.getRuntimeScene().getGame();", + " const colors = [object._getLayer1Color(), object._getLayer2Color(), object._getLayer3Color(), object._getLayer4Color()];", + " const textures = [", + " object._getLayer1Texture(),", + " object._getLayer2Texture(),", + " object._getLayer3Texture(),", + " object._getLayer4Texture(),", + " ];", + " const normalMaps = [", + " object._getLayer1NormalMap(),", + " object._getLayer2NormalMap(),", + " object._getLayer3NormalMap(),", + " object._getLayer4NormalMap(),", + " ];", + " for (let layer = 0; layer < LAYER_COUNT; layer++) {", + " this.uniforms.terrainLayerColors.value[layer].set(gdjs.rgbOrHexStringToNumber(colors[layer]));", + " this.uniforms['terrainLayerMap' + layer].value = getLayerTexture(game, textures[layer], false);", + " this.uniforms['terrainLayerNormalMap' + layer].value = getLayerTexture(game, normalMaps[layer], true);", + " }", + " this.uniforms.terrainHasNormalMaps.value = normalMaps.some((normalMap) => !!normalMap) ? 1 : 0;", + " this.uniforms.terrainNormalMapStrength.value = Math.max(object._getNormalMapStrength(), 0);", + " this.uniforms.terrainTextureSize.value = Math.max(object._getTextureSize(), 1);", + " this.uniforms.terrainHeightBlending.value = clamp01(object._getHeightBlending());", + " this.uniforms.terrainTextureVariation.value = clamp01(object._getTextureVariation());", + "", + " const cliffLayer = parseInt(object._getCliffLayer(), 10);", + " const cliffAngle = gdjs.toRad(object._getCliffAngle());", + " const cliffBlendAngle = gdjs.toRad(4);", + " this.uniforms.terrainCliffLayer.value.set(", + " cliffLayer === 1 ? 1 : 0,", + " cliffLayer === 2 ? 1 : 0,", + " cliffLayer === 3 ? 1 : 0,", + " cliffLayer === 4 ? 1 : 0", + " );", + " this.uniforms.terrainCliffCosines.value.set(", + " Math.cos(cliffAngle + cliffBlendAngle),", + " Math.cos(cliffAngle - cliffBlendAngle)", + " );", + "", + " const castShadow = object._getCastShadow();", + " const receiveShadow = object._getReceiveShadow();", + " for (const chunk of this.chunks) {", + " chunk.mesh.castShadow = castShadow;", + " chunk.mesh.receiveShadow = receiveShadow;", + " }", + " }", + "", + " /**", + " * Show the brush circle of the editor.", + " * @param {THREE.Vector3 | null} localPosition", + " * @param {number} radius In scene units.", + " */", + " setBrush(localPosition, radius) {", + " const brush = this.uniforms.terrainBrush.value;", + " if (!localPosition) {", + " brush.w = 0;", + " return;", + " }", + " this.group.updateWorldMatrix(true, false);", + " const worldPosition = this._chunkPosition.copy(localPosition).applyMatrix4(this.group.matrixWorld);", + " const worldScale = this.uniforms.terrainWorldScale.value;", + " brush.set(worldPosition.x * worldScale, worldPosition.y * worldScale, radius, 1);", + " }", + "", + " /** @param {THREE.Camera | null} camera */", + " updateLevelsOfDetail(camera) {", + " if (!camera || !this.group.visible) return;", + " const matrixWorld = this.group.matrixWorld;", + " this._cameraPosition.setFromMatrixPosition(camera.matrixWorld);", + " this._scale.setFromMatrixScale(matrixWorld);", + " const cellSize = AREA_SIZE / this.terrain.data.resolution;", + " const chunkWorldSize = this.chunkCells * cellSize * Math.max(this._scale.x, this._scale.y);", + " for (const chunk of this.chunks) {", + " const distance =", + " this._chunkPosition.copy(chunk.center).applyMatrix4(matrixWorld).distanceTo(this._cameraPosition) /", + " chunkWorldSize;", + " let lod = LOD_DISTANCES.length - 1;", + " while (lod > 0 && distance < LOD_DISTANCES[lod]) lod--;", + " if (lod !== chunk.lod) {", + " chunk.lod = lod;", + " chunk.geometry.setIndex(getLodIndex(this.chunkCells, LOD_STEPS[lod]));", + " }", + " }", + " }", + "", + " disposeChunks() {", + " for (const chunk of this.chunks) {", + " chunk.mesh.removeFromParent();", + " // The index is shared by all chunks: it must stay on the GPU.", + " chunk.geometry.setIndex(null);", + " chunk.geometry.dispose();", + " }", + " this.chunks.length = 0;", + " if (this.splatTexture) this.splatTexture.dispose();", + " this.splatTexture = null;", + " if (this.colorTexture) this.colorTexture.dispose();", + " this.colorTexture = null;", + " }", + "", + " dispose() {", + " this.disposeChunks();", + " this.material.dispose();", + " this.depthMaterial.dispose();", + " this.distanceMaterial.dispose();", + " this.group.removeFromParent();", + " }", + "}", + "", + "/**", + " * Makes a Physics3D behavior use a height field shape following the terrain.", + " * Changes of heights are applied to the shape without recreating it.", + " * @implements {gdjs.Physics3DRuntimeBehavior.BodyUpdater}", + " */", + "class TerrainBodyUpdater {", + " /**", + " * @param {Terrain} terrain", + " * @param {gdjs.Physics3DRuntimeBehavior} physics", + " */", + " constructor(terrain, physics) {", + " this.terrain = terrain;", + " this.physics = physics;", + " this.defaultBodyUpdater = physics.bodyUpdater;", + " this.changedSamples = new SampleRectangle();", + " /** @type {Jolt.HeightFieldShape | null} */", + " this.heightField = null;", + " this.heightScale = 1;", + " this.heightsBufferPointer = 0;", + " this.heightsBufferLength = 0;", + " this.bodyX = 0;", + " this.bodyY = 0;", + " this.bodyZ = 0;", + " this.bodyHeight = 0;", + " physics.bodyType = 'Static';", + " physics.bodyUpdater = this;", + " }", + "", + " createShape() {", + " const { data, object } = this.terrain;", + " const worldInvScale = this.physics._sharedData.worldInvScale;", + " const { size } = data;", + " this.heightScale = object.getDepth() * worldInvScale;", + "", + " // Changes of heights (SetHeights) break collisions unless the sample", + " // count is a power of 2: extra samples have no collision.", + " const sampleCount = Math.pow(2, Math.ceil(Math.log2(size)));", + " const settings = new Jolt.HeightFieldShapeSettings();", + " settings.mScale.Set(", + " (object.getWidth() / data.resolution) * worldInvScale,", + " 1,", + " (object.getHeight() / data.resolution) * worldInvScale", + " );", + " settings.mSampleCount = sampleCount;", + " settings.mBlockSize = 4;", + " // Allows later changes of heights on the whole depth of the object.", + " settings.mMinHeightValue = 0;", + " settings.mMaxHeightValue = this.heightScale;", + " settings.mHeightSamples.resize(sampleCount * sampleCount);", + " const samples = new Float32Array(", + " Jolt.HEAPF32.buffer,", + " Jolt.getPointer(settings.mHeightSamples.data()),", + " sampleCount * sampleCount", + " );", + " const noCollision = Jolt.HeightFieldShapeConstantValues.prototype.cNoCollisionValue;", + " samples.fill(noCollision);", + " // Height fields go along X and Z: rows are flipped so that, once", + " // rotated to have heights along Z, rows go along Y.", + " for (let j = 0; j < size; j++) {", + " const row = (size - 1 - j) * sampleCount;", + " for (let i = 0; i < size; i++) {", + " samples[row + i] = this._getSampleHeight(j * size + i, noCollision);", + " }", + " }", + " const shape = settings.Create().Get();", + " Jolt.destroy(settings);", + " this.heightField = Jolt.castObject(shape, Jolt.HeightFieldShape);", + " this.changedSamples.clear();", + " return shape;", + " }", + "", + " /** @returns {number} The height of a sample in the shape, or no collision in a hole. */", + " _getSampleHeight(index, noCollision) {", + " const height = this.terrain.surfaceHeights[index];", + " return Number.isNaN(height) ? noCollision : height * this.heightScale;", + " }", + "", + " _getBodyPosition() {", + " const { object } = this.terrain;", + " const worldInvScale = this.physics._sharedData.worldInvScale;", + " this.bodyX = object.getX();", + " this.bodyY = object.getY();", + " this.bodyZ = object.getZ();", + " // The origin of the height field is at the bottom of the terrain (see createShape).", + " this.bodyHeight = object.getHeight();", + " return this.physics._sharedData.getRVec3(", + " object.getX() * worldInvScale,", + " (object.getY() + object.getHeight()) * worldInvScale,", + " object.getZ() * worldInvScale", + " );", + " }", + "", + " _getBodyRotation() {", + " // A quarter turn around X: heights go along Z.", + " return this.physics._sharedData.getQuat(Math.SQRT1_2, 0, 0, Math.SQRT1_2);", + " }", + "", + " createAndAddBody() {", + " const { physics } = this;", + " const { bodyInterface } = physics._sharedData;", + " const shape = this.createShape();", + " const settings = new Jolt.BodyCreationSettings(", + " shape,", + " this._getBodyPosition(),", + " this._getBodyRotation(),", + " Jolt.EMotionType_Static,", + " physics.getBodyLayer()", + " );", + " settings.mFriction = physics.friction;", + " settings.mRestitution = physics.restitution;", + " const body = bodyInterface.CreateBody(settings);", + " Jolt.destroy(settings);", + " bodyInterface.AddBody(body.GetID(), Jolt.EActivation_DontActivate);", + " return body;", + " }", + "", + " updateObjectFromBody() {", + " // The terrain is static: the body never moves it.", + " }", + "", + " updateBodyFromObject() {", + " const { physics } = this;", + " const body = physics._body;", + " if (!body) return;", + " const { object } = this.terrain;", + " const { bodyInterface } = physics._sharedData;", + " if (", + " object.getX() !== this.bodyX ||", + " object.getY() !== this.bodyY ||", + " object.getZ() !== this.bodyZ ||", + " object.getHeight() !== this.bodyHeight", + " ) {", + " bodyInterface.SetPositionAndRotation(", + " body.GetID(),", + " this._getBodyPosition(),", + " this._getBodyRotation(),", + " Jolt.EActivation_DontActivate", + " );", + " }", + " if (!this.changedSamples.isEmpty()) {", + " this._applyChangedHeights(body);", + " }", + " }", + "", + " _applyChangedHeights(body) {", + " const { data } = this.terrain;", + " const { size } = data;", + " const { minI, minJ, maxI, maxJ } = this.changedSamples;", + " // Jolt only changes blocks of samples. Rows are flipped and extra", + " // samples have no collision (see createShape).", + " const blockSize = this.heightField.GetBlockSize();", + " const sampleCount = this.heightField.GetSampleCount();", + " const alignDown = (value) => Math.floor(value / blockSize) * blockSize;", + " const alignUp = (value) => Math.min(Math.ceil(value / blockSize) * blockSize, sampleCount);", + " const firstX = alignDown(minI);", + " const firstRow = alignDown(size - 1 - maxJ);", + " const width = alignUp(maxI + 1) - firstX;", + " const height = alignUp(size - minJ) - firstRow;", + " if (width * height > this.heightsBufferLength) {", + " if (this.heightsBufferPointer) Jolt._webidl_free(this.heightsBufferPointer);", + " this.heightsBufferLength = width * height;", + " this.heightsBufferPointer = Jolt._webidl_malloc(this.heightsBufferLength * 4);", + " }", + " const heights = new Float32Array(Jolt.HEAPF32.buffer, this.heightsBufferPointer, width * height);", + " const noCollision = Jolt.HeightFieldShapeConstantValues.prototype.cNoCollisionValue;", + " for (let row = 0; row < height; row++) {", + " const j = size - 1 - (firstRow + row);", + " for (let column = 0; column < width; column++) {", + " const i = firstX + column;", + " heights[row * width + column] =", + " i < size && j >= 0 ? this._getSampleHeight(j * size + i, noCollision) : noCollision;", + " }", + " }", + " const sharedData = this.physics._sharedData;", + " this.heightField.SetHeights(", + " firstX,", + " firstRow,", + " width,", + " height,", + " this.heightsBufferPointer,", + " width,", + " sharedData.jolt.GetTempAllocator()", + " );", + " sharedData.bodyInterface.NotifyShapeChanged(", + " body.GetID(),", + " sharedData.getVec3(0, 0, 0),", + " false,", + " Jolt.EActivation_DontActivate", + " );", + " this._wakeUpBodiesOnChangedSamples();", + " this.changedSamples.clear();", + " }", + "", + " /** Bodies sleeping on the changed area would otherwise float or sink. */", + " _wakeUpBodiesOnChangedSamples() {", + " const { object, data } = this.terrain;", + " const worldInvScale = this.physics._sharedData.worldInvScale;", + " const cellWidth = object.getWidth() / data.resolution;", + " const cellHeight = object.getHeight() / data.resolution;", + " const { minI, minJ, maxI, maxJ } = this.changedSamples;", + " const min = new Jolt.Vec3(", + " (object.getX() + (minI - 1) * cellWidth) * worldInvScale,", + " (object.getY() + (minJ - 1) * cellHeight) * worldInvScale,", + " (object.getZ() - 1) * worldInvScale", + " );", + " const max = new Jolt.Vec3(", + " (object.getX() + (maxI + 1) * cellWidth) * worldInvScale,", + " (object.getY() + (maxJ + 1) * cellHeight) * worldInvScale,", + " (object.getZ() + object.getDepth() + 1) * worldInvScale", + " );", + " const box = new Jolt.AABox(min, max);", + " const broadPhaseLayerFilter = new Jolt.BroadPhaseLayerFilter();", + " const objectLayerFilter = new Jolt.ObjectLayerFilter();", + " this.physics._sharedData.bodyInterface.ActivateBodiesInAABox(box, broadPhaseLayerFilter, objectLayerFilter);", + " Jolt.destroy(min);", + " Jolt.destroy(max);", + " Jolt.destroy(box);", + " Jolt.destroy(broadPhaseLayerFilter);", + " Jolt.destroy(objectLayerFilter);", + " }", + "", + " recreateShape() {", + " const body = this.physics._body;", + " if (!body) return;", + " this.physics._sharedData.bodyInterface.SetShape(", + " body.GetID(),", + " this.createShape(),", + " false,", + " Jolt.EActivation_DontActivate", + " );", + " const { resolution } = this.terrain.data;", + " this.changedSamples.set(0, 0, resolution, resolution);", + " this._wakeUpBodiesOnChangedSamples();", + " this.changedSamples.clear();", + " }", + "", + " destroyBody() {", + " this.defaultBodyUpdater.destroyBody();", + " this.heightField = null;", + " }", + "", + " dispose() {", + " if (this.heightsBufferPointer) Jolt._webidl_free(this.heightsBufferPointer);", + " this.heightsBufferPointer = 0;", + " this.heightsBufferLength = 0;", + " }", + "}", + "", + "/** @param {TerrainData} data */", + "const copyTerrainData = (data) => {", + " const copy = new TerrainData(data.resolution);", + " copy.copyFrom(data);", + " return copy;", + "};", + "", + "// Generating a relief or reading an image is long for big terrains, and done", + "// for every instance and every hot-reload: the results are kept.", + "const baseDataCache = new Map();", + "const MAX_CACHED_BASE_DATA = 8;", + "// Changes of the shape of a terrain kept for consumers of its surface that", + "// missed some versions: older changes update the whole surface.", + "const MAX_SURFACE_CHANGES = 64;", + "// Unique among all terrains: a terrain replaced by another one never has its version.", + "let lastGroundVersion = 0;", + "", + "/**", + " * The terrain of a Terrain3D object: its data, how it's drawn and its", + " * surface (for collisions and navigation meshes). Positions are in the scene.", + " */", + "class Terrain {", + " /** @param {gdjs.CustomRuntimeObject3D} object */", + " constructor(object) {", + " this.object = object;", + " /** @type {TerrainData} */", + " this.data = new TerrainData(DEFAULT_RESOLUTION);", + " /** The data before any sculpting or painting, from the relief or heightmap. */", + " this.baseData = this.data;", + " this.renderer = new TerrainRenderer(this);", + " /** @type {TerrainBodyUpdater[]} */", + " this.bodyUpdaters = [];", + " this.changedSamples = new SampleRectangle();", + " this.hasPaintChanged = false;", + " /** Increased each time the ground changes, for objects following it (like grass). */", + " this.version = 0;", + " /** Increased each time the shape of the ground changes (not its paint or position). */", + " this.surfaceVersion = 0;", + " /**", + " * The last changes of the shape of the ground, for `getChangedArea`.", + " * @type {Array<{version: number, samples: SampleRectangle}>}", + " */", + " this.surfaceChanges = [];", + " /** The changes since this version are all in `surfaceChanges`. */", + " this.surfaceChangesStartVersion = 0;", + " this._stroke = { cellWidth: 1, cellHeight: 1, ax: 0, ay: 0, bx: 0, by: 0, radius: 0 };", + " this._strokeChangedSamples = new SampleRectangle();", + " /** The properties the data was loaded from, to reload it only if they change. */", + " this.loadedFrom = '';", + " /** The heightmap image being loaded: the relief is used until it is. */", + " this.loadingHeightmapImage = '';", + " this.isDisposed = false;", + " /** The last value of the sculpt data property read or saved. */", + " this.sculptData = '';", + " /**", + " * Values saved by the editor tools: the editor sends them back with a", + " * hot-reload, possibly after newer ones were saved.", + " * @type {string[]}", + " */", + " this.savedSculptData = [];", + " /** The heights of the surface: the heights of the data, and NaN in holes. */", + " this.surfaceHeights = new Float32Array(0);", + " this.loadFromProperties();", + " this._transform = this._getTransform();", + " getGrounds(object.getRuntimeScene()).add(this);", + " /**", + " * The shape of the ground, for the physics engine and navigation meshes.", + " * @type {gdjs.Surface}", + " */", + " this.surface = {", + " getVersion: () => this.surfaceVersion,", + " getChangedArea: (sinceVersion) => this.getChangedArea(sinceVersion),", + " getTriangles: () => null,", + " getHeightField: () => ({ columns: this.data.size, rows: this.data.size, heights: this.surfaceHeights }),", + " };", + " if (typeof object.setSurface === 'function') {", + " object.setSurface(this.surface);", + " } else if (gdjs.Physics3DRuntimeBehavior) {", + " // Older GDevelop versions don't use surfaces.", + " for (const behavior of object._behaviors) {", + " if (behavior instanceof gdjs.Physics3DRuntimeBehavior) {", + " this.bodyUpdaters.push(new TerrainBodyUpdater(this, behavior));", + " }", + " }", + " }", + " }", + "", + " _getSourceProperties() {", + " const { object } = this;", + " return [", + " object._getResolution(),", + " object._getRelief(),", + " object._getSeed(),", + " object._getHeightmapImage(),", + " object._getEdits(),", + " ].join('|');", + " }", + "", + " loadFromProperties() {", + " const { object } = this;", + " const resolution = parseInt(object._getResolution(), 10);", + " const validResolution = RESOLUTIONS.indexOf(resolution) !== -1 ? resolution : DEFAULT_RESOLUTION;", + " this.sculptData = object._getSculptData();", + " this.loadedFrom = this._getSourceProperties();", + "", + " this.baseData = this._getBaseData(validResolution);", + " let data = codec.decode(this.sculptData, (resolution) => copyTerrainData(this._getBaseData(resolution)));", + " if (data && data.resolution !== validResolution) {", + " data = data.resampled(validResolution);", + " }", + " this.setData(data || copyTerrainData(this.baseData));", + " }", + "", + " /**", + " * @param {number} resolution", + " * @returns {TerrainData} The data from the relief or the heightmap, shared: it must not be changed.", + " */", + " _getBaseData(resolution) {", + " const { object } = this;", + " const key = [resolution, object._getRelief(), object._getSeed(), object._getHeightmapImage(), object._getEdits()].join('|');", + " const cachedData = baseDataCache.get(key);", + " if (cachedData) return cachedData;", + "", + " const data = new TerrainData(resolution);", + " const heightmapImage = object._getHeightmapImage();", + " const isHeightmapLoaded =", + " !!heightmapImage && loadHeightsFromImage(data, object.getRuntimeScene().getGame(), heightmapImage);", + " if (!isHeightmapLoaded) {", + " relief.generate(data, object._getRelief(), object._getSeed());", + " if (heightmapImage) this._reloadOnceLoaded(heightmapImage);", + " }", + " edits.apply(data, object._getEdits());", + " // Not kept if the image is missing: it may be loaded later.", + " if (isHeightmapLoaded || !heightmapImage) baseDataCache.set(key, data);", + " if (baseDataCache.size > MAX_CACHED_BASE_DATA) {", + " baseDataCache.delete(baseDataCache.keys().next().value);", + " }", + " return data;", + " }", + "", + " /** @param {TerrainData} data */", + " setData(data) {", + " this.version = ++lastGroundVersion;", + " this.surfaceVersion = this.version;", + " this.surfaceChanges.length = 0;", + " this.surfaceChangesStartVersion = this.version;", + " const hasSameResolution = data.resolution === this.data.resolution && this.renderer.chunks.length > 0;", + " this.data = data;", + " const allSamples = new SampleRectangle().set(0, 0, data.resolution, data.resolution);", + " this.surfaceHeights = new Float32Array(data.heights.length);", + " this.renderer.setHasHoles(false);", + " this._updateSurfaceHeights(allSamples);", + " if (hasSameResolution) {", + " this.renderer.updateSamples(allSamples, true);", + " } else {", + " this.renderer.rebuild();", + " }", + " for (const bodyUpdater of this.bodyUpdaters) {", + " bodyUpdater.recreateShape();", + " }", + " }", + "", + " onHotReloading() {", + " const sculptData = this.object._getSculptData();", + " const savedIndex = this.savedSculptData.indexOf(sculptData);", + " if (savedIndex !== -1 && this._getSourceProperties() === this.loadedFrom) {", + " // The terrain already has this data, or newer changes.", + " this.savedSculptData.splice(0, savedIndex + 1);", + " } else if (sculptData !== this.sculptData || this._getSourceProperties() !== this.loadedFrom) {", + " this.savedSculptData.length = 0;", + " this.loadFromProperties();", + " }", + " this.renderer.updateMaterial();", + " }", + "", + " /** @returns {string} The data to save in the sculpt data property. */", + " save() {", + " this.sculptData = codec.encode(this.data, this.baseData);", + " this.savedSculptData.push(this.sculptData);", + " return this.sculptData;", + " }", + "", + " getCellWidth() {", + " return this.object.getWidth() / this.data.resolution;", + " }", + "", + " getCellHeight() {", + " return this.object.getHeight() / this.data.resolution;", + " }", + "", + " _toGridX(x) {", + " return (x - this.object.getX()) / this.getCellWidth();", + " }", + "", + " _toGridY(y) {", + " return (y - this.object.getY()) / this.getCellHeight();", + " }", + "", + " /** @returns {number} The Z position of the ground. */", + " getHeightAt(x, y) {", + " return this.object.getZ() + this.data.sampleHeight(this._toGridX(x), this._toGridY(y)) * this.object.getDepth();", + " }", + "", + " /** @returns {number} The angle of the ground with the horizontal, in degrees. */", + " getSlopeAt(x, y) {", + " const gridX = this._toGridX(x);", + " const gridY = this._toGridY(y);", + " const { data } = this;", + " const depth = this.object.getDepth();", + " const slopeX = ((data.sampleHeight(gridX + 0.5, gridY) - data.sampleHeight(gridX - 0.5, gridY)) * depth) / this.getCellWidth();", + " const slopeY = ((data.sampleHeight(gridX, gridY + 0.5) - data.sampleHeight(gridX, gridY - 0.5)) * depth) / this.getCellHeight();", + " return gdjs.toDegrees(Math.atan(Math.hypot(slopeX, slopeY)));", + " }", + "", + " /** @returns {number} The layer painted the most, from 1 to 4. */", + " getLayerAt(x, y) {", + " return this.data.getDominantLayer(this._toGridX(x), this._toGridY(y)) + 1;", + " }", + "", + " /**", + " * @param {number} layer From 1 to 4.", + " * @returns {number} How much the layer is painted, from 0 to 1.", + " */", + " getLayerWeightAt(x, y, layer) {", + " const layerIndex = clampInteger(layer - 1, 0, LAYER_COUNT - 1);", + " return this.data.sampleLayerWeight(this._toGridX(x), this._toGridY(y), layerIndex);", + " }", + "", + " /** @returns {number[]} The painted tint: red, green and blue, from 0 to 255 (white when there is none). */", + " getTintAt(x, y) {", + " const gridX = this._toGridX(x);", + " const gridY = this._toGridY(y);", + " return [0, 1, 2].map((channel) => Math.round(this.data.sampleColor(gridX, gridY, channel) * 255));", + " }", + "", + " /** @returns {number} How wet the ground is, from 0 (dry) to 1, painted or near the water (see the wet shore). */", + " getWetnessAt(x, y) {", + " const paintedWetness = this.data.sampleColor(this._toGridX(x), this._toGridY(y), 3);", + " const { object } = this;", + " if (!object._getWetShore()) return paintedWetness;", + " const fade = Math.max(object._getWetShoreFade(), 0.001);", + " const shoreWetness = clamp01((object._getWetShoreZ() + fade - this.getHeightAt(x, y)) / fade);", + " // Like the smoothstep of the shader.", + " return Math.max(paintedWetness, shoreWetness * shoreWetness * (3 - 2 * shoreWetness));", + " }", + "", + " _isInArea(x, y) {", + " const { object } = this;", + " return (", + " x >= object.getX() &&", + " x <= object.getX() + object.getWidth() &&", + " y >= object.getY() &&", + " y <= object.getY() + object.getHeight()", + " );", + " }", + "", + " /** @returns {boolean} true if the position is above or under the terrain, and not in a hole. */", + " containsPoint(x, y) {", + " return this._isInArea(x, y) && this.data.sampleSolidity(this._toGridX(x), this._toGridY(y)) >= 0.5;", + " }", + "", + " /** @returns {boolean} true if there is a hole in the terrain at the position, like what is drawn. */", + " isHoleAt(x, y) {", + " return this._isInArea(x, y) && this.data.sampleSolidity(this._toGridX(x), this._toGridY(y)) < 0.5;", + " }", + "", + " _getStroke(x1, y1, x2, y2, radius) {", + " const stroke = this._stroke;", + " stroke.cellWidth = this.getCellWidth();", + " stroke.cellHeight = this.getCellHeight();", + " stroke.ax = x1 - this.object.getX();", + " stroke.ay = y1 - this.object.getY();", + " stroke.bx = x2 - this.object.getX();", + " stroke.by = y2 - this.object.getY();", + " stroke.radius = Math.max(radius, 0);", + " return stroke;", + " }", + "", + " /**", + " * @param {SampleRectangle} samples", + " * @param {boolean} haveHeightsChanged false when only the paint changed.", + " */", + " _onSamplesChanged(samples, haveHeightsChanged) {", + " if (samples.isEmpty()) return;", + " this.version = ++lastGroundVersion;", + " this.changedSamples.add(samples);", + " // Painting changes the surface when it makes holes or fills them.", + " const hasSurfaceChanged = this._updateSurfaceHeights(samples);", + " if (!haveHeightsChanged && !hasSurfaceChanged) return;", + " this.surfaceVersion = this.version;", + " this.surfaceChanges.push({ version: this.version, samples: new SampleRectangle().set(samples.minI, samples.minJ, samples.maxI, samples.maxJ) });", + " if (this.surfaceChanges.length > MAX_SURFACE_CHANGES) {", + " this.surfaceChangesStartVersion = this.surfaceChanges.shift().version;", + " }", + " for (const bodyUpdater of this.bodyUpdaters) {", + " bodyUpdater.changedSamples.add(samples);", + " }", + " }", + "", + " /**", + " * @param {SampleRectangle} samples", + " * @returns {boolean} true if heights of the surface changed, or holes were made or filled.", + " */", + " _updateSurfaceHeights(samples) {", + " const { data, surfaceHeights } = this;", + " let hasChanged = false;", + " let hasHoles = false;", + " for (let j = samples.minJ; j <= samples.maxJ; j++) {", + " for (let i = samples.minI; i <= samples.maxI; i++) {", + " const index = j * data.size + i;", + " const isHole = data.isHole(index);", + " const height = isHole ? NaN : data.heights[index];", + " const previousHeight = surfaceHeights[index];", + " if (isHole) {", + " hasHoles = true;", + " if (Number.isNaN(previousHeight)) continue;", + " } else if (height === previousHeight) {", + " continue;", + " }", + " surfaceHeights[index] = height;", + " hasChanged = true;", + " }", + " }", + " // Filled holes keep the shaders drawing holes until the data is loaded again.", + " if (hasHoles) this.renderer.setHasHoles(true);", + " return hasChanged;", + " }", + "", + " /** @param {number} height In scene units, negative to lower the ground. */", + " raise(x1, y1, x2, y2, radius, height) {", + " const samples = this._strokeChangedSamples;", + " brushes.raise(this.data, this._getStroke(x1, y1, x2, y2, radius), height / this.object.getDepth(), samples);", + " this._onSamplesChanged(samples, true);", + " }", + "", + " /** @param {number} z The Z position to flatten the ground to. */", + " /** @param {number} [toZ] The Z position at the end of the segment, for a slope. The same as z by default. */", + " flatten(x1, y1, x2, y2, radius, z, strength, toZ = z) {", + " const samples = this._strokeChangedSamples;", + " const toHeight = (value) => (value - this.object.getZ()) / this.object.getDepth();", + " brushes.flatten(this.data, this._getStroke(x1, y1, x2, y2, radius), toHeight(z), strength, samples, toHeight(toZ));", + " this._onSamplesChanged(samples, true);", + " }", + "", + " smooth(x1, y1, x2, y2, radius, strength) {", + " const samples = this._strokeChangedSamples;", + " brushes.smooth(this.data, this._getStroke(x1, y1, x2, y2, radius), strength, samples);", + " this._onSamplesChanged(samples, true);", + " }", + "", + " /** @param {number} layer From 1 to 4. */", + " paint(x1, y1, x2, y2, radius, layer, strength) {", + " const layerIndex = clampInteger(layer - 1, 0, LAYER_COUNT - 1);", + " const samples = this._strokeChangedSamples;", + " brushes.paint(this.data, this._getStroke(x1, y1, x2, y2, radius), layerIndex, strength, samples);", + " this.hasPaintChanged = true;", + " this._onSamplesChanged(samples, false);", + " }", + "", + " /** @param {number[]} color The red, green and blue (0-255) of the tint: white removes it. */", + " tint(x1, y1, x2, y2, radius, color, strength) {", + " const samples = this._strokeChangedSamples;", + " brushes.tint(this.data, this._getStroke(x1, y1, x2, y2, radius), color, strength, samples);", + " this.hasPaintChanged = true;", + " this._onSamplesChanged(samples, false);", + " }", + "", + " /** @param {number} wetness From 0 (dry) to 1. */", + " wet(x1, y1, x2, y2, radius, wetness, strength) {", + " const samples = this._strokeChangedSamples;", + " brushes.wet(this.data, this._getStroke(x1, y1, x2, y2, radius), wetness, strength, samples);", + " this.hasPaintChanged = true;", + " this._onSamplesChanged(samples, false);", + " }", + "", + " /** Make a hole around a segment (`fillHoles` fills it again). */", + " makeHole(x1, y1, x2, y2, radius) {", + " const samples = this._strokeChangedSamples;", + " brushes.hole(this.data, this._getStroke(x1, y1, x2, y2, radius), samples);", + " this.hasPaintChanged = true;", + " this._onSamplesChanged(samples, false);", + " }", + "", + " /** Fill the holes around a segment with the layers of the ground around them. */", + " fillHoles(x1, y1, x2, y2, radius) {", + " const samples = this._strokeChangedSamples;", + " brushes.fill(this.data, this._getStroke(x1, y1, x2, y2, radius), samples);", + " this.hasPaintChanged = true;", + " this._onSamplesChanged(samples, false);", + " }", + "", + " /**", + " * Make the entrance of a tunnel or a cave: a hole, with the ground in", + " * front of it flattened to the floor of the tunnel.", + " * @param {number} angle The direction going into the ground, in degrees.", + " * @param {number} floorZ", + " * @param {number} length The length of the flat ground in front of the entrance.", + " */", + " carveEntrance(x, y, radius, angle, floorZ, length) {", + " const directionX = Math.cos(gdjs.toRad(angle));", + " const directionY = Math.sin(gdjs.toRad(angle));", + " this.flatten(x - directionX * length, y - directionY * length, x, y, radius * 1.5, floorZ, 1);", + " this.makeHole(x, y, x + directionX * radius, y + directionY * radius, radius);", + " }", + "", + " generate(reliefName, seed) {", + " this.baseData = new TerrainData(this.data.resolution);", + " relief.generate(this.baseData, reliefName, seed);", + " this.setData(copyTerrainData(this.baseData));", + " }", + "", + " loadHeightmap(imageResourceName) {", + " const baseData = new TerrainData(this.data.resolution);", + " if (loadHeightsFromImage(baseData, this.object.getRuntimeScene().getGame(), imageResourceName)) {", + " baseData.splat.set(this.data.splat);", + " baseData.colors.set(this.data.colors);", + " this.baseData = baseData;", + " this.setData(copyTerrainData(baseData));", + " }", + " }", + "", + " /** @param {gdjs.RuntimeObject[]} objects */", + " placeOnGround(objects) {", + " for (const object of objects) {", + " if (object === this.object || !gdjs.Base3DHandler.is3D(object)) continue;", + " const groundZ = this.getHeightAt(object.getCenterXInScene(), object.getCenterYInScene());", + " // The origin of the object is not always at its bottom.", + " object.setZ(groundZ + object.getZ() - object.getUnrotatedAABBMinZ());", + " }", + " }", + "", + " /**", + " * @returns {THREE.Vector3 | null} The point of the ground under the cursor,", + " * in the terrain group coordinates. Holes are hit too, so that brushes can fill them.", + " */", + " raycastFromCursor(game, raycaster, ndc) {", + " const layer = this.object.getInstanceContainer().getLayer(this.object.getLayer());", + " const camera = layer.getRenderer().getThreeCamera();", + " if (!camera) return null;", + " const inputManager = game.getInputManager();", + " ndc.set(", + " (inputManager.getCursorX() / game.getGameResolutionWidth()) * 2 - 1,", + " -(inputManager.getCursorY() / game.getGameResolutionHeight()) * 2 + 1", + " );", + " raycaster.setFromCamera(ndc, camera);", + " this.renderer.group.updateWorldMatrix(true, false);", + " const ray = raycaster.ray.clone().applyMatrix4(this.renderer.group.matrixWorld.clone().invert());", + " ray.direction.normalize();", + "", + " const box = new THREE.Box3(new THREE.Vector3(0, 0, 0), new THREE.Vector3(AREA_SIZE, AREA_SIZE, AREA_DEPTH));", + " const entry = box.containsPoint(ray.origin) ? ray.origin.clone() : ray.intersectBox(box, new THREE.Vector3());", + " if (!entry) return null;", + " const cellSize = AREA_SIZE / this.data.resolution;", + " const groundZAt = (point) => this.data.sampleHeight(point.x / cellSize, point.y / cellSize) * AREA_DEPTH;", + " const step = cellSize / 2;", + " const point = entry.clone();", + " const previousPoint = entry.clone();", + " for (let distance = 0; distance < AREA_SIZE * 2; distance += step) {", + " if (point.z <= groundZAt(point)) {", + " // Refine between the last point above the ground and this one.", + " for (let iteration = 0; iteration < 8; iteration++) {", + " const middle = previousPoint.clone().lerp(point, 0.5);", + " if (middle.z <= groundZAt(middle)) point.copy(middle);", + " else previousPoint.copy(middle);", + " }", + " return point;", + " }", + " if (!box.containsPoint(point)) return null;", + " previousPoint.copy(point);", + " point.addScaledVector(ray.direction, step);", + " }", + " return null;", + " }", + "", + " /**", + " * @param {THREE.Vector3} localPosition In the terrain group coordinates (like from `raycastFromCursor`).", + " * @returns {{x: number, y: number, z: number}} The position in the scene.", + " */", + " toScenePosition(localPosition) {", + " const { object } = this;", + " return {", + " x: object.getX() + (localPosition.x / AREA_SIZE) * object.getWidth(),", + " y: object.getY() + (localPosition.y / AREA_SIZE) * object.getHeight(),", + " z: object.getZ() + (localPosition.z / AREA_DEPTH) * object.getDepth(),", + " };", + " }", + "", + " /** Draw the changes of the ground (collisions are updated by the physics engine). */", + " applyChanges() {", + " this.renderer.updateSamples(this.changedSamples, this.hasPaintChanged);", + " this.changedSamples.clear();", + " this.hasPaintChanged = false;", + " }", + "", + " /** @returns {number} A number changing each time the ground changes (see `getGrounds`). */", + " getVersion() {", + " return this.version;", + " }", + "", + " /**", + " * @param {number} sinceVersion", + " * @returns {gdjs.SurfaceArea | null} The part of the ground whose shape changed since the version (see `gdjs.Surface`).", + " */", + " getChangedArea(sinceVersion) {", + " if (sinceVersion < this.surfaceChangesStartVersion) return null;", + " const samples = new SampleRectangle();", + " for (const change of this.surfaceChanges) {", + " if (change.version > sinceVersion) samples.add(change.samples);", + " }", + " if (samples.isEmpty()) return { minX: 0, minY: 0, maxX: 0, maxY: 0 };", + " const { resolution } = this.data;", + " return {", + " minX: samples.minI / resolution,", + " minY: samples.minJ / resolution,", + " maxX: samples.maxI / resolution,", + " maxY: samples.maxJ / resolution,", + " };", + " }", + "", + " _getTransform() {", + " const { object } = this;", + " return [object.getX(), object.getY(), object.getZ(), object.getWidth(), object.getHeight(), object.getDepth()].join(',');", + " }", + "", + " /** @param {string} imageResourceName A heightmap image that is not loaded yet. */", + " _reloadOnceLoaded(imageResourceName) {", + " if (this.loadingHeightmapImage === imageResourceName) return;", + " this.loadingHeightmapImage = imageResourceName;", + " const game = this.object.getRuntimeScene().getGame();", + " game.getImageManager()", + " .loadResource(imageResourceName)", + " .then(() => {", + " if (this.isDisposed || this.loadingHeightmapImage !== imageResourceName) return;", + " this.loadingHeightmapImage = '';", + " if (getLoadedImage(game, imageResourceName)) this.loadFromProperties();", + " })", + " .catch(() => {});", + " }", + "", + " update() {", + " const transform = this._getTransform();", + " if (transform !== this._transform) {", + " this._transform = transform;", + " this.version = ++lastGroundVersion;", + " }", + " this.applyChanges();", + " this.renderer.uniforms.terrainWorldScale.value = getWorldScale(this.object);", + " this._updateWetShore();", + " const layer = this.object.getInstanceContainer().getLayer(this.object.getLayer());", + " this.renderer.updateLevelsOfDetail(layer.getRenderer().getThreeCamera());", + " }", + "", + " _updateWetShore() {", + " const { object } = this;", + " // In the coordinates of the terrain group, like the vertices.", + " const depthScale = AREA_DEPTH / Math.max(object.getDepth(), 0.0001);", + " this.renderer.uniforms.terrainWetShore.value.set(", + " (object._getWetShoreZ() - object.getZ()) * depthScale,", + " Math.max(object._getWetShoreFade(), 0.001) * depthScale,", + " object._getWetShore() ? 1 : 0", + " );", + " }", + "", + " dispose() {", + " this.isDisposed = true;", + " getGrounds(this.object.getRuntimeScene()).delete(this);", + " if (typeof this.object.setSurface === 'function' && this.object.getSurface() === this.surface) {", + " this.object.setSurface(null);", + " }", + " this.renderer.dispose();", + " for (const bodyUpdater of this.bodyUpdaters) {", + " bodyUpdater.dispose();", + " }", + " }", + "}", + "", + "/**", + " * @param {gdjs.RuntimeObject} object", + " * @returns {number} The number of scene units by 3D world unit (1 in GDevelop versions before the world scale).", + " */", + "const getWorldScale = (object) => {", + " const scene = object.getRuntimeScene();", + " return typeof scene.getRenderer3DWorldScale === 'function' ? scene.getRenderer3DWorldScale() : 1;", + "};", + "", + "const averageColors = new WeakMap();", + "let white = null;", + "/**", + " * @param {THREE.Texture} texture", + " * @returns {THREE.Color} The average color of the texture (white when it's not loaded).", + " */", + "const getAverageColor = (texture) => {", + " const cachedColor = averageColors.get(texture);", + " if (cachedColor) return cachedColor;", + " const image = texture.image;", + " const isDrawable =", + " typeof document !== 'undefined' &&", + " ((image instanceof HTMLImageElement && image.complete && image.naturalWidth > 0) ||", + " image instanceof HTMLCanvasElement ||", + " (typeof ImageBitmap !== 'undefined' && image instanceof ImageBitmap));", + " if (!isDrawable) return white || (white = new THREE.Color(1, 1, 1));", + " const size = 8;", + " const canvas = document.createElement('canvas');", + " canvas.width = size;", + " canvas.height = size;", + " const context = canvas.getContext('2d');", + " context.drawImage(image, 0, 0, size, size);", + " const pixels = context.getImageData(0, 0, size, size).data;", + " let red = 0;", + " let green = 0;", + " let blue = 0;", + " for (let index = 0; index < pixels.length; index += 4) {", + " red += pixels[index];", + " green += pixels[index + 1];", + " blue += pixels[index + 2];", + " }", + " const count = size * size * 255;", + " const color = new THREE.Color().setRGB(red / count, green / count, blue / count, THREE.SRGBColorSpace);", + " averageColors.set(texture, color);", + " return color;", + "};", + "", + "const svgIcon = (content) =>", + " 'data:image/svg+xml,' +", + " encodeURIComponent('' + content + '');", + "const strokes = 'fill=\"none\" stroke=\"#000\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"';", + "const toolIcons = {", + " Raise: svgIcon(''),", + " Lower: svgIcon(''),", + " Smooth: svgIcon(''),", + " Flatten: svgIcon(''),", + " Size: svgIcon(''),", + " Strength: svgIcon(''),", + " Undo: svgIcon(''),", + " Hole: svgIcon(''),", + " FillHoles: svgIcon(''),", + " Wetness: svgIcon(''),", + "};", + "// Tints for the color brush when the editor has no color picker (older GDevelop versions).", + "const tintPresets = ['#e2c879', '#a5c45a', '#8a6a48', '#c98a62', '#9a9a92', '#ffffff'];", + "", + "/**", + " * The sculpt and paint tools shown in the scene editor when a terrain is", + " * selected. Without the needed editor functions (older GDevelop versions),", + " * nothing is shown: the terrain can still be changed with events.", + " */", + "class TerrainEditorTools {", + " constructor() {", + " /** '' when no brush is used. */", + " this.toolName = '';", + " this.sizePercent = 30;", + " this.strengthPercent = 50;", + " /** @type {Terrain | null} */", + " this.terrain = null;", + " /** @type {gdjs.InGameEditor | null} */", + " this.editor = null;", + " this.isStroking = false;", + " this.flattenHeight = 0;", + " /** The color of the \"Tint\" brush, as \"#rrggbb\". */", + " this.tintColor = tintPresets[0];", + " this.wetnessPercent = 100;", + " this.lastStrokeTime = 0;", + " /** @type {{data: TerrainData, baseData: TerrainData}[]} */", + " this.undoSteps = [];", + " this.raycaster = new THREE.Raycaster();", + " this.ndc = new THREE.Vector2();", + " this._swatchColor = new THREE.Color();", + " }", + "", + " /** @param {gdjs.InGameEditor} editor */", + " update(editor) {", + " if (typeof editor.showToolbar !== 'function') {", + " // Older GDevelop versions: the terrain can only be changed with events.", + " return;", + " }", + " this.editor = editor;", + " const selectedObjects = editor.getSelectedObjects();", + " const terrain =", + " selectedObjects.length === 1 && selectedObjects[0].__terrain3D ? selectedObjects[0].__terrain3D : null;", + " if (terrain !== this.terrain) {", + " this._endStroke();", + " if (this.terrain) this.terrain.renderer.setBrush(null, 0);", + " this.terrain = terrain;", + " this.undoSteps.length = 0;", + " }", + " if (terrain) editor.showToolbar('Terrain3D', this._getToolbarItems(terrain));", + " const game = editor.getRuntimeGame();", + " if (!terrain || !this.toolName) {", + " if (terrain) terrain.renderer.setBrush(null, 0);", + " return;", + " }", + "", + " const inputManager = game.getInputManager();", + " if (inputManager.wasKeyJustPressed(gdjs.evtTools.input.keysNameToCode.Escape)) {", + " this._endStroke();", + " this.toolName = '';", + " terrain.renderer.setBrush(null, 0);", + " return;", + " }", + " editor.captureLeftMouseButton();", + " const hit = terrain.raycastFromCursor(game, this.raycaster, this.ndc);", + " const radius = this._getRadius();", + " terrain.renderer.setBrush(hit, radius);", + "", + " // Older GDevelop versions don't have it.", + " const isPressedForTool =", + " typeof editor.isCapturedLeftMouseButtonPressed === 'function'", + " ? editor.isCapturedLeftMouseButtonPressed()", + " : inputManager.isMouseButtonPressed(0);", + " // The button is released outside the canvas when the mouse is on a toolbar.", + " const isPressed = isPressedForTool && inputManager.isMouseInsideCanvas();", + " if (isPressed && hit && !this.isStroking) {", + " this._startStroke(terrain, hit);", + " }", + " if (this.isStroking && isPressed && hit) {", + " this._applyBrush(terrain, hit, radius);", + " }", + " if (!isPressed) {", + " this._endStroke();", + " }", + " }", + "", + " _getRadius() {", + " const { object } = this.terrain;", + " const maxRadius = Math.max(object.getWidth(), object.getHeight()) / 4;", + " return Math.max(maxRadius * Math.pow(this.sizePercent / 100, 2), this.terrain.getCellWidth());", + " }", + "", + " _startStroke(terrain, hit) {", + " this.isStroking = true;", + " this.lastStrokeTime = performance.now();", + " this.flattenHeight = terrain.toScenePosition(hit).z;", + " this.undoSteps.push({ data: copyTerrainData(terrain.data), baseData: terrain.baseData });", + " const maxUndoSteps = Math.max(Math.floor(UNDO_STEPS_SAMPLES / terrain.data.heights.length), 2);", + " while (this.undoSteps.length > maxUndoSteps) this.undoSteps.shift();", + " }", + "", + " _endStroke() {", + " if (!this.isStroking) return;", + " this.isStroking = false;", + " this._save();", + " }", + "", + " _applyBrush(terrain, hit, radius) {", + " const now = performance.now();", + " const elapsedSeconds = Math.min((now - this.lastStrokeTime) / 1000, 0.1);", + " this.lastStrokeTime = now;", + " const { x, y } = terrain.toScenePosition(hit);", + " const amount = (this.strengthPercent / 100) * elapsedSeconds;", + " const depth = terrain.object.getDepth();", + " if (this.toolName === 'Raise') terrain.raise(x, y, x, y, radius, amount * depth * 0.5);", + " else if (this.toolName === 'Lower') terrain.raise(x, y, x, y, radius, -amount * depth * 0.5);", + " else if (this.toolName === 'Smooth') terrain.smooth(x, y, x, y, radius, amount * 10);", + " else if (this.toolName === 'Flatten') terrain.flatten(x, y, x, y, radius, this.flattenHeight, amount * 10);", + " else if (this.toolName.startsWith('Paint')) {", + " terrain.paint(x, y, x, y, radius, parseInt(this.toolName.slice(5), 10), amount * 8);", + " } else if (this.toolName === 'Hole') {", + " terrain.makeHole(x, y, x, y, radius);", + " } else if (this.toolName === 'FillHoles') {", + " terrain.fillHoles(x, y, x, y, radius);", + " } else if (this.toolName === 'Tint') {", + " terrain.tint(x, y, x, y, radius, gdjs.rgbOrHexToRGBColor(this.tintColor), amount * 8);", + " } else if (this.toolName === 'Wetness') {", + " terrain.wet(x, y, x, y, radius, this.wetnessPercent / 100, amount * 8);", + " }", + " // Drawn now, as the terrain was updated earlier in this frame.", + " terrain.applyChanges();", + " }", + "", + " _save() {", + " const { terrain, editor } = this;", + " if (!terrain || !editor) return;", + " editor.updateObjectProperties(terrain.object.getName(), { SculptData: terrain.save() });", + " }", + "", + " _undo() {", + " const undoStep = this.undoSteps.pop();", + " // The relief, the edits or the resolution may have changed since the stroke.", + " if (!this.terrain || !undoStep || undoStep.baseData !== this.terrain.baseData) {", + " this.undoSteps.length = 0;", + " return;", + " }", + " this.terrain.setData(undoStep.data);", + " this._save();", + " }", + "", + " /** @returns {Array} The settings of the tint or wetness brush, when it's used. */", + " _getColorToolItems() {", + " const { editor } = this;", + " if (this.toolName === 'Tint') {", + " if (editor && typeof editor.isToolbarItemTypeSupported === 'function' && editor.isToolbarItemTypeSupported('color')) {", + " return [", + " {", + " type: 'color',", + " id: 'TintColor',", + " tooltip: 'Color of the tint',", + " value: this.tintColor,", + " onChange: (color) => (this.tintColor = color),", + " },", + " ];", + " }", + " return tintPresets.map((color) => ({", + " type: 'button',", + " id: 'TintPreset' + color,", + " tooltip: color === '#ffffff' ? 'No tint' : 'Tint with this color',", + " color,", + " isActive: this.tintColor === color,", + " onClick: () => (this.tintColor = color),", + " }));", + " }", + " if (this.toolName === 'Wetness') {", + " return [", + " {", + " type: 'slider',", + " id: 'WetnessAmount',", + " tooltip: 'Wetness',", + " iconUrl: toolIcons.Wetness,", + " min: 0,", + " max: 100,", + " value: this.wetnessPercent,", + " onChange: (value) => (this.wetnessPercent = value),", + " },", + " ];", + " }", + " return [];", + " }", + "", + " /**", + " * @param {Terrain} terrain", + " * @returns {Array} The toolbar for the current state (the editor only", + " * updates what changed).", + " */", + " _getToolbarItems(terrain) {", + " const { object } = terrain;", + " const toolButton = (toolName, tooltip, color) => ({", + " type: 'button',", + " id: toolName,", + " tooltip,", + " iconUrl: toolIcons[toolName],", + " color,", + " isActive: this.toolName === toolName,", + " onClick: () => (this.toolName = this.toolName === toolName ? '' : toolName),", + " });", + " const layerColors = [object._getLayer1Color(), object._getLayer2Color(), object._getLayer3Color(), object._getLayer4Color()];", + " // Layers are shown with their color, tinting their texture.", + " const swatchColors = layerColors.map((color, index) =>", + " this._swatchColor", + " .set(gdjs.rgbOrHexStringToNumber(color))", + " .multiply(getAverageColor(terrain.renderer.uniforms['terrainLayerMap' + index].value))", + " .getHexString()", + " );", + " return [", + " toolButton('Raise', 'Raise the ground (drag on the terrain)'),", + " toolButton('Lower', 'Lower the ground'),", + " toolButton('Smooth', 'Smooth the ground'),", + " toolButton('Flatten', 'Flatten the ground to the height where the drag starts'),", + " { type: 'divider', id: 'PaintDivider' },", + " ...swatchColors.map((swatchColor, index) =>", + " toolButton('Paint' + (index + 1), 'Paint layer ' + (index + 1), '#' + swatchColor)", + " ),", + " toolButton('Hole', 'Make a hole, for the entrance of a tunnel or a cave'),", + " toolButton('FillHoles', 'Fill holes, with the layers of the ground around them'),", + " { type: 'divider', id: 'ColorDivider' },", + " toolButton('Tint', 'Tint the ground (white removes the tint)', this.tintColor),", + " toolButton('Wetness', 'Make the ground wet, or dry with a wetness of 0'),", + " ...this._getColorToolItems(),", + " { type: 'divider', id: 'BrushDivider' },", + " {", + " type: 'slider',", + " id: 'Size',", + " tooltip: 'Brush size',", + " iconUrl: toolIcons.Size,", + " min: 1,", + " max: 100,", + " value: this.sizePercent,", + " onChange: (value) => (this.sizePercent = value),", + " },", + " {", + " type: 'slider',", + " id: 'Strength',", + " tooltip: 'Brush strength',", + " iconUrl: toolIcons.Strength,", + " min: 5,", + " max: 100,", + " value: this.strengthPercent,", + " onChange: (value) => (this.strengthPercent = value),", + " },", + " { type: 'button', id: 'Undo', tooltip: 'Undo the last stroke on this terrain', iconUrl: toolIcons.Undo, onClick: () => this._undo() },", + " ];", + " }", + "}", + "", + "const editorTools = new TerrainEditorTools();", + "if (gdjs.registerInGameEditorPostStepCallback) {", + " gdjs.registerInGameEditorPostStepCallback((editor) => editorTools.update(editor));", + "}", + "", + "gdjs.__terrain3DExtension = {", + " Terrain,", + " // Used by the terrain grass and the scattered models.", + " getWorldScale,", + " getGrounds,", + " patchShaderCode,", + " createRandom,", + " findHighestGround,", + " GroundPlacement,", + " svgIcon,", + " toolIcons,", + " // Exposed for tests.", + " TerrainBodyUpdater,", + " TerrainData,", + " SampleRectangle,", + " brushes,", + " edits,", + " codec,", + " relief,", + "};" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [], + "objectGroups": [] + }, + { + "description": "Define grass helper classes JavaScript code.", + "fullName": "Define grass helper classes", + "functionType": "Action", + "name": "DefineGrassHelperClasses", + "private": true, + "sentence": "Define grass helper classes JavaScript code", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "if (gdjs.__terrainGrassExtension) {", + " return;", + "}", + "// Blades grow on the grounds of the scene: the 3D terrains (see `getGrounds`", + "// in the terrain helper).", + "", + "// Size of the object inner area: instances are scaled from it.", + "const AREA_SIZE = 1000;", + "const AREA_DEPTH = 40;", + "const CHUNK_SIZE = 500;", + "const MAX_BENDING_OBJECTS = 8;", + "", + "/**", + " * A blade: a thin, tapered strip. Its width and height are 1: blades are", + " * scaled by the shader.", + " */", + "const createBladeGeometry = () => {", + " const halfWidths = [0.5, 0.42, 0.28, 0];", + " const heights = [0, 0.35, 0.7, 1];", + " const positions = [];", + " halfWidths.forEach((halfWidth, level) => {", + " positions.push(-halfWidth, 0, heights[level]);", + " if (halfWidth > 0) positions.push(halfWidth, 0, heights[level]);", + " });", + " const indices = [0, 1, 2, 1, 3, 2, 2, 3, 4, 3, 5, 4, 4, 5, 6];", + " const geometry = new THREE.InstancedBufferGeometry();", + " geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));", + " geometry.setAttribute('normal', new THREE.Float32BufferAttribute(new Array(positions.length).fill(0), 3));", + " geometry.setIndex(indices);", + " return geometry;", + "};", + "", + "const vertexShaderDeclarations = `", + "attribute vec4 grassRoot;", + "attribute vec2 grassShape;", + "// The tint of the ground at the root (linear, from 0 to 1).", + "attribute vec3 grassGroundTint;", + "uniform mat3 grassWorldToLocal;", + "uniform float grassWorldScale;", + "uniform float grassBladeWidth;", + "uniform float grassTime;", + "uniform float grassWindStrength;", + "uniform float grassWindSpeed;", + "uniform vec4 grassBenders[${MAX_BENDING_OBJECTS}];", + "uniform vec3 grassBaseColor;", + "uniform vec3 grassTipColor;", + "varying vec3 vGrassColor;", + "`;", + "const vertexShaderPosition = `", + "// Blades are shaped in scene units (with Y flipped like the 3D world), then", + "// moved back in the (scaled) object space.", + "float bladeHeight = position.z;", + "float grassAngle = grassRoot.w;", + "float bladeWidth = position.x * grassBladeWidth;", + "vec3 blade = vec3(", + " bladeWidth * cos(grassAngle),", + " bladeWidth * sin(grassAngle),", + " bladeHeight * grassShape.x", + ");", + "vec3 rootWorld = (modelMatrix * vec4(grassRoot.xyz, 1.0)).xyz * grassWorldScale;", + "// Tips bend more than the base.", + "float bendFactor = bladeHeight * bladeHeight * grassShape.x;", + "float gust = sin(grassTime * grassWindSpeed + rootWorld.x * 0.013 + rootWorld.y * 0.011);", + "float flutter = sin(grassTime * grassWindSpeed * 2.7 + grassShape.y * 40.0) * 0.3;", + "blade.xy += vec2(0.8, 0.5) * (gust + flutter + 0.6) * grassWindStrength * 0.35 * bendFactor;", + "for (int i = 0; i < ${MAX_BENDING_OBJECTS}; i++) {", + " vec4 bender = grassBenders[i];", + " if (bender.z <= 0.0) continue;", + " vec2 away = rootWorld.xy - bender.xy;", + " float distanceToBender = length(away);", + " float push = 1.0 - smoothstep(bender.z * 0.3, bender.z, distanceToBender);", + " if (push > 0.0) {", + " blade.xy += normalize(away + vec2(0.001)) * push * bendFactor * 0.9;", + " blade.z *= 1.0 - push * 0.5;", + " }", + "}", + "vec3 transformed = grassRoot.xyz + grassWorldToLocal * (blade / grassWorldScale);", + "vGrassColor = mix(grassBaseColor, grassTipColor, bladeHeight) * (0.85 + 0.3 * grassShape.y) * grassGroundTint;", + "`;", + "const vertexShaderNormal = `", + "// Blades are lit like the ground below them.", + "transformedNormal = normalize((viewMatrix * vec4(0.0, 0.0, 1.0, 0.0)).xyz);", + "`;", + "const fragmentShaderDeclarations = `", + "varying vec3 vGrassColor;", + "`;", + "const fragmentShaderColor = `", + "diffuseColor.rgb *= vGrassColor;", + "`;", + "// Both faces of blades are lit like the ground (not flipped for back faces).", + "const fragmentShaderNormal = `", + "#include ", + "normal = normalize(vNormal);", + "nonPerturbedNormal = normal;", + "`;", + "", + "const { getWorldScale, patchShaderCode, createRandom, findHighestGround, GroundPlacement } = gdjs.__terrain3DExtension;", + "", + "/**", + " * The grass of a terrain grass object: blades drawn with GPU instancing, in chunks", + " * that are hidden when off-screen and thinned out with the distance. They grow", + " * on the grounds below, like 3D terrains (only where a given layer is painted).", + " */", + "class Grass {", + " /** @param {gdjs.CustomRuntimeObject3D} object */", + " constructor(object) {", + " this.object = object;", + " this.group = new THREE.Group();", + " object.get3DRendererObject().add(this.group);", + " this.bladeGeometry = createBladeGeometry();", + " /** @type {Array<{mesh: THREE.Mesh, blades: number, center: THREE.Vector3}>} */", + " this.chunks = [];", + " this.placement = new GroundPlacement(object.getRuntimeScene());", + " this._groundAtBlade = { ground: null, z: 0 };", + " this.uniforms = {", + " grassWorldToLocal: { value: new THREE.Matrix3() },", + " // Scene units by 3D world unit.", + " grassWorldScale: { value: 1 },", + " grassBladeWidth: { value: 1 },", + " grassTime: { value: 0 },", + " grassWindStrength: { value: 1 },", + " grassWindSpeed: { value: 1 },", + " grassBenders: { value: Array.from({ length: MAX_BENDING_OBJECTS }, () => new THREE.Vector4()) },", + " grassBaseColor: { value: new THREE.Color() },", + " grassTipColor: { value: new THREE.Color() },", + " };", + " this.material = new THREE.MeshStandardMaterial({ roughness: 1, metalness: 0, side: THREE.DoubleSide });", + " this.material.onBeforeCompile = (shader) => {", + " Object.assign(shader.uniforms, this.uniforms);", + " shader.vertexShader = patchShaderCode(shader.vertexShader, [", + " ['#include ', '#include \\n' + vertexShaderDeclarations],", + " ['#include ', vertexShaderPosition],", + " ['#include ', '#include \\n' + vertexShaderNormal],", + " ], 'TerrainGrass');", + " shader.fragmentShader = patchShaderCode(shader.fragmentShader, [", + " ['#include ', '#include \\n' + fragmentShaderDeclarations],", + " ['#include ', '#include \\n' + fragmentShaderColor],", + " ['#include ', fragmentShaderNormal],", + " ], 'TerrainGrass');", + " };", + " this.material.customProgramCacheKey = () => 'TerrainGrass';", + "", + " this._matrix = new THREE.Matrix4();", + " this._cameraPosition = new THREE.Vector3();", + " this._chunkPosition = new THREE.Vector3();", + " this._scale = new THREE.Vector3();", + " this.updateFromProperties();", + " }", + "", + " updateFromProperties() {", + " const { object } = this;", + " this.uniforms.grassBaseColor.value.set(gdjs.rgbOrHexStringToNumber(object._getBaseColor()));", + " this.uniforms.grassTipColor.value.set(gdjs.rgbOrHexStringToNumber(object._getTipColor()));", + " this.uniforms.grassWindStrength.value = object._getWindStrength();", + " this.uniforms.grassWindSpeed.value = object._getWindSpeed();", + " this.uniforms.grassBladeWidth.value = Math.max(object._getBladeWidth(), 0);", + " this.material.needsUpdate = true;", + " this.placement.placeAgain();", + " }", + "", + " _getPlacementKey() {", + " const { object } = this;", + " return [", + " object.getX(), object.getY(), object.getZ(), object.getWidth(), object.getHeight(), object.getDepth(),", + " object._getDensity(), object._getBladeHeight(), object._getGroundLayer(), object._getMaxSlope(),", + " object._getSeed(),", + " ].join('|');", + " }", + "", + " /** Place the blades again, for example after the ground or the object changed. */", + " rebuild() {", + " const grounds = Array.from(this.placement.grounds);", + " for (const chunk of this.chunks) {", + " chunk.mesh.removeFromParent();", + " chunk.mesh.geometry.dispose();", + " }", + " this.chunks.length = 0;", + "", + " const { object } = this;", + " const width = object.getWidth();", + " const height = object.getHeight();", + " const scaleX = width / AREA_SIZE;", + " const scaleY = height / AREA_SIZE;", + " const scaleZ = Math.max(object.getDepth() / AREA_DEPTH, 0.0001);", + " const bladesPerChunk = Math.round((Math.max(object._getDensity(), 0) * CHUNK_SIZE * CHUNK_SIZE) / 10000);", + " const groundLayer = parseInt(object._getGroundLayer(), 10) || 0;", + " const maxSlope = object._getMaxSlope();", + " const chunksX = Math.max(Math.ceil(width / CHUNK_SIZE), 1);", + " const chunksY = Math.max(Math.ceil(height / CHUNK_SIZE), 1);", + " const nextRandom = createRandom(object._getSeed() * 7919 + 1);", + " const bladeHeight = Math.max(object._getBladeHeight(), 0);", + "", + " for (let chunkY = 0; chunkY < chunksY; chunkY++) {", + " for (let chunkX = 0; chunkX < chunksX; chunkX++) {", + " const roots = new Float32Array(bladesPerChunk * 4);", + " const shapes = new Float32Array(bladesPerChunk * 2);", + " const groundTints = new Uint8Array(bladesPerChunk * 3).fill(255);", + " let blades = 0;", + " let minZ = Infinity;", + " let maxZ = -Infinity;", + " for (let attempt = 0; attempt < bladesPerChunk; attempt++) {", + " // In scene units, relative to the object.", + " const x = (chunkX + nextRandom()) * CHUNK_SIZE;", + " const y = (chunkY + nextRandom()) * CHUNK_SIZE;", + " const angle = nextRandom() * Math.PI * 2;", + " const heightFactor = 0.6 + 0.8 * nextRandom();", + " const tint = nextRandom();", + " const growth = nextRandom();", + " if (x > width || y > height) continue;", + " const sceneX = object.getX() + x;", + " const sceneY = object.getY() + y;", + " const { ground, z } = findHighestGround(grounds, sceneX, sceneY, this._groundAtBlade);", + " const groundZ = ground ? z : object.getZ();", + " if (ground) {", + " if (groundLayer > 0 && growth > ground.getLayerWeightAt(sceneX, sceneY, groundLayer)) continue;", + " if (ground.getSlopeAt(sceneX, sceneY) > maxSlope) continue;", + " if (typeof ground.getTintAt === 'function') {", + " const tint = ground.getTintAt(sceneX, sceneY);", + " for (let channel = 0; channel < 3; channel++) {", + " groundTints[blades * 3 + channel] = Math.round(255 * Math.pow(tint[channel] / 255, 2.2));", + " }", + " }", + " }", + " const localZ = (groundZ - object.getZ()) / scaleZ;", + " roots[blades * 4] = x / scaleX;", + " roots[blades * 4 + 1] = y / scaleY;", + " roots[blades * 4 + 2] = localZ;", + " roots[blades * 4 + 3] = angle;", + " shapes[blades * 2] = heightFactor * bladeHeight;", + " shapes[blades * 2 + 1] = tint;", + " minZ = Math.min(minZ, localZ);", + " maxZ = Math.max(maxZ, localZ);", + " blades++;", + " }", + " if (blades === 0) continue;", + "", + " const geometry = new THREE.InstancedBufferGeometry();", + " geometry.index = this.bladeGeometry.index;", + " geometry.setAttribute('position', this.bladeGeometry.attributes.position);", + " geometry.setAttribute('normal', this.bladeGeometry.attributes.normal);", + " geometry.setAttribute('grassRoot', new THREE.InstancedBufferAttribute(roots.subarray(0, blades * 4), 4));", + " geometry.setAttribute('grassShape', new THREE.InstancedBufferAttribute(shapes.subarray(0, blades * 2), 2));", + " geometry.setAttribute('grassGroundTint', new THREE.InstancedBufferAttribute(groundTints.subarray(0, blades * 3), 3, true));", + " geometry.instanceCount = blades;", + " // The furthest tips can go with the wind and the bending objects (see the shader).", + " const maxBladeHeight = bladeHeight * 1.4;", + " const lean =", + " (0.35 * 1.9 * 0.943 * Math.max(object._getWindStrength(), 0) + 0.9) * maxBladeHeight +", + " Math.max(object._getBladeWidth(), 0) / 2;", + " const leanX = lean / scaleX;", + " const leanY = lean / scaleY;", + " geometry.boundingBox = new THREE.Box3(", + " new THREE.Vector3((chunkX * CHUNK_SIZE) / scaleX - leanX, (chunkY * CHUNK_SIZE) / scaleY - leanY, minZ),", + " new THREE.Vector3(", + " Math.min((chunkX + 1) * CHUNK_SIZE, width) / scaleX + leanX,", + " Math.min((chunkY + 1) * CHUNK_SIZE, height) / scaleY + leanY,", + " maxZ + (bladeHeight * 1.4) / scaleZ", + " )", + " );", + " geometry.boundingSphere = geometry.boundingBox.getBoundingSphere(new THREE.Sphere());", + " const mesh = new THREE.Mesh(geometry, this.material);", + " mesh.receiveShadow = true;", + " // Raycasts would only test one blade: the selection plane is used instead.", + " mesh.raycast = () => {};", + " this.group.add(mesh);", + " this.chunks.push({ mesh, blades, center: geometry.boundingSphere.center.clone() });", + " }", + " }", + " }", + "", + " _updateBendingObjects() {", + " const benders = this.uniforms.grassBenders.value;", + " const objectName = this.object._getBendingObject();", + " const bendingObjects = objectName ? this.object.getInstanceContainer().getObjects(objectName) || [] : [];", + " for (let index = 0; index < MAX_BENDING_OBJECTS; index++) {", + " const bendingObject = bendingObjects[index];", + " if (!bendingObject) {", + " benders[index].set(0, 0, 0, 0);", + " continue;", + " }", + " // Blades are pushed in the 3D scene, which is flipped on Y.", + " const radius = Math.max(bendingObject.getWidth(), bendingObject.getHeight()) * 1.2;", + " benders[index].set(bendingObject.getCenterXInScene(), -bendingObject.getCenterYInScene(), radius, 0);", + " }", + " }", + "", + " /**", + " * @param {number} elapsedSeconds", + " * @param {number} time In seconds, the same for all grass objects.", + " */", + " update(elapsedSeconds, time, camera) {", + " if (this.placement.shouldPlaceAgain(elapsedSeconds, this._getPlacementKey())) {", + " this.rebuild();", + " }", + "", + " this.uniforms.grassTime.value = time;", + " this.uniforms.grassWorldScale.value = getWorldScale(this.object);", + " // The object transformation is applied just before rendering, it's needed now.", + " this.object.getRenderer().ensureUpToDate();", + " this.group.updateWorldMatrix(true, false);", + " this.uniforms.grassWorldToLocal.value.setFromMatrix4(this._matrix.copy(this.group.matrixWorld).invert());", + " this._updateBendingObjects();", + " if (!camera) return;", + "", + " // Far chunks are hidden, and thinned out before (in 3D world units).", + " const fadeDistance = Math.max(this.object._getFadeDistance(), 1) / getWorldScale(this.object);", + " this._cameraPosition.setFromMatrixPosition(camera.matrixWorld);", + " for (const chunk of this.chunks) {", + " const distance = this._chunkPosition", + " .copy(chunk.center)", + " .applyMatrix4(this.group.matrixWorld)", + " .distanceTo(this._cameraPosition);", + " const density = gdjs.evtTools.common.clamp((fadeDistance - distance) / (fadeDistance * 0.5), 0, 1);", + " chunk.mesh.visible = density > 0;", + " chunk.mesh.geometry.instanceCount = Math.ceil(chunk.blades * density);", + " }", + " }", + "", + " dispose() {", + " for (const chunk of this.chunks) {", + " chunk.mesh.geometry.dispose();", + " }", + " this.bladeGeometry.dispose();", + " this.material.dispose();", + " this.group.removeFromParent();", + " }", + "}", + "", + "gdjs.__terrainGrassExtension = { Grass };" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [], + "objectGroups": [] + }, + { + "description": "Define scattered models helper classes JavaScript code.", + "fullName": "Define scattered models helper classes", + "functionType": "Action", + "name": "DefineScatterHelperClasses", + "private": true, + "sentence": "Define scattered models helper classes JavaScript code", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "if (gdjs.__terrainScatterExtension) {", + " return;", + "}", + "// Copies grow on the grounds of the scene, like the terrain grass (see", + "// `getGrounds` in the terrain helper).", + "", + "const {", + " getWorldScale,", + " getGrounds,", + " patchShaderCode,", + " createRandom,", + " findHighestGround,", + " GroundPlacement,", + " SampleRectangle,", + " brushes,", + " codec,", + " svgIcon,", + " toolIcons,", + "} = gdjs.__terrain3DExtension;", + "", + "// Size of the object inner area: instances are scaled from it.", + "const AREA_SIZE = 2000;", + "const AREA_DEPTH = 200;", + "// The painted density is stored in a grid of this many cells on each side,", + "// whatever the size of the object.", + "const MASK_RESOLUTION = 256;", + "const MASK_FORMAT_VERSION = '1';", + "// Copies are hidden in chunks: big chunks mean fewer draw calls, small ones", + "// hide more copies out of the screen.", + "const MIN_CHUNK_SIZE = 500;", + "const MAX_CHUNK_SIZE = 2000;", + "// Copies shrink to nothing over this part of the draw distance, instead of popping out.", + "const FADE_PART = 0.15;", + "const MAX_COPIES = 200000;", + "// Prisms of the trunk collisions.", + "const TRUNK_SIDES = 6;", + "", + "const clamp01 = (value) => (value < 0 ? 0 : value > 1 ? 1 : value);", + "", + "/**", + " * @returns {number} A number for a cell of the copies grid, changing with the seed.", + " */", + "const hashCell = (seed, i, j) => {", + " let h = Math.imul(i, 374761393) + Math.imul(j, 668265263) + Math.imul(seed, 982451653);", + " h = Math.imul(h ^ (h >>> 13), 1274126177);", + " return (h ^ (h >>> 16)) >>> 0;", + "};", + "", + "/**", + " * How dense copies are, from 0 to 1, in a grid covering the object: painted", + " * with the editor brushes, the edits or events.", + " */", + "class DensityMask {", + " /** @param {number} fillValue From 0 to 255. */", + " constructor(fillValue) {", + " // `resolution` and `size` let terrain brushes paint it.", + " this.resolution = MASK_RESOLUTION;", + " this.size = MASK_RESOLUTION + 1;", + " this.values = new Uint8Array(this.size * this.size).fill(fillValue);", + " }", + "", + " /**", + " * @param {number} u From 0 to 1, along the object width.", + " * @param {number} v From 0 to 1, along the object height.", + " * @returns {number} The density, from 0 to 1, interpolated between samples.", + " */", + " sample(u, v) {", + " const x = clamp01(u) * this.resolution;", + " const y = clamp01(v) * this.resolution;", + " const i = Math.min(Math.floor(x), this.resolution - 1);", + " const j = Math.min(Math.floor(y), this.resolution - 1);", + " const fx = x - i;", + " const fy = y - j;", + " const value = (i, j) => this.values[j * this.size + i];", + " const top = value(i, j) + fx * (value(i + 1, j) - value(i, j));", + " const bottom = value(i, j + 1) + fx * (value(i + 1, j + 1) - value(i, j + 1));", + " return (top + fy * (bottom - top)) / 255;", + " }", + "", + " /**", + " * Bring the density towards a target around a segment: fully up to 70%", + " * of the radius, then fading out (forests have dense centers and soft edges).", + " * @param {{cellWidth: number, cellHeight: number, ax: number, ay: number, bx: number, by: number, radius: number}} stroke", + " * @param {number} density The target, from 0 to 1.", + " * @param {number} strength From 0 to 1.", + " * @param {SampleRectangle} changedSamples", + " */", + " paint(stroke, density, strength, changedSamples) {", + " const { values } = this;", + " const target = Math.round(clamp01(density) * 255);", + " brushes.forEachSample(", + " this,", + " stroke,", + " (index, falloff) => {", + " // The falloff of terrain brushes is (1 - distance² / radius²)².", + " const weight = clamp01(2 * Math.sqrt(falloff));", + " // Rounded towards the target, so that light strokes still paint.", + " const change = (target - values[index]) * clamp01(weight * strength);", + " values[index] += change > 0 ? Math.ceil(change) : Math.floor(change);", + " },", + " changedSamples", + " );", + " }", + "", + " /** @returns {DensityMask} */", + " clone() {", + " const mask = new DensityMask(0);", + " mask.values.set(this.values);", + " return mask;", + " }", + "}", + "", + "/**", + " * Edits written in the Edits property: a JSON list of brush strokes, like", + " * `[{\"tool\": \"add\", \"x\": 0.5, \"y\": 0.5, \"radius\": 0.2}]`. Positions and", + " * radius are fractions of the object size, like the terrain edits.", + " */", + "const applyEdits = (mask, text) => {", + " if (!text.trim()) return;", + " let editList;", + " try {", + " editList = JSON.parse(text);", + " } catch (error) {", + " console.warn('The edits of scattered models are not valid JSON: ' + error.message);", + " return;", + " }", + " if (!Array.isArray(editList)) {", + " console.warn('The edits of scattered models must be a JSON list (like [{\"tool\": \"add\", ...}]).');", + " return;", + " }", + " const numberOr = (value, defaultValue) => (typeof value === 'number' && isFinite(value) ? value : defaultValue);", + " const changedSamples = new SampleRectangle();", + " for (const edit of editList) {", + " if (!edit || typeof edit !== 'object') continue;", + " const x = numberOr(edit.x, 0.5);", + " const y = numberOr(edit.y, 0.5);", + " const stroke = {", + " cellWidth: 1 / mask.resolution,", + " cellHeight: 1 / mask.resolution,", + " ax: x,", + " ay: y,", + " bx: numberOr(edit.toX, x),", + " by: numberOr(edit.toY, y),", + " radius: numberOr(edit.radius, 0.1),", + " };", + " const strength = clamp01(numberOr(edit.strength, 1));", + " if (edit.tool === 'add') {", + " mask.paint(stroke, numberOr(edit.density, 1), strength, changedSamples);", + " } else if (edit.tool === 'erase') {", + " mask.paint(stroke, 0, strength, changedSamples);", + " } else {", + " console.warn('Unknown tool in the edits of scattered models: \"' + edit.tool + '\" (use add or erase).');", + " }", + " }", + "};", + "", + "/**", + " * The density painted in the editor, stored as its difference with the", + " * density from the properties (the coverage and the edits), like the terrain", + " * sculpt data.", + " */", + "const maskCodec = {", + " /**", + " * @param {DensityMask} mask", + " * @param {DensityMask} baseMask", + " */", + " encode(mask, baseMask) {", + " const changes = new Int32Array(mask.values.length);", + " let hasChanges = false;", + " for (let index = 0; index < changes.length; index++) {", + " changes[index] = mask.values[index] - baseMask.values[index];", + " if (changes[index] !== 0) hasChanges = true;", + " }", + " if (!hasChanges) return '';", + " return MASK_FORMAT_VERSION + ';' + MASK_RESOLUTION + ';' + codec.encodeGrids([changes], mask.size);", + " },", + "", + " /**", + " * @param {string} text", + " * @param {DensityMask} baseMask", + " * @returns {DensityMask | null} null if the text is empty or not valid.", + " */", + " decode(text, baseMask) {", + " if (!text) return null;", + " const parts = text.split(';');", + " if (parts[0] !== MASK_FORMAT_VERSION || parseInt(parts[1], 10) !== MASK_RESOLUTION) return null;", + " try {", + " const mask = baseMask.clone();", + " const [changes] = codec.decodeGrids(parts[2], mask.size, 1);", + " for (let index = 0; index < changes.length; index++) {", + " // The base may have changed since the data was saved.", + " mask.values[index] = Math.min(Math.max(mask.values[index] + changes[index], 0), 255);", + " }", + " return mask;", + " } catch (error) {", + " console.error('The painted data of scattered models could not be read:', error);", + " return null;", + " }", + " },", + "};", + "", + "const fadeShaderDeclarations = `", + "uniform float scatterDrawDistance;", + "`;", + "// Instance matrices are applied after this: copies shrink towards their base.", + "const fadeShaderCode = `", + "#ifdef USE_INSTANCING", + "vec3 scatterCopyPosition = (modelMatrix * instanceMatrix * vec4(0.0, 0.0, 0.0, 1.0)).xyz;", + "transformed *= clamp(", + " (scatterDrawDistance - distance(scatterCopyPosition, cameraPosition)) / (scatterDrawDistance * ${FADE_PART}),", + " 0.0,", + " 1.0", + ");", + "#endif", + "`;", + "", + "const flipTriangles = (geometry) => {", + " const index = geometry.index;", + " for (let offset = 0; offset + 2 < index.count; offset += 3) {", + " const second = index.getX(offset + 1);", + " index.setX(offset + 1, index.getX(offset + 2));", + " index.setX(offset + 2, second);", + " }", + "};", + "", + "/**", + " * The parts of a model, ready to be drawn with instancing: each glTF", + " * primitive with its material. Geometries are moved so that the model base", + " * is at the origin, its height is 1 and it stands along Z (like 3D model", + " * objects with a 90° rotation around X).", + " * @param {THREE.Object3D} modelScene", + " * @param {object} uniforms Uniforms added to the materials.", + " * @returns {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}}", + " */", + "const createModelParts = (modelScene, uniforms) => {", + " modelScene.updateMatrixWorld(true);", + " const rootInverse = modelScene.matrixWorld.clone().invert();", + " const standUp = new THREE.Matrix4().makeRotationX(Math.PI / 2);", + " /** @type {Array<{mesh: THREE.Mesh, matrix: THREE.Matrix4}>} */", + " const meshes = [];", + " const box = new THREE.Box3();", + " modelScene.traverse((node) => {", + " if (!node.isMesh || !node.geometry || !node.geometry.attributes.position) return;", + " const matrix = standUp.clone().multiply(rootInverse).multiply(node.matrixWorld);", + " if (!node.geometry.boundingBox) node.geometry.computeBoundingBox();", + " box.union(node.geometry.boundingBox.clone().applyMatrix4(matrix));", + " meshes.push({ mesh: node, matrix });", + " });", + " if (box.isEmpty()) return { parts: [], width: 0, depth: 0 };", + "", + " const height = Math.max(box.max.z - box.min.z, 0.000001);", + " // The Y axis of the scene goes down: the model is flipped, like 3D model objects.", + " const normalization = new THREE.Matrix4()", + " .makeScale(1 / height, -1 / height, 1 / height)", + " .multiply(", + " new THREE.Matrix4().makeTranslation(", + " -(box.min.x + box.max.x) / 2,", + " -(box.min.y + box.max.y) / 2,", + " -box.min.z", + " )", + " );", + " const parts = meshes.map(({ mesh, matrix }) => {", + " let geometry = mesh.geometry.clone();", + " if (!geometry.index) {", + " const indices = Array.from({ length: geometry.attributes.position.count }, (_, index) => index);", + " geometry.setIndex(indices);", + " }", + " geometry.applyMatrix4(normalization.clone().multiply(matrix));", + " // The flip turned the faces inside out.", + " flipTriangles(geometry);", + " geometry.computeBoundingSphere();", + " geometry.computeBoundingBox();", + " const patchMaterial = (originalMaterial) => {", + " const material = originalMaterial.clone();", + " // Like 3D model objects (\"Standard without metalness\"), which look better without an environment map.", + " if (material.metalness) material.metalness = 0;", + " material.onBeforeCompile = (shader) => {", + " Object.assign(shader.uniforms, uniforms);", + " shader.vertexShader = patchShaderCode(shader.vertexShader, [", + " ['#include ', '#include \\n' + fadeShaderDeclarations],", + " ['#include ', '#include \\n' + fadeShaderCode],", + " ], 'ScatteredModels');", + " };", + " material.customProgramCacheKey = () => 'ScatteredModels';", + " return material;", + " };", + " const material = Array.isArray(mesh.material) ? mesh.material.map(patchMaterial) : patchMaterial(mesh.material);", + " return { geometry, material };", + " });", + " return {", + " parts,", + " width: (box.max.x - box.min.x) / height,", + " depth: (box.max.y - box.min.y) / height,", + " };", + "};", + "", + "/**", + " * A copy of the model: its base position in the scene, its scale (the", + " * height of the copy) and the key of its cell in the grid of copies.", + " * @typedef {{x: number, y: number, z: number, angle: number, height: number, key: number}} Copy", + " */", + "", + "/**", + " * The scattered models of an object: copies of a 3D model placed on the", + " * grounds of the scene, drawn with GPU instancing in chunks that are hidden", + " * when far or off-screen.", + " */", + "class Scatter {", + " /** @param {gdjs.CustomRuntimeObject3D} object */", + " constructor(object) {", + " this.object = object;", + " this.group = new THREE.Group();", + " object.get3DRendererObject().add(this.group);", + " /** @type {Array<{firstCellI: number, firstCellJ: number, meshes: THREE.InstancedMesh[], copies: Copy[], center: THREE.Vector3, radius: number}>} */", + " this.chunks = [];", + " /**", + " * The grid of copies: one copy at most in each cell, cells grouped in chunks.", + " * @type {{spacing: number, cellsPerChunk: number, cellsX: number, cellsY: number} | null}", + " */", + " this.grid = null;", + " this.placement = new GroundPlacement(object.getRuntimeScene());", + " this.uniforms = {", + " // In 3D world units.", + " scatterDrawDistance: { value: 1 },", + " };", + " /** @type {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}} */", + " this.model = { parts: [], width: 0, depth: 0 };", + " this.modelResourceName = '';", + " this.loadingModelResourceName = '';", + " /**", + " * Copies removed by events (for example, cut trees), by key.", + " * @type {Map}", + " */", + " this.removedCopies = new Map();", + " /** Increased each time copies change, for the collisions. */", + " this.version = 0;", + " this.isDisposed = false;", + " /** The last value of the painted data property read or saved. */", + " this.paintData = '';", + " /**", + " * Values saved by the editor tools: the editor sends them back with a", + " * hot-reload, possibly after newer ones were saved.", + " * @type {string[]}", + " */", + " this.savedPaintData = [];", + " this.maskSource = '';", + " /** @type {DensityMask} */", + " this.baseMask = new DensityMask(255);", + " /** @type {DensityMask} */", + " this.mask = this.baseMask;", + " this.changedSamples = new SampleRectangle();", + " this._stroke = { cellWidth: 1, cellHeight: 1, ax: 0, ay: 0, bx: 0, by: 0, radius: 0 };", + " this._groundAtCopy = { ground: null, z: 0 };", + " this._matrix = new THREE.Matrix4();", + " this._raycastSphere = new THREE.Sphere();", + " this._raycastRay = new THREE.Ray();", + " this._raycastBox = new THREE.Box3();", + " this._raycastInverseMatrix = new THREE.Matrix4();", + " this._raycastScale = new THREE.Vector3();", + " /** Placed on a copy to test the triangles of the model. */", + " this._raycastMesh = new THREE.Mesh();", + " /** @type {THREE.Intersection[]} */", + " this._raycastHits = [];", + " this._color = new THREE.Color();", + " this._tint = new THREE.Color();", + " this._cameraPosition = new THREE.Vector3();", + " this._chunkPosition = new THREE.Vector3();", + " /**", + " * Trunks, for the 3D physics behavior and navigation meshes. Always given,", + " * empty without trunk collisions: otherwise these behaviors would use the", + " * object box, covering the whole area.", + " * @type {gdjs.Surface}", + " */", + " this.surface = {", + " getVersion: () => this.version,", + " getChangedArea: () => null,", + " getTriangles: () => this._getTrunkTriangles(),", + " getHeightField: () => null,", + " };", + " /** The trunk collision properties, changing the surface. */", + " this.trunkKey = '';", + " this.loadMaskFromProperties();", + " this.updateFromProperties();", + " // Before the behaviors create their shapes.", + " this._updateSurface();", + " }", + "", + " _getMaskSource() {", + " return this.object._getCoverage() + '|' + this.object._getEdits();", + " }", + "", + " loadMaskFromProperties() {", + " const { object } = this;", + " this.maskSource = this._getMaskSource();", + " this.paintData = object._getPaintData();", + " this.baseMask = new DensityMask(object._getCoverage() === 'Painted' ? 0 : 255);", + " applyEdits(this.baseMask, object._getEdits());", + " this.mask = maskCodec.decode(this.paintData, this.baseMask) || this.baseMask.clone();", + " this.placement.placeAgain();", + " }", + "", + " onHotReloading() {", + " const paintData = this.object._getPaintData();", + " const savedIndex = this.savedPaintData.indexOf(paintData);", + " if (savedIndex !== -1 && this._getMaskSource() === this.maskSource) {", + " // The object already has this data, or newer changes.", + " this.savedPaintData.splice(0, savedIndex + 1);", + " } else if (paintData !== this.paintData || this._getMaskSource() !== this.maskSource) {", + " this.savedPaintData.length = 0;", + " this.loadMaskFromProperties();", + " }", + " this.updateFromProperties();", + " }", + "", + " /** @returns {string} The data to save in the painted data property. */", + " save() {", + " this.paintData = maskCodec.encode(this.mask, this.baseMask);", + " this.savedPaintData.push(this.paintData);", + " return this.paintData;", + " }", + "", + " updateFromProperties() {", + " const { object } = this;", + " const modelResourceName = object._getModel();", + " if (modelResourceName !== this.modelResourceName) {", + " this.modelResourceName = modelResourceName;", + " this._loadModel();", + " }", + " this.placement.placeAgain();", + " }", + "", + " _disposeModel() {", + " for (const part of this.model.parts) {", + " part.geometry.dispose();", + " for (const material of Array.isArray(part.material) ? part.material : [part.material]) {", + " material.dispose();", + " }", + " }", + " this.model = { parts: [], width: 0, depth: 0 };", + " }", + "", + " _loadModel() {", + " this._disposeChunks(this.chunks);", + " this._disposeModel();", + " const resourceName = this.modelResourceName;", + " if (!resourceName) return;", + " const game = this.object.getRuntimeScene().getGame();", + " const modelManager = game.getModel3DManager();", + " const gltf = modelManager.getModel(resourceName);", + " if (gltf === modelManager._invalidModel) {", + " // Not loaded yet, for example a model just chosen in the editor.", + " if (this.loadingModelResourceName === resourceName) return;", + " this.loadingModelResourceName = resourceName;", + " game.getResourceLoader()", + " .loadResource(resourceName)", + " .then(() => {", + " if (this.isDisposed || this.modelResourceName !== resourceName) return;", + " this.loadingModelResourceName = '';", + " if (modelManager.getModel(resourceName) !== modelManager._invalidModel) {", + " this._loadModel();", + " this.placement.placeAgain();", + " }", + " })", + " .catch(() => {});", + " return;", + " }", + " this.model = createModelParts(gltf.scene, this.uniforms);", + " }", + "", + " _getPlacementKey() {", + " const { object } = this;", + " return [", + " object.getX(), object.getY(), object.getZ(), object.getWidth(), object.getHeight(), object.getDepth(),", + " object._getDensity(), object._getSize(), object._getMinScale(), object._getMaxScale(),", + " object._getRandomRotation(), object._getRandomTint(), object._getGroundLayer(), object._getMaxSlope(),", + " object._getMinGroundZ(), object._getMaxGroundZ(), object._getSeed(), object._getDrawDistance(),", + " object._getCastShadow(), object._getReceiveShadow(), this.model.parts.length,", + " ].join('|');", + " }", + "", + " _getChunkSize() {", + " return Math.min(Math.max(this.object._getDrawDistance() / 4, MIN_CHUNK_SIZE), MAX_CHUNK_SIZE);", + " }", + "", + " /** @returns {number} The distance between copies (one copy per cell of this size at most). */", + " _getSpacing() {", + " const { object } = this;", + " const density = Math.max(object._getDensity(), 0);", + " if (density <= 0) return 0;", + " const spacing = 100 / Math.sqrt(density);", + " // Too many cells would freeze the game: the density is lowered.", + " const cellCount = (object.getWidth() / spacing) * (object.getHeight() / spacing);", + " return cellCount > MAX_COPIES ? spacing * Math.sqrt(cellCount / MAX_COPIES) : spacing;", + " }", + "", + " /**", + " * Find the copy of a cell of the grid, if there is one.", + " * @returns {Copy | null}", + " */", + " _placeCopy(cellI, cellJ, spacing, grounds) {", + " const { object } = this;", + " const width = object.getWidth();", + " const height = object.getHeight();", + " const nextRandom = createRandom(hashCell(Math.floor(object._getSeed()), cellI, cellJ));", + " // Always drawn in the same order, so that a copy stays the same when properties change.", + " const x = (cellI + nextRandom()) * spacing;", + " const y = (cellJ + nextRandom()) * spacing;", + " const presence = nextRandom();", + " const growth = nextRandom();", + " const scaleFactor = nextRandom();", + " const angle = nextRandom() * Math.PI * 2;", + " if (x > width || y > height) return null;", + " if (presence >= this.mask.sample(x / width, y / height)) return null;", + " const key = cellJ * 65536 + cellI;", + " if (this.removedCopies.has(key)) return null;", + "", + " const sceneX = object.getX() + x;", + " const sceneY = object.getY() + y;", + " const { ground, z } = findHighestGround(grounds, sceneX, sceneY, this._groundAtCopy);", + " const groundZ = ground ? z : object.getZ();", + " if (groundZ < object._getMinGroundZ() || groundZ > object._getMaxGroundZ()) return null;", + " const copyHeight =", + " Math.max(object._getSize(), 0) *", + " (object._getMinScale() + (object._getMaxScale() - object._getMinScale()) * scaleFactor);", + " if (ground) {", + " if (ground.getSlopeAt(sceneX, sceneY) > object._getMaxSlope()) return null;", + " const groundLayer = parseInt(object._getGroundLayer(), 10) || 0;", + " if (groundLayer > 0) {", + " if (growth > ground.getLayerWeightAt(sceneX, sceneY, groundLayer)) return null;", + " // Copies don't stand at the border of their layer, so that trees don't cover roads.", + " const margin = (copyHeight * Math.max(this.model.width, this.model.depth)) / 4;", + " for (const [offsetX, offsetY] of [[margin, 0], [-margin, 0], [0, margin], [0, -margin]]) {", + " if (ground.getLayerWeightAt(sceneX + offsetX, sceneY + offsetY, groundLayer) < 0.5) return null;", + " }", + " }", + " }", + " return {", + " x: sceneX,", + " y: sceneY,", + " z: groundZ,", + " angle: object._getRandomRotation() ? angle : 0,", + " height: copyHeight,", + " key,", + " };", + " }", + "", + " _disposeChunks(chunks) {", + " for (const chunk of chunks) {", + " for (const mesh of chunk.meshes) {", + " mesh.removeFromParent();", + " // Geometries and materials are the ones of the model.", + " mesh.dispose();", + " }", + " }", + " if (chunks === this.chunks) this.chunks = [];", + " }", + "", + " /** Place the copies again, for example after the ground or the object changed. */", + " rebuild() {", + " this._disposeChunks(this.chunks);", + " this.version++;", + " const { object } = this;", + " const spacing = this._getSpacing();", + " if (spacing <= 0 || this.model.parts.length === 0) {", + " this.grid = null;", + " return;", + " }", + " const cellsPerChunk = Math.max(Math.round(this._getChunkSize() / spacing), 1);", + " const grid = {", + " spacing,", + " cellsPerChunk,", + " cellsX: Math.ceil(object.getWidth() / spacing),", + " cellsY: Math.ceil(object.getHeight() / spacing),", + " };", + " this.grid = grid;", + " for (let firstCellJ = 0; firstCellJ < grid.cellsY; firstCellJ += cellsPerChunk) {", + " for (let firstCellI = 0; firstCellI < grid.cellsX; firstCellI += cellsPerChunk) {", + " this._createChunk(firstCellI, firstCellJ);", + " }", + " }", + " }", + "", + " /**", + " * Place again the copies of the chunks touching a rectangle of the scene,", + " * for example after painting.", + " */", + " _rebuildArea(minX, minY, maxX, maxY) {", + " const { grid, object } = this;", + " if (!grid) return;", + " const chunkSize = grid.spacing * grid.cellsPerChunk;", + " const firstChunkI = Math.max(Math.floor((minX - object.getX()) / chunkSize), 0);", + " const firstChunkJ = Math.max(Math.floor((minY - object.getY()) / chunkSize), 0);", + " const lastChunkI = Math.min(Math.floor((maxX - object.getX()) / chunkSize), Math.ceil(grid.cellsX / grid.cellsPerChunk) - 1);", + " const lastChunkJ = Math.min(Math.floor((maxY - object.getY()) / chunkSize), Math.ceil(grid.cellsY / grid.cellsPerChunk) - 1);", + " if (firstChunkI > lastChunkI || firstChunkJ > lastChunkJ) return;", + " const isInArea = (chunk) => {", + " const chunkI = chunk.firstCellI / grid.cellsPerChunk;", + " const chunkJ = chunk.firstCellJ / grid.cellsPerChunk;", + " return chunkI >= firstChunkI && chunkI <= lastChunkI && chunkJ >= firstChunkJ && chunkJ <= lastChunkJ;", + " };", + " this._disposeChunks(this.chunks.filter(isInArea));", + " this.chunks = this.chunks.filter((chunk) => !isInArea(chunk));", + " for (let chunkJ = firstChunkJ; chunkJ <= lastChunkJ; chunkJ++) {", + " for (let chunkI = firstChunkI; chunkI <= lastChunkI; chunkI++) {", + " this._createChunk(chunkI * grid.cellsPerChunk, chunkJ * grid.cellsPerChunk);", + " }", + " }", + " this.version++;", + " }", + "", + " _createChunk(firstCellI, firstCellJ) {", + " const { grid } = this;", + " const grounds = Array.from(this.placement.grounds);", + " /** @type {Copy[]} */", + " const copies = [];", + " for (let cellJ = firstCellJ; cellJ < Math.min(firstCellJ + grid.cellsPerChunk, grid.cellsY); cellJ++) {", + " for (let cellI = firstCellI; cellI < Math.min(firstCellI + grid.cellsPerChunk, grid.cellsX); cellI++) {", + " const copy = this._placeCopy(cellI, cellJ, grid.spacing, grounds);", + " if (copy) copies.push(copy);", + " }", + " }", + " if (copies.length === 0) return;", + "", + " const { object } = this;", + " // Copies are placed in scene units, and the object group is scaled", + " // from its inner area: they are scaled back to keep their proportions.", + " const scaleX = Math.max(object.getWidth() / AREA_SIZE, 0.0001);", + " const scaleY = Math.max(object.getHeight() / AREA_SIZE, 0.0001);", + " const scaleZ = Math.max(object.getDepth() / AREA_DEPTH, 0.0001);", + " const matrices = new Float32Array(copies.length * 16);", + " const tintColor = gdjs.rgbOrHexStringToNumber(object._getRandomTint());", + " const hasTint = tintColor !== 0xffffff;", + " const colors = hasTint ? new Float32Array(copies.length * 3) : null;", + " this._tint.setHex(tintColor);", + " const min = new THREE.Vector3(Infinity, Infinity, Infinity);", + " const max = new THREE.Vector3(-Infinity, -Infinity, -Infinity);", + " copies.forEach((copy, index) => {", + " const cos = Math.cos(copy.angle) * copy.height;", + " const sin = Math.sin(copy.angle) * copy.height;", + " const localX = (copy.x - object.getX()) / scaleX;", + " const localY = (copy.y - object.getY()) / scaleY;", + " const localZ = (copy.z - object.getZ()) / scaleZ;", + " this._matrix.set(", + " cos / scaleX, -sin / scaleX, 0, localX,", + " sin / scaleY, cos / scaleY, 0, localY,", + " 0, 0, copy.height / scaleZ, localZ,", + " 0, 0, 0, 1", + " );", + " this._matrix.toArray(matrices, index * 16);", + " if (colors) {", + " // The tint of each copy is between white and the tint color.", + " this._color.setRGB(1, 1, 1).lerp(this._tint, createRandom(copy.key + 1)());", + " this._color.toArray(colors, index * 3);", + " }", + " const reach = (copy.height * Math.max(this.model.width, this.model.depth)) / 2;", + " min.min(this._chunkPosition.set(copy.x - reach, copy.y - reach, copy.z));", + " max.max(this._chunkPosition.set(copy.x + reach, copy.y + reach, copy.z + copy.height));", + " });", + " const instanceMatrix = new THREE.InstancedBufferAttribute(matrices, 16);", + " const instanceColor = colors ? new THREE.InstancedBufferAttribute(colors, 3) : null;", + " const castShadow = object._getCastShadow();", + " const receiveShadow = object._getReceiveShadow();", + " const meshes = this.model.parts.map((part) => {", + " const mesh = new THREE.InstancedMesh(part.geometry, part.material, 0);", + " // All the parts of the model share the positions of the copies.", + " mesh.instanceMatrix = instanceMatrix;", + " mesh.instanceColor = instanceColor;", + " mesh.count = copies.length;", + " mesh.computeBoundingSphere();", + " mesh.castShadow = castShadow;", + " mesh.receiveShadow = receiveShadow;", + " // The first mesh tests all the parts of the copies.", + " mesh.raycast = () => {};", + " this.group.add(mesh);", + " return mesh;", + " });", + " const center = new THREE.Vector3(", + " ((min.x + max.x) / 2 - object.getX()) / scaleX,", + " ((min.y + max.y) / 2 - object.getY()) / scaleY,", + " ((min.z + max.z) / 2 - object.getZ()) / scaleZ", + " );", + " const chunk = { firstCellI, firstCellJ, meshes, copies, center, radius: min.distanceTo(max) / 2 };", + " // Used by the scene editor to select the object when a copy is clicked.", + " meshes[0].raycast = (raycaster, intersects) => this._raycastChunk(chunk, raycaster, intersects);", + " this.chunks.push(chunk);", + " }", + "", + " /**", + " * Add the hits of a ray on the copies of a chunk, like they are drawn. There", + " * can be thousands of copies: only the ones whose box is crossed by the ray", + " * are tested with the triangles of the model.", + " * @param {{meshes: THREE.InstancedMesh[], copies: Copy[], center: THREE.Vector3, radius: number}} chunk", + " * @param {THREE.Raycaster} raycaster", + " * @param {THREE.Intersection[]} intersects", + " */", + " _raycastChunk(chunk, raycaster, intersects) {", + " const { object, group } = this;", + " this._raycastSphere.center.copy(chunk.center).applyMatrix4(group.matrixWorld);", + " this._raycastSphere.radius = chunk.radius / getWorldScale(object);", + " if (!raycaster.ray.intersectsSphere(this._raycastSphere)) return;", + "", + " // The ray in the scene coordinates, like the copies.", + " const scale = this._raycastScale.set(", + " Math.max(object.getWidth() / AREA_SIZE, 0.0001),", + " Math.max(object.getHeight() / AREA_SIZE, 0.0001),", + " Math.max(object.getDepth() / AREA_DEPTH, 0.0001)", + " );", + " const ray = this._raycastRay.copy(raycaster.ray).applyMatrix4(this._raycastInverseMatrix.copy(group.matrixWorld).invert());", + " ray.origin.multiply(scale).add(this._chunkPosition.set(object.getX(), object.getY(), object.getZ()));", + " ray.direction.multiply(scale);", + "", + " const reachPerHeight = Math.max(this.model.width, this.model.depth) / 2;", + " const instanceMatrices = chunk.meshes[0].instanceMatrix.array;", + " chunk.copies.forEach((copy, index) => {", + " const reach = copy.height * reachPerHeight;", + " this._raycastBox.min.set(copy.x - reach, copy.y - reach, copy.z);", + " this._raycastBox.max.set(copy.x + reach, copy.y + reach, copy.z + copy.height);", + " if (!ray.intersectsBox(this._raycastBox)) return;", + " this._raycastMesh.matrixWorld.fromArray(instanceMatrices, index * 16).premultiply(group.matrixWorld);", + " for (const mesh of chunk.meshes) {", + " this._raycastMesh.geometry = mesh.geometry;", + " this._raycastMesh.material = mesh.material;", + " this._raycastHits.length = 0;", + " this._raycastMesh.raycast(raycaster, this._raycastHits);", + " for (const hit of this._raycastHits) {", + " hit.object = mesh;", + " hit.instanceId = index;", + " intersects.push(hit);", + " }", + " }", + " });", + " }", + "", + " /** @param {THREE.Camera | null} camera */", + " _hideFarChunks(camera) {", + " if (!camera) return;", + " const worldScale = getWorldScale(this.object);", + " const drawDistance = Math.max(this.object._getDrawDistance(), 1) / worldScale;", + " this.uniforms.scatterDrawDistance.value = drawDistance;", + " this._cameraPosition.setFromMatrixPosition(camera.matrixWorld);", + " for (const chunk of this.chunks) {", + " const distance = this._chunkPosition", + " .copy(chunk.center)", + " .applyMatrix4(this.group.matrixWorld)", + " .distanceTo(this._cameraPosition);", + " const isVisible = distance - chunk.radius / worldScale < drawDistance;", + " for (const mesh of chunk.meshes) mesh.visible = isVisible;", + " }", + " }", + "", + " /**", + " * @param {number} elapsedSeconds", + " * @param {THREE.Camera | null} camera", + " */", + " update(elapsedSeconds, camera) {", + " if (this.placement.shouldPlaceAgain(elapsedSeconds, this._getPlacementKey())) {", + " this.rebuild();", + " }", + " if (!this.changedSamples.isEmpty()) {", + " const { object } = this;", + " const sampleWidth = object.getWidth() / MASK_RESOLUTION;", + " const sampleHeight = object.getHeight() / MASK_RESOLUTION;", + " const { minI, minJ, maxI, maxJ } = this.changedSamples;", + " // Copies between samples use the density of the samples around them.", + " this._rebuildArea(", + " object.getX() + (minI - 1) * sampleWidth,", + " object.getY() + (minJ - 1) * sampleHeight,", + " object.getX() + (maxI + 1) * sampleWidth,", + " object.getY() + (maxJ + 1) * sampleHeight", + " );", + " this.changedSamples.clear();", + " }", + " // The object transformation is applied just before rendering, it's needed now.", + " this.object.getRenderer().ensureUpToDate();", + " this.group.updateWorldMatrix(true, false);", + " this._hideFarChunks(camera);", + " this._updateSurface();", + " }", + "", + " _updateSurface() {", + " const { object } = this;", + " if (typeof object.setSurface !== 'function') return;", + " const trunkKey = object._getTrunkCollisions() + '|' + object._getTrunkDiameter();", + " if (trunkKey !== this.trunkKey) {", + " this.trunkKey = trunkKey;", + " this.version++;", + " }", + " if (object.getSurface() !== this.surface) object.setSurface(this.surface);", + " }", + "", + " /** @returns {Copy[]} */", + " getCopies() {", + " const copies = [];", + " for (const chunk of this.chunks) copies.push(...chunk.copies);", + " return copies;", + " }", + "", + " /**", + " * @returns {Copy | null} The copy whose base is the nearest to a position (on X and Y), if any.", + " */", + " findNearestCopy(x, y) {", + " let nearestCopy = null;", + " let nearestDistanceSquared = Infinity;", + " for (const chunk of this.chunks) {", + " for (const copy of chunk.copies) {", + " const distanceSquared = (copy.x - x) * (copy.x - x) + (copy.y - y) * (copy.y - y);", + " if (distanceSquared < nearestDistanceSquared) {", + " nearestDistanceSquared = distanceSquared;", + " nearestCopy = copy;", + " }", + " }", + " }", + " return nearestCopy;", + " }", + "", + " getCopyCount() {", + " let count = 0;", + " for (const chunk of this.chunks) count += chunk.copies.length;", + " return count;", + " }", + "", + " /** Remove the copies whose base is in a circle (they come back when the scene is restarted). */", + " removeCopies(x, y, radius) {", + " let hasRemovedCopies = false;", + " for (const chunk of this.chunks) {", + " for (const copy of chunk.copies) {", + " if ((copy.x - x) * (copy.x - x) + (copy.y - y) * (copy.y - y) <= radius * radius) {", + " this.removedCopies.set(copy.key, copy);", + " hasRemovedCopies = true;", + " }", + " }", + " }", + " if (hasRemovedCopies) this._rebuildArea(x - radius, y - radius, x + radius, y + radius);", + " }", + "", + " /** Paint the full density around a position, and bring back the copies removed there. */", + " addCopies(x, y, radius) {", + " for (const [key, copy] of this.removedCopies) {", + " if ((copy.x - x) * (copy.x - x) + (copy.y - y) * (copy.y - y) <= radius * radius) {", + " this.removedCopies.delete(key);", + " }", + " }", + " this.paint(x, y, x, y, radius, 1, 1);", + " }", + "", + " _getStroke(x1, y1, x2, y2, radius) {", + " const { object } = this;", + " const stroke = this._stroke;", + " stroke.cellWidth = object.getWidth() / MASK_RESOLUTION;", + " stroke.cellHeight = object.getHeight() / MASK_RESOLUTION;", + " stroke.ax = x1 - object.getX();", + " stroke.ay = y1 - object.getY();", + " stroke.bx = x2 - object.getX();", + " stroke.by = y2 - object.getY();", + " stroke.radius = Math.max(radius, 0);", + " return stroke;", + " }", + "", + " /**", + " * Paint the density around a segment (copies are placed again at the next frame).", + " * @param {number} density From 0 (erase) to 1 (as dense as the Density property).", + " * @param {number} strength From 0 to 1.", + " */", + " paint(x1, y1, x2, y2, radius, density, strength) {", + " const changedSamples = new SampleRectangle();", + " this.mask.paint(this._getStroke(x1, y1, x2, y2, radius), density, strength, changedSamples);", + " this.changedSamples.add(changedSamples);", + " }", + "", + " /** @param {DensityMask} mask */", + " setMask(mask) {", + " this.mask = mask;", + " this.rebuild();", + " }", + "", + " /**", + " * Prisms around the trunks, from 0 to 1 in the object box (see `gdjs.Surface`).", + " * @returns {gdjs.SurfaceTriangles}", + " */", + " _getTrunkTriangles() {", + " const { object } = this;", + " const copies = object._getTrunkCollisions() ? this.getCopies() : [];", + " const verticesPerCopy = TRUNK_SIDES * 2 + 1;", + " const positions = new Float32Array(copies.length * verticesPerCopy * 3);", + " const indices = new Uint32Array(copies.length * TRUNK_SIDES * 3 * 3);", + " const width = object.getWidth() || 1;", + " const height = object.getHeight() || 1;", + " const depth = object.getDepth() || 1;", + " let indexOffset = 0;", + " copies.forEach((copy, copyIndex) => {", + " const radius = (Math.max(object._getTrunkDiameter(), 0) / 2) * (copy.height / Math.max(object._getSize(), 1));", + " const top = copy.z + copy.height * 0.8;", + " const firstVertex = copyIndex * verticesPerCopy;", + " const setVertex = (vertex, x, y, z) => {", + " positions[vertex * 3] = (x - object.getX()) / width;", + " positions[vertex * 3 + 1] = (y - object.getY()) / height;", + " positions[vertex * 3 + 2] = (z - object.getZ()) / depth;", + " };", + " for (let side = 0; side < TRUNK_SIDES; side++) {", + " const angle = ((side + 0.5) / TRUNK_SIDES) * Math.PI * 2;", + " const x = copy.x + Math.cos(angle) * radius;", + " const y = copy.y + Math.sin(angle) * radius;", + " // Starting a bit under the ground, which can be sloped.", + " setVertex(firstVertex + side * 2, x, y, copy.z - radius);", + " setVertex(firstVertex + side * 2 + 1, x, y, top);", + " }", + " const center = firstVertex + TRUNK_SIDES * 2;", + " setVertex(center, copy.x, copy.y, top);", + " for (let side = 0; side < TRUNK_SIDES; side++) {", + " const bottom = firstVertex + side * 2;", + " const nextBottom = firstVertex + ((side + 1) % TRUNK_SIDES) * 2;", + " // Triangles face out of the prism (vertices at odd indices are at the top).", + " indices.set([bottom, nextBottom, bottom + 1, nextBottom, nextBottom + 1, bottom + 1], indexOffset);", + " indices.set([center, bottom + 1, nextBottom + 1], indexOffset + 6);", + " indexOffset += 9;", + " }", + " });", + " if (copies.length === 0) {", + " // Without triangles, the physics engine and navigation meshes would use", + " // the object box: a tiny triangle under the object is used instead.", + " return { positions: new Float32Array([0, 0, -1, 0.0001, 0, -1, 0, 0.0001, -1]), indices: new Uint32Array([0, 2, 1]) };", + " }", + " return { positions, indices };", + " }", + "", + " dispose() {", + " this.isDisposed = true;", + " if (typeof this.object.setSurface === 'function' && this.object.getSurface() === this.surface) {", + " this.object.setSurface(null);", + " }", + " this._disposeChunks(this.chunks);", + " this._disposeModel();", + " this.group.removeFromParent();", + " }", + "}", + "", + "const scatterToolIcons = {", + " Add: svgIcon(", + " ''", + " ),", + " Erase: svgIcon(", + " ''", + " ),", + "};", + "", + "/**", + " * The brushes shown in the scene editor when scattered models are selected,", + " * to add or erase copies. Without the needed editor functions (older GDevelop", + " * versions), nothing is shown: copies can still be painted with events.", + " */", + "class ScatterEditorTools {", + " constructor() {", + " /** '' when no brush is used. */", + " this.toolName = '';", + " this.sizePercent = 30;", + " this.strengthPercent = 50;", + " /** @type {Scatter | null} */", + " this.scatter = null;", + " /** @type {gdjs.InGameEditor | null} */", + " this.editor = null;", + " this.isStroking = false;", + " this.lastStrokeTime = 0;", + " /** @type {Array<{mask: DensityMask, baseMask: DensityMask}>} */", + " this.undoSteps = [];", + " this.raycaster = new THREE.Raycaster();", + " this.ndc = new THREE.Vector2();", + " this._groundAtCursor = { ground: null, z: 0 };", + " /** A circle following the ground under the cursor. */", + " this.brushRing = new THREE.LineLoop(", + " new THREE.BufferGeometry().setAttribute('position', new THREE.BufferAttribute(new Float32Array(64 * 3), 3)),", + " new THREE.LineBasicMaterial({ color: 0xffffff, depthTest: false, transparent: true })", + " );", + " this.brushRing.renderOrder = 1000;", + " this.brushRing.frustumCulled = false;", + " this.brushRing.raycast = () => {};", + " }", + "", + " /** @param {gdjs.InGameEditor} editor */", + " update(editor) {", + " if (typeof editor.showToolbar !== 'function') return;", + " this.editor = editor;", + " const selectedObjects = editor.getSelectedObjects();", + " const scatter =", + " selectedObjects.length === 1 && selectedObjects[0].__terrainScatter ? selectedObjects[0].__terrainScatter : null;", + " if (scatter !== this.scatter) {", + " this._endStroke();", + " this.scatter = scatter;", + " this.undoSteps.length = 0;", + " }", + " if (scatter) editor.showToolbar('ScatteredModels', this._getToolbarItems());", + " const game = editor.getRuntimeGame();", + " if (!scatter || !this.toolName) {", + " this.brushRing.removeFromParent();", + " return;", + " }", + " const inputManager = game.getInputManager();", + " if (inputManager.wasKeyJustPressed(gdjs.evtTools.input.keysNameToCode.Escape)) {", + " this._endStroke();", + " this.toolName = '';", + " this.brushRing.removeFromParent();", + " return;", + " }", + " editor.captureLeftMouseButton();", + " const hit = this._getPositionUnderCursor(scatter, game);", + " const radius = this._getRadius();", + " this._showBrushRing(scatter, hit, radius);", + "", + " const isPressedForTool =", + " typeof editor.isCapturedLeftMouseButtonPressed === 'function'", + " ? editor.isCapturedLeftMouseButtonPressed()", + " : inputManager.isMouseButtonPressed(0);", + " // The button is released outside the canvas when the mouse is on a toolbar.", + " const isPressed = isPressedForTool && inputManager.isMouseInsideCanvas();", + " if (isPressed && hit && !this.isStroking) {", + " this._startStroke(scatter);", + " }", + " if (this.isStroking && isPressed && hit) {", + " this._applyBrush(scatter, hit, radius);", + " }", + " if (!isPressed) {", + " this._endStroke();", + " }", + " }", + "", + " _getRadius() {", + " const { object } = this.scatter;", + " const maxRadius = Math.max(object.getWidth(), object.getHeight()) / 4;", + " return Math.max(maxRadius * Math.pow(this.sizePercent / 100, 2), 10);", + " }", + "", + " /**", + " * @returns {{x: number, y: number} | null} The position of the scene under the cursor: on the", + " * highest ground hit, or on the plane at the object Z.", + " */", + " _getPositionUnderCursor(scatter, game) {", + " let hit = null;", + " for (const ground of getGrounds(scatter.object.getRuntimeScene())) {", + " if (typeof ground.raycastFromCursor !== 'function') continue;", + " const localHit = ground.raycastFromCursor(game, this.raycaster, this.ndc);", + " if (!localHit) continue;", + " const sceneHit = ground.toScenePosition(localHit);", + " if (!hit || sceneHit.z > hit.z) hit = sceneHit;", + " }", + " if (hit) return hit;", + "", + " const { object } = scatter;", + " const layer = object.getInstanceContainer().getLayer(object.getLayer());", + " const camera = layer.getRenderer().getThreeCamera();", + " if (!camera) return null;", + " const inputManager = game.getInputManager();", + " this.ndc.set(", + " (inputManager.getCursorX() / game.getGameResolutionWidth()) * 2 - 1,", + " -(inputManager.getCursorY() / game.getGameResolutionHeight()) * 2 + 1", + " );", + " this.raycaster.setFromCamera(this.ndc, camera);", + " scatter.group.updateWorldMatrix(true, false);", + " const ray = this.raycaster.ray.clone().applyMatrix4(scatter.group.matrixWorld.clone().invert());", + " const point = ray.intersectPlane(new THREE.Plane(new THREE.Vector3(0, 0, 1), 0), new THREE.Vector3());", + " if (!point) return null;", + " return {", + " x: object.getX() + (point.x / AREA_SIZE) * object.getWidth(),", + " y: object.getY() + (point.y / AREA_SIZE) * object.getHeight(),", + " };", + " }", + "", + " _showBrushRing(scatter, hit, radius) {", + " if (!hit) {", + " this.brushRing.removeFromParent();", + " return;", + " }", + " const { object } = scatter;", + " if (this.brushRing.parent !== scatter.group) scatter.group.add(this.brushRing);", + " const grounds = getGrounds(object.getRuntimeScene());", + " const positions = this.brushRing.geometry.attributes.position;", + " for (let index = 0; index < positions.count; index++) {", + " const angle = (index / positions.count) * Math.PI * 2;", + " const x = hit.x + Math.cos(angle) * radius;", + " const y = hit.y + Math.sin(angle) * radius;", + " const { ground, z } = findHighestGround(grounds, x, y, this._groundAtCursor);", + " positions.setXYZ(", + " index,", + " ((x - object.getX()) / object.getWidth()) * AREA_SIZE,", + " ((y - object.getY()) / object.getHeight()) * AREA_SIZE,", + " (((ground ? z : object.getZ()) + 2 - object.getZ()) / Math.max(object.getDepth(), 0.0001)) * AREA_DEPTH", + " );", + " }", + " positions.needsUpdate = true;", + " }", + "", + " _startStroke(scatter) {", + " this.isStroking = true;", + " this.lastStrokeTime = performance.now();", + " this.undoSteps.push({ mask: scatter.mask.clone(), baseMask: scatter.baseMask });", + " while (this.undoSteps.length > 30) this.undoSteps.shift();", + " }", + "", + " _endStroke() {", + " if (!this.isStroking) return;", + " this.isStroking = false;", + " this._save();", + " }", + "", + " _applyBrush(scatter, hit, radius) {", + " const now = performance.now();", + " const elapsedSeconds = Math.min((now - this.lastStrokeTime) / 1000, 0.1);", + " this.lastStrokeTime = now;", + " const strength = (this.strengthPercent / 100) * elapsedSeconds * 8;", + " scatter.paint(hit.x, hit.y, hit.x, hit.y, radius, this.toolName === 'Add' ? 1 : 0, strength);", + " }", + "", + " _save() {", + " const { scatter, editor } = this;", + " if (!scatter || !editor) return;", + " editor.updateObjectProperties(scatter.object.getName(), { PaintData: scatter.save() });", + " }", + "", + " _undo() {", + " const undoStep = this.undoSteps.pop();", + " // The coverage or the edits may have changed since the stroke.", + " if (!this.scatter || !undoStep || undoStep.baseMask !== this.scatter.baseMask) {", + " this.undoSteps.length = 0;", + " return;", + " }", + " this.scatter.setMask(undoStep.mask);", + " this._save();", + " }", + "", + " /** @returns {Array} */", + " _getToolbarItems() {", + " const toolButton = (toolName, tooltip) => ({", + " type: 'button',", + " id: toolName,", + " tooltip,", + " iconUrl: scatterToolIcons[toolName],", + " isActive: this.toolName === toolName,", + " onClick: () => (this.toolName = this.toolName === toolName ? '' : toolName),", + " });", + " return [", + " toolButton('Add', 'Add copies (drag on the ground)'),", + " toolButton('Erase', 'Erase copies'),", + " { type: 'divider', id: 'BrushDivider' },", + " {", + " type: 'slider',", + " id: 'Size',", + " tooltip: 'Brush size',", + " iconUrl: toolIcons.Size,", + " min: 1,", + " max: 100,", + " value: this.sizePercent,", + " onChange: (value) => (this.sizePercent = value),", + " },", + " {", + " type: 'slider',", + " id: 'Strength',", + " tooltip: 'Brush strength',", + " iconUrl: toolIcons.Strength,", + " min: 5,", + " max: 100,", + " value: this.strengthPercent,", + " onChange: (value) => (this.strengthPercent = value),", + " },", + " {", + " type: 'button',", + " id: 'Undo',", + " tooltip: 'Undo the last stroke on these scattered models',", + " iconUrl: toolIcons.Undo,", + " onClick: () => this._undo(),", + " },", + " ];", + " }", + "}", + "", + "const editorTools = new ScatterEditorTools();", + "if (gdjs.registerInGameEditorPostStepCallback) {", + " gdjs.registerInGameEditorPostStepCallback((editor) => editorTools.update(editor));", + "}", + "", + "gdjs.__terrainScatterExtension = {", + " Scatter,", + " // Exposed for tests.", + " DensityMask,", + " maskCodec,", + " applyEdits,", + "};", + "" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [], + "objectGroups": [] + } + ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "DefineHelperClasses" + }, + { + "functionName": "DefineGrassHelperClasses" + }, + { + "functionName": "DefineScatterHelperClasses" + } + ] + }, + "eventsBasedBehaviors": [], + "eventsBasedObjects": [ + { + "areaMaxX": 4096, + "areaMaxY": 4096, + "areaMaxZ": 600, + "areaMinX": 0, + "areaMinY": 0, + "areaMinZ": 0, + "assetStoreTag": "3d terrains", + "defaultName": "Terrain", + "description": "A ground with hills, mountains or valleys that can be sculpted and painted in the scene editor, or changed with events. Add the 3D physics behavior to it to make objects walk or roll on it.", + "fullName": "3D terrain", + "helpPath": "", + "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLXRlcnJhaW4iIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTQsNkwxMC4yNSwxMUwxMy4xLDE0LjhMMTEuNSwxNkM5LjgxLDEzLjc1IDcsMTAgNywxMEwxLDE4SDIzTDE0LDZaIiAvPjwvc3ZnPg==", + "is3D": true, + "isUsingLegacyInstancesRenderer": true, + "name": "Terrain3D", + "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", + "objects": [ + { + "assetStoreId": "", + "name": "AreaPlaceholder", + "type": "Scene3D::Cube3DObject", + "variables": [], + "effects": [], + "behaviors": [], + "content": { + "width": 4096, + "height": 4096, + "depth": 600, + "enableTextureTransparency": false, + "facesOrientation": "Y", + "backFaceUpThroughWhichAxisRotation": "X", + "materialType": "Basic", + "tint": "255;255;255", + "frontFaceResourceName": "", + "frontFaceVisible": false, + "frontFaceResourceRepeat": false, + "backFaceResourceName": "", + "backFaceVisible": false, + "backFaceResourceRepeat": false, + "leftFaceResourceName": "", + "leftFaceVisible": false, + "leftFaceResourceRepeat": false, + "rightFaceResourceName": "", + "rightFaceVisible": false, + "rightFaceResourceRepeat": false, + "topFaceResourceName": "", + "topFaceVisible": false, + "topFaceResourceRepeat": false, + "bottomFaceResourceName": "", + "bottomFaceVisible": false, + "bottomFaceResourceRepeat": false + } + } + ], + "objectsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "objectName": "AreaPlaceholder" + } + ] + }, + "objectsGroups": [], + "layers": [ + { + "ambientLightColorB": 200, + "ambientLightColorG": 200, + "ambientLightColorR": 200, + "camera2DPlaneMaxDrawingDistance": 5000, + "camera3DFarPlaneDistance": 10000, + "camera3DFieldOfView": 45, + "camera3DNearPlaneDistance": 3, + "cameraType": "", + "followBaseLayerCamera": false, + "isLightingLayer": false, + "isLocked": false, + "name": "", + "renderingType": "", + "visibility": true, + "cameras": [ + { + "defaultSize": true, + "defaultViewport": true, + "height": 0, + "viewportBottom": 1, + "viewportLeft": 0, + "viewportRight": 1, + "viewportTop": 0, + "width": 0 + } + ], + "effects": [] + } + ], + "instances": [ + { + "angle": 0, + "customSize": true, + "depth": 600, + "height": 4096, + "keepRatio": true, + "layer": "", + "name": "AreaPlaceholder", + "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-004096004096", + "width": 4096, + "x": 0, + "y": 0, + "zOrder": 1, + "numberProperties": [], + "stringProperties": [], + "initialVariables": [] + } + ], + "editionSettings": {}, + "eventsFunctions": [ + { + "fullName": "", + "functionType": "Action", + "name": "onCreated", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Terrain3D::DefineHelperClasses" + }, + "parameters": [ + "", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "for (const child of object.getChildrenContainer().getAdhocListOfAllInstances()) {", + " child.deleteFromScene();", + "}", + "if (object.__terrain3D) object.__terrain3D.dispose();", + "object.__terrain3D = new gdjs.__terrain3DExtension.Terrain(object);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "onDestroy", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrain3D.dispose();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "doStepPostEvents", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrain3D.update();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "onHotReloading", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrain3D.onHotReloading();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "description": "Raise the ground around a position (or lower it with a negative height). It's raised the most at the center and less and less up to the radius.", + "fullName": "Raise or lower the ground", + "functionType": "Action", + "group": "Sculpt", + "name": "RaiseGround", + "sentence": "Raise the ground of _PARAM0_ by _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const height = eventsFunctionContext.getArgument(\"Height\");", + "object.__terrain3D.raise(x, y, x, y, radius, height);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Height to add (negative to lower the ground)", + "name": "Height", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Raise the ground along a line, for example for a ridge, or lower it with a negative height to dig a river bed or a valley.", + "fullName": "Raise or lower the ground along a line", + "functionType": "Action", + "group": "Sculpt", + "name": "RaiseGroundAlongLine", + "sentence": "Raise the ground of _PARAM0_ by _PARAM6_ from _PARAM1_; _PARAM2_ to _PARAM3_; _PARAM4_ with a radius of _PARAM5_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x1 = eventsFunctionContext.getArgument(\"X1\");", + "const y1 = eventsFunctionContext.getArgument(\"Y1\");", + "const x2 = eventsFunctionContext.getArgument(\"X2\");", + "const y2 = eventsFunctionContext.getArgument(\"Y2\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const height = eventsFunctionContext.getArgument(\"Height\");", + "object.__terrain3D.raise(x1, y1, x2, y2, radius, height);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the start", + "name": "X1", + "type": "expression" + }, + { + "description": "Y position of the start", + "name": "Y1", + "type": "expression" + }, + { + "description": "X position of the end", + "name": "X2", + "type": "expression" + }, + { + "description": "Y position of the end", + "name": "Y2", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance from the line.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Height to add (negative to lower the ground)", + "name": "Height", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Flatten the ground around a position to a given height, for example to place a building.", + "fullName": "Flatten the ground", + "functionType": "Action", + "group": "Sculpt", + "name": "FlattenGround", + "sentence": "Flatten the ground of _PARAM0_ to a Z of _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const z = eventsFunctionContext.getArgument(\"Z\");", + "object.__terrain3D.flatten(x, y, x, y, radius, z, 1);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Z position of the flat ground", + "name": "Z", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Smooth the ground around a position, softening bumps and cliffs.", + "fullName": "Smooth the ground", + "functionType": "Action", + "group": "Sculpt", + "name": "SmoothGround", + "sentence": "Smooth the ground of _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_ (strength: _PARAM4_)", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const strength = eventsFunctionContext.getArgument(\"Strength\");", + "object.__terrain3D.smooth(x, y, x, y, radius, strength);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Strength (between 0 and 1)", + "name": "Strength", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Paint a layer (like grass, dirt, rock or sand) on the ground around a position.", + "fullName": "Paint a layer", + "functionType": "Action", + "group": "Paint", + "name": "PaintLayer", + "sentence": "Paint layer _PARAM4_ on _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const layer = eventsFunctionContext.getArgument(\"Layer\");", + "object.__terrain3D.paint(x, y, x, y, radius, layer, 1);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Layer (1 to 4)", + "name": "Layer", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Paint a layer on the ground along a line, for example for a path or a road.", + "fullName": "Paint a layer along a line", + "functionType": "Action", + "group": "Paint", + "name": "PaintLayerAlongLine", + "sentence": "Paint layer _PARAM6_ on _PARAM0_ from _PARAM1_; _PARAM2_ to _PARAM3_; _PARAM4_ with a radius of _PARAM5_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x1 = eventsFunctionContext.getArgument(\"X1\");", + "const y1 = eventsFunctionContext.getArgument(\"Y1\");", + "const x2 = eventsFunctionContext.getArgument(\"X2\");", + "const y2 = eventsFunctionContext.getArgument(\"Y2\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const layer = eventsFunctionContext.getArgument(\"Layer\");", + "object.__terrain3D.paint(x1, y1, x2, y2, radius, layer, 1);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the start", + "name": "X1", + "type": "expression" + }, + { + "description": "Y position of the start", + "name": "Y1", + "type": "expression" + }, + { + "description": "X position of the end", + "name": "X2", + "type": "expression" + }, + { + "description": "Y position of the end", + "name": "Y2", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance from the line.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Layer (1 to 4)", + "name": "Layer", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Tint the ground around a position with a color multiplying its layers, like dry grass or darker mud. White removes the tint.", + "fullName": "Tint the ground", + "functionType": "Action", + "group": "Paint", + "name": "TintGround", + "sentence": "Tint the ground of _PARAM0_ with _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const color = gdjs.rgbOrHexToRGBColor(eventsFunctionContext.getArgument(\"Color\"));", + "object.__terrain3D.tint(x, y, x, y, radius, color, 1);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Color", + "name": "Color", + "type": "color" + } + ], + "objectGroups": [] + }, + { + "description": "Make the ground around a position wet (darker and shiny, like after rain or near water) or dry.", + "fullName": "Change the wetness of the ground", + "functionType": "Action", + "group": "Paint", + "name": "WetGround", + "sentence": "Change the wetness of the ground of _PARAM0_ to _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const wetness = eventsFunctionContext.getArgument(\"Wetness\");", + "object.__terrain3D.wet(x, y, x, y, radius, wetness, 1);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Wetness (from 0 for dry to 1)", + "name": "Wetness", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "the wetness of the ground at a position, from 0 (dry) to 1, painted or near the water (wet shore).", + "fullName": "Ground wetness", + "functionType": "ExpressionAndCondition", + "group": "Paint", + "name": "GroundWetness", + "sentence": "the wetness of the ground at _PARAM1_; _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "eventsFunctionContext.returnValue = object.__terrain3D.getWetnessAt(x, y);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "The tint painted on the ground at a position, like \"255;255;255\" (white when there is none).", + "fullName": "Ground tint", + "functionType": "StringExpression", + "group": "Paint", + "name": "GroundTint", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "eventsFunctionContext.returnValue = object.__terrain3D.getTintAt(x, y).join(\";\");" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "string" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Remove the ground in a circle, for the entrance of a tunnel, a cave or a well: there is nothing drawn and no collision there. Fill it again with the \"Fill holes\" action. A height field can't go under itself: put a 3D model (with a collision shape) for the tunnel or the cave under the ground. The physics engine removes the triangles touching a hole, so the hole in the collisions can be up to a cell larger than the one drawn: make the model go a bit further than the hole, or put rocks around the entrance.", + "fullName": "Make a hole", + "functionType": "Action", + "group": "Holes", + "name": "MakeHole", + "sentence": "Make a hole in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "object.__terrain3D.makeHole(x, y, x, y, radius);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "name": "Radius", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Fill the holes in a circle with the layers of the ground around them: the ground is drawn and has collisions again.", + "fullName": "Fill holes", + "functionType": "Action", + "group": "Holes", + "name": "FillHoles", + "sentence": "Fill the holes in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "object.__terrain3D.fillHoles(x, y, x, y, radius);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "name": "Radius", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Make the entrance of a tunnel or a cave on a slope: a hole, with the ground in front of it flattened to the floor of the tunnel.", + "fullName": "Carve an entrance", + "functionType": "Action", + "group": "Holes", + "name": "CarveEntrance", + "sentence": "Carve an entrance in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_, going in at _PARAM4_° with its floor at Z _PARAM5_ and _PARAM6_ pixels of flat ground in front", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const angle = eventsFunctionContext.getArgument(\"Angle\");", + "const floorZ = eventsFunctionContext.getArgument(\"FloorZ\");", + "const length = eventsFunctionContext.getArgument(\"Length\");", + "object.__terrain3D.carveEntrance(x, y, radius, angle, floorZ, length);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "name": "Radius", + "type": "expression" + }, + { + "description": "Direction going into the ground (in degrees)", + "name": "Angle", + "type": "expression" + }, + { + "description": "Z position of the floor", + "name": "FloorZ", + "type": "expression" + }, + { + "description": "Length of the flat ground in front of the entrance (in pixels)", + "name": "Length", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Check if there is a hole in the ground at a position.", + "fullName": "Hole", + "functionType": "Condition", + "group": "Holes", + "name": "IsHole", + "sentence": "There is a hole in _PARAM0_ at _PARAM1_; _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "eventsFunctionContext.returnValue = object.__terrain3D.isHoleAt(x, y);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "The Z position of the ground at a position, or the given value where there is a hole.", + "fullName": "Ground Z position, with holes", + "functionType": "Expression", + "group": "Holes", + "name": "GroundZWithHoles", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const terrain = object.__terrain3D;", + "eventsFunctionContext.returnValue = terrain.isHoleAt(x, y) ? eventsFunctionContext.getArgument(\"ZInHoles\") : terrain.getHeightAt(x, y);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + }, + { + "description": "Value in holes", + "longDescription": "For example, a Z position far under the ground.", + "name": "ZInHoles", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Replace the ground with a generated relief. Painted layers are reset.", + "fullName": "Generate a relief", + "functionType": "Action", + "group": "Generation", + "name": "GenerateRelief", + "sentence": "Generate a relief of _PARAM1_ on _PARAM0_ with seed _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const relief = eventsFunctionContext.getArgument(\"Relief\");", + "const seed = eventsFunctionContext.getArgument(\"Seed\");", + "object.__terrain3D.generate(relief, seed);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "Relief", + "name": "Relief", + "supplementaryInformation": "[\"Flat\", \"Hills\", \"Mountains\", \"Island\"]", + "type": "stringWithSelector" + }, + { + "description": "Seed", + "longDescription": "Each seed gives a different terrain with the same relief.", + "name": "Seed", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Replace the ground with the heights of an image. Painted layers are kept.", + "fullName": "Load a heightmap", + "functionType": "Action", + "group": "Generation", + "name": "LoadHeightmap", + "sentence": "Load the heights of _PARAM0_ from _PARAM1_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const image = eventsFunctionContext.getArgument(\"Image\");", + "object.__terrain3D.loadHeightmap(image);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "Heightmap image", + "longDescription": "Black is the lowest, white the highest. For more precision, store the height on 16 bits in the red (most significant byte) and green channels.", + "name": "Image", + "type": "imageResource" + } + ], + "objectGroups": [] + }, + { + "description": "Move objects up or down so that their bottom is on the ground, under their center. Useful to place trees, rocks or characters on a terrain.", + "fullName": "Put objects on the ground", + "functionType": "Action", + "group": "Position", + "name": "PutOnGround", + "sentence": "Put _PARAM1_ on the ground of _PARAM0_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrain3D.placeOnGround(eventsFunctionContext.getObjects(\"Objects\"));" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "Objects to put on the ground", + "name": "Objects", + "type": "objectList" + } + ], + "objectGroups": [] + }, + { + "description": "the Z position of the ground at a position of the scene.", + "fullName": "Ground Z position", + "functionType": "ExpressionAndCondition", + "group": "Position", + "name": "GroundZ", + "sentence": "the ground Z position at _PARAM1_; _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "eventsFunctionContext.returnValue = object.__terrain3D.getHeightAt(x, y);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "the slope of the ground (angle with the horizontal, in degrees) at a position of the scene.", + "fullName": "Ground slope", + "functionType": "ExpressionAndCondition", + "group": "Position", + "name": "GroundSlope", + "sentence": "the ground slope at _PARAM1_; _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "eventsFunctionContext.returnValue = object.__terrain3D.getSlopeAt(x, y);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "the layer (from 1 to 4) painted the most at a position of the scene. Useful to play footstep sounds or to slow down characters in water or mud.", + "fullName": "Ground layer", + "functionType": "ExpressionAndCondition", + "group": "Paint", + "name": "GroundLayer", + "sentence": "the ground layer at _PARAM1_; _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "eventsFunctionContext.returnValue = object.__terrain3D.getLayerAt(x, y);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + } + ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + }, + { + "folderName": "Sculpt", + "children": [ + { + "functionName": "RaiseGround" + }, + { + "functionName": "RaiseGroundAlongLine" + }, + { + "functionName": "FlattenGround" + }, + { + "functionName": "SmoothGround" + } + ] + }, + { + "folderName": "Paint", + "children": [ + { + "functionName": "PaintLayer" + }, + { + "functionName": "PaintLayerAlongLine" + }, + { + "functionName": "TintGround" + }, + { + "functionName": "WetGround" + }, + { + "functionName": "GroundWetness" + }, + { + "functionName": "GroundTint" + }, + { + "functionName": "GroundLayer" + } + ] + }, + { + "folderName": "Holes", + "children": [ + { + "functionName": "MakeHole" + }, + { + "functionName": "FillHoles" + }, + { + "functionName": "CarveEntrance" + }, + { + "functionName": "IsHole" + }, + { + "functionName": "GroundZWithHoles" + } + ] + }, + { + "folderName": "Generation", + "children": [ + { + "functionName": "GenerateRelief" + }, + { + "functionName": "LoadHeightmap" + } + ] + }, + { + "folderName": "Position", + "children": [ + { + "functionName": "PutOnGround" + }, + { + "functionName": "GroundZ" + }, + { + "functionName": "GroundSlope" + } + ] + } + ] + }, + "propertyDescriptors": [ + { + "value": "Hills", + "type": "Choice", + "label": "Relief", + "description": "The initial shape, before sculpting.", + "group": "Shape", + "choices": [ + { + "label": "Flat", + "value": "Flat" + }, + { + "label": "Hills", + "value": "Hills" + }, + { + "label": "Mountains", + "value": "Mountains" + }, + { + "label": "Island", + "value": "Island" + } + ], + "name": "Relief" + }, + { + "value": "1", + "type": "Number", + "label": "Seed", + "description": "Each seed gives a different terrain with the same relief.", + "group": "Shape", + "name": "Seed" + }, + { + "value": "", + "type": "Resource", + "label": "Heightmap image", + "description": "Optional. Replaces the relief: black is the lowest, white the highest (the object depth).", + "group": "Shape", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "HeightmapImage" + }, + { + "value": "", + "type": "MultilineString", + "label": "Edits", + "description": "Changes of the relief or heightmap, applied in order, as a JSON list, for example [{\"tool\": \"raise\", \"x\": 0.3, \"y\": 0.4, \"radius\": 0.15, \"height\": 0.4}, {\"tool\": \"paint\", \"x\": 0.1, \"y\": 0.9, \"toX\": 0.8, \"toY\": 0.2, \"radius\": 0.02, \"layer\": 2}]. Tools: raise (height added to the ground, negative to lower), flatten (z: the ground is brought to this height, fully at the center, cutting what is above it and filling what is below; with toX and toY, toZ gives the height at the end of the line, for a road going up), smooth (strength), paint (layer, from 1 to 4, and strength), tint (color, like \"120;140;60\", multiplying the layers, white removing the tint, and strength), wetness (amount, from 0 for dry to 1 for wet, and strength), hole (no ground in the radius, for the entrances of tunnels and caves) and fill (the holes in the radius are filled with the layers of the ground around them). x, y and radius are fractions (0 to 1) of the terrain size (x is (scene X - terrain X) / terrain width). height and z are fractions of its depth: the ground at z is at Z = terrain Z + z * terrain depth. Add toX and toY to change the ground along a line (paths, rivers, ridges). Shown in the scene editor.", + "group": "Shape", + "advanced": true, + "name": "Edits" + }, + { + "value": "256", + "type": "Choice", + "label": "Resolution", + "description": "The number of cells on each side of the terrain. Higher is more detailed but slower.", + "group": "Shape", + "choices": [ + { + "label": "64 x 64", + "value": "64" + }, + { + "label": "128 x 128", + "value": "128" + }, + { + "label": "256 x 256", + "value": "256" + }, + { + "label": "512 x 512", + "value": "512" + }, + { + "label": "1024 x 1024", + "value": "1024" + } + ], + "name": "Resolution" + }, + { + "value": "96;160;64", + "type": "Color", + "label": "Layer 1 color (grass)", + "group": "Layers", + "name": "Layer1Color" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 1 texture", + "description": "Optional. Tinted by the layer color.", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer1Texture" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 1 normal map", + "description": "Optional. The bumps of the texture, lit by the lights (an OpenGL normal map, like the textures of ambientCG).", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer1NormalMap" + }, + { + "value": "150;112;72", + "type": "Color", + "label": "Layer 2 color (dirt)", + "group": "Layers", + "name": "Layer2Color" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 2 texture", + "description": "Optional. Tinted by the layer color.", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer2Texture" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 2 normal map", + "description": "Optional. The bumps of the texture, lit by the lights (an OpenGL normal map, like the textures of ambientCG).", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer2NormalMap" + }, + { + "value": "124;118;112", + "type": "Color", + "label": "Layer 3 color (rock)", + "group": "Layers", + "name": "Layer3Color" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 3 texture", + "description": "Optional. Tinted by the layer color.", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer3Texture" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 3 normal map", + "description": "Optional. The bumps of the texture, lit by the lights (an OpenGL normal map, like the textures of ambientCG).", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer3NormalMap" + }, + { + "value": "224;204;148", + "type": "Color", + "label": "Layer 4 color (sand)", + "group": "Layers", + "name": "Layer4Color" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 4 texture", + "description": "Optional. Tinted by the layer color.", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer4Texture" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 4 normal map", + "description": "Optional. The bumps of the texture, lit by the lights (an OpenGL normal map, like the textures of ambientCG).", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer4NormalMap" + }, + { + "value": "1", + "type": "Number", + "label": "Normal map strength", + "description": "How much the normal maps bump the ground: 0 for none, 1 as they are, more to exaggerate them.", + "group": "Layers", + "name": "NormalMapStrength" + }, + { + "value": "256", + "type": "Number", + "unit": "Pixel", + "label": "Texture size", + "description": "The size, in the scene, of one repetition of the layer textures.", + "group": "Layers", + "name": "TextureSize" + }, + { + "value": "0.5", + "type": "Number", + "label": "Height blending", + "description": "Where layers meet, the bright parts of textures (stones, tufts) cover the dark ones (gaps, sand) instead of fading into each other: 0 for none, 1 for the most.", + "group": "Layers", + "name": "HeightBlending" + }, + { + "value": "0.5", + "type": "Number", + "label": "Texture variation", + "description": "Makes the repetition of textures less visible, with patches of bigger and rotated textures and colors slowly varying: 0 for none, 1 for the most.", + "group": "Layers", + "name": "TextureVariation" + }, + { + "value": "false", + "type": "Boolean", + "label": "Wet shore", + "description": "Make the ground wet (darker and shiny) under a Z position, like the sand of a beach near the water.", + "group": "Wetness", + "name": "WetShore" + }, + { + "value": "0", + "type": "Number", + "unit": "Pixel", + "label": "Wet below Z", + "description": "The ground is wet under this Z position (for example, the Z of a 3D water object).", + "group": "Wetness", + "name": "WetShoreZ" + }, + { + "value": "60", + "type": "Number", + "unit": "Pixel", + "label": "Wet shore fade", + "description": "The ground becomes dry over this height above \"Wet below Z\".", + "group": "Wetness", + "name": "WetShoreFade" + }, + { + "value": "3", + "type": "Choice", + "label": "Layer on steep slopes", + "description": "This layer is shown where the ground is steep, whatever is painted.", + "group": "Layers", + "choices": [ + { + "label": "None", + "value": "0" + }, + { + "label": "Layer 1", + "value": "1" + }, + { + "label": "Layer 2", + "value": "2" + }, + { + "label": "Layer 3", + "value": "3" + }, + { + "label": "Layer 4", + "value": "4" + } + ], + "name": "CliffLayer" + }, + { + "value": "45", + "type": "Number", + "unit": "DegreeAngle", + "label": "Steep slope angle", + "group": "Layers", + "name": "CliffAngle" + }, + { + "value": "true", + "type": "Boolean", + "label": "Cast shadows", + "group": "Rendering", + "name": "CastShadow" + }, + { + "value": "true", + "type": "Boolean", + "label": "Receive shadows", + "group": "Rendering", + "name": "ReceiveShadow" + }, + { + "value": "", + "type": "String", + "label": "Sculpted and painted data", + "description": "Written by the sculpt and paint tools of the scene editor, in a compact format not meant to be read or written by hand: use Edits instead. Clear it to go back to the relief, heightmap and edits.", + "advanced": true, + "name": "SculptData" + } + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Shape", + "children": [ + { + "propertyName": "Relief" + }, + { + "propertyName": "Seed" + }, + { + "propertyName": "HeightmapImage" + }, + { + "propertyName": "Edits" + }, + { + "propertyName": "Resolution" + } + ] + }, + { + "folderName": "Layers", + "children": [ + { + "propertyName": "Layer1Color" + }, + { + "propertyName": "Layer1Texture" + }, + { + "propertyName": "Layer1NormalMap" + }, + { + "propertyName": "Layer2Color" + }, + { + "propertyName": "Layer2Texture" + }, + { + "propertyName": "Layer2NormalMap" + }, + { + "propertyName": "Layer3Color" + }, + { + "propertyName": "Layer3Texture" + }, + { + "propertyName": "Layer3NormalMap" + }, + { + "propertyName": "Layer4Color" + }, + { + "propertyName": "Layer4Texture" + }, + { + "propertyName": "Layer4NormalMap" + }, + { + "propertyName": "NormalMapStrength" + }, + { + "propertyName": "TextureSize" + }, + { + "propertyName": "HeightBlending" + }, + { + "propertyName": "TextureVariation" + }, + { + "propertyName": "CliffLayer" + }, + { + "propertyName": "CliffAngle" + } + ] + }, + { + "folderName": "Wetness", + "children": [ + { + "propertyName": "WetShore" + }, + { + "propertyName": "WetShoreZ" + }, + { + "propertyName": "WetShoreFade" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "CastShadow" + }, + { + "propertyName": "ReceiveShadow" + } + ] + }, + { + "propertyName": "SculptData" + } + ] + }, + "variants": [] + }, + { + "areaMaxX": 1000, + "areaMaxY": 1000, + "areaMaxZ": 40, + "areaMinX": 0, + "areaMinY": 0, + "areaMinZ": 0, + "defaultName": "Grass", + "description": "A field of grass blades swaying in the wind and pushed aside by characters. They grow in the area of the object, on the highest 3D terrain at each point (one field can cover several terrains), or at the Z position of the object where there is no terrain.", + "fullName": "Terrain grass", + "helpPath": "", + "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGQ9Ik0zIDIxYzEtNSAxLTkgMC0xMyAyIDMgMyA3IDMgMTN6bTQgMGMwLTYgMS0xMSA0LTE2LTEgNS0xIDEwIDAgMTZ6bTUgMGMwLTUgMi05IDUtMTItMiA0LTIgOC0xIDEyem01IDBjMS00IDItNiA0LTgtMSAzLTEgNS0xIDh6Ii8+PC9zdmc+", + "is3D": true, + "isUsingLegacyInstancesRenderer": true, + "name": "TerrainGrass", + "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/732ef90b7fcf5dd9171fe95d0cf262e09159b487304915a4693baa893d9ce16c_grass.svg", + "objects": [ + { + "assetStoreId": "", + "name": "AreaPlaceholder", + "type": "Scene3D::Cube3DObject", + "variables": [], + "effects": [], + "behaviors": [], + "content": { + "width": 1000, + "height": 1000, + "depth": 40, + "enableTextureTransparency": false, + "facesOrientation": "Y", + "backFaceUpThroughWhichAxisRotation": "X", + "materialType": "Basic", + "tint": "255;255;255", + "frontFaceResourceName": "", + "frontFaceVisible": false, + "frontFaceResourceRepeat": false, + "backFaceResourceName": "", + "backFaceVisible": false, + "backFaceResourceRepeat": false, + "leftFaceResourceName": "", + "leftFaceVisible": false, + "leftFaceResourceRepeat": false, + "rightFaceResourceName": "", + "rightFaceVisible": false, + "rightFaceResourceRepeat": false, + "topFaceResourceName": "", + "topFaceVisible": false, + "topFaceResourceRepeat": false, + "bottomFaceResourceName": "", + "bottomFaceVisible": false, + "bottomFaceResourceRepeat": false + } + } + ], + "objectsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "objectName": "AreaPlaceholder" + } + ] + }, + "objectsGroups": [], + "layers": [ + { + "ambientLightColorB": 200, + "ambientLightColorG": 200, + "ambientLightColorR": 200, + "camera2DPlaneMaxDrawingDistance": 5000, + "camera3DFarPlaneDistance": 10000, + "camera3DFieldOfView": 45, + "camera3DNearPlaneDistance": 3, + "cameraType": "", + "followBaseLayerCamera": false, + "isLightingLayer": false, + "isLocked": false, + "name": "", + "renderingType": "", + "visibility": true, + "cameras": [ + { + "defaultSize": true, + "defaultViewport": true, + "height": 0, + "viewportBottom": 1, + "viewportLeft": 0, + "viewportRight": 1, + "viewportTop": 0, + "width": 0 + } + ], + "effects": [] + } + ], + "instances": [ + { + "angle": 0, + "customSize": true, + "depth": 40, + "height": 1000, + "keepRatio": true, + "layer": "", + "name": "AreaPlaceholder", + "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-001000001000", + "width": 1000, + "x": 0, + "y": 0, + "zOrder": 1, + "numberProperties": [], + "stringProperties": [], + "initialVariables": [] + } + ], + "editionSettings": {}, + "eventsFunctions": [ + { + "fullName": "", + "functionType": "Action", + "name": "onCreated", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Terrain3D::DefineHelperClasses" + }, + "parameters": [ + "", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Terrain3D::DefineGrassHelperClasses" + }, + "parameters": [ + "", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "for (const child of object.getChildrenContainer().getAdhocListOfAllInstances()) {", + " child.deleteFromScene();", + "}", + "if (object.__terrainGrass) object.__terrainGrass.dispose();", + "object.__terrainGrass = new gdjs.__terrainGrassExtension.Grass(object);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "onDestroy", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrainGrass.dispose();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "doStepPostEvents", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const game = runtimeScene.getGame();", + "// Time doesn't pass in the scene editor: real time is used to show the wind.", + "const isInGameEdition = game.isInGameEdition && game.isInGameEdition();", + "const elapsedTime = isInGameEdition ? 1 / 60 : object.getElapsedTime() / 1000;", + "const time = isInGameEdition", + " ? performance.now() / 1000", + " : object.getRuntimeScene().getTimeManager().getTimeFromStart() / 1000;", + "const layer = object.getInstanceContainer().getLayer(object.getLayer());", + "object.__terrainGrass.update(elapsedTime, time, layer.getRenderer().getThreeCamera());" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "onHotReloading", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrainGrass.updateFromProperties();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" + } + ], + "objectGroups": [] + } + ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + } + ] + }, + "propertyDescriptors": [ + { + "value": "60", + "type": "Number", + "label": "Density", + "description": "The number of blades in a square of 100 x 100 pixels.", + "group": "Blades", + "name": "Density" + }, + { + "value": "36", + "type": "Number", + "unit": "Pixel", + "label": "Blade height", + "description": "The average height of blades (each blade is between 60% and 140% of it).", + "group": "Blades", + "name": "BladeHeight" + }, + { + "value": "5", + "type": "Number", + "unit": "Pixel", + "label": "Blade width", + "group": "Blades", + "name": "BladeWidth" + }, + { + "value": "52;104;36", + "type": "Color", + "label": "Base color", + "group": "Blades", + "name": "BaseColor" + }, + { + "value": "150;196;82", + "type": "Color", + "label": "Tip color", + "group": "Blades", + "name": "TipColor" + }, + { + "value": "1", + "type": "Number", + "label": "Seed", + "description": "Each seed places blades differently.", + "group": "Blades", + "name": "Seed" + }, + { + "value": "1", + "type": "Choice", + "label": "Grows on terrain layer", + "description": "On a 3D terrain, blades only grow where this layer is painted. A new terrain has layer 1 painted everywhere.", + "group": "Ground", + "choices": [ + { + "label": "Any", + "value": "0" + }, + { + "label": "Layer 1", + "value": "1" + }, + { + "label": "Layer 2", + "value": "2" + }, + { + "label": "Layer 3", + "value": "3" + }, + { + "label": "Layer 4", + "value": "4" + } + ], + "name": "GroundLayer" + }, + { + "value": "40", + "type": "Number", + "unit": "DegreeAngle", + "label": "Maximum slope", + "description": "No blades grow on steeper ground.", + "group": "Ground", + "name": "MaxSlope" + }, + { + "value": "1", + "type": "Number", + "label": "Wind strength", + "description": "0 for no wind.", + "group": "Wind", + "name": "WindStrength" + }, + { + "value": "2", + "type": "Number", + "label": "Wind speed", + "group": "Wind", + "name": "WindSpeed" + }, + { + "value": "", + "type": "String", + "label": "Object bending the grass", + "description": "The name of an object pushing blades aside when walking through the grass, like the player. The 8 first instances are used.", + "group": "Wind", + "name": "BendingObject" + }, + { + "value": "2500", + "type": "Number", + "unit": "Pixel", + "label": "Fade distance", + "description": "Blades are less and less dense up to this distance from the camera, and hidden after.", + "group": "Rendering", + "name": "FadeDistance" + } + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Blades", + "children": [ + { + "propertyName": "Density" + }, + { + "propertyName": "BladeHeight" + }, + { + "propertyName": "BladeWidth" + }, + { + "propertyName": "BaseColor" + }, + { + "propertyName": "TipColor" + }, + { + "propertyName": "Seed" + } + ] + }, + { + "folderName": "Ground", + "children": [ + { + "propertyName": "GroundLayer" + }, + { + "propertyName": "MaxSlope" + } + ] + }, + { + "folderName": "Wind", + "children": [ + { + "propertyName": "WindStrength" + }, + { + "propertyName": "WindSpeed" + }, + { + "propertyName": "BendingObject" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "FadeDistance" + } + ] + } + ] + }, + "variants": [] + }, + { + "areaMaxX": 2000, + "areaMaxY": 2000, + "areaMaxZ": 200, + "areaMinX": 0, + "areaMinY": 0, + "areaMinZ": 0, + "assetStoreTag": "3d scattered models", + "defaultName": "ScatteredModels", + "description": "Many copies of a 3D model (trees, rocks, bushes, flowers...) placed in the area of the object and drawn all at once, which is much faster than an object for each one. They stand on the highest 3D terrain at each point, or at the Z position of the object where there is no terrain. Paint where they grow with the brushes of the scene editor.", + "fullName": "Scattered models (forests, rocks...)", + "helpPath": "", + "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGQ9Ik0xMCAyMXYtM0gzbDUtNUg1bDUtNUg3bDUtNSA1IDVoLTNsNSA1aC0zbDUgNWgtN3YzeiIvPjwvc3ZnPg==", + "is3D": true, + "isUsingLegacyInstancesRenderer": true, + "name": "ScatteredModels", + "previewIconUrl": "", + "objects": [ + { + "assetStoreId": "", + "name": "AreaPlaceholder", + "type": "Scene3D::Cube3DObject", + "variables": [], + "effects": [], + "behaviors": [], + "content": { + "width": 2000, + "height": 2000, + "depth": 200, + "enableTextureTransparency": false, + "facesOrientation": "Y", + "backFaceUpThroughWhichAxisRotation": "X", + "materialType": "Basic", + "tint": "255;255;255", + "frontFaceResourceName": "", + "frontFaceVisible": false, + "frontFaceResourceRepeat": false, + "backFaceResourceName": "", + "backFaceVisible": false, + "backFaceResourceRepeat": false, + "leftFaceResourceName": "", + "leftFaceVisible": false, + "leftFaceResourceRepeat": false, + "rightFaceResourceName": "", + "rightFaceVisible": false, + "rightFaceResourceRepeat": false, + "topFaceResourceName": "", + "topFaceVisible": false, + "topFaceResourceRepeat": false, + "bottomFaceResourceName": "", + "bottomFaceVisible": false, + "bottomFaceResourceRepeat": false + } + } + ], + "objectsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "objectName": "AreaPlaceholder" + } + ] + }, + "objectsGroups": [], + "layers": [ + { + "ambientLightColorB": 200, + "ambientLightColorG": 200, + "ambientLightColorR": 200, + "camera2DPlaneMaxDrawingDistance": 5000, + "camera3DFarPlaneDistance": 10000, + "camera3DFieldOfView": 45, + "camera3DNearPlaneDistance": 3, + "cameraType": "", + "followBaseLayerCamera": false, + "isLightingLayer": false, + "isLocked": false, + "name": "", + "renderingType": "", + "visibility": true, + "cameras": [ + { + "defaultSize": true, + "defaultViewport": true, + "height": 0, + "viewportBottom": 1, + "viewportLeft": 0, + "viewportRight": 1, + "viewportTop": 0, + "width": 0 + } + ], + "effects": [] + } + ], + "instances": [ + { + "angle": 0, + "customSize": true, + "depth": 200, + "height": 2000, + "keepRatio": true, + "layer": "", + "name": "AreaPlaceholder", + "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-002000002000", + "width": 2000, + "x": 0, + "y": 0, + "zOrder": 1, + "numberProperties": [], + "stringProperties": [], + "initialVariables": [] + } + ], + "editionSettings": {}, + "eventsFunctions": [ + { + "fullName": "", + "functionType": "Action", + "name": "onCreated", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Terrain3D::DefineHelperClasses" + }, + "parameters": [ + "", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Terrain3D::DefineScatterHelperClasses" + }, + "parameters": [ + "", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "for (const child of object.getChildrenContainer().getAdhocListOfAllInstances()) {", + " child.deleteFromScene();", + "}", + "if (object.__terrainScatter) object.__terrainScatter.dispose();", + "object.__terrainScatter = new gdjs.__terrainScatterExtension.Scatter(object);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "onDestroy", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrainScatter.dispose();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "doStepPostEvents", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const game = runtimeScene.getGame();", + "const isInGameEdition = game.isInGameEdition && game.isInGameEdition();", + "const elapsedTime = isInGameEdition ? 1 / 60 : object.getElapsedTime() / 1000;", + "const layer = object.getInstanceContainer().getLayer(object.getLayer());", + "object.__terrainScatter.update(elapsedTime, layer.getRenderer().getThreeCamera());" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "onHotReloading", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrainScatter.onHotReloading();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "description": "Paint the density of copies around a position, up to the density of the object (copies removed with \"Erase copies\" come back). The effect fades out up to the radius.", + "fullName": "Add copies", + "functionType": "Action", + "group": "Paint", + "name": "AddCopies", + "sentence": "Add copies of _PARAM0_ around _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "object.__terrainScatter.addCopies(x, y, radius);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "name": "Radius", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Remove the copies standing in a circle, for example a tree cut by the player (use the position of the nearest copy and a small radius) or a forest cleared for a building. They come back when the scene is restarted.", + "fullName": "Erase copies", + "functionType": "Action", + "group": "Paint", + "name": "EraseCopies", + "sentence": "Erase the copies of _PARAM0_ in a circle at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "object.__terrainScatter.removeCopies(x, y, radius);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "name": "Radius", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "the number of copies placed in the area.", + "fullName": "Number of copies", + "functionType": "ExpressionAndCondition", + "group": "Copies", + "name": "CopyCount", + "sentence": "the number of copies", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "eventsFunctionContext.returnValue = object.__terrainScatter.getCopyCount();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "description": "The X position of the base of the copy nearest to a position (the position itself if there is no copy).", + "fullName": "X position of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyX", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? copy.x : x;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "The Y position of the base of the copy nearest to a position (the position itself if there is no copy).", + "fullName": "Y position of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyY", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? copy.y : y;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "The Z position of the base of the copy nearest to a position (on the ground).", + "fullName": "Z position of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyZ", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? copy.z : object.getZ();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "the distance (on X and Y) from a position to the base of the nearest copy. It's a very big number when there is no copy.", + "fullName": "Distance to the nearest copy", + "functionType": "ExpressionAndCondition", + "group": "Copies", + "name": "NearestCopyDistance", + "sentence": "the distance from _PARAM1_; _PARAM2_ to the nearest copy", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? Math.hypot(copy.x - x, copy.y - y) : Number.MAX_VALUE;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + } + ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + }, + { + "folderName": "Paint", + "children": [ + { + "functionName": "AddCopies" + }, + { + "functionName": "EraseCopies" + } + ] + }, + { + "folderName": "Copies", + "children": [ + { + "functionName": "CopyCount" + }, + { + "functionName": "NearestCopyX" + }, + { + "functionName": "NearestCopyY" + }, + { + "functionName": "NearestCopyZ" + }, + { + "functionName": "NearestCopyDistance" + } + ] + } + ] + }, + "propertyDescriptors": [ + { + "value": "", + "type": "Resource", + "label": "3D model", + "description": "The model copied everywhere (a .glb file). Models standing on the Y axis, like most models of the asset store, are stood up along Z.", + "group": "Model", + "extraInformation": [ + "model3D" + ], + "choices": [], + "name": "Model" + }, + { + "value": "300", + "type": "Number", + "unit": "Pixel", + "label": "Height of a copy", + "description": "Copies keep the proportions of the model.", + "group": "Model", + "name": "Size" + }, + { + "value": "0.8", + "type": "Number", + "label": "Smallest random scale", + "description": "Each copy is scaled at random between the smallest and the largest scale.", + "group": "Model", + "name": "MinScale" + }, + { + "value": "1.2", + "type": "Number", + "label": "Largest random scale", + "group": "Model", + "name": "MaxScale" + }, + { + "value": "true", + "type": "Boolean", + "label": "Random rotation", + "description": "Turn each copy around the Z axis at random.", + "group": "Model", + "name": "RandomRotation" + }, + { + "value": "255;255;255", + "type": "Color", + "label": "Random tint", + "description": "Each copy is tinted between white and this color, at random. White for no tint.", + "group": "Model", + "name": "RandomTint" + }, + { + "value": "0.3", + "type": "Number", + "label": "Density", + "description": "The number of copies in a square of 100 x 100 pixels where the density is fully painted (0.3 is about one copy every 180 pixels). Copies never overlap their neighbors much: they are placed at random in a grid.", + "group": "Placement", + "name": "Density" + }, + { + "value": "1", + "type": "Number", + "label": "Seed", + "description": "Each seed places copies differently.", + "group": "Placement", + "name": "Seed" + }, + { + "value": "Everywhere", + "type": "Choice", + "label": "Copies grow", + "description": "Everywhere in the area, or only where they are painted with the \"Add copies\" brush (or the edits).", + "group": "Placement", + "choices": [ + { + "label": "Everywhere (erase them with the brush)", + "value": "Everywhere" + }, + { + "label": "Only where painted", + "value": "Painted" + } + ], + "name": "Coverage" + }, + { + "value": "1", + "type": "Choice", + "label": "Grows on terrain layer", + "description": "On a 3D terrain, copies only grow where this layer is painted, away from its borders (so that trees don't stand on roads painted with another layer). A new terrain has layer 1 painted everywhere.", + "group": "Ground", + "choices": [ + { + "label": "Any", + "value": "0" + }, + { + "label": "Layer 1", + "value": "1" + }, + { + "label": "Layer 2", + "value": "2" + }, + { + "label": "Layer 3", + "value": "3" + }, + { + "label": "Layer 4", + "value": "4" + } + ], + "name": "GroundLayer" + }, + { + "value": "30", + "type": "Number", + "unit": "DegreeAngle", + "label": "Maximum slope", + "description": "No copies grow on steeper ground.", + "group": "Ground", + "name": "MaxSlope" + }, + { + "value": "-100000", + "type": "Number", + "unit": "Pixel", + "label": "Minimum ground Z", + "description": "No copies grow where the ground is lower (for example, under the water level).", + "group": "Ground", + "name": "MinGroundZ" + }, + { + "value": "100000", + "type": "Number", + "unit": "Pixel", + "label": "Maximum ground Z", + "description": "No copies grow where the ground is higher (for example, above the tree line).", + "group": "Ground", + "name": "MaxGroundZ" + }, + { + "value": "false", + "type": "Boolean", + "label": "Trunk collisions", + "description": "Give each copy a static collision shape around its trunk, for the 3D physics behavior (add it to this object, with the \"Static\" type) and navigation meshes. Without them, these behaviors have no effect on this object.", + "group": "Collisions", + "name": "TrunkCollisions" + }, + { + "value": "30", + "type": "Number", + "unit": "Pixel", + "label": "Trunk diameter", + "description": "The diameter of the collision shape of a copy at the height given in \"Height of a copy\" (it is scaled with the copy).", + "group": "Collisions", + "name": "TrunkDiameter" + }, + { + "value": "8000", + "type": "Number", + "unit": "Pixel", + "label": "Draw distance", + "description": "Copies shrink and are hidden at this distance from the camera.", + "group": "Rendering", + "name": "DrawDistance" + }, + { + "value": "true", + "type": "Boolean", + "label": "Cast shadows", + "group": "Rendering", + "name": "CastShadow" + }, + { + "value": "true", + "type": "Boolean", + "label": "Receive shadows", + "group": "Rendering", + "name": "ReceiveShadow" + }, + { + "value": "", + "type": "MultilineString", + "label": "Edits", + "description": "Where copies are added or erased, applied in order, as a JSON list, for example [{\"tool\": \"add\", \"x\": 0.3, \"y\": 0.4, \"radius\": 0.15}, {\"tool\": \"erase\", \"x\": 0.1, \"y\": 0.9, \"toX\": 0.8, \"toY\": 0.2, \"radius\": 0.02}]. Tools: add (density: from 0 to 1, 1 by default, how dense copies are compared to the Density property) and erase. strength (from 0 to 1) makes a stroke lighter. x, y and radius are fractions (0 to 1) of the object size (x is (scene X - object X) / object width). Add toX and toY to paint along a line (a row of trees, a path through a forest). With \"Copies grow: only where painted\", only the added areas have copies. Shown in the scene editor.", + "group": "Placement", + "advanced": true, + "name": "Edits" + }, + { + "value": "", + "type": "String", + "label": "Painted data", + "description": "Written by the brushes of the scene editor, in a compact format not meant to be read or written by hand: use Edits instead. Clear it to go back to the coverage and the edits.", + "advanced": true, + "name": "PaintData" + } + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Model", + "children": [ + { + "propertyName": "Model" + }, + { + "propertyName": "Size" + }, + { + "propertyName": "MinScale" + }, + { + "propertyName": "MaxScale" + }, + { + "propertyName": "RandomRotation" + }, + { + "propertyName": "RandomTint" + } + ] + }, + { + "folderName": "Placement", + "children": [ + { + "propertyName": "Density" + }, + { + "propertyName": "Seed" + }, + { + "propertyName": "Coverage" + }, + { + "propertyName": "Edits" + } + ] + }, + { + "folderName": "Ground", + "children": [ + { + "propertyName": "GroundLayer" + }, + { + "propertyName": "MaxSlope" + }, + { + "propertyName": "MinGroundZ" + }, + { + "propertyName": "MaxGroundZ" + } + ] + }, + { + "folderName": "Collisions", + "children": [ + { + "propertyName": "TrunkCollisions" + }, + { + "propertyName": "TrunkDiameter" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "DrawDistance" + }, + { + "propertyName": "CastShadow" + }, + { + "propertyName": "ReceiveShadow" + } + ] + }, + { + "propertyName": "PaintData" + } + ] + }, + "variants": [] + } + ] } ], "externalLayouts": [] diff --git a/examples/starting-3d-camera-monitoring/assets/StartingWall.png b/examples/starting-3d-camera-monitoring/assets/StartingWall.png index b3a097887..06278d6e5 100644 Binary files a/examples/starting-3d-camera-monitoring/assets/StartingWall.png and b/examples/starting-3d-camera-monitoring/assets/StartingWall.png differ diff --git a/examples/starting-3d-car-racing/assets/StartingWall.png b/examples/starting-3d-car-racing/assets/StartingWall.png index b3a097887..06278d6e5 100644 Binary files a/examples/starting-3d-car-racing/assets/StartingWall.png and b/examples/starting-3d-car-racing/assets/StartingWall.png differ diff --git a/examples/starting-3d-car-racing/starting-3d-car-racing.json b/examples/starting-3d-car-racing/starting-3d-car-racing.json index 46e3cff8a..57e15dc05 100644 --- a/examples/starting-3d-car-racing/starting-3d-car-racing.json +++ b/examples/starting-3d-car-racing/starting-3d-car-racing.json @@ -1,7 +1,7 @@ { "firstLayout": "", "gdVersion": { - "build": 283, + "build": 284, "major": 5, "minor": 6, "revision": 0 @@ -229,6 +229,14 @@ "name": "assets/Flat dark joystick border horizontal.png", "smoothed": true, "userAdded": false + }, + { + "file": "assets/StartingGround.png", + "kind": "image", + "metadata": "", + "name": "assets/StartingGround.png", + "smoothed": true, + "userAdded": false } ] }, @@ -245,12 +253,13 @@ "mangledName": "Game_32Scene", "name": "Game Scene", "r": 255, + "renderer3DWorldScale": 100, "standardSortMethod": true, "stopSoundsOnStartup": true, "title": "", "v": 255, "uiSettings": { - "grid": false, + "grid": true, "gridType": "rectangular", "gridWidth": 64, "gridHeight": 64, @@ -260,7 +269,7 @@ "gridOffsetZ": 0, "gridColor": 10401023, "gridAlpha": 0.8, - "snap": false, + "snap": true, "zoomFactor": 0.49508400737491426, "windowMask": false, "selectedLayer": "", @@ -305,24 +314,6 @@ "stringProperties": [], "initialVariables": [] }, - { - "angle": 0, - "customSize": true, - "depth": 10, - "height": 256, - "layer": "", - "locked": true, - "name": "Grass_3D", - "persistentUuid": "0ded717a-a950-4abf-8393-0a554a43b2f2", - "width": 4608, - "x": -1664, - "y": 2880, - "z": -10, - "zOrder": 7, - "numberProperties": [], - "stringProperties": [], - "initialVariables": [] - }, { "angle": 0, "customSize": false, @@ -338,24 +329,6 @@ "stringProperties": [], "initialVariables": [] }, - { - "angle": 0, - "customSize": true, - "depth": 10, - "height": 1280, - "layer": "", - "locked": true, - "name": "Grass_3D", - "persistentUuid": "f9ea97fc-7ed3-421a-8559-e7f829e15f39", - "width": 256, - "x": 1920, - "y": 832, - "z": -10, - "zOrder": 7, - "numberProperties": [], - "stringProperties": [], - "initialVariables": [] - }, { "angle": 0, "customSize": false, @@ -440,60 +413,6 @@ "stringProperties": [], "initialVariables": [] }, - { - "angle": 0, - "customSize": true, - "depth": 10, - "height": 256, - "layer": "", - "locked": true, - "name": "Grass_3D", - "persistentUuid": "4228bb12-f49e-4d04-846f-2b11fdd409f4", - "width": 3072, - "x": -896, - "y": 2112, - "z": -10, - "zOrder": 7, - "numberProperties": [], - "stringProperties": [], - "initialVariables": [] - }, - { - "angle": 0, - "customSize": true, - "depth": 10, - "height": 2816, - "layer": "", - "locked": true, - "name": "Grass_3D", - "persistentUuid": "083123ad-65ee-4fe8-a533-7012fe3919b3", - "width": 256, - "x": 2688, - "y": 64, - "z": -10, - "zOrder": 7, - "numberProperties": [], - "stringProperties": [], - "initialVariables": [] - }, - { - "angle": 0, - "customSize": true, - "depth": 10, - "height": 256, - "layer": "", - "locked": true, - "name": "Grass_3D", - "persistentUuid": "957d4411-69db-4efc-b0d8-d85469aa992f", - "width": 3072, - "x": -896, - "y": 576, - "z": -10, - "zOrder": 7, - "numberProperties": [], - "stringProperties": [], - "initialVariables": [] - }, { "angle": 0, "customSize": true, @@ -548,60 +467,6 @@ "stringProperties": [], "initialVariables": [] }, - { - "angle": 0, - "customSize": true, - "depth": 10, - "height": 1280, - "layer": "", - "locked": true, - "name": "Grass_3D", - "persistentUuid": "2f4cd3b9-bb95-46f1-977c-fd57f24b1adf", - "width": 256, - "x": -896, - "y": 832, - "z": -10, - "zOrder": 7, - "numberProperties": [], - "stringProperties": [], - "initialVariables": [] - }, - { - "angle": 0, - "customSize": true, - "depth": 10, - "height": 2816, - "layer": "", - "locked": true, - "name": "Grass_3D", - "persistentUuid": "8981075f-191e-4f4f-9fc4-8aa4689a6c16", - "width": 256, - "x": -1664, - "y": 64, - "z": -10, - "zOrder": 7, - "numberProperties": [], - "stringProperties": [], - "initialVariables": [] - }, - { - "angle": 0, - "customSize": true, - "depth": 10, - "height": 256, - "layer": "", - "locked": true, - "name": "Grass_3D", - "persistentUuid": "6456938f-188e-4c70-846a-08648d40a731", - "width": 4608, - "x": -1664, - "y": -192, - "z": -10, - "zOrder": 7, - "numberProperties": [], - "stringProperties": [], - "initialVariables": [] - }, { "angle": 270, "customSize": false, @@ -910,6 +775,23 @@ "numberProperties": [], "stringProperties": [], "initialVariables": [] + }, + { + "angle": 0, + "customSize": true, + "height": 3328, + "layer": "", + "locked": true, + "name": "Terrain", + "persistentUuid": "bd85f7c9-b3ca-4f8e-ad57-acb797a0be2b", + "width": 4608, + "x": -1664, + "y": -192, + "z": -1, + "zOrder": 0, + "numberProperties": [], + "stringProperties": [], + "initialVariables": [] } ], "objects": [ @@ -982,75 +864,6 @@ "tileScale": 0.2 } }, - { - "assetStoreId": "", - "name": "Grass_3D", - "persistentUuid": "1500144b-19f3-4b69-b97a-b52f4e1a49ae", - "type": "Scene3D::Cube3DObject", - "variables": [], - "effects": [], - "behaviors": [ - { - "name": "Physics3D", - "type": "Physics3D::Physics3DBehavior", - "object3D": "Object3D", - "bodyType": "Static", - "bullet": false, - "fixedRotation": false, - "shape": "Box", - "shapeOrientation": "Z", - "shapeDimensionA": 0, - "shapeDimensionB": 0, - "shapeDimensionC": 0, - "density": 1, - "friction": 6, - "restitution": 0.1, - "linearDamping": 0.1, - "angularDamping": 0.1, - "gravityScale": 1, - "layers": 17, - "masks": 17, - "shapeOffsetX": 0, - "shapeOffsetY": 0, - "shapeOffsetZ": 0, - "massCenterOffsetX": 0, - "massCenterOffsetY": 0, - "massCenterOffsetZ": 0, - "massOverride": 0 - } - ], - "content": { - "width": 128, - "height": 128, - "depth": 128, - "enableTextureTransparency": false, - "facesOrientation": "Y", - "frontFaceResourceName": "assets\\StartingGround.png", - "backFaceResourceName": "assets\\StartingGround.png", - "backFaceUpThroughWhichAxisRotation": "X", - "leftFaceResourceName": "assets\\StartingGround.png", - "rightFaceResourceName": "assets\\StartingGround.png", - "topFaceResourceName": "assets\\StartingGround.png", - "bottomFaceResourceName": "assets\\StartingGround.png", - "frontFaceVisible": true, - "backFaceVisible": true, - "leftFaceVisible": true, - "rightFaceVisible": true, - "topFaceVisible": true, - "bottomFaceVisible": true, - "frontFaceResourceRepeat": true, - "backFaceResourceRepeat": true, - "leftFaceResourceRepeat": true, - "rightFaceResourceRepeat": true, - "topFaceResourceRepeat": true, - "bottomFaceResourceRepeat": true, - "materialType": "StandardWithoutMetalness", - "tint": "255;255;255", - "isCastingShadow": false, - "isReceivingShadow": true, - "tileScale": 0.2 - } - }, { "assetStoreId": "cddab55130dbd5a0b36f763200e876fb514aa608e1b057a136ebdb5f80006e2c", "name": "PlayerCar", @@ -1652,6 +1465,60 @@ "isReceivingShadow": true, "tileScale": 0.2 } + }, + { + "assetStoreId": "", + "name": "Terrain", + "persistentUuid": "a7f31fbc-dd5a-433a-9b33-2817645ac8d4", + "type": "Terrain3D::Terrain3D", + "variant": "", + "variables": [], + "effects": [], + "behaviors": [ + { + "name": "Physics3D", + "type": "Physics3D::Physics3DBehavior", + "object3D": "Object3D", + "bodyType": "Static", + "bullet": false, + "fixedRotation": false, + "shape": "Box", + "meshShapeResourceName": "", + "shapeOrientation": "Z", + "shapeDimensionA": 0, + "shapeDimensionB": 0, + "shapeDimensionC": 0, + "shapeOffsetX": 0, + "shapeOffsetY": 0, + "shapeOffsetZ": 0, + "massCenterOffsetX": 0, + "massCenterOffsetY": 0, + "massCenterOffsetZ": 0, + "massOverride": 0, + "density": 1, + "friction": 6, + "restitution": 0.1, + "linearDamping": 0.1, + "angularDamping": 0.1, + "gravityScale": 1, + "layers": 17, + "masks": 17 + } + ], + "content": { + "SculptData": "2;256;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Lz87Hw4kKCYaDBAkMDg0KBwQDAQFIz9PUUc3Kx8VDQQ2TkguC22rAbcBkQE7IwUNFxcXFRkZFxMLExUZEw0TGyEdEwABGiguKhYPKTMpCyJMYFxCLEhWUjYOBAQEAQsBFiAiGCGdAekB+QHFAVswjgGwAZYBQBY6UFhUKDlzgQFbEyQ6NiALUYsBmwGJAU8FBggEAgABNmBoThgXJysnFQgwRkQuCgkRBwgoKg4BCQcNPV1hVTMMOEg+HAYQDgoEAiY4NiYKK0lRPxEWHiAaDgkxR0s7Exg0PDIUBgoQGBwUDSMjHwFAbHheHi1VYVMrAAEBAAIIEi46NioQBxMbFQMUHBoWDgIQIB4MHzcnHRMHCzdNRysGIiYkGgwMJC4yLB4SCgIBAyE9R0c/Kx0RDQcIQFxaOAlxvQHLAacBRScBCw0RDw0PERMLBwsPEw8LCRcZFw0AARQiJB4QDyUrIw0aPEhINB40Qj4sCgQGBAIHAAEUHhwQGXu3AcMBmwFBMnyWAX40Eig6PjgYNWVtUQscMDAeAzlre209AwQGBgICLE5WRBYRHyMdEQYqNjQmBgsPEQIYGgQHCwULMUVLPyEQMkA2GAYKDAoAAQYeKiwiAiE/RTUPDhgWFgoFKTs9Lw8WLjQoEgIGDA4SDA8fIxcBMlxeTBYdQ01DIwIBAwEEDiQsLCAQBRMXEwUOFBQSCAISHCAQDSEZDwUDCS0/PSUEFh4YFgYMGCIiIBQKBAEDAxkrNTcpHRELAwMINkpILgFblwGnAYkBOyUCAwMFAwEFBQUDAQMHBQUDBQcHCQcCCgwQDAYHDxEPBQoYHhwUDBQaGhICAgIEAQACBgwMCAk1SU1BFxQ2PjYWBhIWFhgGFystIQcOFBYOARcpMSsbAAECAgICAhIiIhwKBwsNDQcEEBYWDgQFCQUBCAYCBQMDBREbHRcNCBgaGAoCBgQCBAABDBQSCgQRFx0XBwYGDAYGBQ8ZGRMFChIWEggAAQICBgQEBQ8PCwEWIigcCg0ZHxsNAQABAQABAgYOFBQOBAEFBwcDCAgICAQCBg4QCAMLBQMDAAEFERkZEQABCgoMBgQCCgwOCggCAQEBAQsRFRMRCwUBAQMEEhwcEgMnO0c7FwCAAgMAgAICAIEEAgDIlgEBAQQA/gEBAgUA/QEBBAMA/gECAwIBAP4BBAUEAP8BAgcA/wECAIECAgD/AQIDAP8BAQQAgQQCAP8BAgEAgAICAP8BAgEAgAICAIACAgCBBAEAgAICAIACAgCbuQIFAIACAQD/AQEAAQCAAgQAgAIBAIEEAQDIlgECAgMA/gECAQYA/QECAwQA/gEBBAECAP4BAwYDAP8BAQgA/wEBAIECAQD/AQEEAP8BAgMAgQQBAP8BAQIAgAIBAP8BAQIAgAIBAIACAQCBBAIAgAIBAIACAQCbuQIGAIACAgD/AQIAAQCBhAQAgYQEAIGEBACBhAQAgYQEAIGEBA==", + "Layer1Texture": "assets/StartingGround.png", + "Layer1Color": "255;255;255", + "HeightBlending": 0.5, + "TextureVariation": 0, + "TextureSize": 128, + "CliffLayer": "3", + "Layer2Texture": "", + "Layer3Texture": "", + "CliffAngle": 45, + "CastShadow": false, + "Layer2Color": "255;255;255" + } } ], "objectsFolderStructure": { @@ -1667,7 +1534,7 @@ "objectName": "FinishLine_3D" }, { - "objectName": "Grass_3D" + "objectName": "Terrain" }, { "objectName": "CheckpointArrow" @@ -2809,7 +2676,6 @@ "name": "ThirdPersonCamera", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -4753,7 +4619,6 @@ "unit": "Second", "label": "Halfway time for rotation", "group": "Catch-up speed", - "quickCustomizationVisibility": "hidden", "name": "RotationHalfwayDuration" }, { @@ -4762,7 +4627,6 @@ "unit": "Second", "label": "Halfway time for elevation rotation", "group": "Catch-up speed", - "quickCustomizationVisibility": "hidden", "name": "ElevationHalfwayDuration" }, { @@ -4771,7 +4635,6 @@ "unit": "Second", "label": "Halfway time on Z axis", "group": "Catch-up speed", - "quickCustomizationVisibility": "hidden", "name": "TranslationZHalfwayDuration" }, { @@ -4829,7 +4692,6 @@ "label": "Follow free area top border on Z axis", "group": "Position", "advanced": true, - "quickCustomizationVisibility": "hidden", "name": "FollowFreeAreaZMax" }, { @@ -4839,7 +4701,6 @@ "label": "Follow free area bottom border on Z axis", "group": "Position", "advanced": true, - "quickCustomizationVisibility": "hidden", "name": "FollowFreeAreaZMin" }, { @@ -4889,14 +4750,12 @@ "value": "true", "type": "Boolean", "label": "Automatically rotate the camera with the object", - "quickCustomizationVisibility": "hidden", "name": "IsRotatingWithObject" }, { "value": "false", "type": "Boolean", "label": "Automatically rotate the camera with the object (elevation)", - "quickCustomizationVisibility": "hidden", "name": "IsElevatingWithObject" }, { @@ -8977,7 +8836,6 @@ "name": "PlatformerMultitouchMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -9231,7 +9089,6 @@ "name": "Platformer3DMultitouchMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -9406,7 +9263,6 @@ "name": "Shooter3DMultitouchMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -9570,7 +9426,6 @@ "name": "FirstPersonMultitouchMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -10775,7 +10630,6 @@ "name": "PhysicsCar3DMultitouchMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -10988,7 +10842,6 @@ "name": "TopDownMultitouchMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -13067,7 +12920,6 @@ "name": "PhysicsCar3DKeyboardMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -13293,6 +13145,7802 @@ } ], "eventsBasedObjects": [] + }, + { + "author": "", + "category": "General", + "dimension": "3D", + "extensionNamespace": "", + "fullName": "3D terrain", + "gdevelopVersion": ">=5.5.222", + "helpPath": "", + "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLXRlcnJhaW4iIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTQsNkwxMC4yNSwxMUwxMy4xLDE0LjhMMTEuNSwxNkM5LjgxLDEzLjc1IDcsMTAgNywxMEwxLDE4SDIzTDE0LDZaIiAvPjwvc3ZnPg==", + "name": "Terrain3D", + "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", + "shortDescription": "A ground with hills and valleys that can be sculpted and painted in the editor, with collisions.", + "version": "0.1.0", + "description": [ + "A 3D terrain object for open worlds, adventure or racing games: hills, mountains, islands or valleys, with painted layers of grass, dirt, rock or sand.", + "", + "- **Sculpt and paint in the scene editor**: select a terrain to show the tools (raise, lower, smooth, flatten and paint 4 layers).", + "- **Start from a relief** (hills, mountains, island) or **from a heightmap image**.", + "- **Steep slopes automatically show rock** (or any layer).", + "- **Natural borders between layers**: stones and tufts of a texture cover the gaps of the other where layers meet (height blending), and the repetition of textures is made less visible (texture variation).", + "- **Physics**: add the **3D physics** behavior to the terrain. Characters and objects collide with the ground exactly. The terrain is always static.", + "- **Pathfinding**: add the **floor/obstacle for pathfinding (navmesh based)** behavior to the terrain, so that characters with the navmesh pathfinding behavior walk on its hills and around its cliffs (in recent GDevelop versions).", + "- **Shape it with the Edits property**: a list of raise, flatten, smooth and paint strokes written in JSON, shown in the scene editor. It's readable and can be changed by hand or by an AI agent, like the relief, the heightmap and the other properties. Actions only change the terrain during the game.", + "- **Change it during the game** with actions (raise, lower, flatten, smooth, paint, along a line for paths and rivers) and read the ground height, slope or layer with expressions.", + "- **Grass**: add a **terrain grass** object over the terrain. Thousands of blades sway in the wind, grow where a layer (like grass) is painted and where the ground is not too steep, and are pushed aside by characters. Sculpt or paint the terrain and the grass follows.", + "- **Forests, rocks and bushes**: add a **scattered models** object over the terrain. It places hundreds or thousands of copies of a 3D model (trees, rocks, bushes, flowers...) on the ground and draws them all at once, which is much faster than an object for each. Copies grow where a layer is painted, away from steep slopes, and follow the ground when it's sculpted. Add or erase them with the brushes of the scene editor or with the Edits property, give trees trunk collisions, and cut them down with actions. Ready-made forests, rocks and bushes are in the asset store.", + "- **Tunnels and caves**: make holes in the ground (and fill them again) with the brushes, the Edits property or actions, and put a 3D model of the tunnel or the cave (with a mesh collision shape) under the ground. Nothing is drawn in holes, and characters, cars and rays go through them.", + "- **Made for large worlds**: the terrain is split in chunks that are hidden when off-screen and simplified when far away, and collisions are updated only where the ground changes." + ], + "origin": { + "identifier": "Terrain3D", + "name": "gdevelop-extension-store" + }, + "tags": [ + "3d", + "terrain", + "ground", + "heightmap", + "landscape", + "open world", + "grass", + "foliage", + "forest", + "trees", + "tunnel", + "cave" + ], + "authorIds": [], + "dependencies": [], + "globalVariables": [], + "sceneVariables": [], + "eventsFunctions": [ + { + "description": "Define helper classes JavaScript code.", + "fullName": "Define helper classes", + "functionType": "Action", + "name": "DefineHelperClasses", + "private": true, + "sentence": "Define helper classes JavaScript code", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "if (gdjs.__terrain3DExtension) {", + " return;", + "}", + "", + "// Tested by the gameplay tests of the \"3D island terrain\" example (GDevelop-examples repository).", + "", + "const groundsByScene = new WeakMap();", + "/**", + " * The grounds of a scene (the terrains), followed by the terrain grass.", + " *", + " * A ground has: `getVersion()` (changing when the ground changes),", + " * `containsPoint(x, y)`, `getHeightAt(x, y)` (Z position of the ground),", + " * `getSlopeAt(x, y)` (in degrees), `getLayerWeightAt(x, y, layer)` (how", + " * much a painted layer, from 1 to 4, is there, from 0 to 1) and optionally", + " * `getTintAt(x, y)` (the painted tint, as red, green and blue from 0 to 255).", + " * @param {gdjs.RuntimeScene} runtimeScene", + " * @returns {Set}", + " */", + "const getGrounds = (runtimeScene) => {", + " let grounds = groundsByScene.get(runtimeScene);", + " if (!grounds) {", + " grounds = new Set();", + " groundsByScene.set(runtimeScene, grounds);", + " }", + " return grounds;", + "};", + "", + "/**", + " * @param {number} seed", + " * @returns {() => number} Random numbers between 0 (included) and 1 (excluded), the same for the same seed.", + " */", + "const createRandom = (seed) => {", + " let state = seed >>> 0 || 1;", + " return () => {", + " state ^= state << 13;", + " state ^= state >>> 17;", + " state ^= state << 5;", + " return (state >>> 0) / 4294967296;", + " };", + "};", + "", + "/**", + " * Find the highest ground at a position: grounds can overlap (like an island over a sea floor).", + " * @param {Iterable} grounds", + " * @param {{ground: object | null, z: number}} result Set to the ground (null if there is none) and its Z position.", + " */", + "const findHighestGround = (grounds, x, y, result) => {", + " result.ground = null;", + " for (const ground of grounds) {", + " if (!ground.containsPoint(x, y)) continue;", + " const z = ground.getHeightAt(x, y);", + " if (!result.ground || z > result.z) {", + " result.ground = ground;", + " result.z = z;", + " }", + " }", + " return result;", + "};", + "", + "// The grounds or the objects placed on them can change every frame (while", + "// sculpting or moving them in the editor): things are placed again at most this often.", + "const PLACEMENT_DELAY = 0.3;", + "", + "/**", + " * Tells when things placed on the grounds of a scene (grass blades, scattered", + " * models) must be placed again: after the grounds or the given properties", + " * changed, at most every `PLACEMENT_DELAY` seconds.", + " */", + "class GroundPlacement {", + " /** @param {gdjs.RuntimeScene} runtimeScene */", + " constructor(runtimeScene) {", + " this.grounds = getGrounds(runtimeScene);", + " this.placedFrom = '';", + " this.timeSincePlacement = 0;", + " }", + "", + " /**", + " * @param {number} elapsedSeconds", + " * @param {string} propertiesKey Changes when the things must be placed again.", + " */", + " shouldPlaceAgain(elapsedSeconds, propertiesKey) {", + " this.timeSincePlacement += elapsedSeconds;", + " if (this.placedFrom && this.timeSincePlacement < PLACEMENT_DELAY) return false;", + " let placementKey = propertiesKey;", + " for (const ground of this.grounds) placementKey += '|' + ground.getVersion();", + " if (placementKey === this.placedFrom) return false;", + " this.placedFrom = placementKey;", + " this.timeSincePlacement = 0;", + " return true;", + " }", + "", + " placeAgain() {", + " this.placedFrom = '';", + " }", + "}", + "", + "/**", + " * Replace parts of the shader code of a Three.js material, with an error if", + " * a part is missing (for example after an update of Three.js).", + " * @param {string} shaderCode", + " * @param {Array<[string, string]>} replacements The searched code and its replacement.", + " * @param {string} materialName", + " */", + "const patchShaderCode = (shaderCode, replacements, materialName) => {", + " for (const [searchedCode, newCode] of replacements) {", + " if (!shaderCode.includes(searchedCode)) {", + " console.error(`${materialName}: \"${searchedCode}\" was not found in the shader, which won't look as expected.`);", + " }", + " shaderCode = shaderCode.replace(searchedCode, newCode);", + " }", + " return shaderCode;", + "};", + "", + "// Size of the object inner area. Instances are scaled from it, so these only", + "// define the default size of a new terrain.", + "const AREA_SIZE = 4096;", + "const AREA_DEPTH = 600;", + "", + "// Terrains have at most 8 x 8 chunks (so at most 64 draw calls), of at least", + "// 32 x 32 cells.", + "const MIN_CHUNK_CELLS = 32;", + "const MAX_CHUNKS_PER_SIDE = 8;", + "/** @param {number} resolution */", + "const getChunkCells = (resolution) => Math.max(MIN_CHUNK_CELLS, resolution / MAX_CHUNKS_PER_SIDE);", + "// Grid steps of each level of detail, and the distance (in chunk sizes)", + "// from which each one is used.", + "const LOD_STEPS = [1, 2, 4];", + "const LOD_DISTANCES = [0, 3, 6];", + "", + "const LAYER_COUNT = 4;", + "const RESOLUTIONS = [64, 128, 256, 512, 1024];", + "const DEFAULT_RESOLUTION = 256;", + "// Each undo step is a copy of the data: fewer are kept for big terrains.", + "const UNDO_STEPS_SAMPLES = 20 * 257 * 257;", + "", + "const clamp01 = (value) => (value < 0 ? 0 : value > 1 ? 1 : value);", + "const clampInteger = (value, min, max) =>", + " value < min ? min : value > max ? max : Math.floor(value);", + "", + "/**", + " * A rectangle of grid samples, inclusive. Empty when `minI > maxI`.", + " */", + "class SampleRectangle {", + " constructor() {", + " this.clear();", + " }", + "", + " clear() {", + " this.minI = Number.MAX_SAFE_INTEGER;", + " this.minJ = Number.MAX_SAFE_INTEGER;", + " this.maxI = -1;", + " this.maxJ = -1;", + " }", + "", + " isEmpty() {", + " return this.minI > this.maxI;", + " }", + "", + " /** @param {SampleRectangle} other */", + " add(other) {", + " if (other.isEmpty()) return;", + " this.minI = Math.min(this.minI, other.minI);", + " this.minJ = Math.min(this.minJ, other.minJ);", + " this.maxI = Math.max(this.maxI, other.maxI);", + " this.maxJ = Math.max(this.maxJ, other.maxJ);", + " }", + "", + " set(minI, minJ, maxI, maxJ) {", + " this.minI = minI;", + " this.minJ = minJ;", + " this.maxI = maxI;", + " this.maxJ = maxJ;", + " return this;", + " }", + "}", + "", + "/**", + " * The heights (between 0 and 1) and the painted layers weights of a square", + " * grid of `size * size` samples.", + " */", + "class TerrainData {", + " /** @param {number} resolution The number of cells on each side. */", + " constructor(resolution) {", + " this.resolution = resolution;", + " this.size = resolution + 1;", + " this.heights = new Float32Array(this.size * this.size);", + " /** 4 weights (0-255) per sample, one for each layer. */", + " this.splat = new Uint8Array(this.size * this.size * LAYER_COUNT);", + " this.fillLayer(0);", + " /**", + " * 4 bytes per sample: a tint multiplying the layers (red, green and", + " * blue, white for none) and the wetness (0 for dry).", + " */", + " this.colors = new Uint8Array(this.size * this.size * 4);", + " this.clearColors();", + " }", + "", + " clearColors() {", + " this.colors.fill(255);", + " for (let index = 3; index < this.colors.length; index += 4) {", + " this.colors[index] = 0;", + " }", + " }", + "", + " /** @param {number} layer */", + " fillLayer(layer) {", + " this.splat.fill(0);", + " for (let index = layer; index < this.splat.length; index += LAYER_COUNT) {", + " this.splat[index] = 255;", + " }", + " }", + "", + " getHeight(i, j) {", + " const last = this.resolution;", + " const clampedI = i < 0 ? 0 : i > last ? last : i;", + " const clampedJ = j < 0 ? 0 : j > last ? last : j;", + " return this.heights[clampedJ * this.size + clampedI];", + " }", + "", + " /**", + " * @param {number} gridX", + " * @param {number} gridY", + " * @returns {number} The height interpolated on the triangles that are drawn.", + " */", + " sampleHeight(gridX, gridY) {", + " const x = Math.min(Math.max(gridX, 0), this.resolution);", + " const y = Math.min(Math.max(gridY, 0), this.resolution);", + " const i = Math.min(Math.floor(x), this.resolution - 1);", + " const j = Math.min(Math.floor(y), this.resolution - 1);", + " const fx = x - i;", + " const fy = y - j;", + " const topRight = this.getHeight(i + 1, j);", + " const bottomLeft = this.getHeight(i, j + 1);", + " if (fx + fy <= 1) {", + " const topLeft = this.getHeight(i, j);", + " return topLeft + fx * (topRight - topLeft) + fy * (bottomLeft - topLeft);", + " }", + " const bottomRight = this.getHeight(i + 1, j + 1);", + " return (", + " bottomRight +", + " (1 - fx) * (bottomLeft - bottomRight) +", + " (1 - fy) * (topRight - bottomRight)", + " );", + " }", + "", + " /** @returns {number} The weight (0-1) of a layer, interpolated between samples. */", + " sampleLayerWeight(gridX, gridY, layer) {", + " const x = Math.min(Math.max(gridX, 0), this.resolution);", + " const y = Math.min(Math.max(gridY, 0), this.resolution);", + " const i = Math.min(Math.floor(x), this.resolution - 1);", + " const j = Math.min(Math.floor(y), this.resolution - 1);", + " const fx = x - i;", + " const fy = y - j;", + " const weight = (i, j) => this.splat[(j * this.size + i) * LAYER_COUNT + layer];", + " const top = weight(i, j) + fx * (weight(i + 1, j) - weight(i, j));", + " const bottom = weight(i, j + 1) + fx * (weight(i + 1, j + 1) - weight(i, j + 1));", + " return (top + fy * (bottom - top)) / 255;", + " }", + "", + " /**", + " * @param {number} channel 0, 1 and 2 for the tint, 3 for the wetness.", + " * @returns {number} The channel (0-1), interpolated between samples.", + " */", + " sampleColor(gridX, gridY, channel) {", + " const x = Math.min(Math.max(gridX, 0), this.resolution);", + " const y = Math.min(Math.max(gridY, 0), this.resolution);", + " const i = Math.min(Math.floor(x), this.resolution - 1);", + " const j = Math.min(Math.floor(y), this.resolution - 1);", + " const fx = x - i;", + " const fy = y - j;", + " const value = (i, j) => this.colors[(j * this.size + i) * 4 + channel];", + " const top = value(i, j) + fx * (value(i + 1, j) - value(i, j));", + " const bottom = value(i, j + 1) + fx * (value(i + 1, j + 1) - value(i, j + 1));", + " return (top + fy * (bottom - top)) / 255;", + " }", + "", + " /**", + " * @param {number} index The index of a sample.", + " * @returns {boolean} true if no layer is painted on the sample: it's a hole.", + " */", + " isHole(index) {", + " const offset = index * LAYER_COUNT;", + " return (this.splat[offset] | this.splat[offset + 1] | this.splat[offset + 2] | this.splat[offset + 3]) === 0;", + " }", + "", + " /**", + " * @returns {number} The sum of the layer weights (0-1), interpolated between samples: there is a", + " * hole where it's under 0.5, like what is drawn.", + " */", + " sampleSolidity(gridX, gridY) {", + " let solidity = 0;", + " for (let layer = 0; layer < LAYER_COUNT; layer++) solidity += this.sampleLayerWeight(gridX, gridY, layer);", + " return solidity;", + " }", + "", + " /** @returns {number} The index (from 0) of the layer painted the most. */", + " getDominantLayer(gridX, gridY) {", + " const i = clampInteger(Math.round(gridX), 0, this.resolution);", + " const j = clampInteger(Math.round(gridY), 0, this.resolution);", + " const offset = (j * this.size + i) * LAYER_COUNT;", + " let dominantLayer = 0;", + " for (let layer = 1; layer < LAYER_COUNT; layer++) {", + " if (this.splat[offset + layer] > this.splat[offset + dominantLayer]) {", + " dominantLayer = layer;", + " }", + " }", + " return dominantLayer;", + " }", + "", + " /** @param {TerrainData} other */", + " copyFrom(other) {", + " this.heights.set(other.heights);", + " this.splat.set(other.splat);", + " this.colors.set(other.colors);", + " }", + "", + " /**", + " * @param {number} resolution", + " * @returns {TerrainData} A copy of this data with another resolution.", + " */", + " resampled(resolution) {", + " const data = new TerrainData(resolution);", + " const ratio = this.resolution / resolution;", + " for (let j = 0; j < data.size; j++) {", + " for (let i = 0; i < data.size; i++) {", + " const index = j * data.size + i;", + " data.heights[index] = this.sampleHeight(i * ratio, j * ratio);", + " const sourceOffset =", + " (Math.round(j * ratio) * this.size + Math.round(i * ratio)) * LAYER_COUNT;", + " for (let layer = 0; layer < LAYER_COUNT; layer++) {", + " data.splat[index * LAYER_COUNT + layer] = this.splat[sourceOffset + layer];", + " }", + " for (let channel = 0; channel < 4; channel++) {", + " data.colors[index * 4 + channel] = this.colors[sourceOffset + channel];", + " }", + " }", + " }", + " return data;", + " }", + "}", + "", + "/**", + " * Brushes change the samples around a segment, with an effect going smoothly", + " * from full on the segment to none at `radius`. A point is a segment of", + " * length 0. Positions are in scene units relative to the terrain origin.", + " */", + "const brushes = {", + " /**", + " * @param {TerrainData} data", + " * @param {{cellWidth: number, cellHeight: number, ax: number, ay: number, bx: number, by: number, radius: number}} stroke", + " * @param {(sampleIndex: number, weight: number, t: number) => void} apply Called with the weight of", + " * the brush and the position (from 0 to 1) of the nearest point of the segment.", + " * @param {SampleRectangle} changedSamples", + " */", + " forEachSample(data, stroke, apply, changedSamples) {", + " const { cellWidth, cellHeight, ax, ay, bx, by, radius } = stroke;", + " const last = data.resolution;", + " const minI = clampInteger(Math.floor((Math.min(ax, bx) - radius) / cellWidth), 0, last);", + " const maxI = clampInteger(Math.ceil((Math.max(ax, bx) + radius) / cellWidth), 0, last);", + " const minJ = clampInteger(Math.floor((Math.min(ay, by) - radius) / cellHeight), 0, last);", + " const maxJ = clampInteger(Math.ceil((Math.max(ay, by) + radius) / cellHeight), 0, last);", + " changedSamples.set(minI, minJ, maxI, maxJ);", + " if (radius <= 0) return;", + "", + " const abX = bx - ax;", + " const abY = by - ay;", + " const abLengthSquared = abX * abX + abY * abY;", + " const radiusSquared = radius * radius;", + " for (let j = minJ; j <= maxJ; j++) {", + " const y = j * cellHeight;", + " for (let i = minI; i <= maxI; i++) {", + " const x = i * cellWidth;", + " const t =", + " abLengthSquared > 0", + " ? clamp01(((x - ax) * abX + (y - ay) * abY) / abLengthSquared)", + " : 0;", + " const dx = x - (ax + t * abX);", + " const dy = y - (ay + t * abY);", + " const distanceSquared = dx * dx + dy * dy;", + " if (distanceSquared >= radiusSquared) continue;", + " const falloff = 1 - distanceSquared / radiusSquared;", + " apply(j * data.size + i, falloff * falloff, t);", + " }", + " }", + " },", + "", + " raise(data, stroke, amount, changedSamples) {", + " const { heights } = data;", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index, weight) => {", + " heights[index] = clamp01(heights[index] + amount * weight);", + " },", + " changedSamples", + " );", + " },", + "", + " /**", + " * @param {number} targetHeight", + " * @param {number} [endTargetHeight] The target at the end of the segment, for a slope (like a road", + " * going up). The same target as at its start by default.", + " */", + " flatten(data, stroke, targetHeight, strength, changedSamples, endTargetHeight = targetHeight) {", + " const { heights } = data;", + " const startTarget = clamp01(targetHeight);", + " const endTarget = clamp01(endTargetHeight);", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index, weight, t) => {", + " const target = startTarget + (endTarget - startTarget) * t;", + " heights[index] += (target - heights[index]) * clamp01(weight * strength);", + " },", + " changedSamples", + " );", + " },", + "", + " smooth(data, stroke, strength, changedSamples) {", + " const { heights, size } = data;", + " const changes = [];", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index, weight) => {", + " const i = index % size;", + " const j = (index - i) / size;", + " let sum = 0;", + " for (let dj = -1; dj <= 1; dj++) {", + " for (let di = -1; di <= 1; di++) {", + " sum += data.getHeight(i + di, j + dj);", + " }", + " }", + " changes.push(index, heights[index] + (sum / 9 - heights[index]) * clamp01(weight * strength));", + " },", + " changedSamples", + " );", + " // Written after all averages are computed, so that the result", + " // doesn't depend on the order in which samples are visited.", + " for (let index = 0; index < changes.length; index += 2) {", + " heights[changes[index]] = changes[index + 1];", + " }", + " },", + "", + " paint(data, stroke, layer, strength, changedSamples) {", + " const { splat } = data;", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index, weight) => {", + " const amount = clamp01(weight * strength);", + " const offset = index * LAYER_COUNT;", + " for (let otherLayer = 0; otherLayer < LAYER_COUNT; otherLayer++) {", + " const target = otherLayer === layer ? 255 : 0;", + " const value = splat[offset + otherLayer];", + " // Rounded towards the target, so that light strokes still paint.", + " const change = (target - value) * amount;", + " splat[offset + otherLayer] = value + (change > 0 ? Math.ceil(change) : Math.floor(change));", + " }", + " },", + " changedSamples", + " );", + " },", + "", + " /** Remove all the layers, making a hole (`fill` or painting a layer fills it again). */", + " hole(data, stroke, changedSamples) {", + " const { splat } = data;", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index) => {", + " splat.fill(0, index * LAYER_COUNT, (index + 1) * LAYER_COUNT);", + " },", + " changedSamples", + " );", + " },", + "", + " /**", + " * Fill the holes in the radius with the layers of the ground around them:", + " * each pass fills the holes next to the ground with the average of their", + " * neighbors, from the edges of the holes inward.", + " */", + " fill(data, stroke, changedSamples) {", + " const { splat, size, resolution } = data;", + " /** @type {number[]} */", + " let holes = [];", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index) => {", + " if (data.isHole(index)) holes.push(index);", + " },", + " changedSamples", + " );", + " const filledWeights = new Float32Array(holes.length * LAYER_COUNT);", + " while (holes.length > 0) {", + " /** @type {number[]} */", + " const remainingHoles = [];", + " /** @type {number[]} */", + " const filledHoles = [];", + " for (const index of holes) {", + " const i = index % size;", + " const j = (index - i) / size;", + " const weights = filledWeights.subarray(filledHoles.length * LAYER_COUNT, (filledHoles.length + 1) * LAYER_COUNT);", + " weights.fill(0);", + " for (let neighborJ = Math.max(j - 1, 0); neighborJ <= Math.min(j + 1, resolution); neighborJ++) {", + " for (let neighborI = Math.max(i - 1, 0); neighborI <= Math.min(i + 1, resolution); neighborI++) {", + " const neighbor = neighborJ * size + neighborI;", + " if (data.isHole(neighbor)) continue;", + " for (let layer = 0; layer < LAYER_COUNT; layer++) weights[layer] += splat[neighbor * LAYER_COUNT + layer];", + " }", + " }", + " if (weights[0] + weights[1] + weights[2] + weights[3] > 0) filledHoles.push(index);", + " else remainingHoles.push(index);", + " }", + " if (filledHoles.length === 0) {", + " // No ground around (a hole bigger than the brush): the first layer is used.", + " for (const index of remainingHoles) splat[index * LAYER_COUNT] = 255;", + " break;", + " }", + " // Filled after the pass, so that the holes are filled from the edges inward.", + " filledHoles.forEach((index, filledIndex) => {", + " const weights = filledWeights.subarray(filledIndex * LAYER_COUNT, (filledIndex + 1) * LAYER_COUNT);", + " const total = weights[0] + weights[1] + weights[2] + weights[3];", + " for (let layer = 0; layer < LAYER_COUNT; layer++) {", + " splat[index * LAYER_COUNT + layer] = Math.round((weights[layer] / total) * 255);", + " }", + " });", + " holes = remainingHoles;", + " }", + " },", + "", + " /** @param {number[]} color The red, green and blue (0-255) of the tint: white removes it. */", + " tint(data, stroke, color, strength, changedSamples) {", + " brushes.paintColors(data, stroke, 0, color, strength, changedSamples);", + " },", + "", + " /** @param {number} wetness From 0 (dry) to 1. */", + " wet(data, stroke, wetness, strength, changedSamples) {", + " brushes.paintColors(data, stroke, 3, [Math.round(clamp01(wetness) * 255)], strength, changedSamples);", + " },", + "", + " /**", + " * Bring channels of the colors (see `TerrainData.colors`) towards targets.", + " * @param {number} firstChannel", + " * @param {number[]} targets From 0 to 255, for the channels from the first one.", + " */", + " paintColors(data, stroke, firstChannel, targets, strength, changedSamples) {", + " const { colors } = data;", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index, weight) => {", + " const amount = clamp01(weight * strength);", + " for (let channel = 0; channel < targets.length; channel++) {", + " const offset = index * 4 + firstChannel + channel;", + " // Rounded towards the target, so that light strokes still paint.", + " const change = (targets[channel] - colors[offset]) * amount;", + " colors[offset] += change > 0 ? Math.ceil(change) : Math.floor(change);", + " }", + " },", + " changedSamples", + " );", + " },", + "};", + "", + "const relief = (() => {", + " const hash = (x, y, seed) => {", + " let h = Math.imul(x, 374761393) + Math.imul(y, 668265263) + Math.imul(seed, 982451653);", + " h = Math.imul(h ^ (h >>> 13), 1274126177);", + " return ((h ^ (h >>> 16)) >>> 0) / 4294967296;", + " };", + " const valueNoise = (x, y, seed) => {", + " const x0 = Math.floor(x);", + " const y0 = Math.floor(y);", + " const fx = x - x0;", + " const fy = y - y0;", + " const sx = fx * fx * (3 - 2 * fx);", + " const sy = fy * fy * (3 - 2 * fy);", + " const top = hash(x0, y0, seed) + sx * (hash(x0 + 1, y0, seed) - hash(x0, y0, seed));", + " const bottom =", + " hash(x0, y0 + 1, seed) + sx * (hash(x0 + 1, y0 + 1, seed) - hash(x0, y0 + 1, seed));", + " return top + sy * (bottom - top);", + " };", + " /** Fractal noise between 0 and 1. */", + " const fractalNoise = (x, y, seed) => {", + " let value = 0;", + " let amplitude = 0.5;", + " let frequency = 1;", + " for (let octave = 0; octave < 5; octave++) {", + " value += amplitude * valueNoise(x * frequency, y * frequency, seed + octave);", + " amplitude *= 0.5;", + " frequency *= 2;", + " }", + " return value / 0.96875;", + " };", + " const smoothstep = (edge0, edge1, x) => {", + " const t = clamp01((x - edge0) / (edge1 - edge0));", + " return t * t * (3 - 2 * t);", + " };", + " const reliefHeights = {", + " Flat: () => 0,", + " Hills: (u, v, seed) => 0.05 + 0.5 * Math.pow(fractalNoise(u * 3, v * 3, seed), 2),", + " Mountains: (u, v, seed) => {", + " const ridge = 1 - Math.abs(2 * fractalNoise(u * 2.5, v * 2.5, seed) - 1);", + " return 0.9 * Math.pow(ridge, 2.5);", + " },", + " Island: (u, v, seed) => {", + " const distanceToCenter = 2 * Math.hypot(u - 0.5, v - 0.5);", + " const coast = 0.2 * (fractalNoise(u * 4, v * 4, seed + 7) - 0.5);", + " const land = smoothstep(0.95, 0.55, distanceToCenter + coast);", + " return land * (0.12 + 0.6 * Math.pow(fractalNoise(u * 3, v * 3, seed), 1.5));", + " },", + " };", + " const beachLayer = 3;", + "", + " return {", + " /**", + " * @param {TerrainData} data", + " * @param {string} reliefName Flat, Hills, Mountains or Island.", + " * @param {number} seed", + " */", + " generate(data, reliefName, seed) {", + " const getHeight = reliefHeights[reliefName] || reliefHeights.Flat;", + " const integerSeed = Math.floor(seed) | 0;", + " data.fillLayer(0);", + " for (let j = 0; j < data.size; j++) {", + " for (let i = 0; i < data.size; i++) {", + " const index = j * data.size + i;", + " const height = clamp01(", + " getHeight(i / data.resolution, j / data.resolution, integerSeed)", + " );", + " data.heights[index] = height;", + " if (reliefName === 'Island') {", + " const sand = Math.round(255 * smoothstep(0.16, 0.1, height));", + " const offset = index * LAYER_COUNT;", + " data.splat[offset] = 255 - sand;", + " data.splat[offset + beachLayer] = sand;", + " }", + " }", + " }", + " },", + " };", + "})();", + "", + "/**", + " * Edits written in the Edits property: a JSON list of brush strokes, like", + " * `[{\"tool\": \"raise\", \"x\": 0.5, \"y\": 0.5, \"radius\": 0.2, \"height\": 0.3}]`,", + " * applied on the relief or the heightmap. Positions and radius are fractions", + " * of the terrain size, heights and Z positions fractions of its depth: they", + " * are the same for all instances, whatever their size. They are made to be", + " * read and written by people and by AI agents.", + " */", + "const edits = {", + " /**", + " * @param {TerrainData} data", + " * @param {string} text", + " */", + " apply(data, text) {", + " if (!text.trim()) return;", + " let editList;", + " try {", + " editList = JSON.parse(text);", + " } catch (error) {", + " console.warn('The edits of a terrain are not valid JSON: ' + error.message);", + " return;", + " }", + " if (!Array.isArray(editList)) {", + " console.warn('The edits of a terrain must be a JSON list (like [{\"tool\": \"raise\", ...}]).');", + " return;", + " }", + " const numberOr = (value, defaultValue) => (typeof value === 'number' && isFinite(value) ? value : defaultValue);", + " const changedSamples = new SampleRectangle();", + " for (const edit of editList) {", + " if (!edit || typeof edit !== 'object') continue;", + " const x = numberOr(edit.x, 0.5);", + " const y = numberOr(edit.y, 0.5);", + " const stroke = {", + " cellWidth: 1 / data.resolution,", + " cellHeight: 1 / data.resolution,", + " ax: x,", + " ay: y,", + " bx: numberOr(edit.toX, x),", + " by: numberOr(edit.toY, y),", + " radius: numberOr(edit.radius, 0.1),", + " };", + " const strength = clamp01(numberOr(edit.strength, 1));", + " if (edit.tool === 'raise') {", + " brushes.raise(data, stroke, numberOr(edit.height, 0.1), changedSamples);", + " } else if (edit.tool === 'flatten') {", + " const z = numberOr(edit.z, 0);", + " brushes.flatten(data, stroke, z, strength, changedSamples, numberOr(edit.toZ, z));", + " } else if (edit.tool === 'smooth') {", + " // Smoothing the same fraction of the terrain at any resolution.", + " const passes = Math.max(Math.round(((1 + 9 * strength) * data.resolution) / 256), 1);", + " for (let pass = 0; pass < passes; pass++) brushes.smooth(data, stroke, 1, changedSamples);", + " } else if (edit.tool === 'paint') {", + " const layer = clampInteger(numberOr(edit.layer, 1) - 1, 0, LAYER_COUNT - 1);", + " brushes.paint(data, stroke, layer, strength, changedSamples);", + " } else if (edit.tool === 'tint') {", + " const color = gdjs.rgbOrHexToRGBColor(typeof edit.color === 'string' ? edit.color : '255;255;255');", + " brushes.tint(data, stroke, color, strength, changedSamples);", + " } else if (edit.tool === 'wetness') {", + " brushes.wet(data, stroke, numberOr(edit.amount, 1), strength, changedSamples);", + " } else if (edit.tool === 'hole') {", + " brushes.hole(data, stroke, changedSamples);", + " } else if (edit.tool === 'fill') {", + " brushes.fill(data, stroke, changedSamples);", + " } else {", + " console.warn('Unknown tool in the edits of a terrain: \"' + edit.tool + '\" (use raise, flatten, smooth, paint, tint, wetness, hole or fill).');", + " }", + " }", + " },", + "};", + "", + "/**", + " * @param {gdjs.RuntimeGame} game", + " * @param {string} imageResourceName", + " * @returns {CanvasImageSource | null} The image, or null if it's not loaded (yet).", + " */", + "const getLoadedImage = (game, imageResourceName) => {", + " const imageManager = game.getImageManager();", + " if (typeof imageManager.getImageSource === 'function') return imageManager.getImageSource(imageResourceName);", + " // Older GDevelop versions: a 192 x 192 placeholder is given for images that are not loaded.", + " const texture = imageManager.getPIXITexture(imageResourceName);", + " return texture && texture !== imageManager.getInvalidPIXITexture() ? texture.baseTexture.getDrawableSource() : null;", + "};", + "", + "/**", + " * Loads heights from an image: black is the lowest, white the highest.", + " * Heights have 16 bits of precision when the red channel holds the most", + " * significant byte and the green one the least significant (a gray image", + " * works as expected too).", + " * @param {TerrainData} data", + " * @param {gdjs.RuntimeGame} game", + " * @param {string} imageResourceName", + " * @returns {boolean} true if the image could be read.", + " */", + "const loadHeightsFromImage = (data, game, imageResourceName) => {", + " const source = getLoadedImage(game, imageResourceName);", + " if (!source) return false;", + "", + " const canvas = document.createElement('canvas');", + " canvas.width = source.width;", + " canvas.height = source.height;", + " const context = canvas.getContext('2d', { willReadFrequently: true });", + " if (!context) return false;", + " context.drawImage(source, 0, 0);", + " const pixels = context.getImageData(0, 0, source.width, source.height).data;", + " // Pixels are decoded before being interpolated, to keep the 16 bits.", + " const pixelHeight = (x, y) => {", + " const offset = (y * source.width + x) * 4;", + " return (256 * pixels[offset] + pixels[offset + 1]) / 65535;", + " };", + " for (let j = 0; j < data.size; j++) {", + " const y = (j / data.resolution) * (source.height - 1);", + " const y0 = Math.floor(y);", + " const y1 = Math.min(y0 + 1, source.height - 1);", + " for (let i = 0; i < data.size; i++) {", + " const x = (i / data.resolution) * (source.width - 1);", + " const x0 = Math.floor(x);", + " const x1 = Math.min(x0 + 1, source.width - 1);", + " const top = pixelHeight(x0, y0) + (x - x0) * (pixelHeight(x1, y0) - pixelHeight(x0, y0));", + " const bottom = pixelHeight(x0, y1) + (x - x0) * (pixelHeight(x1, y1) - pixelHeight(x0, y1));", + " data.heights[j * data.size + i] = top + (y - y0) * (bottom - top);", + " }", + " }", + " return true;", + "};", + "", + "/**", + " * Stores the data in a string. Only the differences with the base data (from", + " * the relief or the heightmap) are stored: heights on 16 bits, layer weights", + " * on 8 bits, then (since format 2) the tint and wetness on 8 bits. Each value is predicted from its neighbors and the", + " * prediction errors are written as variable length integers, with runs of", + " * zeros collapsed. Untouched or smoothly changed areas take almost no space.", + " */", + "const codec = (() => {", + " const formatVersion = '2';", + " // Format 1 has no tint and wetness.", + " const readableFormatVersions = ['1', '2'];", + "", + " class ByteWriter {", + " constructor() {", + " this.bytes = new Uint8Array(1024);", + " this.length = 0;", + " }", + " writeVarUint(value) {", + " if (this.length + 5 > this.bytes.length) {", + " const bytes = new Uint8Array(this.bytes.length * 2);", + " bytes.set(this.bytes);", + " this.bytes = bytes;", + " }", + " while (value >= 0x80) {", + " this.bytes[this.length++] = (value & 0x7f) | 0x80;", + " value >>>= 7;", + " }", + " this.bytes[this.length++] = value;", + " }", + " }", + "", + " class ByteReader {", + " /** @param {Uint8Array} bytes */", + " constructor(bytes) {", + " this.bytes = bytes;", + " this.position = 0;", + " }", + " readVarUint() {", + " let value = 0;", + " let shift = 0;", + " let byte;", + " do {", + " if (this.position >= this.bytes.length) throw new Error('Truncated terrain data.');", + " byte = this.bytes[this.position++];", + " value += (byte & 0x7f) * Math.pow(2, shift);", + " shift += 7;", + " } while (byte & 0x80);", + " return value;", + " }", + " }", + "", + " /** Predicts a value from the left, top and top-left ones. */", + " const predict = (values, size, index) => {", + " const i = index % size;", + " if (index < size) return i === 0 ? 0 : values[index - 1];", + " if (i === 0) return values[index - size];", + " return values[index - 1] + values[index - size] - values[index - size - 1];", + " };", + "", + " /**", + " * @param {ByteWriter} writer", + " * @param {Int32Array} values A grid of `size * size` values.", + " */", + " const writeGrid = (writer, values, size) => {", + " let zeroRun = 0;", + " for (let index = 0; index < values.length; index++) {", + " const error = values[index] - predict(values, size, index);", + " if (error === 0) {", + " zeroRun++;", + " continue;", + " }", + " if (zeroRun > 0) {", + " writer.writeVarUint(0);", + " writer.writeVarUint(zeroRun);", + " zeroRun = 0;", + " }", + " // Zigzag: small negative and positive numbers get small codes.", + " writer.writeVarUint(error > 0 ? error * 2 : -error * 2 - 1);", + " }", + " if (zeroRun > 0) {", + " writer.writeVarUint(0);", + " writer.writeVarUint(zeroRun);", + " }", + " };", + "", + " /** @param {ByteReader} reader */", + " const readGrid = (reader, size) => {", + " const values = new Int32Array(size * size);", + " let zeroRun = 0;", + " for (let index = 0; index < values.length; index++) {", + " let error = 0;", + " if (zeroRun > 0) {", + " zeroRun--;", + " } else {", + " const code = reader.readVarUint();", + " if (code === 0) {", + " zeroRun = reader.readVarUint() - 1;", + " } else {", + " error = code % 2 === 0 ? code / 2 : -(code + 1) / 2;", + " }", + " }", + " values[index] = predict(values, size, index) + error;", + " }", + " return values;", + " };", + "", + " const toBase64 = (bytes, length) => {", + " let binary = '';", + " for (let offset = 0; offset < length; offset += 0x8000) {", + " binary += String.fromCharCode.apply(null, bytes.subarray(offset, Math.min(offset + 0x8000, length)));", + " }", + " return btoa(binary);", + " };", + "", + " const fromBase64 = (base64) => {", + " const binary = atob(base64);", + " const bytes = new Uint8Array(binary.length);", + " for (let index = 0; index < binary.length; index++) {", + " bytes[index] = binary.charCodeAt(index);", + " }", + " return bytes;", + " };", + "", + " return {", + " /**", + " * @param {TerrainData} data", + " * @param {TerrainData} baseData The data without changes, with the same resolution.", + " */", + " encode(data, baseData) {", + " const writer = new ByteWriter();", + " const values = new Int32Array(data.heights.length);", + " for (let index = 0; index < values.length; index++) {", + " values[index] = Math.round((data.heights[index] - baseData.heights[index]) * 65535);", + " }", + " writeGrid(writer, values, data.size);", + " for (let layer = 0; layer < LAYER_COUNT; layer++) {", + " for (let index = 0; index < values.length; index++) {", + " const offset = index * LAYER_COUNT + layer;", + " values[index] = data.splat[offset] - baseData.splat[offset];", + " }", + " writeGrid(writer, values, data.size);", + " }", + " // Not painted, these grids are zeros: a few bytes each.", + " for (let channel = 0; channel < 4; channel++) {", + " for (let index = 0; index < values.length; index++) {", + " const offset = index * 4 + channel;", + " values[index] = data.colors[offset] - baseData.colors[offset];", + " }", + " writeGrid(writer, values, data.size);", + " }", + " return formatVersion + ';' + data.resolution + ';' + toBase64(writer.bytes, writer.length);", + " },", + "", + " /**", + " * Store grids of integers in a string, like the terrain data. Used by other objects (the scattered models).", + " * @param {Int32Array[]} grids Grids of `size * size` values.", + " * @param {number} size", + " */", + " encodeGrids(grids, size) {", + " const writer = new ByteWriter();", + " for (const grid of grids) writeGrid(writer, grid, size);", + " return toBase64(writer.bytes, writer.length);", + " },", + "", + " /**", + " * @param {string} base64 A string from `encodeGrids`.", + " * @param {number} size", + " * @param {number} count The number of grids.", + " * @returns {Int32Array[]} The grids (an error is thrown if the string is not valid).", + " */", + " decodeGrids(base64, size, count) {", + " const reader = new ByteReader(fromBase64(base64));", + " return Array.from({ length: count }, () => readGrid(reader, size));", + " },", + "", + " /**", + " * @param {string} text", + " * @param {(resolution: number) => TerrainData} createBaseData", + " * @returns {TerrainData | null} null if the text is empty or not valid.", + " */", + " decode(text, createBaseData) {", + " if (!text) return null;", + " const parts = text.split(';');", + " const resolution = parseInt(parts[1], 10);", + " if (readableFormatVersions.indexOf(parts[0]) === -1 || RESOLUTIONS.indexOf(resolution) === -1) {", + " return null;", + " }", + " try {", + " const reader = new ByteReader(fromBase64(parts[2]));", + " const data = createBaseData(resolution);", + " const heightChanges = readGrid(reader, data.size);", + " for (let index = 0; index < heightChanges.length; index++) {", + " data.heights[index] = clamp01(data.heights[index] + heightChanges[index] / 65535);", + " }", + " for (let layer = 0; layer < LAYER_COUNT; layer++) {", + " const weightChanges = readGrid(reader, data.size);", + " for (let index = 0; index < weightChanges.length; index++) {", + " const offset = index * LAYER_COUNT + layer;", + " // The base may have changed since the data was saved.", + " data.splat[offset] = Math.min(Math.max(data.splat[offset] + weightChanges[index], 0), 255);", + " }", + " }", + " if (parts[0] !== '1') {", + " for (let channel = 0; channel < 4; channel++) {", + " const colorChanges = readGrid(reader, data.size);", + " for (let index = 0; index < colorChanges.length; index++) {", + " const offset = index * 4 + channel;", + " data.colors[offset] = Math.min(Math.max(data.colors[offset] + colorChanges[index], 0), 255);", + " }", + " }", + " }", + " return data;", + " } catch (error) {", + " console.error('Terrain data could not be read:', error);", + " return null;", + " }", + " },", + " };", + "})();", + "", + "const lodIndices = new Map();", + "/**", + " * The triangles of a chunk, shared by all chunks of the same size. Chunk", + " * borders have a skirt going down, hiding the gaps between chunks drawn", + " * with different levels of detail.", + " * @param {number} chunkCells", + " * @param {number} step", + " */", + "const getLodIndex = (chunkCells, step) => {", + " const key = chunkCells + ';' + step;", + " const existingIndex = lodIndices.get(key);", + " if (existingIndex) return existingIndex;", + "", + " const sideVertices = chunkCells + 1;", + " const gridVertices = sideVertices * sideVertices;", + " const indices = [];", + " const grid = (i, j) => j * sideVertices + i;", + " for (let j = 0; j < chunkCells; j += step) {", + " for (let i = 0; i < chunkCells; i += step) {", + " const topLeft = grid(i, j);", + " const topRight = grid(i + step, j);", + " const bottomLeft = grid(i, j + step);", + " const bottomRight = grid(i + step, j + step);", + " indices.push(topLeft, topRight, bottomLeft, topRight, bottomRight, bottomLeft);", + " }", + " }", + " const borders = [", + " (k) => grid(k, 0),", + " (k) => grid(k, chunkCells),", + " (k) => grid(0, k),", + " (k) => grid(chunkCells, k),", + " ];", + " borders.forEach((borderVertex, border) => {", + " const skirtVertex = (k) => gridVertices + border * sideVertices + k;", + " for (let k = 0; k < chunkCells; k += step) {", + " const a = borderVertex(k);", + " const b = borderVertex(k + step);", + " const skirtA = skirtVertex(k);", + " const skirtB = skirtVertex(k + step);", + " // Both faces, as a skirt can be seen from either side.", + " indices.push(a, b, skirtB, a, skirtB, skirtA, a, skirtB, b, a, skirtA, skirtB);", + " }", + " });", + " const index = new THREE.BufferAttribute(new Uint16Array(indices), 1);", + " lodIndices.set(key, index);", + " return index;", + "};", + "", + "const vertexShaderDeclarations = `", + "varying vec2 vTerrainLocalXY;", + "varying float vTerrainLocalZ;", + "uniform float terrainWorldScale;", + "varying vec2 vTerrainWorldXY;", + "varying float vTerrainUp;", + "`;", + "const vertexShaderCode = `", + "vTerrainLocalXY = position.xy;", + "vTerrainLocalZ = position.z;", + "vTerrainWorldXY = (modelMatrix * vec4(transformed, 1.0)).xy * terrainWorldScale;", + "vTerrainUp = normalize(transpose(inverse(mat3(modelMatrix))) * objectNormal).z;", + "`;", + "const fragmentShaderDeclarations = `", + "uniform sampler2D terrainSplatMap;", + "uniform vec2 terrainSplatUvScaleAndOffset;", + "uniform vec3 terrainLayerColors[4];", + "uniform sampler2D terrainLayerMap0;", + "uniform sampler2D terrainLayerMap1;", + "uniform sampler2D terrainLayerMap2;", + "uniform sampler2D terrainLayerMap3;", + "uniform sampler2D terrainLayerNormalMap0;", + "uniform sampler2D terrainLayerNormalMap1;", + "uniform sampler2D terrainLayerNormalMap2;", + "uniform sampler2D terrainLayerNormalMap3;", + "uniform float terrainHasNormalMaps;", + "uniform float terrainNormalMapStrength;", + "uniform float terrainTextureSize;", + "uniform float terrainHeightBlending;", + "uniform float terrainTextureVariation;", + "uniform vec4 terrainCliffLayer;", + "uniform vec2 terrainCliffCosines;", + "uniform vec4 terrainBrush;", + "uniform sampler2D terrainColorMap;", + "uniform vec3 terrainWetShore;", + "varying vec2 vTerrainLocalXY;", + "varying float vTerrainLocalZ;", + "varying vec2 vTerrainWorldXY;", + "varying float vTerrainUp;", + "", + "float terrainHash(vec2 p) {", + " return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);", + "}", + "// The rotation of the copies of textures used to make repetition less visible.", + "const mat2 terrainCopyRotation = mat2(0.8, -0.6, 0.6, 0.8);", + "/** A bump of a normal map (from -1 to 1), mixed with its rotated copy in patches. */", + "vec3 terrainSampleNormal(sampler2D normalMap, vec2 uv, vec2 uv2, float patches) {", + " vec3 bump = texture2D(normalMap, uv).xyz * 2.0 - 1.0;", + " if (patches <= 0.0) return bump;", + " vec3 copyBump = texture2D(normalMap, uv2).xyz * 2.0 - 1.0;", + " // The bumps of the copy are rotated back with the inverse rotation.", + " copyBump.xy = transpose(terrainCopyRotation) * copyBump.xy;", + " return mix(bump, copyBump, patches);", + "}", + "float terrainNoise(vec2 p) {", + " vec2 i = floor(p);", + " vec2 f = fract(p);", + " vec2 u = f * f * (3.0 - 2.0 * f);", + " return mix(", + " mix(terrainHash(i), terrainHash(i + vec2(1.0, 0.0)), u.x),", + " mix(terrainHash(i + vec2(0.0, 1.0)), terrainHash(i + vec2(1.0, 1.0)), u.x),", + " u.y", + " );", + "}", + "`;", + "// Holes are samples without any painted layer: the interpolated sum of the", + "// weights gives smooth edges. Only compiled for terrains having holes, as", + "// discard makes drawing slower on some GPUs.", + "const holeShaderCode = `", + "#ifdef TERRAIN_HOLES", + "if (dot(texture2D(terrainSplatMap, vTerrainLocalXY * terrainSplatUvScaleAndOffset.x + terrainSplatUvScaleAndOffset.y), vec4(1.0)) < 0.5) discard;", + "#endif", + "`;", + "// For the shadows: the depth of holes is not drawn.", + "const holeDepthShaderDeclarations = `", + "uniform sampler2D terrainSplatMap;", + "uniform vec2 terrainSplatUvScaleAndOffset;", + "varying vec2 vTerrainLocalXY;", + "`;", + "", + "const fragmentShaderCode = holeShaderCode + `", + "// Borders between layers are made irregular and sharper, to look painted.", + "vec2 terrainNoisePosition = vTerrainWorldXY / 24.0;", + "vec4 terrainJitter = vec4(", + " terrainNoise(terrainNoisePosition),", + " terrainNoise(terrainNoisePosition + 19.1),", + " terrainNoise(terrainNoisePosition + 37.7),", + " terrainNoise(terrainNoisePosition + 53.3)", + ");", + "// Layer weights are in a texture, so that painted details stay the same", + "// whatever the level of detail of the ground.", + "vec2 terrainSplatUv = vTerrainLocalXY * terrainSplatUvScaleAndOffset.x + terrainSplatUvScaleAndOffset.y;", + "vec4 terrainWeights = texture2D(terrainSplatMap, terrainSplatUv) * (0.7 + 0.6 * terrainJitter);", + "terrainWeights = terrainWeights * terrainWeights;", + "terrainWeights /= max(dot(terrainWeights, vec4(1.0)), 0.00001);", + "float terrainCliff = 1.0 - smoothstep(terrainCliffCosines.x, terrainCliffCosines.y, vTerrainUp);", + "terrainWeights = mix(terrainWeights, terrainCliffLayer, terrainCliff * dot(terrainCliffLayer, vec4(1.0)));", + "", + "vec2 terrainUv = vTerrainWorldXY / terrainTextureSize;", + "vec3 terrainTexture0 = texture2D(terrainLayerMap0, terrainUv).rgb;", + "vec3 terrainTexture1 = texture2D(terrainLayerMap1, terrainUv).rgb;", + "vec3 terrainTexture2 = texture2D(terrainLayerMap2, terrainUv).rgb;", + "vec3 terrainTexture3 = texture2D(terrainLayerMap3, terrainUv).rgb;", + "", + "// Less visible repetition: in irregular patches, textures are replaced by a", + "// rotated and shifted copy of themselves, and colors slowly vary.", + "float terrainPatches = 0.0;", + "vec2 terrainUv2 = terrainUv;", + "float terrainColorVariation = 1.0;", + "if (terrainTextureVariation > 0.0) {", + " // Up to half of the ground is covered by the copies, each patch showing", + " // only one of them (mixing them would make them blurry).", + " float terrainPatchThreshold = 1.0 - 0.5 * terrainTextureVariation;", + " terrainPatches = smoothstep(terrainPatchThreshold - 0.05, terrainPatchThreshold + 0.05, terrainNoise(terrainUv * 0.23 + 7.3));", + " terrainUv2 = terrainCopyRotation * terrainUv + vec2(0.31, 0.73);", + " terrainTexture0 = mix(terrainTexture0, texture2D(terrainLayerMap0, terrainUv2).rgb, terrainPatches);", + " terrainTexture1 = mix(terrainTexture1, texture2D(terrainLayerMap1, terrainUv2).rgb, terrainPatches);", + " terrainTexture2 = mix(terrainTexture2, texture2D(terrainLayerMap2, terrainUv2).rgb, terrainPatches);", + " terrainTexture3 = mix(terrainTexture3, texture2D(terrainLayerMap3, terrainUv2).rgb, terrainPatches);", + " terrainColorVariation += terrainTextureVariation * 0.3 *", + " (terrainNoise(terrainUv * 0.11) + 0.5 * terrainNoise(terrainUv * 0.37 + 3.1) - 0.75);", + "}", + "", + "// Height-based blending: the brightness of a texture, compared to its", + "// average, is used as its height. Where layers meet, the high parts (stones,", + "// tufts) cover the low parts (sand, gaps) instead of fading into each other.", + "if (terrainHeightBlending > 0.0) {", + " const vec3 terrainLuminance = vec3(0.299, 0.587, 0.114);", + " vec4 terrainHeights = vec4(", + " dot(terrainTexture0 - textureLod(terrainLayerMap0, vec2(0.5), 16.0).rgb, terrainLuminance),", + " dot(terrainTexture1 - textureLod(terrainLayerMap1, vec2(0.5), 16.0).rgb, terrainLuminance),", + " dot(terrainTexture2 - textureLod(terrainLayerMap2, vec2(0.5), 16.0).rgb, terrainLuminance),", + " dot(terrainTexture3 - textureLod(terrainLayerMap3, vec2(0.5), 16.0).rgb, terrainLuminance)", + " );", + " vec4 terrainHeightWeights = terrainWeights + terrainHeights;", + " float terrainTop = max(max(terrainHeightWeights.x, terrainHeightWeights.y), max(terrainHeightWeights.z, terrainHeightWeights.w));", + " // Only painted layers (and the cliff layer) can show.", + " vec4 terrainHeightBlend = max(terrainHeightWeights - terrainTop + 0.1, 0.0) * step(0.004, terrainWeights);", + " terrainHeightBlend /= max(dot(terrainHeightBlend, vec4(1.0)), 0.00001);", + " terrainWeights = mix(terrainWeights, terrainHeightBlend, terrainHeightBlending);", + "}", + "", + "vec3 terrainColor = terrainColorVariation * (", + " terrainWeights.x * terrainLayerColors[0] * terrainTexture0 +", + " terrainWeights.y * terrainLayerColors[1] * terrainTexture1 +", + " terrainWeights.z * terrainLayerColors[2] * terrainTexture2 +", + " terrainWeights.w * terrainLayerColors[3] * terrainTexture3);", + "", + "// The painted tint multiplies all layers. Wet ground is darker (and", + "// shinier, see the roughness).", + "vec4 terrainTintAndWetness = texture2D(terrainColorMap, terrainSplatUv);", + "float terrainWetness = max(", + " terrainTintAndWetness.a,", + " terrainWetShore.z * (1.0 - smoothstep(terrainWetShore.x, terrainWetShore.x + terrainWetShore.y, vTerrainLocalZ))", + ");", + "terrainColor *= terrainTintAndWetness.rgb * (1.0 - 0.4 * terrainWetness);", + "diffuseColor.rgb *= terrainColor;", + "", + "if (terrainBrush.w > 0.5) {", + " float brushDistance = distance(vTerrainWorldXY, terrainBrush.xy);", + " float ringWidth = max(terrainBrush.z * 0.04, 1.5);", + " float ring = 1.0 - smoothstep(0.0, ringWidth, abs(brushDistance - terrainBrush.z));", + " float disc = brushDistance < terrainBrush.z ? 0.12 : 0.0;", + " diffuseColor.rgb = mix(diffuseColor.rgb, vec3(1.0), max(ring * 0.85, disc));", + "}", + "`;", + "", + "const fragmentShaderRoughnessCode = `", + "roughnessFactor = mix(roughnessFactor, 0.3, terrainWetness);", + "`;", + "", + "const fragmentShaderNormalCode = `", + "if (terrainHasNormalMaps > 0.5) {", + " vec3 terrainMapNormal =", + " terrainWeights.x * terrainSampleNormal(terrainLayerNormalMap0, terrainUv, terrainUv2, terrainPatches) +", + " terrainWeights.y * terrainSampleNormal(terrainLayerNormalMap1, terrainUv, terrainUv2, terrainPatches) +", + " terrainWeights.z * terrainSampleNormal(terrainLayerNormalMap2, terrainUv, terrainUv2, terrainPatches) +", + " terrainWeights.w * terrainSampleNormal(terrainLayerNormalMap3, terrainUv, terrainUv2, terrainPatches);", + " terrainMapNormal.xy *= terrainNormalMapStrength;", + " // The X and Y of textures follow the X and Y of the 3D world, projected on", + " // the ground (the camera space of the normal is used).", + " vec3 terrainTangent = (viewMatrix * vec4(1.0, 0.0, 0.0, 0.0)).xyz;", + " terrainTangent -= normal * dot(normal, terrainTangent);", + " // A ground facing the X axis (a rotated terrain) uses the Y axis instead.", + " if (dot(terrainTangent, terrainTangent) < 0.000001) {", + " terrainTangent = (viewMatrix * vec4(0.0, 1.0, 0.0, 0.0)).xyz;", + " terrainTangent -= normal * dot(normal, terrainTangent);", + " }", + " terrainTangent = normalize(terrainTangent);", + " vec3 terrainBitangent = cross(normal, terrainTangent);", + " normal = normalize(mat3(terrainTangent, terrainBitangent, normal) * terrainMapNormal);", + "}", + "`;", + "", + "let whiteTexture = null;", + "const getWhiteTexture = () => {", + " if (!whiteTexture) {", + " whiteTexture = new THREE.DataTexture(new Uint8Array([255, 255, 255, 255]), 1, 1);", + " whiteTexture.needsUpdate = true;", + " }", + " return whiteTexture;", + "};", + "", + "let flatNormalTexture = null;", + "const getFlatNormalTexture = () => {", + " if (!flatNormalTexture) {", + " flatNormalTexture = new THREE.DataTexture(new Uint8Array([128, 128, 255, 255]), 1, 1);", + " flatNormalTexture.needsUpdate = true;", + " }", + " return flatNormalTexture;", + "};", + "", + "const layerTextures = new WeakMap();", + "const layerNormalMaps = new WeakMap();", + "/**", + " * Textures of the image manager have no mipmaps: they would shimmer when", + " * repeated in the distance.", + " * @param {gdjs.RuntimeGame} game", + " * @param {string} resourceName", + " * @param {boolean} isNormalMap Normal maps are directions, not colors.", + " */", + "const getLayerTexture = (game, resourceName, isNormalMap) => {", + " if (!resourceName) return isNormalMap ? getFlatNormalTexture() : getWhiteTexture();", + " const imageTexture = game.getImageManager().getThreeTexture(resourceName);", + " const textures = isNormalMap ? layerNormalMaps : layerTextures;", + " let texture = textures.get(imageTexture);", + " if (!texture) {", + " texture = imageTexture.clone();", + " if (isNormalMap) texture.colorSpace = THREE.NoColorSpace;", + " texture.wrapS = THREE.RepeatWrapping;", + " texture.wrapT = THREE.RepeatWrapping;", + " texture.minFilter = THREE.LinearMipmapLinearFilter;", + " texture.generateMipmaps = true;", + " texture.anisotropy = 4;", + " texture.needsUpdate = true;", + " textures.set(imageTexture, texture);", + " }", + " return texture;", + "};", + "", + "/**", + " * A texture of 4 bytes per sample of a grid of `size * size` samples (layer", + " * weights, tint and wetness...), interpolated between samples.", + " * @param {Uint8Array} bytes", + " * @param {number} size", + " */", + "const createGridTexture = (bytes, size) => {", + " const texture = new THREE.DataTexture(bytes, size, size);", + " texture.magFilter = THREE.LinearFilter;", + " texture.minFilter = THREE.LinearFilter;", + " texture.userData.isUploaded = false;", + " texture.onUpdate = () => {", + " texture.userData.isUploaded = true;", + " };", + " return texture;", + "};", + "", + "/**", + " * Send the rows of the changed samples to the GPU, or the whole texture", + " * before its first upload (and in older GDevelop versions).", + " * @param {THREE.DataTexture} texture", + " * @param {SampleRectangle} samples", + " */", + "const uploadGridTextureRows = (texture, samples) => {", + " if (texture.userData.isUploaded && typeof texture.addUpdateRange === 'function') {", + " const size = texture.image.width;", + " for (let j = samples.minJ; j <= samples.maxJ; j++) {", + " texture.addUpdateRange((j * size + samples.minI) * 4, (samples.maxI - samples.minI + 1) * 4);", + " }", + " }", + " texture.needsUpdate = true;", + "};", + "", + "class TerrainRenderer {", + " /** @param {Terrain} terrain */", + " constructor(terrain) {", + " this.terrain = terrain;", + " this.group = new THREE.Group();", + " terrain.object.get3DRendererObject().add(this.group);", + " /** @type {Array<{mesh: THREE.Mesh, geometry: THREE.BufferGeometry, firstI: number, firstJ: number, center: THREE.Vector3, lod: number}>} */", + " this.chunks = [];", + " this.chunksPerSide = 0;", + " this.chunkCells = 0;", + " this.chunkSideVertices = 0;", + " this.chunkGridVertices = 0;", + " this.chunkVertices = 0;", + " /** @type {THREE.DataTexture | null} */", + " this.splatTexture = null;", + " /** @type {THREE.DataTexture | null} */", + " this.colorTexture = null;", + " this.uniforms = {", + " terrainSplatMap: { value: getWhiteTexture() },", + " terrainSplatUvScaleAndOffset: { value: new THREE.Vector2() },", + " terrainLayerColors: { value: [new THREE.Color(), new THREE.Color(), new THREE.Color(), new THREE.Color()] },", + " terrainLayerMap0: { value: getWhiteTexture() },", + " terrainLayerMap1: { value: getWhiteTexture() },", + " terrainLayerMap2: { value: getWhiteTexture() },", + " terrainLayerMap3: { value: getWhiteTexture() },", + " terrainLayerNormalMap0: { value: getFlatNormalTexture() },", + " terrainLayerNormalMap1: { value: getFlatNormalTexture() },", + " terrainLayerNormalMap2: { value: getFlatNormalTexture() },", + " terrainLayerNormalMap3: { value: getFlatNormalTexture() },", + " terrainHasNormalMaps: { value: 0 },", + " terrainNormalMapStrength: { value: 1 },", + " terrainTextureSize: { value: 256 },", + " terrainHeightBlending: { value: 0 },", + " terrainTextureVariation: { value: 0 },", + " terrainCliffLayer: { value: new THREE.Vector4() },", + " terrainCliffCosines: { value: new THREE.Vector2() },", + " terrainBrush: { value: new THREE.Vector4() },", + " terrainColorMap: { value: getWhiteTexture() },", + " // Z (in the terrain group) under which the ground is wet, fade height, and 1 if it's used.", + " terrainWetShore: { value: new THREE.Vector3() },", + " // Scene units by 3D world unit.", + " terrainWorldScale: { value: 1 },", + " };", + " this.material = new THREE.MeshStandardMaterial({ roughness: 1, metalness: 0 });", + " this.material.onBeforeCompile = (shader) => {", + " Object.assign(shader.uniforms, this.uniforms);", + " shader.vertexShader = patchShaderCode(shader.vertexShader, [", + " ['#include ', '#include \\n' + vertexShaderDeclarations],", + " ['#include ', '#include \\n' + vertexShaderCode],", + " ], 'Terrain3D');", + " shader.fragmentShader = patchShaderCode(shader.fragmentShader, [", + " ['#include ', '#include \\n' + fragmentShaderDeclarations],", + " ['#include ', fragmentShaderCode],", + " ['#include ', '#include \\n' + fragmentShaderRoughnessCode],", + " ['#include ', '#include \\n' + fragmentShaderNormalCode],", + " ], 'Terrain3D');", + " };", + " this.material.customProgramCacheKey = () => (this.hasHoles ? 'Terrain3DWithHoles' : 'Terrain3D');", + " this.hasHoles = false;", + " // Without them, holes would cast shadows.", + " this.depthMaterial = this._createHoleDepthMaterial(new THREE.MeshDepthMaterial());", + " this.distanceMaterial = this._createHoleDepthMaterial(new THREE.MeshDistanceMaterial());", + " this._cameraPosition = new THREE.Vector3();", + " this._chunkPosition = new THREE.Vector3();", + " this._scale = new THREE.Vector3();", + " }", + "", + " /**", + " * @param {THREE.MeshDepthMaterial | THREE.MeshDistanceMaterial} material", + " * @returns The material, drawing nothing in holes.", + " */", + " _createHoleDepthMaterial(material) {", + " material.defines = { TERRAIN_HOLES: '' };", + " material.onBeforeCompile = (shader) => {", + " Object.assign(shader.uniforms, this.uniforms);", + " shader.vertexShader = patchShaderCode(shader.vertexShader, [", + " ['#include ', '#include \\nvarying vec2 vTerrainLocalXY;'],", + " ['#include ', '#include \\nvTerrainLocalXY = position.xy;'],", + " ], 'Terrain3D shadows');", + " shader.fragmentShader = patchShaderCode(shader.fragmentShader, [", + " ['#include ', '#include \\n' + holeDepthShaderDeclarations],", + " ['#include ', holeShaderCode + '#include '],", + " ], 'Terrain3D shadows');", + " };", + " material.customProgramCacheKey = () => 'Terrain3DHoles';", + " return material;", + " }", + "", + " /**", + " * Use the shaders drawing holes, or the faster ones without them.", + " * @param {boolean} hasHoles", + " */", + " setHasHoles(hasHoles) {", + " if (hasHoles === this.hasHoles) return;", + " this.hasHoles = hasHoles;", + " // The material has its own defines (STANDARD).", + " if (hasHoles) this.material.defines.TERRAIN_HOLES = '';", + " else delete this.material.defines.TERRAIN_HOLES;", + " this.material.needsUpdate = true;", + " for (const chunk of this.chunks) this._setChunkShadowMaterials(chunk.mesh);", + " }", + "", + " /** @param {THREE.Mesh} mesh */", + " _setChunkShadowMaterials(mesh) {", + " mesh.customDepthMaterial = this.hasHoles ? this.depthMaterial : undefined;", + " mesh.customDistanceMaterial = this.hasHoles ? this.distanceMaterial : undefined;", + " }", + "", + " /** Recreate all the chunks, for example after the resolution changed. */", + " rebuild() {", + " this.disposeChunks();", + " const { data } = this.terrain;", + " this.chunkCells = getChunkCells(data.resolution);", + " this.chunkSideVertices = this.chunkCells + 1;", + " this.chunkGridVertices = this.chunkSideVertices * this.chunkSideVertices;", + " this.chunkVertices = this.chunkGridVertices + 4 * this.chunkSideVertices;", + " this.chunksPerSide = data.resolution / this.chunkCells;", + " const cellSize = AREA_SIZE / data.resolution;", + " // Sample centers are at the texel centers.", + " this.splatTexture = createGridTexture(data.splat, data.size);", + " this.uniforms.terrainSplatMap.value = this.splatTexture;", + " this.colorTexture = createGridTexture(data.colors, data.size);", + " // Tints are colors chosen like the layer colors.", + " this.colorTexture.colorSpace = THREE.SRGBColorSpace;", + " this.uniforms.terrainColorMap.value = this.colorTexture;", + " this.uniforms.terrainSplatUvScaleAndOffset.value.set(1 / (cellSize * data.size), 0.5 / data.size);", + " for (let chunkJ = 0; chunkJ < this.chunksPerSide; chunkJ++) {", + " for (let chunkI = 0; chunkI < this.chunksPerSide; chunkI++) {", + " const firstI = chunkI * this.chunkCells;", + " const firstJ = chunkJ * this.chunkCells;", + " const positions = new Float32Array(this.chunkVertices * 3);", + " for (let j = 0; j < this.chunkSideVertices; j++) {", + " for (let i = 0; i < this.chunkSideVertices; i++) {", + " const vertex = j * this.chunkSideVertices + i;", + " positions[vertex * 3] = (firstI + i) * cellSize;", + " positions[vertex * 3 + 1] = (firstJ + j) * cellSize;", + " }", + " }", + " // Skirt vertices are below border vertices.", + " this._forEachSkirtVertex((skirtVertex, borderVertex) => {", + " positions[skirtVertex * 3] = positions[borderVertex * 3];", + " positions[skirtVertex * 3 + 1] = positions[borderVertex * 3 + 1];", + " });", + " const geometry = new THREE.BufferGeometry();", + " geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));", + " geometry.setAttribute('normal', new THREE.BufferAttribute(new Float32Array(this.chunkVertices * 3), 3));", + " geometry.setIndex(getLodIndex(this.chunkCells, LOD_STEPS[0]));", + " geometry.boundingBox = new THREE.Box3();", + " geometry.boundingSphere = new THREE.Sphere();", + " const mesh = new THREE.Mesh(geometry, this.material);", + " mesh.matrixAutoUpdate = false;", + " // Rays hit the ground at its full resolution, whatever the level of", + " // detail drawn, and go through holes.", + " const chunkCells = this.chunkCells;", + " const terrainData = () => this.terrain.data;", + " const localPoint = new THREE.Vector3();", + " mesh.raycast = function (raycaster, intersects) {", + " const drawnIndex = geometry.index;", + " const firstIntersection = intersects.length;", + " geometry.setIndex(getLodIndex(chunkCells, LOD_STEPS[0]));", + " THREE.Mesh.prototype.raycast.call(this, raycaster, intersects);", + " geometry.setIndex(drawnIndex);", + " const data = terrainData();", + " const cellSize = AREA_SIZE / data.resolution;", + " for (let index = intersects.length - 1; index >= firstIntersection; index--) {", + " this.worldToLocal(localPoint.copy(intersects[index].point));", + " if (data.sampleSolidity(localPoint.x / cellSize, localPoint.y / cellSize) < 0.5) {", + " intersects.splice(index, 1);", + " }", + " }", + " };", + " this._setChunkShadowMaterials(mesh);", + " this.group.add(mesh);", + " this.chunks.push({", + " mesh,", + " geometry,", + " firstI,", + " firstJ,", + " center: new THREE.Vector3(", + " (firstI + this.chunkCells / 2) * cellSize,", + " (firstJ + this.chunkCells / 2) * cellSize,", + " 0", + " ),", + " lod: 0,", + " });", + " }", + " }", + " this.updateSamples(new SampleRectangle().set(0, 0, data.resolution, data.resolution), true);", + " this.updateMaterial();", + " }", + "", + " _forEachSkirtVertex(callback) {", + " for (let k = 0; k < this.chunkSideVertices; k++) {", + " callback(this.chunkGridVertices + k, k);", + " callback(this.chunkGridVertices + this.chunkSideVertices + k, this.chunkCells * this.chunkSideVertices + k);", + " callback(this.chunkGridVertices + 2 * this.chunkSideVertices + k, k * this.chunkSideVertices);", + " callback(", + " this.chunkGridVertices + 3 * this.chunkSideVertices + k,", + " k * this.chunkSideVertices + this.chunkCells", + " );", + " }", + " }", + "", + " /**", + " * Skirts go down only to the lowest point a coarser neighbor chunk can", + " * draw along the border, so that they can't be seen through holes.", + " * @param {Float32Array} positions", + " */", + " _updateSkirtHeights(positions) {", + " const coarsestStep = LOD_STEPS[LOD_STEPS.length - 1];", + " const borderVertices = [", + " (k) => k,", + " (k) => this.chunkCells * this.chunkSideVertices + k,", + " (k) => k * this.chunkSideVertices,", + " (k) => k * this.chunkSideVertices + this.chunkCells,", + " ];", + " borderVertices.forEach((borderVertex, border) => {", + " for (let k = 0; k < this.chunkSideVertices; k++) {", + " let minZ = Infinity;", + " const from = Math.max(k - coarsestStep, 0);", + " const to = Math.min(k + coarsestStep, this.chunkCells);", + " for (let other = from; other <= to; other++) {", + " minZ = Math.min(minZ, positions[borderVertex(other) * 3 + 2]);", + " }", + " const skirtVertex = this.chunkGridVertices + border * this.chunkSideVertices + k;", + " positions[skirtVertex * 3 + 2] = minZ;", + " }", + " });", + " }", + "", + " /**", + " * Update the vertices of the chunks sharing the given samples.", + " * @param {SampleRectangle} samples", + " * @param {boolean} hasPaintChanged", + " */", + " updateSamples(samples, hasPaintChanged) {", + " if (samples.isEmpty()) return;", + " const { data } = this.terrain;", + " const cellSize = AREA_SIZE / data.resolution;", + " const slopeFactor = AREA_DEPTH / cellSize;", + " // Normals of samples next to changed ones change too.", + " const minI = Math.max(samples.minI - 1, 0);", + " const minJ = Math.max(samples.minJ - 1, 0);", + " const maxI = Math.min(samples.maxI + 1, data.resolution);", + " const maxJ = Math.min(samples.maxJ + 1, data.resolution);", + " const firstChunkI = Math.max(Math.ceil(minI / this.chunkCells) - 1, 0);", + " const firstChunkJ = Math.max(Math.ceil(minJ / this.chunkCells) - 1, 0);", + " const lastChunkI = Math.min(Math.floor(maxI / this.chunkCells), this.chunksPerSide - 1);", + " const lastChunkJ = Math.min(Math.floor(maxJ / this.chunkCells), this.chunksPerSide - 1);", + "", + " for (let chunkJ = firstChunkJ; chunkJ <= lastChunkJ; chunkJ++) {", + " for (let chunkI = firstChunkI; chunkI <= lastChunkI; chunkI++) {", + " const chunk = this.chunks[chunkJ * this.chunksPerSide + chunkI];", + " const positions = chunk.geometry.attributes.position.array;", + " const normals = chunk.geometry.attributes.normal.array;", + " const fromI = Math.max(minI - chunk.firstI, 0);", + " const toI = Math.min(maxI - chunk.firstI, this.chunkCells);", + " const fromJ = Math.max(minJ - chunk.firstJ, 0);", + " const toJ = Math.min(maxJ - chunk.firstJ, this.chunkCells);", + " for (let j = fromJ; j <= toJ; j++) {", + " const gridJ = chunk.firstJ + j;", + " for (let i = fromI; i <= toI; i++) {", + " const gridI = chunk.firstI + i;", + " const vertex = j * this.chunkSideVertices + i;", + " const sample = gridJ * data.size + gridI;", + " positions[vertex * 3 + 2] = data.heights[sample] * AREA_DEPTH;", + "", + " const leftI = Math.max(gridI - 1, 0);", + " const rightI = Math.min(gridI + 1, data.resolution);", + " const upJ = Math.max(gridJ - 1, 0);", + " const downJ = Math.min(gridJ + 1, data.resolution);", + " const nx =", + " ((data.getHeight(leftI, gridJ) - data.getHeight(rightI, gridJ)) * slopeFactor) /", + " (rightI - leftI);", + " const ny =", + " ((data.getHeight(gridI, upJ) - data.getHeight(gridI, downJ)) * slopeFactor) /", + " (downJ - upJ);", + " const inverseLength = 1 / Math.sqrt(nx * nx + ny * ny + 1);", + " normals[vertex * 3] = nx * inverseLength;", + " normals[vertex * 3 + 1] = ny * inverseLength;", + " normals[vertex * 3 + 2] = inverseLength;", + " }", + " }", + " this._forEachSkirtVertex((skirtVertex, borderVertex) => {", + " for (let axis = 0; axis < 3; axis++) {", + " normals[skirtVertex * 3 + axis] = normals[borderVertex * 3 + axis];", + " }", + " });", + " this._updateSkirtHeights(positions);", + " let maxZ = 0;", + " for (let vertex = 0; vertex < this.chunkGridVertices; vertex++) {", + " maxZ = Math.max(maxZ, positions[vertex * 3 + 2]);", + " }", + " const chunkSize = this.chunkCells * cellSize;", + " chunk.geometry.boundingBox.min.set(chunk.firstI * cellSize, chunk.firstJ * cellSize, 0);", + " chunk.geometry.boundingBox.max.set(", + " chunk.firstI * cellSize + chunkSize,", + " chunk.firstJ * cellSize + chunkSize,", + " maxZ", + " );", + " chunk.geometry.boundingBox.getBoundingSphere(chunk.geometry.boundingSphere);", + " chunk.center.z = maxZ / 2;", + " chunk.geometry.attributes.position.needsUpdate = true;", + " chunk.geometry.attributes.normal.needsUpdate = true;", + " }", + " }", + " if (hasPaintChanged) {", + " // The data may have been replaced by one with the same resolution.", + " this.splatTexture.image.data = data.splat;", + " uploadGridTextureRows(this.splatTexture, samples);", + " this.colorTexture.image.data = data.colors;", + " uploadGridTextureRows(this.colorTexture, samples);", + " }", + " }", + "", + " updateMaterial() {", + " const { object } = this.terrain;", + " const game = object.getRuntimeScene().getGame();", + " const colors = [object._getLayer1Color(), object._getLayer2Color(), object._getLayer3Color(), object._getLayer4Color()];", + " const textures = [", + " object._getLayer1Texture(),", + " object._getLayer2Texture(),", + " object._getLayer3Texture(),", + " object._getLayer4Texture(),", + " ];", + " const normalMaps = [", + " object._getLayer1NormalMap(),", + " object._getLayer2NormalMap(),", + " object._getLayer3NormalMap(),", + " object._getLayer4NormalMap(),", + " ];", + " for (let layer = 0; layer < LAYER_COUNT; layer++) {", + " this.uniforms.terrainLayerColors.value[layer].set(gdjs.rgbOrHexStringToNumber(colors[layer]));", + " this.uniforms['terrainLayerMap' + layer].value = getLayerTexture(game, textures[layer], false);", + " this.uniforms['terrainLayerNormalMap' + layer].value = getLayerTexture(game, normalMaps[layer], true);", + " }", + " this.uniforms.terrainHasNormalMaps.value = normalMaps.some((normalMap) => !!normalMap) ? 1 : 0;", + " this.uniforms.terrainNormalMapStrength.value = Math.max(object._getNormalMapStrength(), 0);", + " this.uniforms.terrainTextureSize.value = Math.max(object._getTextureSize(), 1);", + " this.uniforms.terrainHeightBlending.value = clamp01(object._getHeightBlending());", + " this.uniforms.terrainTextureVariation.value = clamp01(object._getTextureVariation());", + "", + " const cliffLayer = parseInt(object._getCliffLayer(), 10);", + " const cliffAngle = gdjs.toRad(object._getCliffAngle());", + " const cliffBlendAngle = gdjs.toRad(4);", + " this.uniforms.terrainCliffLayer.value.set(", + " cliffLayer === 1 ? 1 : 0,", + " cliffLayer === 2 ? 1 : 0,", + " cliffLayer === 3 ? 1 : 0,", + " cliffLayer === 4 ? 1 : 0", + " );", + " this.uniforms.terrainCliffCosines.value.set(", + " Math.cos(cliffAngle + cliffBlendAngle),", + " Math.cos(cliffAngle - cliffBlendAngle)", + " );", + "", + " const castShadow = object._getCastShadow();", + " const receiveShadow = object._getReceiveShadow();", + " for (const chunk of this.chunks) {", + " chunk.mesh.castShadow = castShadow;", + " chunk.mesh.receiveShadow = receiveShadow;", + " }", + " }", + "", + " /**", + " * Show the brush circle of the editor.", + " * @param {THREE.Vector3 | null} localPosition", + " * @param {number} radius In scene units.", + " */", + " setBrush(localPosition, radius) {", + " const brush = this.uniforms.terrainBrush.value;", + " if (!localPosition) {", + " brush.w = 0;", + " return;", + " }", + " this.group.updateWorldMatrix(true, false);", + " const worldPosition = this._chunkPosition.copy(localPosition).applyMatrix4(this.group.matrixWorld);", + " const worldScale = this.uniforms.terrainWorldScale.value;", + " brush.set(worldPosition.x * worldScale, worldPosition.y * worldScale, radius, 1);", + " }", + "", + " /** @param {THREE.Camera | null} camera */", + " updateLevelsOfDetail(camera) {", + " if (!camera || !this.group.visible) return;", + " const matrixWorld = this.group.matrixWorld;", + " this._cameraPosition.setFromMatrixPosition(camera.matrixWorld);", + " this._scale.setFromMatrixScale(matrixWorld);", + " const cellSize = AREA_SIZE / this.terrain.data.resolution;", + " const chunkWorldSize = this.chunkCells * cellSize * Math.max(this._scale.x, this._scale.y);", + " for (const chunk of this.chunks) {", + " const distance =", + " this._chunkPosition.copy(chunk.center).applyMatrix4(matrixWorld).distanceTo(this._cameraPosition) /", + " chunkWorldSize;", + " let lod = LOD_DISTANCES.length - 1;", + " while (lod > 0 && distance < LOD_DISTANCES[lod]) lod--;", + " if (lod !== chunk.lod) {", + " chunk.lod = lod;", + " chunk.geometry.setIndex(getLodIndex(this.chunkCells, LOD_STEPS[lod]));", + " }", + " }", + " }", + "", + " disposeChunks() {", + " for (const chunk of this.chunks) {", + " chunk.mesh.removeFromParent();", + " // The index is shared by all chunks: it must stay on the GPU.", + " chunk.geometry.setIndex(null);", + " chunk.geometry.dispose();", + " }", + " this.chunks.length = 0;", + " if (this.splatTexture) this.splatTexture.dispose();", + " this.splatTexture = null;", + " if (this.colorTexture) this.colorTexture.dispose();", + " this.colorTexture = null;", + " }", + "", + " dispose() {", + " this.disposeChunks();", + " this.material.dispose();", + " this.depthMaterial.dispose();", + " this.distanceMaterial.dispose();", + " this.group.removeFromParent();", + " }", + "}", + "", + "/**", + " * Makes a Physics3D behavior use a height field shape following the terrain.", + " * Changes of heights are applied to the shape without recreating it.", + " * @implements {gdjs.Physics3DRuntimeBehavior.BodyUpdater}", + " */", + "class TerrainBodyUpdater {", + " /**", + " * @param {Terrain} terrain", + " * @param {gdjs.Physics3DRuntimeBehavior} physics", + " */", + " constructor(terrain, physics) {", + " this.terrain = terrain;", + " this.physics = physics;", + " this.defaultBodyUpdater = physics.bodyUpdater;", + " this.changedSamples = new SampleRectangle();", + " /** @type {Jolt.HeightFieldShape | null} */", + " this.heightField = null;", + " this.heightScale = 1;", + " this.heightsBufferPointer = 0;", + " this.heightsBufferLength = 0;", + " this.bodyX = 0;", + " this.bodyY = 0;", + " this.bodyZ = 0;", + " this.bodyHeight = 0;", + " physics.bodyType = 'Static';", + " physics.bodyUpdater = this;", + " }", + "", + " createShape() {", + " const { data, object } = this.terrain;", + " const worldInvScale = this.physics._sharedData.worldInvScale;", + " const { size } = data;", + " this.heightScale = object.getDepth() * worldInvScale;", + "", + " // Changes of heights (SetHeights) break collisions unless the sample", + " // count is a power of 2: extra samples have no collision.", + " const sampleCount = Math.pow(2, Math.ceil(Math.log2(size)));", + " const settings = new Jolt.HeightFieldShapeSettings();", + " settings.mScale.Set(", + " (object.getWidth() / data.resolution) * worldInvScale,", + " 1,", + " (object.getHeight() / data.resolution) * worldInvScale", + " );", + " settings.mSampleCount = sampleCount;", + " settings.mBlockSize = 4;", + " // Allows later changes of heights on the whole depth of the object.", + " settings.mMinHeightValue = 0;", + " settings.mMaxHeightValue = this.heightScale;", + " settings.mHeightSamples.resize(sampleCount * sampleCount);", + " const samples = new Float32Array(", + " Jolt.HEAPF32.buffer,", + " Jolt.getPointer(settings.mHeightSamples.data()),", + " sampleCount * sampleCount", + " );", + " const noCollision = Jolt.HeightFieldShapeConstantValues.prototype.cNoCollisionValue;", + " samples.fill(noCollision);", + " // Height fields go along X and Z: rows are flipped so that, once", + " // rotated to have heights along Z, rows go along Y.", + " for (let j = 0; j < size; j++) {", + " const row = (size - 1 - j) * sampleCount;", + " for (let i = 0; i < size; i++) {", + " samples[row + i] = this._getSampleHeight(j * size + i, noCollision);", + " }", + " }", + " const shape = settings.Create().Get();", + " Jolt.destroy(settings);", + " this.heightField = Jolt.castObject(shape, Jolt.HeightFieldShape);", + " this.changedSamples.clear();", + " return shape;", + " }", + "", + " /** @returns {number} The height of a sample in the shape, or no collision in a hole. */", + " _getSampleHeight(index, noCollision) {", + " const height = this.terrain.surfaceHeights[index];", + " return Number.isNaN(height) ? noCollision : height * this.heightScale;", + " }", + "", + " _getBodyPosition() {", + " const { object } = this.terrain;", + " const worldInvScale = this.physics._sharedData.worldInvScale;", + " this.bodyX = object.getX();", + " this.bodyY = object.getY();", + " this.bodyZ = object.getZ();", + " // The origin of the height field is at the bottom of the terrain (see createShape).", + " this.bodyHeight = object.getHeight();", + " return this.physics._sharedData.getRVec3(", + " object.getX() * worldInvScale,", + " (object.getY() + object.getHeight()) * worldInvScale,", + " object.getZ() * worldInvScale", + " );", + " }", + "", + " _getBodyRotation() {", + " // A quarter turn around X: heights go along Z.", + " return this.physics._sharedData.getQuat(Math.SQRT1_2, 0, 0, Math.SQRT1_2);", + " }", + "", + " createAndAddBody() {", + " const { physics } = this;", + " const { bodyInterface } = physics._sharedData;", + " const shape = this.createShape();", + " const settings = new Jolt.BodyCreationSettings(", + " shape,", + " this._getBodyPosition(),", + " this._getBodyRotation(),", + " Jolt.EMotionType_Static,", + " physics.getBodyLayer()", + " );", + " settings.mFriction = physics.friction;", + " settings.mRestitution = physics.restitution;", + " const body = bodyInterface.CreateBody(settings);", + " Jolt.destroy(settings);", + " bodyInterface.AddBody(body.GetID(), Jolt.EActivation_DontActivate);", + " return body;", + " }", + "", + " updateObjectFromBody() {", + " // The terrain is static: the body never moves it.", + " }", + "", + " updateBodyFromObject() {", + " const { physics } = this;", + " const body = physics._body;", + " if (!body) return;", + " const { object } = this.terrain;", + " const { bodyInterface } = physics._sharedData;", + " if (", + " object.getX() !== this.bodyX ||", + " object.getY() !== this.bodyY ||", + " object.getZ() !== this.bodyZ ||", + " object.getHeight() !== this.bodyHeight", + " ) {", + " bodyInterface.SetPositionAndRotation(", + " body.GetID(),", + " this._getBodyPosition(),", + " this._getBodyRotation(),", + " Jolt.EActivation_DontActivate", + " );", + " }", + " if (!this.changedSamples.isEmpty()) {", + " this._applyChangedHeights(body);", + " }", + " }", + "", + " _applyChangedHeights(body) {", + " const { data } = this.terrain;", + " const { size } = data;", + " const { minI, minJ, maxI, maxJ } = this.changedSamples;", + " // Jolt only changes blocks of samples. Rows are flipped and extra", + " // samples have no collision (see createShape).", + " const blockSize = this.heightField.GetBlockSize();", + " const sampleCount = this.heightField.GetSampleCount();", + " const alignDown = (value) => Math.floor(value / blockSize) * blockSize;", + " const alignUp = (value) => Math.min(Math.ceil(value / blockSize) * blockSize, sampleCount);", + " const firstX = alignDown(minI);", + " const firstRow = alignDown(size - 1 - maxJ);", + " const width = alignUp(maxI + 1) - firstX;", + " const height = alignUp(size - minJ) - firstRow;", + " if (width * height > this.heightsBufferLength) {", + " if (this.heightsBufferPointer) Jolt._webidl_free(this.heightsBufferPointer);", + " this.heightsBufferLength = width * height;", + " this.heightsBufferPointer = Jolt._webidl_malloc(this.heightsBufferLength * 4);", + " }", + " const heights = new Float32Array(Jolt.HEAPF32.buffer, this.heightsBufferPointer, width * height);", + " const noCollision = Jolt.HeightFieldShapeConstantValues.prototype.cNoCollisionValue;", + " for (let row = 0; row < height; row++) {", + " const j = size - 1 - (firstRow + row);", + " for (let column = 0; column < width; column++) {", + " const i = firstX + column;", + " heights[row * width + column] =", + " i < size && j >= 0 ? this._getSampleHeight(j * size + i, noCollision) : noCollision;", + " }", + " }", + " const sharedData = this.physics._sharedData;", + " this.heightField.SetHeights(", + " firstX,", + " firstRow,", + " width,", + " height,", + " this.heightsBufferPointer,", + " width,", + " sharedData.jolt.GetTempAllocator()", + " );", + " sharedData.bodyInterface.NotifyShapeChanged(", + " body.GetID(),", + " sharedData.getVec3(0, 0, 0),", + " false,", + " Jolt.EActivation_DontActivate", + " );", + " this._wakeUpBodiesOnChangedSamples();", + " this.changedSamples.clear();", + " }", + "", + " /** Bodies sleeping on the changed area would otherwise float or sink. */", + " _wakeUpBodiesOnChangedSamples() {", + " const { object, data } = this.terrain;", + " const worldInvScale = this.physics._sharedData.worldInvScale;", + " const cellWidth = object.getWidth() / data.resolution;", + " const cellHeight = object.getHeight() / data.resolution;", + " const { minI, minJ, maxI, maxJ } = this.changedSamples;", + " const min = new Jolt.Vec3(", + " (object.getX() + (minI - 1) * cellWidth) * worldInvScale,", + " (object.getY() + (minJ - 1) * cellHeight) * worldInvScale,", + " (object.getZ() - 1) * worldInvScale", + " );", + " const max = new Jolt.Vec3(", + " (object.getX() + (maxI + 1) * cellWidth) * worldInvScale,", + " (object.getY() + (maxJ + 1) * cellHeight) * worldInvScale,", + " (object.getZ() + object.getDepth() + 1) * worldInvScale", + " );", + " const box = new Jolt.AABox(min, max);", + " const broadPhaseLayerFilter = new Jolt.BroadPhaseLayerFilter();", + " const objectLayerFilter = new Jolt.ObjectLayerFilter();", + " this.physics._sharedData.bodyInterface.ActivateBodiesInAABox(box, broadPhaseLayerFilter, objectLayerFilter);", + " Jolt.destroy(min);", + " Jolt.destroy(max);", + " Jolt.destroy(box);", + " Jolt.destroy(broadPhaseLayerFilter);", + " Jolt.destroy(objectLayerFilter);", + " }", + "", + " recreateShape() {", + " const body = this.physics._body;", + " if (!body) return;", + " this.physics._sharedData.bodyInterface.SetShape(", + " body.GetID(),", + " this.createShape(),", + " false,", + " Jolt.EActivation_DontActivate", + " );", + " const { resolution } = this.terrain.data;", + " this.changedSamples.set(0, 0, resolution, resolution);", + " this._wakeUpBodiesOnChangedSamples();", + " this.changedSamples.clear();", + " }", + "", + " destroyBody() {", + " this.defaultBodyUpdater.destroyBody();", + " this.heightField = null;", + " }", + "", + " dispose() {", + " if (this.heightsBufferPointer) Jolt._webidl_free(this.heightsBufferPointer);", + " this.heightsBufferPointer = 0;", + " this.heightsBufferLength = 0;", + " }", + "}", + "", + "/** @param {TerrainData} data */", + "const copyTerrainData = (data) => {", + " const copy = new TerrainData(data.resolution);", + " copy.copyFrom(data);", + " return copy;", + "};", + "", + "// Generating a relief or reading an image is long for big terrains, and done", + "// for every instance and every hot-reload: the results are kept.", + "const baseDataCache = new Map();", + "const MAX_CACHED_BASE_DATA = 8;", + "// Changes of the shape of a terrain kept for consumers of its surface that", + "// missed some versions: older changes update the whole surface.", + "const MAX_SURFACE_CHANGES = 64;", + "// Unique among all terrains: a terrain replaced by another one never has its version.", + "let lastGroundVersion = 0;", + "", + "/**", + " * The terrain of a Terrain3D object: its data, how it's drawn and its", + " * surface (for collisions and navigation meshes). Positions are in the scene.", + " */", + "class Terrain {", + " /** @param {gdjs.CustomRuntimeObject3D} object */", + " constructor(object) {", + " this.object = object;", + " /** @type {TerrainData} */", + " this.data = new TerrainData(DEFAULT_RESOLUTION);", + " /** The data before any sculpting or painting, from the relief or heightmap. */", + " this.baseData = this.data;", + " this.renderer = new TerrainRenderer(this);", + " /** @type {TerrainBodyUpdater[]} */", + " this.bodyUpdaters = [];", + " this.changedSamples = new SampleRectangle();", + " this.hasPaintChanged = false;", + " /** Increased each time the ground changes, for objects following it (like grass). */", + " this.version = 0;", + " /** Increased each time the shape of the ground changes (not its paint or position). */", + " this.surfaceVersion = 0;", + " /**", + " * The last changes of the shape of the ground, for `getChangedArea`.", + " * @type {Array<{version: number, samples: SampleRectangle}>}", + " */", + " this.surfaceChanges = [];", + " /** The changes since this version are all in `surfaceChanges`. */", + " this.surfaceChangesStartVersion = 0;", + " this._stroke = { cellWidth: 1, cellHeight: 1, ax: 0, ay: 0, bx: 0, by: 0, radius: 0 };", + " this._strokeChangedSamples = new SampleRectangle();", + " /** The properties the data was loaded from, to reload it only if they change. */", + " this.loadedFrom = '';", + " /** The heightmap image being loaded: the relief is used until it is. */", + " this.loadingHeightmapImage = '';", + " this.isDisposed = false;", + " /** The last value of the sculpt data property read or saved. */", + " this.sculptData = '';", + " /**", + " * Values saved by the editor tools: the editor sends them back with a", + " * hot-reload, possibly after newer ones were saved.", + " * @type {string[]}", + " */", + " this.savedSculptData = [];", + " /** The heights of the surface: the heights of the data, and NaN in holes. */", + " this.surfaceHeights = new Float32Array(0);", + " this.loadFromProperties();", + " this._transform = this._getTransform();", + " getGrounds(object.getRuntimeScene()).add(this);", + " /**", + " * The shape of the ground, for the physics engine and navigation meshes.", + " * @type {gdjs.Surface}", + " */", + " this.surface = {", + " getVersion: () => this.surfaceVersion,", + " getChangedArea: (sinceVersion) => this.getChangedArea(sinceVersion),", + " getTriangles: () => null,", + " getHeightField: () => ({ columns: this.data.size, rows: this.data.size, heights: this.surfaceHeights }),", + " };", + " if (typeof object.setSurface === 'function') {", + " object.setSurface(this.surface);", + " } else if (gdjs.Physics3DRuntimeBehavior) {", + " // Older GDevelop versions don't use surfaces.", + " for (const behavior of object._behaviors) {", + " if (behavior instanceof gdjs.Physics3DRuntimeBehavior) {", + " this.bodyUpdaters.push(new TerrainBodyUpdater(this, behavior));", + " }", + " }", + " }", + " }", + "", + " _getSourceProperties() {", + " const { object } = this;", + " return [", + " object._getResolution(),", + " object._getRelief(),", + " object._getSeed(),", + " object._getHeightmapImage(),", + " object._getEdits(),", + " ].join('|');", + " }", + "", + " loadFromProperties() {", + " const { object } = this;", + " const resolution = parseInt(object._getResolution(), 10);", + " const validResolution = RESOLUTIONS.indexOf(resolution) !== -1 ? resolution : DEFAULT_RESOLUTION;", + " this.sculptData = object._getSculptData();", + " this.loadedFrom = this._getSourceProperties();", + "", + " this.baseData = this._getBaseData(validResolution);", + " let data = codec.decode(this.sculptData, (resolution) => copyTerrainData(this._getBaseData(resolution)));", + " if (data && data.resolution !== validResolution) {", + " data = data.resampled(validResolution);", + " }", + " this.setData(data || copyTerrainData(this.baseData));", + " }", + "", + " /**", + " * @param {number} resolution", + " * @returns {TerrainData} The data from the relief or the heightmap, shared: it must not be changed.", + " */", + " _getBaseData(resolution) {", + " const { object } = this;", + " const key = [resolution, object._getRelief(), object._getSeed(), object._getHeightmapImage(), object._getEdits()].join('|');", + " const cachedData = baseDataCache.get(key);", + " if (cachedData) return cachedData;", + "", + " const data = new TerrainData(resolution);", + " const heightmapImage = object._getHeightmapImage();", + " const isHeightmapLoaded =", + " !!heightmapImage && loadHeightsFromImage(data, object.getRuntimeScene().getGame(), heightmapImage);", + " if (!isHeightmapLoaded) {", + " relief.generate(data, object._getRelief(), object._getSeed());", + " if (heightmapImage) this._reloadOnceLoaded(heightmapImage);", + " }", + " edits.apply(data, object._getEdits());", + " // Not kept if the image is missing: it may be loaded later.", + " if (isHeightmapLoaded || !heightmapImage) baseDataCache.set(key, data);", + " if (baseDataCache.size > MAX_CACHED_BASE_DATA) {", + " baseDataCache.delete(baseDataCache.keys().next().value);", + " }", + " return data;", + " }", + "", + " /** @param {TerrainData} data */", + " setData(data) {", + " this.version = ++lastGroundVersion;", + " this.surfaceVersion = this.version;", + " this.surfaceChanges.length = 0;", + " this.surfaceChangesStartVersion = this.version;", + " const hasSameResolution = data.resolution === this.data.resolution && this.renderer.chunks.length > 0;", + " this.data = data;", + " const allSamples = new SampleRectangle().set(0, 0, data.resolution, data.resolution);", + " this.surfaceHeights = new Float32Array(data.heights.length);", + " this.renderer.setHasHoles(false);", + " this._updateSurfaceHeights(allSamples);", + " if (hasSameResolution) {", + " this.renderer.updateSamples(allSamples, true);", + " } else {", + " this.renderer.rebuild();", + " }", + " for (const bodyUpdater of this.bodyUpdaters) {", + " bodyUpdater.recreateShape();", + " }", + " }", + "", + " onHotReloading() {", + " const sculptData = this.object._getSculptData();", + " const savedIndex = this.savedSculptData.indexOf(sculptData);", + " if (savedIndex !== -1 && this._getSourceProperties() === this.loadedFrom) {", + " // The terrain already has this data, or newer changes.", + " this.savedSculptData.splice(0, savedIndex + 1);", + " } else if (sculptData !== this.sculptData || this._getSourceProperties() !== this.loadedFrom) {", + " this.savedSculptData.length = 0;", + " this.loadFromProperties();", + " }", + " this.renderer.updateMaterial();", + " }", + "", + " /** @returns {string} The data to save in the sculpt data property. */", + " save() {", + " this.sculptData = codec.encode(this.data, this.baseData);", + " this.savedSculptData.push(this.sculptData);", + " return this.sculptData;", + " }", + "", + " getCellWidth() {", + " return this.object.getWidth() / this.data.resolution;", + " }", + "", + " getCellHeight() {", + " return this.object.getHeight() / this.data.resolution;", + " }", + "", + " _toGridX(x) {", + " return (x - this.object.getX()) / this.getCellWidth();", + " }", + "", + " _toGridY(y) {", + " return (y - this.object.getY()) / this.getCellHeight();", + " }", + "", + " /** @returns {number} The Z position of the ground. */", + " getHeightAt(x, y) {", + " return this.object.getZ() + this.data.sampleHeight(this._toGridX(x), this._toGridY(y)) * this.object.getDepth();", + " }", + "", + " /** @returns {number} The angle of the ground with the horizontal, in degrees. */", + " getSlopeAt(x, y) {", + " const gridX = this._toGridX(x);", + " const gridY = this._toGridY(y);", + " const { data } = this;", + " const depth = this.object.getDepth();", + " const slopeX = ((data.sampleHeight(gridX + 0.5, gridY) - data.sampleHeight(gridX - 0.5, gridY)) * depth) / this.getCellWidth();", + " const slopeY = ((data.sampleHeight(gridX, gridY + 0.5) - data.sampleHeight(gridX, gridY - 0.5)) * depth) / this.getCellHeight();", + " return gdjs.toDegrees(Math.atan(Math.hypot(slopeX, slopeY)));", + " }", + "", + " /** @returns {number} The layer painted the most, from 1 to 4. */", + " getLayerAt(x, y) {", + " return this.data.getDominantLayer(this._toGridX(x), this._toGridY(y)) + 1;", + " }", + "", + " /**", + " * @param {number} layer From 1 to 4.", + " * @returns {number} How much the layer is painted, from 0 to 1.", + " */", + " getLayerWeightAt(x, y, layer) {", + " const layerIndex = clampInteger(layer - 1, 0, LAYER_COUNT - 1);", + " return this.data.sampleLayerWeight(this._toGridX(x), this._toGridY(y), layerIndex);", + " }", + "", + " /** @returns {number[]} The painted tint: red, green and blue, from 0 to 255 (white when there is none). */", + " getTintAt(x, y) {", + " const gridX = this._toGridX(x);", + " const gridY = this._toGridY(y);", + " return [0, 1, 2].map((channel) => Math.round(this.data.sampleColor(gridX, gridY, channel) * 255));", + " }", + "", + " /** @returns {number} How wet the ground is, from 0 (dry) to 1, painted or near the water (see the wet shore). */", + " getWetnessAt(x, y) {", + " const paintedWetness = this.data.sampleColor(this._toGridX(x), this._toGridY(y), 3);", + " const { object } = this;", + " if (!object._getWetShore()) return paintedWetness;", + " const fade = Math.max(object._getWetShoreFade(), 0.001);", + " const shoreWetness = clamp01((object._getWetShoreZ() + fade - this.getHeightAt(x, y)) / fade);", + " // Like the smoothstep of the shader.", + " return Math.max(paintedWetness, shoreWetness * shoreWetness * (3 - 2 * shoreWetness));", + " }", + "", + " _isInArea(x, y) {", + " const { object } = this;", + " return (", + " x >= object.getX() &&", + " x <= object.getX() + object.getWidth() &&", + " y >= object.getY() &&", + " y <= object.getY() + object.getHeight()", + " );", + " }", + "", + " /** @returns {boolean} true if the position is above or under the terrain, and not in a hole. */", + " containsPoint(x, y) {", + " return this._isInArea(x, y) && this.data.sampleSolidity(this._toGridX(x), this._toGridY(y)) >= 0.5;", + " }", + "", + " /** @returns {boolean} true if there is a hole in the terrain at the position, like what is drawn. */", + " isHoleAt(x, y) {", + " return this._isInArea(x, y) && this.data.sampleSolidity(this._toGridX(x), this._toGridY(y)) < 0.5;", + " }", + "", + " _getStroke(x1, y1, x2, y2, radius) {", + " const stroke = this._stroke;", + " stroke.cellWidth = this.getCellWidth();", + " stroke.cellHeight = this.getCellHeight();", + " stroke.ax = x1 - this.object.getX();", + " stroke.ay = y1 - this.object.getY();", + " stroke.bx = x2 - this.object.getX();", + " stroke.by = y2 - this.object.getY();", + " stroke.radius = Math.max(radius, 0);", + " return stroke;", + " }", + "", + " /**", + " * @param {SampleRectangle} samples", + " * @param {boolean} haveHeightsChanged false when only the paint changed.", + " */", + " _onSamplesChanged(samples, haveHeightsChanged) {", + " if (samples.isEmpty()) return;", + " this.version = ++lastGroundVersion;", + " this.changedSamples.add(samples);", + " // Painting changes the surface when it makes holes or fills them.", + " const hasSurfaceChanged = this._updateSurfaceHeights(samples);", + " if (!haveHeightsChanged && !hasSurfaceChanged) return;", + " this.surfaceVersion = this.version;", + " this.surfaceChanges.push({ version: this.version, samples: new SampleRectangle().set(samples.minI, samples.minJ, samples.maxI, samples.maxJ) });", + " if (this.surfaceChanges.length > MAX_SURFACE_CHANGES) {", + " this.surfaceChangesStartVersion = this.surfaceChanges.shift().version;", + " }", + " for (const bodyUpdater of this.bodyUpdaters) {", + " bodyUpdater.changedSamples.add(samples);", + " }", + " }", + "", + " /**", + " * @param {SampleRectangle} samples", + " * @returns {boolean} true if heights of the surface changed, or holes were made or filled.", + " */", + " _updateSurfaceHeights(samples) {", + " const { data, surfaceHeights } = this;", + " let hasChanged = false;", + " let hasHoles = false;", + " for (let j = samples.minJ; j <= samples.maxJ; j++) {", + " for (let i = samples.minI; i <= samples.maxI; i++) {", + " const index = j * data.size + i;", + " const isHole = data.isHole(index);", + " const height = isHole ? NaN : data.heights[index];", + " const previousHeight = surfaceHeights[index];", + " if (isHole) {", + " hasHoles = true;", + " if (Number.isNaN(previousHeight)) continue;", + " } else if (height === previousHeight) {", + " continue;", + " }", + " surfaceHeights[index] = height;", + " hasChanged = true;", + " }", + " }", + " // Filled holes keep the shaders drawing holes until the data is loaded again.", + " if (hasHoles) this.renderer.setHasHoles(true);", + " return hasChanged;", + " }", + "", + " /** @param {number} height In scene units, negative to lower the ground. */", + " raise(x1, y1, x2, y2, radius, height) {", + " const samples = this._strokeChangedSamples;", + " brushes.raise(this.data, this._getStroke(x1, y1, x2, y2, radius), height / this.object.getDepth(), samples);", + " this._onSamplesChanged(samples, true);", + " }", + "", + " /** @param {number} z The Z position to flatten the ground to. */", + " /** @param {number} [toZ] The Z position at the end of the segment, for a slope. The same as z by default. */", + " flatten(x1, y1, x2, y2, radius, z, strength, toZ = z) {", + " const samples = this._strokeChangedSamples;", + " const toHeight = (value) => (value - this.object.getZ()) / this.object.getDepth();", + " brushes.flatten(this.data, this._getStroke(x1, y1, x2, y2, radius), toHeight(z), strength, samples, toHeight(toZ));", + " this._onSamplesChanged(samples, true);", + " }", + "", + " smooth(x1, y1, x2, y2, radius, strength) {", + " const samples = this._strokeChangedSamples;", + " brushes.smooth(this.data, this._getStroke(x1, y1, x2, y2, radius), strength, samples);", + " this._onSamplesChanged(samples, true);", + " }", + "", + " /** @param {number} layer From 1 to 4. */", + " paint(x1, y1, x2, y2, radius, layer, strength) {", + " const layerIndex = clampInteger(layer - 1, 0, LAYER_COUNT - 1);", + " const samples = this._strokeChangedSamples;", + " brushes.paint(this.data, this._getStroke(x1, y1, x2, y2, radius), layerIndex, strength, samples);", + " this.hasPaintChanged = true;", + " this._onSamplesChanged(samples, false);", + " }", + "", + " /** @param {number[]} color The red, green and blue (0-255) of the tint: white removes it. */", + " tint(x1, y1, x2, y2, radius, color, strength) {", + " const samples = this._strokeChangedSamples;", + " brushes.tint(this.data, this._getStroke(x1, y1, x2, y2, radius), color, strength, samples);", + " this.hasPaintChanged = true;", + " this._onSamplesChanged(samples, false);", + " }", + "", + " /** @param {number} wetness From 0 (dry) to 1. */", + " wet(x1, y1, x2, y2, radius, wetness, strength) {", + " const samples = this._strokeChangedSamples;", + " brushes.wet(this.data, this._getStroke(x1, y1, x2, y2, radius), wetness, strength, samples);", + " this.hasPaintChanged = true;", + " this._onSamplesChanged(samples, false);", + " }", + "", + " /** Make a hole around a segment (`fillHoles` fills it again). */", + " makeHole(x1, y1, x2, y2, radius) {", + " const samples = this._strokeChangedSamples;", + " brushes.hole(this.data, this._getStroke(x1, y1, x2, y2, radius), samples);", + " this.hasPaintChanged = true;", + " this._onSamplesChanged(samples, false);", + " }", + "", + " /** Fill the holes around a segment with the layers of the ground around them. */", + " fillHoles(x1, y1, x2, y2, radius) {", + " const samples = this._strokeChangedSamples;", + " brushes.fill(this.data, this._getStroke(x1, y1, x2, y2, radius), samples);", + " this.hasPaintChanged = true;", + " this._onSamplesChanged(samples, false);", + " }", + "", + " /**", + " * Make the entrance of a tunnel or a cave: a hole, with the ground in", + " * front of it flattened to the floor of the tunnel.", + " * @param {number} angle The direction going into the ground, in degrees.", + " * @param {number} floorZ", + " * @param {number} length The length of the flat ground in front of the entrance.", + " */", + " carveEntrance(x, y, radius, angle, floorZ, length) {", + " const directionX = Math.cos(gdjs.toRad(angle));", + " const directionY = Math.sin(gdjs.toRad(angle));", + " this.flatten(x - directionX * length, y - directionY * length, x, y, radius * 1.5, floorZ, 1);", + " this.makeHole(x, y, x + directionX * radius, y + directionY * radius, radius);", + " }", + "", + " generate(reliefName, seed) {", + " this.baseData = new TerrainData(this.data.resolution);", + " relief.generate(this.baseData, reliefName, seed);", + " this.setData(copyTerrainData(this.baseData));", + " }", + "", + " loadHeightmap(imageResourceName) {", + " const baseData = new TerrainData(this.data.resolution);", + " if (loadHeightsFromImage(baseData, this.object.getRuntimeScene().getGame(), imageResourceName)) {", + " baseData.splat.set(this.data.splat);", + " baseData.colors.set(this.data.colors);", + " this.baseData = baseData;", + " this.setData(copyTerrainData(baseData));", + " }", + " }", + "", + " /** @param {gdjs.RuntimeObject[]} objects */", + " placeOnGround(objects) {", + " for (const object of objects) {", + " if (object === this.object || !gdjs.Base3DHandler.is3D(object)) continue;", + " const groundZ = this.getHeightAt(object.getCenterXInScene(), object.getCenterYInScene());", + " // The origin of the object is not always at its bottom.", + " object.setZ(groundZ + object.getZ() - object.getUnrotatedAABBMinZ());", + " }", + " }", + "", + " /**", + " * @returns {THREE.Vector3 | null} The point of the ground under the cursor,", + " * in the terrain group coordinates. Holes are hit too, so that brushes can fill them.", + " */", + " raycastFromCursor(game, raycaster, ndc) {", + " const layer = this.object.getInstanceContainer().getLayer(this.object.getLayer());", + " const camera = layer.getRenderer().getThreeCamera();", + " if (!camera) return null;", + " const inputManager = game.getInputManager();", + " ndc.set(", + " (inputManager.getCursorX() / game.getGameResolutionWidth()) * 2 - 1,", + " -(inputManager.getCursorY() / game.getGameResolutionHeight()) * 2 + 1", + " );", + " raycaster.setFromCamera(ndc, camera);", + " this.renderer.group.updateWorldMatrix(true, false);", + " const ray = raycaster.ray.clone().applyMatrix4(this.renderer.group.matrixWorld.clone().invert());", + " ray.direction.normalize();", + "", + " const box = new THREE.Box3(new THREE.Vector3(0, 0, 0), new THREE.Vector3(AREA_SIZE, AREA_SIZE, AREA_DEPTH));", + " const entry = box.containsPoint(ray.origin) ? ray.origin.clone() : ray.intersectBox(box, new THREE.Vector3());", + " if (!entry) return null;", + " const cellSize = AREA_SIZE / this.data.resolution;", + " const groundZAt = (point) => this.data.sampleHeight(point.x / cellSize, point.y / cellSize) * AREA_DEPTH;", + " const step = cellSize / 2;", + " const point = entry.clone();", + " const previousPoint = entry.clone();", + " for (let distance = 0; distance < AREA_SIZE * 2; distance += step) {", + " if (point.z <= groundZAt(point)) {", + " // Refine between the last point above the ground and this one.", + " for (let iteration = 0; iteration < 8; iteration++) {", + " const middle = previousPoint.clone().lerp(point, 0.5);", + " if (middle.z <= groundZAt(middle)) point.copy(middle);", + " else previousPoint.copy(middle);", + " }", + " return point;", + " }", + " if (!box.containsPoint(point)) return null;", + " previousPoint.copy(point);", + " point.addScaledVector(ray.direction, step);", + " }", + " return null;", + " }", + "", + " /**", + " * @param {THREE.Vector3} localPosition In the terrain group coordinates (like from `raycastFromCursor`).", + " * @returns {{x: number, y: number, z: number}} The position in the scene.", + " */", + " toScenePosition(localPosition) {", + " const { object } = this;", + " return {", + " x: object.getX() + (localPosition.x / AREA_SIZE) * object.getWidth(),", + " y: object.getY() + (localPosition.y / AREA_SIZE) * object.getHeight(),", + " z: object.getZ() + (localPosition.z / AREA_DEPTH) * object.getDepth(),", + " };", + " }", + "", + " /** Draw the changes of the ground (collisions are updated by the physics engine). */", + " applyChanges() {", + " this.renderer.updateSamples(this.changedSamples, this.hasPaintChanged);", + " this.changedSamples.clear();", + " this.hasPaintChanged = false;", + " }", + "", + " /** @returns {number} A number changing each time the ground changes (see `getGrounds`). */", + " getVersion() {", + " return this.version;", + " }", + "", + " /**", + " * @param {number} sinceVersion", + " * @returns {gdjs.SurfaceArea | null} The part of the ground whose shape changed since the version (see `gdjs.Surface`).", + " */", + " getChangedArea(sinceVersion) {", + " if (sinceVersion < this.surfaceChangesStartVersion) return null;", + " const samples = new SampleRectangle();", + " for (const change of this.surfaceChanges) {", + " if (change.version > sinceVersion) samples.add(change.samples);", + " }", + " if (samples.isEmpty()) return { minX: 0, minY: 0, maxX: 0, maxY: 0 };", + " const { resolution } = this.data;", + " return {", + " minX: samples.minI / resolution,", + " minY: samples.minJ / resolution,", + " maxX: samples.maxI / resolution,", + " maxY: samples.maxJ / resolution,", + " };", + " }", + "", + " _getTransform() {", + " const { object } = this;", + " return [object.getX(), object.getY(), object.getZ(), object.getWidth(), object.getHeight(), object.getDepth()].join(',');", + " }", + "", + " /** @param {string} imageResourceName A heightmap image that is not loaded yet. */", + " _reloadOnceLoaded(imageResourceName) {", + " if (this.loadingHeightmapImage === imageResourceName) return;", + " this.loadingHeightmapImage = imageResourceName;", + " const game = this.object.getRuntimeScene().getGame();", + " game.getImageManager()", + " .loadResource(imageResourceName)", + " .then(() => {", + " if (this.isDisposed || this.loadingHeightmapImage !== imageResourceName) return;", + " this.loadingHeightmapImage = '';", + " if (getLoadedImage(game, imageResourceName)) this.loadFromProperties();", + " })", + " .catch(() => {});", + " }", + "", + " update() {", + " const transform = this._getTransform();", + " if (transform !== this._transform) {", + " this._transform = transform;", + " this.version = ++lastGroundVersion;", + " }", + " this.applyChanges();", + " this.renderer.uniforms.terrainWorldScale.value = getWorldScale(this.object);", + " this._updateWetShore();", + " const layer = this.object.getInstanceContainer().getLayer(this.object.getLayer());", + " this.renderer.updateLevelsOfDetail(layer.getRenderer().getThreeCamera());", + " }", + "", + " _updateWetShore() {", + " const { object } = this;", + " // In the coordinates of the terrain group, like the vertices.", + " const depthScale = AREA_DEPTH / Math.max(object.getDepth(), 0.0001);", + " this.renderer.uniforms.terrainWetShore.value.set(", + " (object._getWetShoreZ() - object.getZ()) * depthScale,", + " Math.max(object._getWetShoreFade(), 0.001) * depthScale,", + " object._getWetShore() ? 1 : 0", + " );", + " }", + "", + " dispose() {", + " this.isDisposed = true;", + " getGrounds(this.object.getRuntimeScene()).delete(this);", + " if (typeof this.object.setSurface === 'function' && this.object.getSurface() === this.surface) {", + " this.object.setSurface(null);", + " }", + " this.renderer.dispose();", + " for (const bodyUpdater of this.bodyUpdaters) {", + " bodyUpdater.dispose();", + " }", + " }", + "}", + "", + "/**", + " * @param {gdjs.RuntimeObject} object", + " * @returns {number} The number of scene units by 3D world unit (1 in GDevelop versions before the world scale).", + " */", + "const getWorldScale = (object) => {", + " const scene = object.getRuntimeScene();", + " return typeof scene.getRenderer3DWorldScale === 'function' ? scene.getRenderer3DWorldScale() : 1;", + "};", + "", + "const averageColors = new WeakMap();", + "let white = null;", + "/**", + " * @param {THREE.Texture} texture", + " * @returns {THREE.Color} The average color of the texture (white when it's not loaded).", + " */", + "const getAverageColor = (texture) => {", + " const cachedColor = averageColors.get(texture);", + " if (cachedColor) return cachedColor;", + " const image = texture.image;", + " const isDrawable =", + " typeof document !== 'undefined' &&", + " ((image instanceof HTMLImageElement && image.complete && image.naturalWidth > 0) ||", + " image instanceof HTMLCanvasElement ||", + " (typeof ImageBitmap !== 'undefined' && image instanceof ImageBitmap));", + " if (!isDrawable) return white || (white = new THREE.Color(1, 1, 1));", + " const size = 8;", + " const canvas = document.createElement('canvas');", + " canvas.width = size;", + " canvas.height = size;", + " const context = canvas.getContext('2d');", + " context.drawImage(image, 0, 0, size, size);", + " const pixels = context.getImageData(0, 0, size, size).data;", + " let red = 0;", + " let green = 0;", + " let blue = 0;", + " for (let index = 0; index < pixels.length; index += 4) {", + " red += pixels[index];", + " green += pixels[index + 1];", + " blue += pixels[index + 2];", + " }", + " const count = size * size * 255;", + " const color = new THREE.Color().setRGB(red / count, green / count, blue / count, THREE.SRGBColorSpace);", + " averageColors.set(texture, color);", + " return color;", + "};", + "", + "const svgIcon = (content) =>", + " 'data:image/svg+xml,' +", + " encodeURIComponent('' + content + '');", + "const strokes = 'fill=\"none\" stroke=\"#000\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"';", + "const toolIcons = {", + " Raise: svgIcon(''),", + " Lower: svgIcon(''),", + " Smooth: svgIcon(''),", + " Flatten: svgIcon(''),", + " Size: svgIcon(''),", + " Strength: svgIcon(''),", + " Undo: svgIcon(''),", + " Hole: svgIcon(''),", + " FillHoles: svgIcon(''),", + " Wetness: svgIcon(''),", + "};", + "// Tints for the color brush when the editor has no color picker (older GDevelop versions).", + "const tintPresets = ['#e2c879', '#a5c45a', '#8a6a48', '#c98a62', '#9a9a92', '#ffffff'];", + "", + "/**", + " * The sculpt and paint tools shown in the scene editor when a terrain is", + " * selected. Without the needed editor functions (older GDevelop versions),", + " * nothing is shown: the terrain can still be changed with events.", + " */", + "class TerrainEditorTools {", + " constructor() {", + " /** '' when no brush is used. */", + " this.toolName = '';", + " this.sizePercent = 30;", + " this.strengthPercent = 50;", + " /** @type {Terrain | null} */", + " this.terrain = null;", + " /** @type {gdjs.InGameEditor | null} */", + " this.editor = null;", + " this.isStroking = false;", + " this.flattenHeight = 0;", + " /** The color of the \"Tint\" brush, as \"#rrggbb\". */", + " this.tintColor = tintPresets[0];", + " this.wetnessPercent = 100;", + " this.lastStrokeTime = 0;", + " /** @type {{data: TerrainData, baseData: TerrainData}[]} */", + " this.undoSteps = [];", + " this.raycaster = new THREE.Raycaster();", + " this.ndc = new THREE.Vector2();", + " this._swatchColor = new THREE.Color();", + " }", + "", + " /** @param {gdjs.InGameEditor} editor */", + " update(editor) {", + " if (typeof editor.showToolbar !== 'function') {", + " // Older GDevelop versions: the terrain can only be changed with events.", + " return;", + " }", + " this.editor = editor;", + " const selectedObjects = editor.getSelectedObjects();", + " const terrain =", + " selectedObjects.length === 1 && selectedObjects[0].__terrain3D ? selectedObjects[0].__terrain3D : null;", + " if (terrain !== this.terrain) {", + " this._endStroke();", + " if (this.terrain) this.terrain.renderer.setBrush(null, 0);", + " this.terrain = terrain;", + " this.undoSteps.length = 0;", + " }", + " if (terrain) editor.showToolbar('Terrain3D', this._getToolbarItems(terrain));", + " const game = editor.getRuntimeGame();", + " if (!terrain || !this.toolName) {", + " if (terrain) terrain.renderer.setBrush(null, 0);", + " return;", + " }", + "", + " const inputManager = game.getInputManager();", + " if (inputManager.wasKeyJustPressed(gdjs.evtTools.input.keysNameToCode.Escape)) {", + " this._endStroke();", + " this.toolName = '';", + " terrain.renderer.setBrush(null, 0);", + " return;", + " }", + " editor.captureLeftMouseButton();", + " const hit = terrain.raycastFromCursor(game, this.raycaster, this.ndc);", + " const radius = this._getRadius();", + " terrain.renderer.setBrush(hit, radius);", + "", + " // Older GDevelop versions don't have it.", + " const isPressedForTool =", + " typeof editor.isCapturedLeftMouseButtonPressed === 'function'", + " ? editor.isCapturedLeftMouseButtonPressed()", + " : inputManager.isMouseButtonPressed(0);", + " // The button is released outside the canvas when the mouse is on a toolbar.", + " const isPressed = isPressedForTool && inputManager.isMouseInsideCanvas();", + " if (isPressed && hit && !this.isStroking) {", + " this._startStroke(terrain, hit);", + " }", + " if (this.isStroking && isPressed && hit) {", + " this._applyBrush(terrain, hit, radius);", + " }", + " if (!isPressed) {", + " this._endStroke();", + " }", + " }", + "", + " _getRadius() {", + " const { object } = this.terrain;", + " const maxRadius = Math.max(object.getWidth(), object.getHeight()) / 4;", + " return Math.max(maxRadius * Math.pow(this.sizePercent / 100, 2), this.terrain.getCellWidth());", + " }", + "", + " _startStroke(terrain, hit) {", + " this.isStroking = true;", + " this.lastStrokeTime = performance.now();", + " this.flattenHeight = terrain.toScenePosition(hit).z;", + " this.undoSteps.push({ data: copyTerrainData(terrain.data), baseData: terrain.baseData });", + " const maxUndoSteps = Math.max(Math.floor(UNDO_STEPS_SAMPLES / terrain.data.heights.length), 2);", + " while (this.undoSteps.length > maxUndoSteps) this.undoSteps.shift();", + " }", + "", + " _endStroke() {", + " if (!this.isStroking) return;", + " this.isStroking = false;", + " this._save();", + " }", + "", + " _applyBrush(terrain, hit, radius) {", + " const now = performance.now();", + " const elapsedSeconds = Math.min((now - this.lastStrokeTime) / 1000, 0.1);", + " this.lastStrokeTime = now;", + " const { x, y } = terrain.toScenePosition(hit);", + " const amount = (this.strengthPercent / 100) * elapsedSeconds;", + " const depth = terrain.object.getDepth();", + " if (this.toolName === 'Raise') terrain.raise(x, y, x, y, radius, amount * depth * 0.5);", + " else if (this.toolName === 'Lower') terrain.raise(x, y, x, y, radius, -amount * depth * 0.5);", + " else if (this.toolName === 'Smooth') terrain.smooth(x, y, x, y, radius, amount * 10);", + " else if (this.toolName === 'Flatten') terrain.flatten(x, y, x, y, radius, this.flattenHeight, amount * 10);", + " else if (this.toolName.startsWith('Paint')) {", + " terrain.paint(x, y, x, y, radius, parseInt(this.toolName.slice(5), 10), amount * 8);", + " } else if (this.toolName === 'Hole') {", + " terrain.makeHole(x, y, x, y, radius);", + " } else if (this.toolName === 'FillHoles') {", + " terrain.fillHoles(x, y, x, y, radius);", + " } else if (this.toolName === 'Tint') {", + " terrain.tint(x, y, x, y, radius, gdjs.rgbOrHexToRGBColor(this.tintColor), amount * 8);", + " } else if (this.toolName === 'Wetness') {", + " terrain.wet(x, y, x, y, radius, this.wetnessPercent / 100, amount * 8);", + " }", + " // Drawn now, as the terrain was updated earlier in this frame.", + " terrain.applyChanges();", + " }", + "", + " _save() {", + " const { terrain, editor } = this;", + " if (!terrain || !editor) return;", + " editor.updateObjectProperties(terrain.object.getName(), { SculptData: terrain.save() });", + " }", + "", + " _undo() {", + " const undoStep = this.undoSteps.pop();", + " // The relief, the edits or the resolution may have changed since the stroke.", + " if (!this.terrain || !undoStep || undoStep.baseData !== this.terrain.baseData) {", + " this.undoSteps.length = 0;", + " return;", + " }", + " this.terrain.setData(undoStep.data);", + " this._save();", + " }", + "", + " /** @returns {Array} The settings of the tint or wetness brush, when it's used. */", + " _getColorToolItems() {", + " const { editor } = this;", + " if (this.toolName === 'Tint') {", + " if (editor && typeof editor.isToolbarItemTypeSupported === 'function' && editor.isToolbarItemTypeSupported('color')) {", + " return [", + " {", + " type: 'color',", + " id: 'TintColor',", + " tooltip: 'Color of the tint',", + " value: this.tintColor,", + " onChange: (color) => (this.tintColor = color),", + " },", + " ];", + " }", + " return tintPresets.map((color) => ({", + " type: 'button',", + " id: 'TintPreset' + color,", + " tooltip: color === '#ffffff' ? 'No tint' : 'Tint with this color',", + " color,", + " isActive: this.tintColor === color,", + " onClick: () => (this.tintColor = color),", + " }));", + " }", + " if (this.toolName === 'Wetness') {", + " return [", + " {", + " type: 'slider',", + " id: 'WetnessAmount',", + " tooltip: 'Wetness',", + " iconUrl: toolIcons.Wetness,", + " min: 0,", + " max: 100,", + " value: this.wetnessPercent,", + " onChange: (value) => (this.wetnessPercent = value),", + " },", + " ];", + " }", + " return [];", + " }", + "", + " /**", + " * @param {Terrain} terrain", + " * @returns {Array} The toolbar for the current state (the editor only", + " * updates what changed).", + " */", + " _getToolbarItems(terrain) {", + " const { object } = terrain;", + " const toolButton = (toolName, tooltip, color) => ({", + " type: 'button',", + " id: toolName,", + " tooltip,", + " iconUrl: toolIcons[toolName],", + " color,", + " isActive: this.toolName === toolName,", + " onClick: () => (this.toolName = this.toolName === toolName ? '' : toolName),", + " });", + " const layerColors = [object._getLayer1Color(), object._getLayer2Color(), object._getLayer3Color(), object._getLayer4Color()];", + " // Layers are shown with their color, tinting their texture.", + " const swatchColors = layerColors.map((color, index) =>", + " this._swatchColor", + " .set(gdjs.rgbOrHexStringToNumber(color))", + " .multiply(getAverageColor(terrain.renderer.uniforms['terrainLayerMap' + index].value))", + " .getHexString()", + " );", + " return [", + " toolButton('Raise', 'Raise the ground (drag on the terrain)'),", + " toolButton('Lower', 'Lower the ground'),", + " toolButton('Smooth', 'Smooth the ground'),", + " toolButton('Flatten', 'Flatten the ground to the height where the drag starts'),", + " { type: 'divider', id: 'PaintDivider' },", + " ...swatchColors.map((swatchColor, index) =>", + " toolButton('Paint' + (index + 1), 'Paint layer ' + (index + 1), '#' + swatchColor)", + " ),", + " toolButton('Hole', 'Make a hole, for the entrance of a tunnel or a cave'),", + " toolButton('FillHoles', 'Fill holes, with the layers of the ground around them'),", + " { type: 'divider', id: 'ColorDivider' },", + " toolButton('Tint', 'Tint the ground (white removes the tint)', this.tintColor),", + " toolButton('Wetness', 'Make the ground wet, or dry with a wetness of 0'),", + " ...this._getColorToolItems(),", + " { type: 'divider', id: 'BrushDivider' },", + " {", + " type: 'slider',", + " id: 'Size',", + " tooltip: 'Brush size',", + " iconUrl: toolIcons.Size,", + " min: 1,", + " max: 100,", + " value: this.sizePercent,", + " onChange: (value) => (this.sizePercent = value),", + " },", + " {", + " type: 'slider',", + " id: 'Strength',", + " tooltip: 'Brush strength',", + " iconUrl: toolIcons.Strength,", + " min: 5,", + " max: 100,", + " value: this.strengthPercent,", + " onChange: (value) => (this.strengthPercent = value),", + " },", + " { type: 'button', id: 'Undo', tooltip: 'Undo the last stroke on this terrain', iconUrl: toolIcons.Undo, onClick: () => this._undo() },", + " ];", + " }", + "}", + "", + "const editorTools = new TerrainEditorTools();", + "if (gdjs.registerInGameEditorPostStepCallback) {", + " gdjs.registerInGameEditorPostStepCallback((editor) => editorTools.update(editor));", + "}", + "", + "gdjs.__terrain3DExtension = {", + " Terrain,", + " // Used by the terrain grass and the scattered models.", + " getWorldScale,", + " getGrounds,", + " patchShaderCode,", + " createRandom,", + " findHighestGround,", + " GroundPlacement,", + " svgIcon,", + " toolIcons,", + " // Exposed for tests.", + " TerrainBodyUpdater,", + " TerrainData,", + " SampleRectangle,", + " brushes,", + " edits,", + " codec,", + " relief,", + "};" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [], + "objectGroups": [] + }, + { + "description": "Define grass helper classes JavaScript code.", + "fullName": "Define grass helper classes", + "functionType": "Action", + "name": "DefineGrassHelperClasses", + "private": true, + "sentence": "Define grass helper classes JavaScript code", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "if (gdjs.__terrainGrassExtension) {", + " return;", + "}", + "// Blades grow on the grounds of the scene: the 3D terrains (see `getGrounds`", + "// in the terrain helper).", + "", + "// Size of the object inner area: instances are scaled from it.", + "const AREA_SIZE = 1000;", + "const AREA_DEPTH = 40;", + "const CHUNK_SIZE = 500;", + "const MAX_BENDING_OBJECTS = 8;", + "", + "/**", + " * A blade: a thin, tapered strip. Its width and height are 1: blades are", + " * scaled by the shader.", + " */", + "const createBladeGeometry = () => {", + " const halfWidths = [0.5, 0.42, 0.28, 0];", + " const heights = [0, 0.35, 0.7, 1];", + " const positions = [];", + " halfWidths.forEach((halfWidth, level) => {", + " positions.push(-halfWidth, 0, heights[level]);", + " if (halfWidth > 0) positions.push(halfWidth, 0, heights[level]);", + " });", + " const indices = [0, 1, 2, 1, 3, 2, 2, 3, 4, 3, 5, 4, 4, 5, 6];", + " const geometry = new THREE.InstancedBufferGeometry();", + " geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));", + " geometry.setAttribute('normal', new THREE.Float32BufferAttribute(new Array(positions.length).fill(0), 3));", + " geometry.setIndex(indices);", + " return geometry;", + "};", + "", + "const vertexShaderDeclarations = `", + "attribute vec4 grassRoot;", + "attribute vec2 grassShape;", + "// The tint of the ground at the root (linear, from 0 to 1).", + "attribute vec3 grassGroundTint;", + "uniform mat3 grassWorldToLocal;", + "uniform float grassWorldScale;", + "uniform float grassBladeWidth;", + "uniform float grassTime;", + "uniform float grassWindStrength;", + "uniform float grassWindSpeed;", + "uniform vec4 grassBenders[${MAX_BENDING_OBJECTS}];", + "uniform vec3 grassBaseColor;", + "uniform vec3 grassTipColor;", + "varying vec3 vGrassColor;", + "`;", + "const vertexShaderPosition = `", + "// Blades are shaped in scene units (with Y flipped like the 3D world), then", + "// moved back in the (scaled) object space.", + "float bladeHeight = position.z;", + "float grassAngle = grassRoot.w;", + "float bladeWidth = position.x * grassBladeWidth;", + "vec3 blade = vec3(", + " bladeWidth * cos(grassAngle),", + " bladeWidth * sin(grassAngle),", + " bladeHeight * grassShape.x", + ");", + "vec3 rootWorld = (modelMatrix * vec4(grassRoot.xyz, 1.0)).xyz * grassWorldScale;", + "// Tips bend more than the base.", + "float bendFactor = bladeHeight * bladeHeight * grassShape.x;", + "float gust = sin(grassTime * grassWindSpeed + rootWorld.x * 0.013 + rootWorld.y * 0.011);", + "float flutter = sin(grassTime * grassWindSpeed * 2.7 + grassShape.y * 40.0) * 0.3;", + "blade.xy += vec2(0.8, 0.5) * (gust + flutter + 0.6) * grassWindStrength * 0.35 * bendFactor;", + "for (int i = 0; i < ${MAX_BENDING_OBJECTS}; i++) {", + " vec4 bender = grassBenders[i];", + " if (bender.z <= 0.0) continue;", + " vec2 away = rootWorld.xy - bender.xy;", + " float distanceToBender = length(away);", + " float push = 1.0 - smoothstep(bender.z * 0.3, bender.z, distanceToBender);", + " if (push > 0.0) {", + " blade.xy += normalize(away + vec2(0.001)) * push * bendFactor * 0.9;", + " blade.z *= 1.0 - push * 0.5;", + " }", + "}", + "vec3 transformed = grassRoot.xyz + grassWorldToLocal * (blade / grassWorldScale);", + "vGrassColor = mix(grassBaseColor, grassTipColor, bladeHeight) * (0.85 + 0.3 * grassShape.y) * grassGroundTint;", + "`;", + "const vertexShaderNormal = `", + "// Blades are lit like the ground below them.", + "transformedNormal = normalize((viewMatrix * vec4(0.0, 0.0, 1.0, 0.0)).xyz);", + "`;", + "const fragmentShaderDeclarations = `", + "varying vec3 vGrassColor;", + "`;", + "const fragmentShaderColor = `", + "diffuseColor.rgb *= vGrassColor;", + "`;", + "// Both faces of blades are lit like the ground (not flipped for back faces).", + "const fragmentShaderNormal = `", + "#include ", + "normal = normalize(vNormal);", + "nonPerturbedNormal = normal;", + "`;", + "", + "const { getWorldScale, patchShaderCode, createRandom, findHighestGround, GroundPlacement } = gdjs.__terrain3DExtension;", + "", + "/**", + " * The grass of a terrain grass object: blades drawn with GPU instancing, in chunks", + " * that are hidden when off-screen and thinned out with the distance. They grow", + " * on the grounds below, like 3D terrains (only where a given layer is painted).", + " */", + "class Grass {", + " /** @param {gdjs.CustomRuntimeObject3D} object */", + " constructor(object) {", + " this.object = object;", + " this.group = new THREE.Group();", + " object.get3DRendererObject().add(this.group);", + " this.bladeGeometry = createBladeGeometry();", + " /** @type {Array<{mesh: THREE.Mesh, blades: number, center: THREE.Vector3}>} */", + " this.chunks = [];", + " this.placement = new GroundPlacement(object.getRuntimeScene());", + " this._groundAtBlade = { ground: null, z: 0 };", + " this.uniforms = {", + " grassWorldToLocal: { value: new THREE.Matrix3() },", + " // Scene units by 3D world unit.", + " grassWorldScale: { value: 1 },", + " grassBladeWidth: { value: 1 },", + " grassTime: { value: 0 },", + " grassWindStrength: { value: 1 },", + " grassWindSpeed: { value: 1 },", + " grassBenders: { value: Array.from({ length: MAX_BENDING_OBJECTS }, () => new THREE.Vector4()) },", + " grassBaseColor: { value: new THREE.Color() },", + " grassTipColor: { value: new THREE.Color() },", + " };", + " this.material = new THREE.MeshStandardMaterial({ roughness: 1, metalness: 0, side: THREE.DoubleSide });", + " this.material.onBeforeCompile = (shader) => {", + " Object.assign(shader.uniforms, this.uniforms);", + " shader.vertexShader = patchShaderCode(shader.vertexShader, [", + " ['#include ', '#include \\n' + vertexShaderDeclarations],", + " ['#include ', vertexShaderPosition],", + " ['#include ', '#include \\n' + vertexShaderNormal],", + " ], 'TerrainGrass');", + " shader.fragmentShader = patchShaderCode(shader.fragmentShader, [", + " ['#include ', '#include \\n' + fragmentShaderDeclarations],", + " ['#include ', '#include \\n' + fragmentShaderColor],", + " ['#include ', fragmentShaderNormal],", + " ], 'TerrainGrass');", + " };", + " this.material.customProgramCacheKey = () => 'TerrainGrass';", + "", + " this._matrix = new THREE.Matrix4();", + " this._cameraPosition = new THREE.Vector3();", + " this._chunkPosition = new THREE.Vector3();", + " this._scale = new THREE.Vector3();", + " this.updateFromProperties();", + " }", + "", + " updateFromProperties() {", + " const { object } = this;", + " this.uniforms.grassBaseColor.value.set(gdjs.rgbOrHexStringToNumber(object._getBaseColor()));", + " this.uniforms.grassTipColor.value.set(gdjs.rgbOrHexStringToNumber(object._getTipColor()));", + " this.uniforms.grassWindStrength.value = object._getWindStrength();", + " this.uniforms.grassWindSpeed.value = object._getWindSpeed();", + " this.uniforms.grassBladeWidth.value = Math.max(object._getBladeWidth(), 0);", + " this.material.needsUpdate = true;", + " this.placement.placeAgain();", + " }", + "", + " _getPlacementKey() {", + " const { object } = this;", + " return [", + " object.getX(), object.getY(), object.getZ(), object.getWidth(), object.getHeight(), object.getDepth(),", + " object._getDensity(), object._getBladeHeight(), object._getGroundLayer(), object._getMaxSlope(),", + " object._getSeed(),", + " ].join('|');", + " }", + "", + " /** Place the blades again, for example after the ground or the object changed. */", + " rebuild() {", + " const grounds = Array.from(this.placement.grounds);", + " for (const chunk of this.chunks) {", + " chunk.mesh.removeFromParent();", + " chunk.mesh.geometry.dispose();", + " }", + " this.chunks.length = 0;", + "", + " const { object } = this;", + " const width = object.getWidth();", + " const height = object.getHeight();", + " const scaleX = width / AREA_SIZE;", + " const scaleY = height / AREA_SIZE;", + " const scaleZ = Math.max(object.getDepth() / AREA_DEPTH, 0.0001);", + " const bladesPerChunk = Math.round((Math.max(object._getDensity(), 0) * CHUNK_SIZE * CHUNK_SIZE) / 10000);", + " const groundLayer = parseInt(object._getGroundLayer(), 10) || 0;", + " const maxSlope = object._getMaxSlope();", + " const chunksX = Math.max(Math.ceil(width / CHUNK_SIZE), 1);", + " const chunksY = Math.max(Math.ceil(height / CHUNK_SIZE), 1);", + " const nextRandom = createRandom(object._getSeed() * 7919 + 1);", + " const bladeHeight = Math.max(object._getBladeHeight(), 0);", + "", + " for (let chunkY = 0; chunkY < chunksY; chunkY++) {", + " for (let chunkX = 0; chunkX < chunksX; chunkX++) {", + " const roots = new Float32Array(bladesPerChunk * 4);", + " const shapes = new Float32Array(bladesPerChunk * 2);", + " const groundTints = new Uint8Array(bladesPerChunk * 3).fill(255);", + " let blades = 0;", + " let minZ = Infinity;", + " let maxZ = -Infinity;", + " for (let attempt = 0; attempt < bladesPerChunk; attempt++) {", + " // In scene units, relative to the object.", + " const x = (chunkX + nextRandom()) * CHUNK_SIZE;", + " const y = (chunkY + nextRandom()) * CHUNK_SIZE;", + " const angle = nextRandom() * Math.PI * 2;", + " const heightFactor = 0.6 + 0.8 * nextRandom();", + " const tint = nextRandom();", + " const growth = nextRandom();", + " if (x > width || y > height) continue;", + " const sceneX = object.getX() + x;", + " const sceneY = object.getY() + y;", + " const { ground, z } = findHighestGround(grounds, sceneX, sceneY, this._groundAtBlade);", + " const groundZ = ground ? z : object.getZ();", + " if (ground) {", + " if (groundLayer > 0 && growth > ground.getLayerWeightAt(sceneX, sceneY, groundLayer)) continue;", + " if (ground.getSlopeAt(sceneX, sceneY) > maxSlope) continue;", + " if (typeof ground.getTintAt === 'function') {", + " const tint = ground.getTintAt(sceneX, sceneY);", + " for (let channel = 0; channel < 3; channel++) {", + " groundTints[blades * 3 + channel] = Math.round(255 * Math.pow(tint[channel] / 255, 2.2));", + " }", + " }", + " }", + " const localZ = (groundZ - object.getZ()) / scaleZ;", + " roots[blades * 4] = x / scaleX;", + " roots[blades * 4 + 1] = y / scaleY;", + " roots[blades * 4 + 2] = localZ;", + " roots[blades * 4 + 3] = angle;", + " shapes[blades * 2] = heightFactor * bladeHeight;", + " shapes[blades * 2 + 1] = tint;", + " minZ = Math.min(minZ, localZ);", + " maxZ = Math.max(maxZ, localZ);", + " blades++;", + " }", + " if (blades === 0) continue;", + "", + " const geometry = new THREE.InstancedBufferGeometry();", + " geometry.index = this.bladeGeometry.index;", + " geometry.setAttribute('position', this.bladeGeometry.attributes.position);", + " geometry.setAttribute('normal', this.bladeGeometry.attributes.normal);", + " geometry.setAttribute('grassRoot', new THREE.InstancedBufferAttribute(roots.subarray(0, blades * 4), 4));", + " geometry.setAttribute('grassShape', new THREE.InstancedBufferAttribute(shapes.subarray(0, blades * 2), 2));", + " geometry.setAttribute('grassGroundTint', new THREE.InstancedBufferAttribute(groundTints.subarray(0, blades * 3), 3, true));", + " geometry.instanceCount = blades;", + " // The furthest tips can go with the wind and the bending objects (see the shader).", + " const maxBladeHeight = bladeHeight * 1.4;", + " const lean =", + " (0.35 * 1.9 * 0.943 * Math.max(object._getWindStrength(), 0) + 0.9) * maxBladeHeight +", + " Math.max(object._getBladeWidth(), 0) / 2;", + " const leanX = lean / scaleX;", + " const leanY = lean / scaleY;", + " geometry.boundingBox = new THREE.Box3(", + " new THREE.Vector3((chunkX * CHUNK_SIZE) / scaleX - leanX, (chunkY * CHUNK_SIZE) / scaleY - leanY, minZ),", + " new THREE.Vector3(", + " Math.min((chunkX + 1) * CHUNK_SIZE, width) / scaleX + leanX,", + " Math.min((chunkY + 1) * CHUNK_SIZE, height) / scaleY + leanY,", + " maxZ + (bladeHeight * 1.4) / scaleZ", + " )", + " );", + " geometry.boundingSphere = geometry.boundingBox.getBoundingSphere(new THREE.Sphere());", + " const mesh = new THREE.Mesh(geometry, this.material);", + " mesh.receiveShadow = true;", + " // Raycasts would only test one blade: the selection plane is used instead.", + " mesh.raycast = () => {};", + " this.group.add(mesh);", + " this.chunks.push({ mesh, blades, center: geometry.boundingSphere.center.clone() });", + " }", + " }", + " }", + "", + " _updateBendingObjects() {", + " const benders = this.uniforms.grassBenders.value;", + " const objectName = this.object._getBendingObject();", + " const bendingObjects = objectName ? this.object.getInstanceContainer().getObjects(objectName) || [] : [];", + " for (let index = 0; index < MAX_BENDING_OBJECTS; index++) {", + " const bendingObject = bendingObjects[index];", + " if (!bendingObject) {", + " benders[index].set(0, 0, 0, 0);", + " continue;", + " }", + " // Blades are pushed in the 3D scene, which is flipped on Y.", + " const radius = Math.max(bendingObject.getWidth(), bendingObject.getHeight()) * 1.2;", + " benders[index].set(bendingObject.getCenterXInScene(), -bendingObject.getCenterYInScene(), radius, 0);", + " }", + " }", + "", + " /**", + " * @param {number} elapsedSeconds", + " * @param {number} time In seconds, the same for all grass objects.", + " */", + " update(elapsedSeconds, time, camera) {", + " if (this.placement.shouldPlaceAgain(elapsedSeconds, this._getPlacementKey())) {", + " this.rebuild();", + " }", + "", + " this.uniforms.grassTime.value = time;", + " this.uniforms.grassWorldScale.value = getWorldScale(this.object);", + " // The object transformation is applied just before rendering, it's needed now.", + " this.object.getRenderer().ensureUpToDate();", + " this.group.updateWorldMatrix(true, false);", + " this.uniforms.grassWorldToLocal.value.setFromMatrix4(this._matrix.copy(this.group.matrixWorld).invert());", + " this._updateBendingObjects();", + " if (!camera) return;", + "", + " // Far chunks are hidden, and thinned out before (in 3D world units).", + " const fadeDistance = Math.max(this.object._getFadeDistance(), 1) / getWorldScale(this.object);", + " this._cameraPosition.setFromMatrixPosition(camera.matrixWorld);", + " for (const chunk of this.chunks) {", + " const distance = this._chunkPosition", + " .copy(chunk.center)", + " .applyMatrix4(this.group.matrixWorld)", + " .distanceTo(this._cameraPosition);", + " const density = gdjs.evtTools.common.clamp((fadeDistance - distance) / (fadeDistance * 0.5), 0, 1);", + " chunk.mesh.visible = density > 0;", + " chunk.mesh.geometry.instanceCount = Math.ceil(chunk.blades * density);", + " }", + " }", + "", + " dispose() {", + " for (const chunk of this.chunks) {", + " chunk.mesh.geometry.dispose();", + " }", + " this.bladeGeometry.dispose();", + " this.material.dispose();", + " this.group.removeFromParent();", + " }", + "}", + "", + "gdjs.__terrainGrassExtension = { Grass };" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [], + "objectGroups": [] + }, + { + "description": "Define scattered models helper classes JavaScript code.", + "fullName": "Define scattered models helper classes", + "functionType": "Action", + "name": "DefineScatterHelperClasses", + "private": true, + "sentence": "Define scattered models helper classes JavaScript code", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "if (gdjs.__terrainScatterExtension) {", + " return;", + "}", + "// Copies grow on the grounds of the scene, like the terrain grass (see", + "// `getGrounds` in the terrain helper).", + "", + "const {", + " getWorldScale,", + " getGrounds,", + " patchShaderCode,", + " createRandom,", + " findHighestGround,", + " GroundPlacement,", + " SampleRectangle,", + " brushes,", + " codec,", + " svgIcon,", + " toolIcons,", + "} = gdjs.__terrain3DExtension;", + "", + "// Size of the object inner area: instances are scaled from it.", + "const AREA_SIZE = 2000;", + "const AREA_DEPTH = 200;", + "// The painted density is stored in a grid of this many cells on each side,", + "// whatever the size of the object.", + "const MASK_RESOLUTION = 256;", + "const MASK_FORMAT_VERSION = '1';", + "// Copies are hidden in chunks: big chunks mean fewer draw calls, small ones", + "// hide more copies out of the screen.", + "const MIN_CHUNK_SIZE = 500;", + "const MAX_CHUNK_SIZE = 2000;", + "// Copies shrink to nothing over this part of the draw distance, instead of popping out.", + "const FADE_PART = 0.15;", + "const MAX_COPIES = 200000;", + "// Prisms of the trunk collisions.", + "const TRUNK_SIDES = 6;", + "", + "const clamp01 = (value) => (value < 0 ? 0 : value > 1 ? 1 : value);", + "", + "/**", + " * @returns {number} A number for a cell of the copies grid, changing with the seed.", + " */", + "const hashCell = (seed, i, j) => {", + " let h = Math.imul(i, 374761393) + Math.imul(j, 668265263) + Math.imul(seed, 982451653);", + " h = Math.imul(h ^ (h >>> 13), 1274126177);", + " return (h ^ (h >>> 16)) >>> 0;", + "};", + "", + "/**", + " * How dense copies are, from 0 to 1, in a grid covering the object: painted", + " * with the editor brushes, the edits or events.", + " */", + "class DensityMask {", + " /** @param {number} fillValue From 0 to 255. */", + " constructor(fillValue) {", + " // `resolution` and `size` let terrain brushes paint it.", + " this.resolution = MASK_RESOLUTION;", + " this.size = MASK_RESOLUTION + 1;", + " this.values = new Uint8Array(this.size * this.size).fill(fillValue);", + " }", + "", + " /**", + " * @param {number} u From 0 to 1, along the object width.", + " * @param {number} v From 0 to 1, along the object height.", + " * @returns {number} The density, from 0 to 1, interpolated between samples.", + " */", + " sample(u, v) {", + " const x = clamp01(u) * this.resolution;", + " const y = clamp01(v) * this.resolution;", + " const i = Math.min(Math.floor(x), this.resolution - 1);", + " const j = Math.min(Math.floor(y), this.resolution - 1);", + " const fx = x - i;", + " const fy = y - j;", + " const value = (i, j) => this.values[j * this.size + i];", + " const top = value(i, j) + fx * (value(i + 1, j) - value(i, j));", + " const bottom = value(i, j + 1) + fx * (value(i + 1, j + 1) - value(i, j + 1));", + " return (top + fy * (bottom - top)) / 255;", + " }", + "", + " /**", + " * Bring the density towards a target around a segment: fully up to 70%", + " * of the radius, then fading out (forests have dense centers and soft edges).", + " * @param {{cellWidth: number, cellHeight: number, ax: number, ay: number, bx: number, by: number, radius: number}} stroke", + " * @param {number} density The target, from 0 to 1.", + " * @param {number} strength From 0 to 1.", + " * @param {SampleRectangle} changedSamples", + " */", + " paint(stroke, density, strength, changedSamples) {", + " const { values } = this;", + " const target = Math.round(clamp01(density) * 255);", + " brushes.forEachSample(", + " this,", + " stroke,", + " (index, falloff) => {", + " // The falloff of terrain brushes is (1 - distance² / radius²)².", + " const weight = clamp01(2 * Math.sqrt(falloff));", + " // Rounded towards the target, so that light strokes still paint.", + " const change = (target - values[index]) * clamp01(weight * strength);", + " values[index] += change > 0 ? Math.ceil(change) : Math.floor(change);", + " },", + " changedSamples", + " );", + " }", + "", + " /** @returns {DensityMask} */", + " clone() {", + " const mask = new DensityMask(0);", + " mask.values.set(this.values);", + " return mask;", + " }", + "}", + "", + "/**", + " * Edits written in the Edits property: a JSON list of brush strokes, like", + " * `[{\"tool\": \"add\", \"x\": 0.5, \"y\": 0.5, \"radius\": 0.2}]`. Positions and", + " * radius are fractions of the object size, like the terrain edits.", + " */", + "const applyEdits = (mask, text) => {", + " if (!text.trim()) return;", + " let editList;", + " try {", + " editList = JSON.parse(text);", + " } catch (error) {", + " console.warn('The edits of scattered models are not valid JSON: ' + error.message);", + " return;", + " }", + " if (!Array.isArray(editList)) {", + " console.warn('The edits of scattered models must be a JSON list (like [{\"tool\": \"add\", ...}]).');", + " return;", + " }", + " const numberOr = (value, defaultValue) => (typeof value === 'number' && isFinite(value) ? value : defaultValue);", + " const changedSamples = new SampleRectangle();", + " for (const edit of editList) {", + " if (!edit || typeof edit !== 'object') continue;", + " const x = numberOr(edit.x, 0.5);", + " const y = numberOr(edit.y, 0.5);", + " const stroke = {", + " cellWidth: 1 / mask.resolution,", + " cellHeight: 1 / mask.resolution,", + " ax: x,", + " ay: y,", + " bx: numberOr(edit.toX, x),", + " by: numberOr(edit.toY, y),", + " radius: numberOr(edit.radius, 0.1),", + " };", + " const strength = clamp01(numberOr(edit.strength, 1));", + " if (edit.tool === 'add') {", + " mask.paint(stroke, numberOr(edit.density, 1), strength, changedSamples);", + " } else if (edit.tool === 'erase') {", + " mask.paint(stroke, 0, strength, changedSamples);", + " } else {", + " console.warn('Unknown tool in the edits of scattered models: \"' + edit.tool + '\" (use add or erase).');", + " }", + " }", + "};", + "", + "/**", + " * The density painted in the editor, stored as its difference with the", + " * density from the properties (the coverage and the edits), like the terrain", + " * sculpt data.", + " */", + "const maskCodec = {", + " /**", + " * @param {DensityMask} mask", + " * @param {DensityMask} baseMask", + " */", + " encode(mask, baseMask) {", + " const changes = new Int32Array(mask.values.length);", + " let hasChanges = false;", + " for (let index = 0; index < changes.length; index++) {", + " changes[index] = mask.values[index] - baseMask.values[index];", + " if (changes[index] !== 0) hasChanges = true;", + " }", + " if (!hasChanges) return '';", + " return MASK_FORMAT_VERSION + ';' + MASK_RESOLUTION + ';' + codec.encodeGrids([changes], mask.size);", + " },", + "", + " /**", + " * @param {string} text", + " * @param {DensityMask} baseMask", + " * @returns {DensityMask | null} null if the text is empty or not valid.", + " */", + " decode(text, baseMask) {", + " if (!text) return null;", + " const parts = text.split(';');", + " if (parts[0] !== MASK_FORMAT_VERSION || parseInt(parts[1], 10) !== MASK_RESOLUTION) return null;", + " try {", + " const mask = baseMask.clone();", + " const [changes] = codec.decodeGrids(parts[2], mask.size, 1);", + " for (let index = 0; index < changes.length; index++) {", + " // The base may have changed since the data was saved.", + " mask.values[index] = Math.min(Math.max(mask.values[index] + changes[index], 0), 255);", + " }", + " return mask;", + " } catch (error) {", + " console.error('The painted data of scattered models could not be read:', error);", + " return null;", + " }", + " },", + "};", + "", + "const fadeShaderDeclarations = `", + "uniform float scatterDrawDistance;", + "`;", + "// Instance matrices are applied after this: copies shrink towards their base.", + "const fadeShaderCode = `", + "#ifdef USE_INSTANCING", + "vec3 scatterCopyPosition = (modelMatrix * instanceMatrix * vec4(0.0, 0.0, 0.0, 1.0)).xyz;", + "transformed *= clamp(", + " (scatterDrawDistance - distance(scatterCopyPosition, cameraPosition)) / (scatterDrawDistance * ${FADE_PART}),", + " 0.0,", + " 1.0", + ");", + "#endif", + "`;", + "", + "const flipTriangles = (geometry) => {", + " const index = geometry.index;", + " for (let offset = 0; offset + 2 < index.count; offset += 3) {", + " const second = index.getX(offset + 1);", + " index.setX(offset + 1, index.getX(offset + 2));", + " index.setX(offset + 2, second);", + " }", + "};", + "", + "/**", + " * The parts of a model, ready to be drawn with instancing: each glTF", + " * primitive with its material. Geometries are moved so that the model base", + " * is at the origin, its height is 1 and it stands along Z (like 3D model", + " * objects with a 90° rotation around X).", + " * @param {THREE.Object3D} modelScene", + " * @param {object} uniforms Uniforms added to the materials.", + " * @returns {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}}", + " */", + "const createModelParts = (modelScene, uniforms) => {", + " modelScene.updateMatrixWorld(true);", + " const rootInverse = modelScene.matrixWorld.clone().invert();", + " const standUp = new THREE.Matrix4().makeRotationX(Math.PI / 2);", + " /** @type {Array<{mesh: THREE.Mesh, matrix: THREE.Matrix4}>} */", + " const meshes = [];", + " const box = new THREE.Box3();", + " modelScene.traverse((node) => {", + " if (!node.isMesh || !node.geometry || !node.geometry.attributes.position) return;", + " const matrix = standUp.clone().multiply(rootInverse).multiply(node.matrixWorld);", + " if (!node.geometry.boundingBox) node.geometry.computeBoundingBox();", + " box.union(node.geometry.boundingBox.clone().applyMatrix4(matrix));", + " meshes.push({ mesh: node, matrix });", + " });", + " if (box.isEmpty()) return { parts: [], width: 0, depth: 0 };", + "", + " const height = Math.max(box.max.z - box.min.z, 0.000001);", + " // The Y axis of the scene goes down: the model is flipped, like 3D model objects.", + " const normalization = new THREE.Matrix4()", + " .makeScale(1 / height, -1 / height, 1 / height)", + " .multiply(", + " new THREE.Matrix4().makeTranslation(", + " -(box.min.x + box.max.x) / 2,", + " -(box.min.y + box.max.y) / 2,", + " -box.min.z", + " )", + " );", + " const parts = meshes.map(({ mesh, matrix }) => {", + " let geometry = mesh.geometry.clone();", + " if (!geometry.index) {", + " const indices = Array.from({ length: geometry.attributes.position.count }, (_, index) => index);", + " geometry.setIndex(indices);", + " }", + " geometry.applyMatrix4(normalization.clone().multiply(matrix));", + " // The flip turned the faces inside out.", + " flipTriangles(geometry);", + " geometry.computeBoundingSphere();", + " geometry.computeBoundingBox();", + " const patchMaterial = (originalMaterial) => {", + " const material = originalMaterial.clone();", + " // Like 3D model objects (\"Standard without metalness\"), which look better without an environment map.", + " if (material.metalness) material.metalness = 0;", + " material.onBeforeCompile = (shader) => {", + " Object.assign(shader.uniforms, uniforms);", + " shader.vertexShader = patchShaderCode(shader.vertexShader, [", + " ['#include ', '#include \\n' + fadeShaderDeclarations],", + " ['#include ', '#include \\n' + fadeShaderCode],", + " ], 'ScatteredModels');", + " };", + " material.customProgramCacheKey = () => 'ScatteredModels';", + " return material;", + " };", + " const material = Array.isArray(mesh.material) ? mesh.material.map(patchMaterial) : patchMaterial(mesh.material);", + " return { geometry, material };", + " });", + " return {", + " parts,", + " width: (box.max.x - box.min.x) / height,", + " depth: (box.max.y - box.min.y) / height,", + " };", + "};", + "", + "/**", + " * A copy of the model: its base position in the scene, its scale (the", + " * height of the copy) and the key of its cell in the grid of copies.", + " * @typedef {{x: number, y: number, z: number, angle: number, height: number, key: number}} Copy", + " */", + "", + "/**", + " * The scattered models of an object: copies of a 3D model placed on the", + " * grounds of the scene, drawn with GPU instancing in chunks that are hidden", + " * when far or off-screen.", + " */", + "class Scatter {", + " /** @param {gdjs.CustomRuntimeObject3D} object */", + " constructor(object) {", + " this.object = object;", + " this.group = new THREE.Group();", + " object.get3DRendererObject().add(this.group);", + " /** @type {Array<{firstCellI: number, firstCellJ: number, meshes: THREE.InstancedMesh[], copies: Copy[], center: THREE.Vector3, radius: number}>} */", + " this.chunks = [];", + " /**", + " * The grid of copies: one copy at most in each cell, cells grouped in chunks.", + " * @type {{spacing: number, cellsPerChunk: number, cellsX: number, cellsY: number} | null}", + " */", + " this.grid = null;", + " this.placement = new GroundPlacement(object.getRuntimeScene());", + " this.uniforms = {", + " // In 3D world units.", + " scatterDrawDistance: { value: 1 },", + " };", + " /** @type {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}} */", + " this.model = { parts: [], width: 0, depth: 0 };", + " this.modelResourceName = '';", + " this.loadingModelResourceName = '';", + " /**", + " * Copies removed by events (for example, cut trees), by key.", + " * @type {Map}", + " */", + " this.removedCopies = new Map();", + " /** Increased each time copies change, for the collisions. */", + " this.version = 0;", + " this.isDisposed = false;", + " /** The last value of the painted data property read or saved. */", + " this.paintData = '';", + " /**", + " * Values saved by the editor tools: the editor sends them back with a", + " * hot-reload, possibly after newer ones were saved.", + " * @type {string[]}", + " */", + " this.savedPaintData = [];", + " this.maskSource = '';", + " /** @type {DensityMask} */", + " this.baseMask = new DensityMask(255);", + " /** @type {DensityMask} */", + " this.mask = this.baseMask;", + " this.changedSamples = new SampleRectangle();", + " this._stroke = { cellWidth: 1, cellHeight: 1, ax: 0, ay: 0, bx: 0, by: 0, radius: 0 };", + " this._groundAtCopy = { ground: null, z: 0 };", + " this._matrix = new THREE.Matrix4();", + " this._raycastSphere = new THREE.Sphere();", + " this._raycastRay = new THREE.Ray();", + " this._raycastBox = new THREE.Box3();", + " this._raycastInverseMatrix = new THREE.Matrix4();", + " this._raycastScale = new THREE.Vector3();", + " /** Placed on a copy to test the triangles of the model. */", + " this._raycastMesh = new THREE.Mesh();", + " /** @type {THREE.Intersection[]} */", + " this._raycastHits = [];", + " this._color = new THREE.Color();", + " this._tint = new THREE.Color();", + " this._cameraPosition = new THREE.Vector3();", + " this._chunkPosition = new THREE.Vector3();", + " /**", + " * Trunks, for the 3D physics behavior and navigation meshes. Always given,", + " * empty without trunk collisions: otherwise these behaviors would use the", + " * object box, covering the whole area.", + " * @type {gdjs.Surface}", + " */", + " this.surface = {", + " getVersion: () => this.version,", + " getChangedArea: () => null,", + " getTriangles: () => this._getTrunkTriangles(),", + " getHeightField: () => null,", + " };", + " /** The trunk collision properties, changing the surface. */", + " this.trunkKey = '';", + " this.loadMaskFromProperties();", + " this.updateFromProperties();", + " // Before the behaviors create their shapes.", + " this._updateSurface();", + " }", + "", + " _getMaskSource() {", + " return this.object._getCoverage() + '|' + this.object._getEdits();", + " }", + "", + " loadMaskFromProperties() {", + " const { object } = this;", + " this.maskSource = this._getMaskSource();", + " this.paintData = object._getPaintData();", + " this.baseMask = new DensityMask(object._getCoverage() === 'Painted' ? 0 : 255);", + " applyEdits(this.baseMask, object._getEdits());", + " this.mask = maskCodec.decode(this.paintData, this.baseMask) || this.baseMask.clone();", + " this.placement.placeAgain();", + " }", + "", + " onHotReloading() {", + " const paintData = this.object._getPaintData();", + " const savedIndex = this.savedPaintData.indexOf(paintData);", + " if (savedIndex !== -1 && this._getMaskSource() === this.maskSource) {", + " // The object already has this data, or newer changes.", + " this.savedPaintData.splice(0, savedIndex + 1);", + " } else if (paintData !== this.paintData || this._getMaskSource() !== this.maskSource) {", + " this.savedPaintData.length = 0;", + " this.loadMaskFromProperties();", + " }", + " this.updateFromProperties();", + " }", + "", + " /** @returns {string} The data to save in the painted data property. */", + " save() {", + " this.paintData = maskCodec.encode(this.mask, this.baseMask);", + " this.savedPaintData.push(this.paintData);", + " return this.paintData;", + " }", + "", + " updateFromProperties() {", + " const { object } = this;", + " const modelResourceName = object._getModel();", + " if (modelResourceName !== this.modelResourceName) {", + " this.modelResourceName = modelResourceName;", + " this._loadModel();", + " }", + " this.placement.placeAgain();", + " }", + "", + " _disposeModel() {", + " for (const part of this.model.parts) {", + " part.geometry.dispose();", + " for (const material of Array.isArray(part.material) ? part.material : [part.material]) {", + " material.dispose();", + " }", + " }", + " this.model = { parts: [], width: 0, depth: 0 };", + " }", + "", + " _loadModel() {", + " this._disposeChunks(this.chunks);", + " this._disposeModel();", + " const resourceName = this.modelResourceName;", + " if (!resourceName) return;", + " const game = this.object.getRuntimeScene().getGame();", + " const modelManager = game.getModel3DManager();", + " const gltf = modelManager.getModel(resourceName);", + " if (gltf === modelManager._invalidModel) {", + " // Not loaded yet, for example a model just chosen in the editor.", + " if (this.loadingModelResourceName === resourceName) return;", + " this.loadingModelResourceName = resourceName;", + " game.getResourceLoader()", + " .loadResource(resourceName)", + " .then(() => {", + " if (this.isDisposed || this.modelResourceName !== resourceName) return;", + " this.loadingModelResourceName = '';", + " if (modelManager.getModel(resourceName) !== modelManager._invalidModel) {", + " this._loadModel();", + " this.placement.placeAgain();", + " }", + " })", + " .catch(() => {});", + " return;", + " }", + " this.model = createModelParts(gltf.scene, this.uniforms);", + " }", + "", + " _getPlacementKey() {", + " const { object } = this;", + " return [", + " object.getX(), object.getY(), object.getZ(), object.getWidth(), object.getHeight(), object.getDepth(),", + " object._getDensity(), object._getSize(), object._getMinScale(), object._getMaxScale(),", + " object._getRandomRotation(), object._getRandomTint(), object._getGroundLayer(), object._getMaxSlope(),", + " object._getMinGroundZ(), object._getMaxGroundZ(), object._getSeed(), object._getDrawDistance(),", + " object._getCastShadow(), object._getReceiveShadow(), this.model.parts.length,", + " ].join('|');", + " }", + "", + " _getChunkSize() {", + " return Math.min(Math.max(this.object._getDrawDistance() / 4, MIN_CHUNK_SIZE), MAX_CHUNK_SIZE);", + " }", + "", + " /** @returns {number} The distance between copies (one copy per cell of this size at most). */", + " _getSpacing() {", + " const { object } = this;", + " const density = Math.max(object._getDensity(), 0);", + " if (density <= 0) return 0;", + " const spacing = 100 / Math.sqrt(density);", + " // Too many cells would freeze the game: the density is lowered.", + " const cellCount = (object.getWidth() / spacing) * (object.getHeight() / spacing);", + " return cellCount > MAX_COPIES ? spacing * Math.sqrt(cellCount / MAX_COPIES) : spacing;", + " }", + "", + " /**", + " * Find the copy of a cell of the grid, if there is one.", + " * @returns {Copy | null}", + " */", + " _placeCopy(cellI, cellJ, spacing, grounds) {", + " const { object } = this;", + " const width = object.getWidth();", + " const height = object.getHeight();", + " const nextRandom = createRandom(hashCell(Math.floor(object._getSeed()), cellI, cellJ));", + " // Always drawn in the same order, so that a copy stays the same when properties change.", + " const x = (cellI + nextRandom()) * spacing;", + " const y = (cellJ + nextRandom()) * spacing;", + " const presence = nextRandom();", + " const growth = nextRandom();", + " const scaleFactor = nextRandom();", + " const angle = nextRandom() * Math.PI * 2;", + " if (x > width || y > height) return null;", + " if (presence >= this.mask.sample(x / width, y / height)) return null;", + " const key = cellJ * 65536 + cellI;", + " if (this.removedCopies.has(key)) return null;", + "", + " const sceneX = object.getX() + x;", + " const sceneY = object.getY() + y;", + " const { ground, z } = findHighestGround(grounds, sceneX, sceneY, this._groundAtCopy);", + " const groundZ = ground ? z : object.getZ();", + " if (groundZ < object._getMinGroundZ() || groundZ > object._getMaxGroundZ()) return null;", + " const copyHeight =", + " Math.max(object._getSize(), 0) *", + " (object._getMinScale() + (object._getMaxScale() - object._getMinScale()) * scaleFactor);", + " if (ground) {", + " if (ground.getSlopeAt(sceneX, sceneY) > object._getMaxSlope()) return null;", + " const groundLayer = parseInt(object._getGroundLayer(), 10) || 0;", + " if (groundLayer > 0) {", + " if (growth > ground.getLayerWeightAt(sceneX, sceneY, groundLayer)) return null;", + " // Copies don't stand at the border of their layer, so that trees don't cover roads.", + " const margin = (copyHeight * Math.max(this.model.width, this.model.depth)) / 4;", + " for (const [offsetX, offsetY] of [[margin, 0], [-margin, 0], [0, margin], [0, -margin]]) {", + " if (ground.getLayerWeightAt(sceneX + offsetX, sceneY + offsetY, groundLayer) < 0.5) return null;", + " }", + " }", + " }", + " return {", + " x: sceneX,", + " y: sceneY,", + " z: groundZ,", + " angle: object._getRandomRotation() ? angle : 0,", + " height: copyHeight,", + " key,", + " };", + " }", + "", + " _disposeChunks(chunks) {", + " for (const chunk of chunks) {", + " for (const mesh of chunk.meshes) {", + " mesh.removeFromParent();", + " // Geometries and materials are the ones of the model.", + " mesh.dispose();", + " }", + " }", + " if (chunks === this.chunks) this.chunks = [];", + " }", + "", + " /** Place the copies again, for example after the ground or the object changed. */", + " rebuild() {", + " this._disposeChunks(this.chunks);", + " this.version++;", + " const { object } = this;", + " const spacing = this._getSpacing();", + " if (spacing <= 0 || this.model.parts.length === 0) {", + " this.grid = null;", + " return;", + " }", + " const cellsPerChunk = Math.max(Math.round(this._getChunkSize() / spacing), 1);", + " const grid = {", + " spacing,", + " cellsPerChunk,", + " cellsX: Math.ceil(object.getWidth() / spacing),", + " cellsY: Math.ceil(object.getHeight() / spacing),", + " };", + " this.grid = grid;", + " for (let firstCellJ = 0; firstCellJ < grid.cellsY; firstCellJ += cellsPerChunk) {", + " for (let firstCellI = 0; firstCellI < grid.cellsX; firstCellI += cellsPerChunk) {", + " this._createChunk(firstCellI, firstCellJ);", + " }", + " }", + " }", + "", + " /**", + " * Place again the copies of the chunks touching a rectangle of the scene,", + " * for example after painting.", + " */", + " _rebuildArea(minX, minY, maxX, maxY) {", + " const { grid, object } = this;", + " if (!grid) return;", + " const chunkSize = grid.spacing * grid.cellsPerChunk;", + " const firstChunkI = Math.max(Math.floor((minX - object.getX()) / chunkSize), 0);", + " const firstChunkJ = Math.max(Math.floor((minY - object.getY()) / chunkSize), 0);", + " const lastChunkI = Math.min(Math.floor((maxX - object.getX()) / chunkSize), Math.ceil(grid.cellsX / grid.cellsPerChunk) - 1);", + " const lastChunkJ = Math.min(Math.floor((maxY - object.getY()) / chunkSize), Math.ceil(grid.cellsY / grid.cellsPerChunk) - 1);", + " if (firstChunkI > lastChunkI || firstChunkJ > lastChunkJ) return;", + " const isInArea = (chunk) => {", + " const chunkI = chunk.firstCellI / grid.cellsPerChunk;", + " const chunkJ = chunk.firstCellJ / grid.cellsPerChunk;", + " return chunkI >= firstChunkI && chunkI <= lastChunkI && chunkJ >= firstChunkJ && chunkJ <= lastChunkJ;", + " };", + " this._disposeChunks(this.chunks.filter(isInArea));", + " this.chunks = this.chunks.filter((chunk) => !isInArea(chunk));", + " for (let chunkJ = firstChunkJ; chunkJ <= lastChunkJ; chunkJ++) {", + " for (let chunkI = firstChunkI; chunkI <= lastChunkI; chunkI++) {", + " this._createChunk(chunkI * grid.cellsPerChunk, chunkJ * grid.cellsPerChunk);", + " }", + " }", + " this.version++;", + " }", + "", + " _createChunk(firstCellI, firstCellJ) {", + " const { grid } = this;", + " const grounds = Array.from(this.placement.grounds);", + " /** @type {Copy[]} */", + " const copies = [];", + " for (let cellJ = firstCellJ; cellJ < Math.min(firstCellJ + grid.cellsPerChunk, grid.cellsY); cellJ++) {", + " for (let cellI = firstCellI; cellI < Math.min(firstCellI + grid.cellsPerChunk, grid.cellsX); cellI++) {", + " const copy = this._placeCopy(cellI, cellJ, grid.spacing, grounds);", + " if (copy) copies.push(copy);", + " }", + " }", + " if (copies.length === 0) return;", + "", + " const { object } = this;", + " // Copies are placed in scene units, and the object group is scaled", + " // from its inner area: they are scaled back to keep their proportions.", + " const scaleX = Math.max(object.getWidth() / AREA_SIZE, 0.0001);", + " const scaleY = Math.max(object.getHeight() / AREA_SIZE, 0.0001);", + " const scaleZ = Math.max(object.getDepth() / AREA_DEPTH, 0.0001);", + " const matrices = new Float32Array(copies.length * 16);", + " const tintColor = gdjs.rgbOrHexStringToNumber(object._getRandomTint());", + " const hasTint = tintColor !== 0xffffff;", + " const colors = hasTint ? new Float32Array(copies.length * 3) : null;", + " this._tint.setHex(tintColor);", + " const min = new THREE.Vector3(Infinity, Infinity, Infinity);", + " const max = new THREE.Vector3(-Infinity, -Infinity, -Infinity);", + " copies.forEach((copy, index) => {", + " const cos = Math.cos(copy.angle) * copy.height;", + " const sin = Math.sin(copy.angle) * copy.height;", + " const localX = (copy.x - object.getX()) / scaleX;", + " const localY = (copy.y - object.getY()) / scaleY;", + " const localZ = (copy.z - object.getZ()) / scaleZ;", + " this._matrix.set(", + " cos / scaleX, -sin / scaleX, 0, localX,", + " sin / scaleY, cos / scaleY, 0, localY,", + " 0, 0, copy.height / scaleZ, localZ,", + " 0, 0, 0, 1", + " );", + " this._matrix.toArray(matrices, index * 16);", + " if (colors) {", + " // The tint of each copy is between white and the tint color.", + " this._color.setRGB(1, 1, 1).lerp(this._tint, createRandom(copy.key + 1)());", + " this._color.toArray(colors, index * 3);", + " }", + " const reach = (copy.height * Math.max(this.model.width, this.model.depth)) / 2;", + " min.min(this._chunkPosition.set(copy.x - reach, copy.y - reach, copy.z));", + " max.max(this._chunkPosition.set(copy.x + reach, copy.y + reach, copy.z + copy.height));", + " });", + " const instanceMatrix = new THREE.InstancedBufferAttribute(matrices, 16);", + " const instanceColor = colors ? new THREE.InstancedBufferAttribute(colors, 3) : null;", + " const castShadow = object._getCastShadow();", + " const receiveShadow = object._getReceiveShadow();", + " const meshes = this.model.parts.map((part) => {", + " const mesh = new THREE.InstancedMesh(part.geometry, part.material, 0);", + " // All the parts of the model share the positions of the copies.", + " mesh.instanceMatrix = instanceMatrix;", + " mesh.instanceColor = instanceColor;", + " mesh.count = copies.length;", + " mesh.computeBoundingSphere();", + " mesh.castShadow = castShadow;", + " mesh.receiveShadow = receiveShadow;", + " // The first mesh tests all the parts of the copies.", + " mesh.raycast = () => {};", + " this.group.add(mesh);", + " return mesh;", + " });", + " const center = new THREE.Vector3(", + " ((min.x + max.x) / 2 - object.getX()) / scaleX,", + " ((min.y + max.y) / 2 - object.getY()) / scaleY,", + " ((min.z + max.z) / 2 - object.getZ()) / scaleZ", + " );", + " const chunk = { firstCellI, firstCellJ, meshes, copies, center, radius: min.distanceTo(max) / 2 };", + " // Used by the scene editor to select the object when a copy is clicked.", + " meshes[0].raycast = (raycaster, intersects) => this._raycastChunk(chunk, raycaster, intersects);", + " this.chunks.push(chunk);", + " }", + "", + " /**", + " * Add the hits of a ray on the copies of a chunk, like they are drawn. There", + " * can be thousands of copies: only the ones whose box is crossed by the ray", + " * are tested with the triangles of the model.", + " * @param {{meshes: THREE.InstancedMesh[], copies: Copy[], center: THREE.Vector3, radius: number}} chunk", + " * @param {THREE.Raycaster} raycaster", + " * @param {THREE.Intersection[]} intersects", + " */", + " _raycastChunk(chunk, raycaster, intersects) {", + " const { object, group } = this;", + " this._raycastSphere.center.copy(chunk.center).applyMatrix4(group.matrixWorld);", + " this._raycastSphere.radius = chunk.radius / getWorldScale(object);", + " if (!raycaster.ray.intersectsSphere(this._raycastSphere)) return;", + "", + " // The ray in the scene coordinates, like the copies.", + " const scale = this._raycastScale.set(", + " Math.max(object.getWidth() / AREA_SIZE, 0.0001),", + " Math.max(object.getHeight() / AREA_SIZE, 0.0001),", + " Math.max(object.getDepth() / AREA_DEPTH, 0.0001)", + " );", + " const ray = this._raycastRay.copy(raycaster.ray).applyMatrix4(this._raycastInverseMatrix.copy(group.matrixWorld).invert());", + " ray.origin.multiply(scale).add(this._chunkPosition.set(object.getX(), object.getY(), object.getZ()));", + " ray.direction.multiply(scale);", + "", + " const reachPerHeight = Math.max(this.model.width, this.model.depth) / 2;", + " const instanceMatrices = chunk.meshes[0].instanceMatrix.array;", + " chunk.copies.forEach((copy, index) => {", + " const reach = copy.height * reachPerHeight;", + " this._raycastBox.min.set(copy.x - reach, copy.y - reach, copy.z);", + " this._raycastBox.max.set(copy.x + reach, copy.y + reach, copy.z + copy.height);", + " if (!ray.intersectsBox(this._raycastBox)) return;", + " this._raycastMesh.matrixWorld.fromArray(instanceMatrices, index * 16).premultiply(group.matrixWorld);", + " for (const mesh of chunk.meshes) {", + " this._raycastMesh.geometry = mesh.geometry;", + " this._raycastMesh.material = mesh.material;", + " this._raycastHits.length = 0;", + " this._raycastMesh.raycast(raycaster, this._raycastHits);", + " for (const hit of this._raycastHits) {", + " hit.object = mesh;", + " hit.instanceId = index;", + " intersects.push(hit);", + " }", + " }", + " });", + " }", + "", + " /** @param {THREE.Camera | null} camera */", + " _hideFarChunks(camera) {", + " if (!camera) return;", + " const worldScale = getWorldScale(this.object);", + " const drawDistance = Math.max(this.object._getDrawDistance(), 1) / worldScale;", + " this.uniforms.scatterDrawDistance.value = drawDistance;", + " this._cameraPosition.setFromMatrixPosition(camera.matrixWorld);", + " for (const chunk of this.chunks) {", + " const distance = this._chunkPosition", + " .copy(chunk.center)", + " .applyMatrix4(this.group.matrixWorld)", + " .distanceTo(this._cameraPosition);", + " const isVisible = distance - chunk.radius / worldScale < drawDistance;", + " for (const mesh of chunk.meshes) mesh.visible = isVisible;", + " }", + " }", + "", + " /**", + " * @param {number} elapsedSeconds", + " * @param {THREE.Camera | null} camera", + " */", + " update(elapsedSeconds, camera) {", + " if (this.placement.shouldPlaceAgain(elapsedSeconds, this._getPlacementKey())) {", + " this.rebuild();", + " }", + " if (!this.changedSamples.isEmpty()) {", + " const { object } = this;", + " const sampleWidth = object.getWidth() / MASK_RESOLUTION;", + " const sampleHeight = object.getHeight() / MASK_RESOLUTION;", + " const { minI, minJ, maxI, maxJ } = this.changedSamples;", + " // Copies between samples use the density of the samples around them.", + " this._rebuildArea(", + " object.getX() + (minI - 1) * sampleWidth,", + " object.getY() + (minJ - 1) * sampleHeight,", + " object.getX() + (maxI + 1) * sampleWidth,", + " object.getY() + (maxJ + 1) * sampleHeight", + " );", + " this.changedSamples.clear();", + " }", + " // The object transformation is applied just before rendering, it's needed now.", + " this.object.getRenderer().ensureUpToDate();", + " this.group.updateWorldMatrix(true, false);", + " this._hideFarChunks(camera);", + " this._updateSurface();", + " }", + "", + " _updateSurface() {", + " const { object } = this;", + " if (typeof object.setSurface !== 'function') return;", + " const trunkKey = object._getTrunkCollisions() + '|' + object._getTrunkDiameter();", + " if (trunkKey !== this.trunkKey) {", + " this.trunkKey = trunkKey;", + " this.version++;", + " }", + " if (object.getSurface() !== this.surface) object.setSurface(this.surface);", + " }", + "", + " /** @returns {Copy[]} */", + " getCopies() {", + " const copies = [];", + " for (const chunk of this.chunks) copies.push(...chunk.copies);", + " return copies;", + " }", + "", + " /**", + " * @returns {Copy | null} The copy whose base is the nearest to a position (on X and Y), if any.", + " */", + " findNearestCopy(x, y) {", + " let nearestCopy = null;", + " let nearestDistanceSquared = Infinity;", + " for (const chunk of this.chunks) {", + " for (const copy of chunk.copies) {", + " const distanceSquared = (copy.x - x) * (copy.x - x) + (copy.y - y) * (copy.y - y);", + " if (distanceSquared < nearestDistanceSquared) {", + " nearestDistanceSquared = distanceSquared;", + " nearestCopy = copy;", + " }", + " }", + " }", + " return nearestCopy;", + " }", + "", + " getCopyCount() {", + " let count = 0;", + " for (const chunk of this.chunks) count += chunk.copies.length;", + " return count;", + " }", + "", + " /** Remove the copies whose base is in a circle (they come back when the scene is restarted). */", + " removeCopies(x, y, radius) {", + " let hasRemovedCopies = false;", + " for (const chunk of this.chunks) {", + " for (const copy of chunk.copies) {", + " if ((copy.x - x) * (copy.x - x) + (copy.y - y) * (copy.y - y) <= radius * radius) {", + " this.removedCopies.set(copy.key, copy);", + " hasRemovedCopies = true;", + " }", + " }", + " }", + " if (hasRemovedCopies) this._rebuildArea(x - radius, y - radius, x + radius, y + radius);", + " }", + "", + " /** Paint the full density around a position, and bring back the copies removed there. */", + " addCopies(x, y, radius) {", + " for (const [key, copy] of this.removedCopies) {", + " if ((copy.x - x) * (copy.x - x) + (copy.y - y) * (copy.y - y) <= radius * radius) {", + " this.removedCopies.delete(key);", + " }", + " }", + " this.paint(x, y, x, y, radius, 1, 1);", + " }", + "", + " _getStroke(x1, y1, x2, y2, radius) {", + " const { object } = this;", + " const stroke = this._stroke;", + " stroke.cellWidth = object.getWidth() / MASK_RESOLUTION;", + " stroke.cellHeight = object.getHeight() / MASK_RESOLUTION;", + " stroke.ax = x1 - object.getX();", + " stroke.ay = y1 - object.getY();", + " stroke.bx = x2 - object.getX();", + " stroke.by = y2 - object.getY();", + " stroke.radius = Math.max(radius, 0);", + " return stroke;", + " }", + "", + " /**", + " * Paint the density around a segment (copies are placed again at the next frame).", + " * @param {number} density From 0 (erase) to 1 (as dense as the Density property).", + " * @param {number} strength From 0 to 1.", + " */", + " paint(x1, y1, x2, y2, radius, density, strength) {", + " const changedSamples = new SampleRectangle();", + " this.mask.paint(this._getStroke(x1, y1, x2, y2, radius), density, strength, changedSamples);", + " this.changedSamples.add(changedSamples);", + " }", + "", + " /** @param {DensityMask} mask */", + " setMask(mask) {", + " this.mask = mask;", + " this.rebuild();", + " }", + "", + " /**", + " * Prisms around the trunks, from 0 to 1 in the object box (see `gdjs.Surface`).", + " * @returns {gdjs.SurfaceTriangles}", + " */", + " _getTrunkTriangles() {", + " const { object } = this;", + " const copies = object._getTrunkCollisions() ? this.getCopies() : [];", + " const verticesPerCopy = TRUNK_SIDES * 2 + 1;", + " const positions = new Float32Array(copies.length * verticesPerCopy * 3);", + " const indices = new Uint32Array(copies.length * TRUNK_SIDES * 3 * 3);", + " const width = object.getWidth() || 1;", + " const height = object.getHeight() || 1;", + " const depth = object.getDepth() || 1;", + " let indexOffset = 0;", + " copies.forEach((copy, copyIndex) => {", + " const radius = (Math.max(object._getTrunkDiameter(), 0) / 2) * (copy.height / Math.max(object._getSize(), 1));", + " const top = copy.z + copy.height * 0.8;", + " const firstVertex = copyIndex * verticesPerCopy;", + " const setVertex = (vertex, x, y, z) => {", + " positions[vertex * 3] = (x - object.getX()) / width;", + " positions[vertex * 3 + 1] = (y - object.getY()) / height;", + " positions[vertex * 3 + 2] = (z - object.getZ()) / depth;", + " };", + " for (let side = 0; side < TRUNK_SIDES; side++) {", + " const angle = ((side + 0.5) / TRUNK_SIDES) * Math.PI * 2;", + " const x = copy.x + Math.cos(angle) * radius;", + " const y = copy.y + Math.sin(angle) * radius;", + " // Starting a bit under the ground, which can be sloped.", + " setVertex(firstVertex + side * 2, x, y, copy.z - radius);", + " setVertex(firstVertex + side * 2 + 1, x, y, top);", + " }", + " const center = firstVertex + TRUNK_SIDES * 2;", + " setVertex(center, copy.x, copy.y, top);", + " for (let side = 0; side < TRUNK_SIDES; side++) {", + " const bottom = firstVertex + side * 2;", + " const nextBottom = firstVertex + ((side + 1) % TRUNK_SIDES) * 2;", + " // Triangles face out of the prism (vertices at odd indices are at the top).", + " indices.set([bottom, nextBottom, bottom + 1, nextBottom, nextBottom + 1, bottom + 1], indexOffset);", + " indices.set([center, bottom + 1, nextBottom + 1], indexOffset + 6);", + " indexOffset += 9;", + " }", + " });", + " if (copies.length === 0) {", + " // Without triangles, the physics engine and navigation meshes would use", + " // the object box: a tiny triangle under the object is used instead.", + " return { positions: new Float32Array([0, 0, -1, 0.0001, 0, -1, 0, 0.0001, -1]), indices: new Uint32Array([0, 2, 1]) };", + " }", + " return { positions, indices };", + " }", + "", + " dispose() {", + " this.isDisposed = true;", + " if (typeof this.object.setSurface === 'function' && this.object.getSurface() === this.surface) {", + " this.object.setSurface(null);", + " }", + " this._disposeChunks(this.chunks);", + " this._disposeModel();", + " this.group.removeFromParent();", + " }", + "}", + "", + "const scatterToolIcons = {", + " Add: svgIcon(", + " ''", + " ),", + " Erase: svgIcon(", + " ''", + " ),", + "};", + "", + "/**", + " * The brushes shown in the scene editor when scattered models are selected,", + " * to add or erase copies. Without the needed editor functions (older GDevelop", + " * versions), nothing is shown: copies can still be painted with events.", + " */", + "class ScatterEditorTools {", + " constructor() {", + " /** '' when no brush is used. */", + " this.toolName = '';", + " this.sizePercent = 30;", + " this.strengthPercent = 50;", + " /** @type {Scatter | null} */", + " this.scatter = null;", + " /** @type {gdjs.InGameEditor | null} */", + " this.editor = null;", + " this.isStroking = false;", + " this.lastStrokeTime = 0;", + " /** @type {Array<{mask: DensityMask, baseMask: DensityMask}>} */", + " this.undoSteps = [];", + " this.raycaster = new THREE.Raycaster();", + " this.ndc = new THREE.Vector2();", + " this._groundAtCursor = { ground: null, z: 0 };", + " /** A circle following the ground under the cursor. */", + " this.brushRing = new THREE.LineLoop(", + " new THREE.BufferGeometry().setAttribute('position', new THREE.BufferAttribute(new Float32Array(64 * 3), 3)),", + " new THREE.LineBasicMaterial({ color: 0xffffff, depthTest: false, transparent: true })", + " );", + " this.brushRing.renderOrder = 1000;", + " this.brushRing.frustumCulled = false;", + " this.brushRing.raycast = () => {};", + " }", + "", + " /** @param {gdjs.InGameEditor} editor */", + " update(editor) {", + " if (typeof editor.showToolbar !== 'function') return;", + " this.editor = editor;", + " const selectedObjects = editor.getSelectedObjects();", + " const scatter =", + " selectedObjects.length === 1 && selectedObjects[0].__terrainScatter ? selectedObjects[0].__terrainScatter : null;", + " if (scatter !== this.scatter) {", + " this._endStroke();", + " this.scatter = scatter;", + " this.undoSteps.length = 0;", + " }", + " if (scatter) editor.showToolbar('ScatteredModels', this._getToolbarItems());", + " const game = editor.getRuntimeGame();", + " if (!scatter || !this.toolName) {", + " this.brushRing.removeFromParent();", + " return;", + " }", + " const inputManager = game.getInputManager();", + " if (inputManager.wasKeyJustPressed(gdjs.evtTools.input.keysNameToCode.Escape)) {", + " this._endStroke();", + " this.toolName = '';", + " this.brushRing.removeFromParent();", + " return;", + " }", + " editor.captureLeftMouseButton();", + " const hit = this._getPositionUnderCursor(scatter, game);", + " const radius = this._getRadius();", + " this._showBrushRing(scatter, hit, radius);", + "", + " const isPressedForTool =", + " typeof editor.isCapturedLeftMouseButtonPressed === 'function'", + " ? editor.isCapturedLeftMouseButtonPressed()", + " : inputManager.isMouseButtonPressed(0);", + " // The button is released outside the canvas when the mouse is on a toolbar.", + " const isPressed = isPressedForTool && inputManager.isMouseInsideCanvas();", + " if (isPressed && hit && !this.isStroking) {", + " this._startStroke(scatter);", + " }", + " if (this.isStroking && isPressed && hit) {", + " this._applyBrush(scatter, hit, radius);", + " }", + " if (!isPressed) {", + " this._endStroke();", + " }", + " }", + "", + " _getRadius() {", + " const { object } = this.scatter;", + " const maxRadius = Math.max(object.getWidth(), object.getHeight()) / 4;", + " return Math.max(maxRadius * Math.pow(this.sizePercent / 100, 2), 10);", + " }", + "", + " /**", + " * @returns {{x: number, y: number} | null} The position of the scene under the cursor: on the", + " * highest ground hit, or on the plane at the object Z.", + " */", + " _getPositionUnderCursor(scatter, game) {", + " let hit = null;", + " for (const ground of getGrounds(scatter.object.getRuntimeScene())) {", + " if (typeof ground.raycastFromCursor !== 'function') continue;", + " const localHit = ground.raycastFromCursor(game, this.raycaster, this.ndc);", + " if (!localHit) continue;", + " const sceneHit = ground.toScenePosition(localHit);", + " if (!hit || sceneHit.z > hit.z) hit = sceneHit;", + " }", + " if (hit) return hit;", + "", + " const { object } = scatter;", + " const layer = object.getInstanceContainer().getLayer(object.getLayer());", + " const camera = layer.getRenderer().getThreeCamera();", + " if (!camera) return null;", + " const inputManager = game.getInputManager();", + " this.ndc.set(", + " (inputManager.getCursorX() / game.getGameResolutionWidth()) * 2 - 1,", + " -(inputManager.getCursorY() / game.getGameResolutionHeight()) * 2 + 1", + " );", + " this.raycaster.setFromCamera(this.ndc, camera);", + " scatter.group.updateWorldMatrix(true, false);", + " const ray = this.raycaster.ray.clone().applyMatrix4(scatter.group.matrixWorld.clone().invert());", + " const point = ray.intersectPlane(new THREE.Plane(new THREE.Vector3(0, 0, 1), 0), new THREE.Vector3());", + " if (!point) return null;", + " return {", + " x: object.getX() + (point.x / AREA_SIZE) * object.getWidth(),", + " y: object.getY() + (point.y / AREA_SIZE) * object.getHeight(),", + " };", + " }", + "", + " _showBrushRing(scatter, hit, radius) {", + " if (!hit) {", + " this.brushRing.removeFromParent();", + " return;", + " }", + " const { object } = scatter;", + " if (this.brushRing.parent !== scatter.group) scatter.group.add(this.brushRing);", + " const grounds = getGrounds(object.getRuntimeScene());", + " const positions = this.brushRing.geometry.attributes.position;", + " for (let index = 0; index < positions.count; index++) {", + " const angle = (index / positions.count) * Math.PI * 2;", + " const x = hit.x + Math.cos(angle) * radius;", + " const y = hit.y + Math.sin(angle) * radius;", + " const { ground, z } = findHighestGround(grounds, x, y, this._groundAtCursor);", + " positions.setXYZ(", + " index,", + " ((x - object.getX()) / object.getWidth()) * AREA_SIZE,", + " ((y - object.getY()) / object.getHeight()) * AREA_SIZE,", + " (((ground ? z : object.getZ()) + 2 - object.getZ()) / Math.max(object.getDepth(), 0.0001)) * AREA_DEPTH", + " );", + " }", + " positions.needsUpdate = true;", + " }", + "", + " _startStroke(scatter) {", + " this.isStroking = true;", + " this.lastStrokeTime = performance.now();", + " this.undoSteps.push({ mask: scatter.mask.clone(), baseMask: scatter.baseMask });", + " while (this.undoSteps.length > 30) this.undoSteps.shift();", + " }", + "", + " _endStroke() {", + " if (!this.isStroking) return;", + " this.isStroking = false;", + " this._save();", + " }", + "", + " _applyBrush(scatter, hit, radius) {", + " const now = performance.now();", + " const elapsedSeconds = Math.min((now - this.lastStrokeTime) / 1000, 0.1);", + " this.lastStrokeTime = now;", + " const strength = (this.strengthPercent / 100) * elapsedSeconds * 8;", + " scatter.paint(hit.x, hit.y, hit.x, hit.y, radius, this.toolName === 'Add' ? 1 : 0, strength);", + " }", + "", + " _save() {", + " const { scatter, editor } = this;", + " if (!scatter || !editor) return;", + " editor.updateObjectProperties(scatter.object.getName(), { PaintData: scatter.save() });", + " }", + "", + " _undo() {", + " const undoStep = this.undoSteps.pop();", + " // The coverage or the edits may have changed since the stroke.", + " if (!this.scatter || !undoStep || undoStep.baseMask !== this.scatter.baseMask) {", + " this.undoSteps.length = 0;", + " return;", + " }", + " this.scatter.setMask(undoStep.mask);", + " this._save();", + " }", + "", + " /** @returns {Array} */", + " _getToolbarItems() {", + " const toolButton = (toolName, tooltip) => ({", + " type: 'button',", + " id: toolName,", + " tooltip,", + " iconUrl: scatterToolIcons[toolName],", + " isActive: this.toolName === toolName,", + " onClick: () => (this.toolName = this.toolName === toolName ? '' : toolName),", + " });", + " return [", + " toolButton('Add', 'Add copies (drag on the ground)'),", + " toolButton('Erase', 'Erase copies'),", + " { type: 'divider', id: 'BrushDivider' },", + " {", + " type: 'slider',", + " id: 'Size',", + " tooltip: 'Brush size',", + " iconUrl: toolIcons.Size,", + " min: 1,", + " max: 100,", + " value: this.sizePercent,", + " onChange: (value) => (this.sizePercent = value),", + " },", + " {", + " type: 'slider',", + " id: 'Strength',", + " tooltip: 'Brush strength',", + " iconUrl: toolIcons.Strength,", + " min: 5,", + " max: 100,", + " value: this.strengthPercent,", + " onChange: (value) => (this.strengthPercent = value),", + " },", + " {", + " type: 'button',", + " id: 'Undo',", + " tooltip: 'Undo the last stroke on these scattered models',", + " iconUrl: toolIcons.Undo,", + " onClick: () => this._undo(),", + " },", + " ];", + " }", + "}", + "", + "const editorTools = new ScatterEditorTools();", + "if (gdjs.registerInGameEditorPostStepCallback) {", + " gdjs.registerInGameEditorPostStepCallback((editor) => editorTools.update(editor));", + "}", + "", + "gdjs.__terrainScatterExtension = {", + " Scatter,", + " // Exposed for tests.", + " DensityMask,", + " maskCodec,", + " applyEdits,", + "};", + "" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [], + "objectGroups": [] + } + ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "DefineHelperClasses" + }, + { + "functionName": "DefineGrassHelperClasses" + }, + { + "functionName": "DefineScatterHelperClasses" + } + ] + }, + "eventsBasedBehaviors": [], + "eventsBasedObjects": [ + { + "areaMaxX": 4096, + "areaMaxY": 4096, + "areaMaxZ": 600, + "areaMinX": 0, + "areaMinY": 0, + "areaMinZ": 0, + "assetStoreTag": "3d terrains", + "defaultName": "Terrain", + "description": "A ground with hills, mountains or valleys that can be sculpted and painted in the scene editor, or changed with events. Add the 3D physics behavior to it to make objects walk or roll on it.", + "fullName": "3D terrain", + "helpPath": "", + "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLXRlcnJhaW4iIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTQsNkwxMC4yNSwxMUwxMy4xLDE0LjhMMTEuNSwxNkM5LjgxLDEzLjc1IDcsMTAgNywxMEwxLDE4SDIzTDE0LDZaIiAvPjwvc3ZnPg==", + "is3D": true, + "isUsingLegacyInstancesRenderer": true, + "name": "Terrain3D", + "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", + "objects": [ + { + "assetStoreId": "", + "name": "AreaPlaceholder", + "type": "Scene3D::Cube3DObject", + "variables": [], + "effects": [], + "behaviors": [], + "content": { + "width": 4096, + "height": 4096, + "depth": 600, + "enableTextureTransparency": false, + "facesOrientation": "Y", + "backFaceUpThroughWhichAxisRotation": "X", + "materialType": "Basic", + "tint": "255;255;255", + "frontFaceResourceName": "", + "frontFaceVisible": false, + "frontFaceResourceRepeat": false, + "backFaceResourceName": "", + "backFaceVisible": false, + "backFaceResourceRepeat": false, + "leftFaceResourceName": "", + "leftFaceVisible": false, + "leftFaceResourceRepeat": false, + "rightFaceResourceName": "", + "rightFaceVisible": false, + "rightFaceResourceRepeat": false, + "topFaceResourceName": "", + "topFaceVisible": false, + "topFaceResourceRepeat": false, + "bottomFaceResourceName": "", + "bottomFaceVisible": false, + "bottomFaceResourceRepeat": false + } + } + ], + "objectsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "objectName": "AreaPlaceholder" + } + ] + }, + "objectsGroups": [], + "layers": [ + { + "ambientLightColorB": 200, + "ambientLightColorG": 200, + "ambientLightColorR": 200, + "camera2DPlaneMaxDrawingDistance": 5000, + "camera3DFarPlaneDistance": 10000, + "camera3DFieldOfView": 45, + "camera3DNearPlaneDistance": 3, + "cameraType": "", + "followBaseLayerCamera": false, + "isLightingLayer": false, + "isLocked": false, + "name": "", + "renderingType": "", + "visibility": true, + "cameras": [ + { + "defaultSize": true, + "defaultViewport": true, + "height": 0, + "viewportBottom": 1, + "viewportLeft": 0, + "viewportRight": 1, + "viewportTop": 0, + "width": 0 + } + ], + "effects": [] + } + ], + "instances": [ + { + "angle": 0, + "customSize": true, + "depth": 600, + "height": 4096, + "keepRatio": true, + "layer": "", + "name": "AreaPlaceholder", + "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-004096004096", + "width": 4096, + "x": 0, + "y": 0, + "zOrder": 1, + "numberProperties": [], + "stringProperties": [], + "initialVariables": [] + } + ], + "editionSettings": {}, + "eventsFunctions": [ + { + "fullName": "", + "functionType": "Action", + "name": "onCreated", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Terrain3D::DefineHelperClasses" + }, + "parameters": [ + "", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "for (const child of object.getChildrenContainer().getAdhocListOfAllInstances()) {", + " child.deleteFromScene();", + "}", + "if (object.__terrain3D) object.__terrain3D.dispose();", + "object.__terrain3D = new gdjs.__terrain3DExtension.Terrain(object);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "onDestroy", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrain3D.dispose();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "doStepPostEvents", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrain3D.update();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "onHotReloading", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrain3D.onHotReloading();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "description": "Raise the ground around a position (or lower it with a negative height). It's raised the most at the center and less and less up to the radius.", + "fullName": "Raise or lower the ground", + "functionType": "Action", + "group": "Sculpt", + "name": "RaiseGround", + "sentence": "Raise the ground of _PARAM0_ by _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const height = eventsFunctionContext.getArgument(\"Height\");", + "object.__terrain3D.raise(x, y, x, y, radius, height);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Height to add (negative to lower the ground)", + "name": "Height", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Raise the ground along a line, for example for a ridge, or lower it with a negative height to dig a river bed or a valley.", + "fullName": "Raise or lower the ground along a line", + "functionType": "Action", + "group": "Sculpt", + "name": "RaiseGroundAlongLine", + "sentence": "Raise the ground of _PARAM0_ by _PARAM6_ from _PARAM1_; _PARAM2_ to _PARAM3_; _PARAM4_ with a radius of _PARAM5_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x1 = eventsFunctionContext.getArgument(\"X1\");", + "const y1 = eventsFunctionContext.getArgument(\"Y1\");", + "const x2 = eventsFunctionContext.getArgument(\"X2\");", + "const y2 = eventsFunctionContext.getArgument(\"Y2\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const height = eventsFunctionContext.getArgument(\"Height\");", + "object.__terrain3D.raise(x1, y1, x2, y2, radius, height);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the start", + "name": "X1", + "type": "expression" + }, + { + "description": "Y position of the start", + "name": "Y1", + "type": "expression" + }, + { + "description": "X position of the end", + "name": "X2", + "type": "expression" + }, + { + "description": "Y position of the end", + "name": "Y2", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance from the line.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Height to add (negative to lower the ground)", + "name": "Height", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Flatten the ground around a position to a given height, for example to place a building.", + "fullName": "Flatten the ground", + "functionType": "Action", + "group": "Sculpt", + "name": "FlattenGround", + "sentence": "Flatten the ground of _PARAM0_ to a Z of _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const z = eventsFunctionContext.getArgument(\"Z\");", + "object.__terrain3D.flatten(x, y, x, y, radius, z, 1);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Z position of the flat ground", + "name": "Z", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Smooth the ground around a position, softening bumps and cliffs.", + "fullName": "Smooth the ground", + "functionType": "Action", + "group": "Sculpt", + "name": "SmoothGround", + "sentence": "Smooth the ground of _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_ (strength: _PARAM4_)", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const strength = eventsFunctionContext.getArgument(\"Strength\");", + "object.__terrain3D.smooth(x, y, x, y, radius, strength);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Strength (between 0 and 1)", + "name": "Strength", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Paint a layer (like grass, dirt, rock or sand) on the ground around a position.", + "fullName": "Paint a layer", + "functionType": "Action", + "group": "Paint", + "name": "PaintLayer", + "sentence": "Paint layer _PARAM4_ on _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const layer = eventsFunctionContext.getArgument(\"Layer\");", + "object.__terrain3D.paint(x, y, x, y, radius, layer, 1);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Layer (1 to 4)", + "name": "Layer", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Paint a layer on the ground along a line, for example for a path or a road.", + "fullName": "Paint a layer along a line", + "functionType": "Action", + "group": "Paint", + "name": "PaintLayerAlongLine", + "sentence": "Paint layer _PARAM6_ on _PARAM0_ from _PARAM1_; _PARAM2_ to _PARAM3_; _PARAM4_ with a radius of _PARAM5_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x1 = eventsFunctionContext.getArgument(\"X1\");", + "const y1 = eventsFunctionContext.getArgument(\"Y1\");", + "const x2 = eventsFunctionContext.getArgument(\"X2\");", + "const y2 = eventsFunctionContext.getArgument(\"Y2\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const layer = eventsFunctionContext.getArgument(\"Layer\");", + "object.__terrain3D.paint(x1, y1, x2, y2, radius, layer, 1);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the start", + "name": "X1", + "type": "expression" + }, + { + "description": "Y position of the start", + "name": "Y1", + "type": "expression" + }, + { + "description": "X position of the end", + "name": "X2", + "type": "expression" + }, + { + "description": "Y position of the end", + "name": "Y2", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance from the line.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Layer (1 to 4)", + "name": "Layer", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Tint the ground around a position with a color multiplying its layers, like dry grass or darker mud. White removes the tint.", + "fullName": "Tint the ground", + "functionType": "Action", + "group": "Paint", + "name": "TintGround", + "sentence": "Tint the ground of _PARAM0_ with _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const color = gdjs.rgbOrHexToRGBColor(eventsFunctionContext.getArgument(\"Color\"));", + "object.__terrain3D.tint(x, y, x, y, radius, color, 1);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Color", + "name": "Color", + "type": "color" + } + ], + "objectGroups": [] + }, + { + "description": "Make the ground around a position wet (darker and shiny, like after rain or near water) or dry.", + "fullName": "Change the wetness of the ground", + "functionType": "Action", + "group": "Paint", + "name": "WetGround", + "sentence": "Change the wetness of the ground of _PARAM0_ to _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const wetness = eventsFunctionContext.getArgument(\"Wetness\");", + "object.__terrain3D.wet(x, y, x, y, radius, wetness, 1);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Wetness (from 0 for dry to 1)", + "name": "Wetness", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "the wetness of the ground at a position, from 0 (dry) to 1, painted or near the water (wet shore).", + "fullName": "Ground wetness", + "functionType": "ExpressionAndCondition", + "group": "Paint", + "name": "GroundWetness", + "sentence": "the wetness of the ground at _PARAM1_; _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "eventsFunctionContext.returnValue = object.__terrain3D.getWetnessAt(x, y);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "The tint painted on the ground at a position, like \"255;255;255\" (white when there is none).", + "fullName": "Ground tint", + "functionType": "StringExpression", + "group": "Paint", + "name": "GroundTint", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "eventsFunctionContext.returnValue = object.__terrain3D.getTintAt(x, y).join(\";\");" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "string" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Remove the ground in a circle, for the entrance of a tunnel, a cave or a well: there is nothing drawn and no collision there. Fill it again with the \"Fill holes\" action. A height field can't go under itself: put a 3D model (with a collision shape) for the tunnel or the cave under the ground. The physics engine removes the triangles touching a hole, so the hole in the collisions can be up to a cell larger than the one drawn: make the model go a bit further than the hole, or put rocks around the entrance.", + "fullName": "Make a hole", + "functionType": "Action", + "group": "Holes", + "name": "MakeHole", + "sentence": "Make a hole in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "object.__terrain3D.makeHole(x, y, x, y, radius);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "name": "Radius", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Fill the holes in a circle with the layers of the ground around them: the ground is drawn and has collisions again.", + "fullName": "Fill holes", + "functionType": "Action", + "group": "Holes", + "name": "FillHoles", + "sentence": "Fill the holes in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "object.__terrain3D.fillHoles(x, y, x, y, radius);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "name": "Radius", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Make the entrance of a tunnel or a cave on a slope: a hole, with the ground in front of it flattened to the floor of the tunnel.", + "fullName": "Carve an entrance", + "functionType": "Action", + "group": "Holes", + "name": "CarveEntrance", + "sentence": "Carve an entrance in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_, going in at _PARAM4_° with its floor at Z _PARAM5_ and _PARAM6_ pixels of flat ground in front", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const angle = eventsFunctionContext.getArgument(\"Angle\");", + "const floorZ = eventsFunctionContext.getArgument(\"FloorZ\");", + "const length = eventsFunctionContext.getArgument(\"Length\");", + "object.__terrain3D.carveEntrance(x, y, radius, angle, floorZ, length);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "name": "Radius", + "type": "expression" + }, + { + "description": "Direction going into the ground (in degrees)", + "name": "Angle", + "type": "expression" + }, + { + "description": "Z position of the floor", + "name": "FloorZ", + "type": "expression" + }, + { + "description": "Length of the flat ground in front of the entrance (in pixels)", + "name": "Length", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Check if there is a hole in the ground at a position.", + "fullName": "Hole", + "functionType": "Condition", + "group": "Holes", + "name": "IsHole", + "sentence": "There is a hole in _PARAM0_ at _PARAM1_; _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "eventsFunctionContext.returnValue = object.__terrain3D.isHoleAt(x, y);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "The Z position of the ground at a position, or the given value where there is a hole.", + "fullName": "Ground Z position, with holes", + "functionType": "Expression", + "group": "Holes", + "name": "GroundZWithHoles", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const terrain = object.__terrain3D;", + "eventsFunctionContext.returnValue = terrain.isHoleAt(x, y) ? eventsFunctionContext.getArgument(\"ZInHoles\") : terrain.getHeightAt(x, y);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + }, + { + "description": "Value in holes", + "longDescription": "For example, a Z position far under the ground.", + "name": "ZInHoles", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Replace the ground with a generated relief. Painted layers are reset.", + "fullName": "Generate a relief", + "functionType": "Action", + "group": "Generation", + "name": "GenerateRelief", + "sentence": "Generate a relief of _PARAM1_ on _PARAM0_ with seed _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const relief = eventsFunctionContext.getArgument(\"Relief\");", + "const seed = eventsFunctionContext.getArgument(\"Seed\");", + "object.__terrain3D.generate(relief, seed);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "Relief", + "name": "Relief", + "supplementaryInformation": "[\"Flat\", \"Hills\", \"Mountains\", \"Island\"]", + "type": "stringWithSelector" + }, + { + "description": "Seed", + "longDescription": "Each seed gives a different terrain with the same relief.", + "name": "Seed", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Replace the ground with the heights of an image. Painted layers are kept.", + "fullName": "Load a heightmap", + "functionType": "Action", + "group": "Generation", + "name": "LoadHeightmap", + "sentence": "Load the heights of _PARAM0_ from _PARAM1_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const image = eventsFunctionContext.getArgument(\"Image\");", + "object.__terrain3D.loadHeightmap(image);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "Heightmap image", + "longDescription": "Black is the lowest, white the highest. For more precision, store the height on 16 bits in the red (most significant byte) and green channels.", + "name": "Image", + "type": "imageResource" + } + ], + "objectGroups": [] + }, + { + "description": "Move objects up or down so that their bottom is on the ground, under their center. Useful to place trees, rocks or characters on a terrain.", + "fullName": "Put objects on the ground", + "functionType": "Action", + "group": "Position", + "name": "PutOnGround", + "sentence": "Put _PARAM1_ on the ground of _PARAM0_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrain3D.placeOnGround(eventsFunctionContext.getObjects(\"Objects\"));" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "Objects to put on the ground", + "name": "Objects", + "type": "objectList" + } + ], + "objectGroups": [] + }, + { + "description": "the Z position of the ground at a position of the scene.", + "fullName": "Ground Z position", + "functionType": "ExpressionAndCondition", + "group": "Position", + "name": "GroundZ", + "sentence": "the ground Z position at _PARAM1_; _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "eventsFunctionContext.returnValue = object.__terrain3D.getHeightAt(x, y);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "the slope of the ground (angle with the horizontal, in degrees) at a position of the scene.", + "fullName": "Ground slope", + "functionType": "ExpressionAndCondition", + "group": "Position", + "name": "GroundSlope", + "sentence": "the ground slope at _PARAM1_; _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "eventsFunctionContext.returnValue = object.__terrain3D.getSlopeAt(x, y);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "the layer (from 1 to 4) painted the most at a position of the scene. Useful to play footstep sounds or to slow down characters in water or mud.", + "fullName": "Ground layer", + "functionType": "ExpressionAndCondition", + "group": "Paint", + "name": "GroundLayer", + "sentence": "the ground layer at _PARAM1_; _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "eventsFunctionContext.returnValue = object.__terrain3D.getLayerAt(x, y);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + } + ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + }, + { + "folderName": "Sculpt", + "children": [ + { + "functionName": "RaiseGround" + }, + { + "functionName": "RaiseGroundAlongLine" + }, + { + "functionName": "FlattenGround" + }, + { + "functionName": "SmoothGround" + } + ] + }, + { + "folderName": "Paint", + "children": [ + { + "functionName": "PaintLayer" + }, + { + "functionName": "PaintLayerAlongLine" + }, + { + "functionName": "TintGround" + }, + { + "functionName": "WetGround" + }, + { + "functionName": "GroundWetness" + }, + { + "functionName": "GroundTint" + }, + { + "functionName": "GroundLayer" + } + ] + }, + { + "folderName": "Holes", + "children": [ + { + "functionName": "MakeHole" + }, + { + "functionName": "FillHoles" + }, + { + "functionName": "CarveEntrance" + }, + { + "functionName": "IsHole" + }, + { + "functionName": "GroundZWithHoles" + } + ] + }, + { + "folderName": "Generation", + "children": [ + { + "functionName": "GenerateRelief" + }, + { + "functionName": "LoadHeightmap" + } + ] + }, + { + "folderName": "Position", + "children": [ + { + "functionName": "PutOnGround" + }, + { + "functionName": "GroundZ" + }, + { + "functionName": "GroundSlope" + } + ] + } + ] + }, + "propertyDescriptors": [ + { + "value": "Hills", + "type": "Choice", + "label": "Relief", + "description": "The initial shape, before sculpting.", + "group": "Shape", + "choices": [ + { + "label": "Flat", + "value": "Flat" + }, + { + "label": "Hills", + "value": "Hills" + }, + { + "label": "Mountains", + "value": "Mountains" + }, + { + "label": "Island", + "value": "Island" + } + ], + "name": "Relief" + }, + { + "value": "1", + "type": "Number", + "label": "Seed", + "description": "Each seed gives a different terrain with the same relief.", + "group": "Shape", + "name": "Seed" + }, + { + "value": "", + "type": "Resource", + "label": "Heightmap image", + "description": "Optional. Replaces the relief: black is the lowest, white the highest (the object depth).", + "group": "Shape", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "HeightmapImage" + }, + { + "value": "", + "type": "MultilineString", + "label": "Edits", + "description": "Changes of the relief or heightmap, applied in order, as a JSON list, for example [{\"tool\": \"raise\", \"x\": 0.3, \"y\": 0.4, \"radius\": 0.15, \"height\": 0.4}, {\"tool\": \"paint\", \"x\": 0.1, \"y\": 0.9, \"toX\": 0.8, \"toY\": 0.2, \"radius\": 0.02, \"layer\": 2}]. Tools: raise (height added to the ground, negative to lower), flatten (z: the ground is brought to this height, fully at the center, cutting what is above it and filling what is below; with toX and toY, toZ gives the height at the end of the line, for a road going up), smooth (strength), paint (layer, from 1 to 4, and strength), tint (color, like \"120;140;60\", multiplying the layers, white removing the tint, and strength), wetness (amount, from 0 for dry to 1 for wet, and strength), hole (no ground in the radius, for the entrances of tunnels and caves) and fill (the holes in the radius are filled with the layers of the ground around them). x, y and radius are fractions (0 to 1) of the terrain size (x is (scene X - terrain X) / terrain width). height and z are fractions of its depth: the ground at z is at Z = terrain Z + z * terrain depth. Add toX and toY to change the ground along a line (paths, rivers, ridges). Shown in the scene editor.", + "group": "Shape", + "advanced": true, + "name": "Edits" + }, + { + "value": "256", + "type": "Choice", + "label": "Resolution", + "description": "The number of cells on each side of the terrain. Higher is more detailed but slower.", + "group": "Shape", + "choices": [ + { + "label": "64 x 64", + "value": "64" + }, + { + "label": "128 x 128", + "value": "128" + }, + { + "label": "256 x 256", + "value": "256" + }, + { + "label": "512 x 512", + "value": "512" + }, + { + "label": "1024 x 1024", + "value": "1024" + } + ], + "name": "Resolution" + }, + { + "value": "96;160;64", + "type": "Color", + "label": "Layer 1 color (grass)", + "group": "Layers", + "name": "Layer1Color" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 1 texture", + "description": "Optional. Tinted by the layer color.", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer1Texture" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 1 normal map", + "description": "Optional. The bumps of the texture, lit by the lights (an OpenGL normal map, like the textures of ambientCG).", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer1NormalMap" + }, + { + "value": "150;112;72", + "type": "Color", + "label": "Layer 2 color (dirt)", + "group": "Layers", + "name": "Layer2Color" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 2 texture", + "description": "Optional. Tinted by the layer color.", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer2Texture" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 2 normal map", + "description": "Optional. The bumps of the texture, lit by the lights (an OpenGL normal map, like the textures of ambientCG).", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer2NormalMap" + }, + { + "value": "124;118;112", + "type": "Color", + "label": "Layer 3 color (rock)", + "group": "Layers", + "name": "Layer3Color" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 3 texture", + "description": "Optional. Tinted by the layer color.", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer3Texture" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 3 normal map", + "description": "Optional. The bumps of the texture, lit by the lights (an OpenGL normal map, like the textures of ambientCG).", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer3NormalMap" + }, + { + "value": "224;204;148", + "type": "Color", + "label": "Layer 4 color (sand)", + "group": "Layers", + "name": "Layer4Color" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 4 texture", + "description": "Optional. Tinted by the layer color.", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer4Texture" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 4 normal map", + "description": "Optional. The bumps of the texture, lit by the lights (an OpenGL normal map, like the textures of ambientCG).", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer4NormalMap" + }, + { + "value": "1", + "type": "Number", + "label": "Normal map strength", + "description": "How much the normal maps bump the ground: 0 for none, 1 as they are, more to exaggerate them.", + "group": "Layers", + "name": "NormalMapStrength" + }, + { + "value": "256", + "type": "Number", + "unit": "Pixel", + "label": "Texture size", + "description": "The size, in the scene, of one repetition of the layer textures.", + "group": "Layers", + "name": "TextureSize" + }, + { + "value": "0.5", + "type": "Number", + "label": "Height blending", + "description": "Where layers meet, the bright parts of textures (stones, tufts) cover the dark ones (gaps, sand) instead of fading into each other: 0 for none, 1 for the most.", + "group": "Layers", + "name": "HeightBlending" + }, + { + "value": "0.5", + "type": "Number", + "label": "Texture variation", + "description": "Makes the repetition of textures less visible, with patches of bigger and rotated textures and colors slowly varying: 0 for none, 1 for the most.", + "group": "Layers", + "name": "TextureVariation" + }, + { + "value": "false", + "type": "Boolean", + "label": "Wet shore", + "description": "Make the ground wet (darker and shiny) under a Z position, like the sand of a beach near the water.", + "group": "Wetness", + "name": "WetShore" + }, + { + "value": "0", + "type": "Number", + "unit": "Pixel", + "label": "Wet below Z", + "description": "The ground is wet under this Z position (for example, the Z of a 3D water object).", + "group": "Wetness", + "name": "WetShoreZ" + }, + { + "value": "60", + "type": "Number", + "unit": "Pixel", + "label": "Wet shore fade", + "description": "The ground becomes dry over this height above \"Wet below Z\".", + "group": "Wetness", + "name": "WetShoreFade" + }, + { + "value": "3", + "type": "Choice", + "label": "Layer on steep slopes", + "description": "This layer is shown where the ground is steep, whatever is painted.", + "group": "Layers", + "choices": [ + { + "label": "None", + "value": "0" + }, + { + "label": "Layer 1", + "value": "1" + }, + { + "label": "Layer 2", + "value": "2" + }, + { + "label": "Layer 3", + "value": "3" + }, + { + "label": "Layer 4", + "value": "4" + } + ], + "name": "CliffLayer" + }, + { + "value": "45", + "type": "Number", + "unit": "DegreeAngle", + "label": "Steep slope angle", + "group": "Layers", + "name": "CliffAngle" + }, + { + "value": "true", + "type": "Boolean", + "label": "Cast shadows", + "group": "Rendering", + "name": "CastShadow" + }, + { + "value": "true", + "type": "Boolean", + "label": "Receive shadows", + "group": "Rendering", + "name": "ReceiveShadow" + }, + { + "value": "", + "type": "String", + "label": "Sculpted and painted data", + "description": "Written by the sculpt and paint tools of the scene editor, in a compact format not meant to be read or written by hand: use Edits instead. Clear it to go back to the relief, heightmap and edits.", + "advanced": true, + "name": "SculptData" + } + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Shape", + "children": [ + { + "propertyName": "Relief" + }, + { + "propertyName": "Seed" + }, + { + "propertyName": "HeightmapImage" + }, + { + "propertyName": "Edits" + }, + { + "propertyName": "Resolution" + } + ] + }, + { + "folderName": "Layers", + "children": [ + { + "propertyName": "Layer1Color" + }, + { + "propertyName": "Layer1Texture" + }, + { + "propertyName": "Layer1NormalMap" + }, + { + "propertyName": "Layer2Color" + }, + { + "propertyName": "Layer2Texture" + }, + { + "propertyName": "Layer2NormalMap" + }, + { + "propertyName": "Layer3Color" + }, + { + "propertyName": "Layer3Texture" + }, + { + "propertyName": "Layer3NormalMap" + }, + { + "propertyName": "Layer4Color" + }, + { + "propertyName": "Layer4Texture" + }, + { + "propertyName": "Layer4NormalMap" + }, + { + "propertyName": "NormalMapStrength" + }, + { + "propertyName": "TextureSize" + }, + { + "propertyName": "HeightBlending" + }, + { + "propertyName": "TextureVariation" + }, + { + "propertyName": "CliffLayer" + }, + { + "propertyName": "CliffAngle" + } + ] + }, + { + "folderName": "Wetness", + "children": [ + { + "propertyName": "WetShore" + }, + { + "propertyName": "WetShoreZ" + }, + { + "propertyName": "WetShoreFade" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "CastShadow" + }, + { + "propertyName": "ReceiveShadow" + } + ] + }, + { + "propertyName": "SculptData" + } + ] + }, + "variants": [] + }, + { + "areaMaxX": 1000, + "areaMaxY": 1000, + "areaMaxZ": 40, + "areaMinX": 0, + "areaMinY": 0, + "areaMinZ": 0, + "defaultName": "Grass", + "description": "A field of grass blades swaying in the wind and pushed aside by characters. They grow in the area of the object, on the highest 3D terrain at each point (one field can cover several terrains), or at the Z position of the object where there is no terrain.", + "fullName": "Terrain grass", + "helpPath": "", + "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGQ9Ik0zIDIxYzEtNSAxLTkgMC0xMyAyIDMgMyA3IDMgMTN6bTQgMGMwLTYgMS0xMSA0LTE2LTEgNS0xIDEwIDAgMTZ6bTUgMGMwLTUgMi05IDUtMTItMiA0LTIgOC0xIDEyem01IDBjMS00IDItNiA0LTgtMSAzLTEgNS0xIDh6Ii8+PC9zdmc+", + "is3D": true, + "isUsingLegacyInstancesRenderer": true, + "name": "TerrainGrass", + "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/732ef90b7fcf5dd9171fe95d0cf262e09159b487304915a4693baa893d9ce16c_grass.svg", + "objects": [ + { + "assetStoreId": "", + "name": "AreaPlaceholder", + "type": "Scene3D::Cube3DObject", + "variables": [], + "effects": [], + "behaviors": [], + "content": { + "width": 1000, + "height": 1000, + "depth": 40, + "enableTextureTransparency": false, + "facesOrientation": "Y", + "backFaceUpThroughWhichAxisRotation": "X", + "materialType": "Basic", + "tint": "255;255;255", + "frontFaceResourceName": "", + "frontFaceVisible": false, + "frontFaceResourceRepeat": false, + "backFaceResourceName": "", + "backFaceVisible": false, + "backFaceResourceRepeat": false, + "leftFaceResourceName": "", + "leftFaceVisible": false, + "leftFaceResourceRepeat": false, + "rightFaceResourceName": "", + "rightFaceVisible": false, + "rightFaceResourceRepeat": false, + "topFaceResourceName": "", + "topFaceVisible": false, + "topFaceResourceRepeat": false, + "bottomFaceResourceName": "", + "bottomFaceVisible": false, + "bottomFaceResourceRepeat": false + } + } + ], + "objectsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "objectName": "AreaPlaceholder" + } + ] + }, + "objectsGroups": [], + "layers": [ + { + "ambientLightColorB": 200, + "ambientLightColorG": 200, + "ambientLightColorR": 200, + "camera2DPlaneMaxDrawingDistance": 5000, + "camera3DFarPlaneDistance": 10000, + "camera3DFieldOfView": 45, + "camera3DNearPlaneDistance": 3, + "cameraType": "", + "followBaseLayerCamera": false, + "isLightingLayer": false, + "isLocked": false, + "name": "", + "renderingType": "", + "visibility": true, + "cameras": [ + { + "defaultSize": true, + "defaultViewport": true, + "height": 0, + "viewportBottom": 1, + "viewportLeft": 0, + "viewportRight": 1, + "viewportTop": 0, + "width": 0 + } + ], + "effects": [] + } + ], + "instances": [ + { + "angle": 0, + "customSize": true, + "depth": 40, + "height": 1000, + "keepRatio": true, + "layer": "", + "name": "AreaPlaceholder", + "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-001000001000", + "width": 1000, + "x": 0, + "y": 0, + "zOrder": 1, + "numberProperties": [], + "stringProperties": [], + "initialVariables": [] + } + ], + "editionSettings": {}, + "eventsFunctions": [ + { + "fullName": "", + "functionType": "Action", + "name": "onCreated", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Terrain3D::DefineHelperClasses" + }, + "parameters": [ + "", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Terrain3D::DefineGrassHelperClasses" + }, + "parameters": [ + "", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "for (const child of object.getChildrenContainer().getAdhocListOfAllInstances()) {", + " child.deleteFromScene();", + "}", + "if (object.__terrainGrass) object.__terrainGrass.dispose();", + "object.__terrainGrass = new gdjs.__terrainGrassExtension.Grass(object);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "onDestroy", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrainGrass.dispose();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "doStepPostEvents", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const game = runtimeScene.getGame();", + "// Time doesn't pass in the scene editor: real time is used to show the wind.", + "const isInGameEdition = game.isInGameEdition && game.isInGameEdition();", + "const elapsedTime = isInGameEdition ? 1 / 60 : object.getElapsedTime() / 1000;", + "const time = isInGameEdition", + " ? performance.now() / 1000", + " : object.getRuntimeScene().getTimeManager().getTimeFromStart() / 1000;", + "const layer = object.getInstanceContainer().getLayer(object.getLayer());", + "object.__terrainGrass.update(elapsedTime, time, layer.getRenderer().getThreeCamera());" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "onHotReloading", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrainGrass.updateFromProperties();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" + } + ], + "objectGroups": [] + } + ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + } + ] + }, + "propertyDescriptors": [ + { + "value": "60", + "type": "Number", + "label": "Density", + "description": "The number of blades in a square of 100 x 100 pixels.", + "group": "Blades", + "name": "Density" + }, + { + "value": "36", + "type": "Number", + "unit": "Pixel", + "label": "Blade height", + "description": "The average height of blades (each blade is between 60% and 140% of it).", + "group": "Blades", + "name": "BladeHeight" + }, + { + "value": "5", + "type": "Number", + "unit": "Pixel", + "label": "Blade width", + "group": "Blades", + "name": "BladeWidth" + }, + { + "value": "52;104;36", + "type": "Color", + "label": "Base color", + "group": "Blades", + "name": "BaseColor" + }, + { + "value": "150;196;82", + "type": "Color", + "label": "Tip color", + "group": "Blades", + "name": "TipColor" + }, + { + "value": "1", + "type": "Number", + "label": "Seed", + "description": "Each seed places blades differently.", + "group": "Blades", + "name": "Seed" + }, + { + "value": "1", + "type": "Choice", + "label": "Grows on terrain layer", + "description": "On a 3D terrain, blades only grow where this layer is painted. A new terrain has layer 1 painted everywhere.", + "group": "Ground", + "choices": [ + { + "label": "Any", + "value": "0" + }, + { + "label": "Layer 1", + "value": "1" + }, + { + "label": "Layer 2", + "value": "2" + }, + { + "label": "Layer 3", + "value": "3" + }, + { + "label": "Layer 4", + "value": "4" + } + ], + "name": "GroundLayer" + }, + { + "value": "40", + "type": "Number", + "unit": "DegreeAngle", + "label": "Maximum slope", + "description": "No blades grow on steeper ground.", + "group": "Ground", + "name": "MaxSlope" + }, + { + "value": "1", + "type": "Number", + "label": "Wind strength", + "description": "0 for no wind.", + "group": "Wind", + "name": "WindStrength" + }, + { + "value": "2", + "type": "Number", + "label": "Wind speed", + "group": "Wind", + "name": "WindSpeed" + }, + { + "value": "", + "type": "String", + "label": "Object bending the grass", + "description": "The name of an object pushing blades aside when walking through the grass, like the player. The 8 first instances are used.", + "group": "Wind", + "name": "BendingObject" + }, + { + "value": "2500", + "type": "Number", + "unit": "Pixel", + "label": "Fade distance", + "description": "Blades are less and less dense up to this distance from the camera, and hidden after.", + "group": "Rendering", + "name": "FadeDistance" + } + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Blades", + "children": [ + { + "propertyName": "Density" + }, + { + "propertyName": "BladeHeight" + }, + { + "propertyName": "BladeWidth" + }, + { + "propertyName": "BaseColor" + }, + { + "propertyName": "TipColor" + }, + { + "propertyName": "Seed" + } + ] + }, + { + "folderName": "Ground", + "children": [ + { + "propertyName": "GroundLayer" + }, + { + "propertyName": "MaxSlope" + } + ] + }, + { + "folderName": "Wind", + "children": [ + { + "propertyName": "WindStrength" + }, + { + "propertyName": "WindSpeed" + }, + { + "propertyName": "BendingObject" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "FadeDistance" + } + ] + } + ] + }, + "variants": [] + }, + { + "areaMaxX": 2000, + "areaMaxY": 2000, + "areaMaxZ": 200, + "areaMinX": 0, + "areaMinY": 0, + "areaMinZ": 0, + "assetStoreTag": "3d scattered models", + "defaultName": "ScatteredModels", + "description": "Many copies of a 3D model (trees, rocks, bushes, flowers...) placed in the area of the object and drawn all at once, which is much faster than an object for each one. They stand on the highest 3D terrain at each point, or at the Z position of the object where there is no terrain. Paint where they grow with the brushes of the scene editor.", + "fullName": "Scattered models (forests, rocks...)", + "helpPath": "", + "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGQ9Ik0xMCAyMXYtM0gzbDUtNUg1bDUtNUg3bDUtNSA1IDVoLTNsNSA1aC0zbDUgNWgtN3YzeiIvPjwvc3ZnPg==", + "is3D": true, + "isUsingLegacyInstancesRenderer": true, + "name": "ScatteredModels", + "previewIconUrl": "", + "objects": [ + { + "assetStoreId": "", + "name": "AreaPlaceholder", + "type": "Scene3D::Cube3DObject", + "variables": [], + "effects": [], + "behaviors": [], + "content": { + "width": 2000, + "height": 2000, + "depth": 200, + "enableTextureTransparency": false, + "facesOrientation": "Y", + "backFaceUpThroughWhichAxisRotation": "X", + "materialType": "Basic", + "tint": "255;255;255", + "frontFaceResourceName": "", + "frontFaceVisible": false, + "frontFaceResourceRepeat": false, + "backFaceResourceName": "", + "backFaceVisible": false, + "backFaceResourceRepeat": false, + "leftFaceResourceName": "", + "leftFaceVisible": false, + "leftFaceResourceRepeat": false, + "rightFaceResourceName": "", + "rightFaceVisible": false, + "rightFaceResourceRepeat": false, + "topFaceResourceName": "", + "topFaceVisible": false, + "topFaceResourceRepeat": false, + "bottomFaceResourceName": "", + "bottomFaceVisible": false, + "bottomFaceResourceRepeat": false + } + } + ], + "objectsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "objectName": "AreaPlaceholder" + } + ] + }, + "objectsGroups": [], + "layers": [ + { + "ambientLightColorB": 200, + "ambientLightColorG": 200, + "ambientLightColorR": 200, + "camera2DPlaneMaxDrawingDistance": 5000, + "camera3DFarPlaneDistance": 10000, + "camera3DFieldOfView": 45, + "camera3DNearPlaneDistance": 3, + "cameraType": "", + "followBaseLayerCamera": false, + "isLightingLayer": false, + "isLocked": false, + "name": "", + "renderingType": "", + "visibility": true, + "cameras": [ + { + "defaultSize": true, + "defaultViewport": true, + "height": 0, + "viewportBottom": 1, + "viewportLeft": 0, + "viewportRight": 1, + "viewportTop": 0, + "width": 0 + } + ], + "effects": [] + } + ], + "instances": [ + { + "angle": 0, + "customSize": true, + "depth": 200, + "height": 2000, + "keepRatio": true, + "layer": "", + "name": "AreaPlaceholder", + "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-002000002000", + "width": 2000, + "x": 0, + "y": 0, + "zOrder": 1, + "numberProperties": [], + "stringProperties": [], + "initialVariables": [] + } + ], + "editionSettings": {}, + "eventsFunctions": [ + { + "fullName": "", + "functionType": "Action", + "name": "onCreated", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Terrain3D::DefineHelperClasses" + }, + "parameters": [ + "", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Terrain3D::DefineScatterHelperClasses" + }, + "parameters": [ + "", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "for (const child of object.getChildrenContainer().getAdhocListOfAllInstances()) {", + " child.deleteFromScene();", + "}", + "if (object.__terrainScatter) object.__terrainScatter.dispose();", + "object.__terrainScatter = new gdjs.__terrainScatterExtension.Scatter(object);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "onDestroy", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrainScatter.dispose();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "doStepPostEvents", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const game = runtimeScene.getGame();", + "const isInGameEdition = game.isInGameEdition && game.isInGameEdition();", + "const elapsedTime = isInGameEdition ? 1 / 60 : object.getElapsedTime() / 1000;", + "const layer = object.getInstanceContainer().getLayer(object.getLayer());", + "object.__terrainScatter.update(elapsedTime, layer.getRenderer().getThreeCamera());" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "onHotReloading", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrainScatter.onHotReloading();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "description": "Paint the density of copies around a position, up to the density of the object (copies removed with \"Erase copies\" come back). The effect fades out up to the radius.", + "fullName": "Add copies", + "functionType": "Action", + "group": "Paint", + "name": "AddCopies", + "sentence": "Add copies of _PARAM0_ around _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "object.__terrainScatter.addCopies(x, y, radius);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "name": "Radius", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Remove the copies standing in a circle, for example a tree cut by the player (use the position of the nearest copy and a small radius) or a forest cleared for a building. They come back when the scene is restarted.", + "fullName": "Erase copies", + "functionType": "Action", + "group": "Paint", + "name": "EraseCopies", + "sentence": "Erase the copies of _PARAM0_ in a circle at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "object.__terrainScatter.removeCopies(x, y, radius);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "name": "Radius", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "the number of copies placed in the area.", + "fullName": "Number of copies", + "functionType": "ExpressionAndCondition", + "group": "Copies", + "name": "CopyCount", + "sentence": "the number of copies", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "eventsFunctionContext.returnValue = object.__terrainScatter.getCopyCount();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "description": "The X position of the base of the copy nearest to a position (the position itself if there is no copy).", + "fullName": "X position of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyX", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? copy.x : x;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "The Y position of the base of the copy nearest to a position (the position itself if there is no copy).", + "fullName": "Y position of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyY", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? copy.y : y;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "The Z position of the base of the copy nearest to a position (on the ground).", + "fullName": "Z position of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyZ", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? copy.z : object.getZ();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "the distance (on X and Y) from a position to the base of the nearest copy. It's a very big number when there is no copy.", + "fullName": "Distance to the nearest copy", + "functionType": "ExpressionAndCondition", + "group": "Copies", + "name": "NearestCopyDistance", + "sentence": "the distance from _PARAM1_; _PARAM2_ to the nearest copy", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? Math.hypot(copy.x - x, copy.y - y) : Number.MAX_VALUE;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + } + ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + }, + { + "folderName": "Paint", + "children": [ + { + "functionName": "AddCopies" + }, + { + "functionName": "EraseCopies" + } + ] + }, + { + "folderName": "Copies", + "children": [ + { + "functionName": "CopyCount" + }, + { + "functionName": "NearestCopyX" + }, + { + "functionName": "NearestCopyY" + }, + { + "functionName": "NearestCopyZ" + }, + { + "functionName": "NearestCopyDistance" + } + ] + } + ] + }, + "propertyDescriptors": [ + { + "value": "", + "type": "Resource", + "label": "3D model", + "description": "The model copied everywhere (a .glb file). Models standing on the Y axis, like most models of the asset store, are stood up along Z.", + "group": "Model", + "extraInformation": [ + "model3D" + ], + "choices": [], + "name": "Model" + }, + { + "value": "300", + "type": "Number", + "unit": "Pixel", + "label": "Height of a copy", + "description": "Copies keep the proportions of the model.", + "group": "Model", + "name": "Size" + }, + { + "value": "0.8", + "type": "Number", + "label": "Smallest random scale", + "description": "Each copy is scaled at random between the smallest and the largest scale.", + "group": "Model", + "name": "MinScale" + }, + { + "value": "1.2", + "type": "Number", + "label": "Largest random scale", + "group": "Model", + "name": "MaxScale" + }, + { + "value": "true", + "type": "Boolean", + "label": "Random rotation", + "description": "Turn each copy around the Z axis at random.", + "group": "Model", + "name": "RandomRotation" + }, + { + "value": "255;255;255", + "type": "Color", + "label": "Random tint", + "description": "Each copy is tinted between white and this color, at random. White for no tint.", + "group": "Model", + "name": "RandomTint" + }, + { + "value": "0.3", + "type": "Number", + "label": "Density", + "description": "The number of copies in a square of 100 x 100 pixels where the density is fully painted (0.3 is about one copy every 180 pixels). Copies never overlap their neighbors much: they are placed at random in a grid.", + "group": "Placement", + "name": "Density" + }, + { + "value": "1", + "type": "Number", + "label": "Seed", + "description": "Each seed places copies differently.", + "group": "Placement", + "name": "Seed" + }, + { + "value": "Everywhere", + "type": "Choice", + "label": "Copies grow", + "description": "Everywhere in the area, or only where they are painted with the \"Add copies\" brush (or the edits).", + "group": "Placement", + "choices": [ + { + "label": "Everywhere (erase them with the brush)", + "value": "Everywhere" + }, + { + "label": "Only where painted", + "value": "Painted" + } + ], + "name": "Coverage" + }, + { + "value": "1", + "type": "Choice", + "label": "Grows on terrain layer", + "description": "On a 3D terrain, copies only grow where this layer is painted, away from its borders (so that trees don't stand on roads painted with another layer). A new terrain has layer 1 painted everywhere.", + "group": "Ground", + "choices": [ + { + "label": "Any", + "value": "0" + }, + { + "label": "Layer 1", + "value": "1" + }, + { + "label": "Layer 2", + "value": "2" + }, + { + "label": "Layer 3", + "value": "3" + }, + { + "label": "Layer 4", + "value": "4" + } + ], + "name": "GroundLayer" + }, + { + "value": "30", + "type": "Number", + "unit": "DegreeAngle", + "label": "Maximum slope", + "description": "No copies grow on steeper ground.", + "group": "Ground", + "name": "MaxSlope" + }, + { + "value": "-100000", + "type": "Number", + "unit": "Pixel", + "label": "Minimum ground Z", + "description": "No copies grow where the ground is lower (for example, under the water level).", + "group": "Ground", + "name": "MinGroundZ" + }, + { + "value": "100000", + "type": "Number", + "unit": "Pixel", + "label": "Maximum ground Z", + "description": "No copies grow where the ground is higher (for example, above the tree line).", + "group": "Ground", + "name": "MaxGroundZ" + }, + { + "value": "false", + "type": "Boolean", + "label": "Trunk collisions", + "description": "Give each copy a static collision shape around its trunk, for the 3D physics behavior (add it to this object, with the \"Static\" type) and navigation meshes. Without them, these behaviors have no effect on this object.", + "group": "Collisions", + "name": "TrunkCollisions" + }, + { + "value": "30", + "type": "Number", + "unit": "Pixel", + "label": "Trunk diameter", + "description": "The diameter of the collision shape of a copy at the height given in \"Height of a copy\" (it is scaled with the copy).", + "group": "Collisions", + "name": "TrunkDiameter" + }, + { + "value": "8000", + "type": "Number", + "unit": "Pixel", + "label": "Draw distance", + "description": "Copies shrink and are hidden at this distance from the camera.", + "group": "Rendering", + "name": "DrawDistance" + }, + { + "value": "true", + "type": "Boolean", + "label": "Cast shadows", + "group": "Rendering", + "name": "CastShadow" + }, + { + "value": "true", + "type": "Boolean", + "label": "Receive shadows", + "group": "Rendering", + "name": "ReceiveShadow" + }, + { + "value": "", + "type": "MultilineString", + "label": "Edits", + "description": "Where copies are added or erased, applied in order, as a JSON list, for example [{\"tool\": \"add\", \"x\": 0.3, \"y\": 0.4, \"radius\": 0.15}, {\"tool\": \"erase\", \"x\": 0.1, \"y\": 0.9, \"toX\": 0.8, \"toY\": 0.2, \"radius\": 0.02}]. Tools: add (density: from 0 to 1, 1 by default, how dense copies are compared to the Density property) and erase. strength (from 0 to 1) makes a stroke lighter. x, y and radius are fractions (0 to 1) of the object size (x is (scene X - object X) / object width). Add toX and toY to paint along a line (a row of trees, a path through a forest). With \"Copies grow: only where painted\", only the added areas have copies. Shown in the scene editor.", + "group": "Placement", + "advanced": true, + "name": "Edits" + }, + { + "value": "", + "type": "String", + "label": "Painted data", + "description": "Written by the brushes of the scene editor, in a compact format not meant to be read or written by hand: use Edits instead. Clear it to go back to the coverage and the edits.", + "advanced": true, + "name": "PaintData" + } + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Model", + "children": [ + { + "propertyName": "Model" + }, + { + "propertyName": "Size" + }, + { + "propertyName": "MinScale" + }, + { + "propertyName": "MaxScale" + }, + { + "propertyName": "RandomRotation" + }, + { + "propertyName": "RandomTint" + } + ] + }, + { + "folderName": "Placement", + "children": [ + { + "propertyName": "Density" + }, + { + "propertyName": "Seed" + }, + { + "propertyName": "Coverage" + }, + { + "propertyName": "Edits" + } + ] + }, + { + "folderName": "Ground", + "children": [ + { + "propertyName": "GroundLayer" + }, + { + "propertyName": "MaxSlope" + }, + { + "propertyName": "MinGroundZ" + }, + { + "propertyName": "MaxGroundZ" + } + ] + }, + { + "folderName": "Collisions", + "children": [ + { + "propertyName": "TrunkCollisions" + }, + { + "propertyName": "TrunkDiameter" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "DrawDistance" + }, + { + "propertyName": "CastShadow" + }, + { + "propertyName": "ReceiveShadow" + } + ] + }, + { + "propertyName": "PaintData" + } + ] + }, + "variants": [] + } + ] } ], "externalLayouts": [] diff --git a/examples/starting-3d-driving/assets/StartingWall.png b/examples/starting-3d-driving/assets/StartingWall.png index b3a097887..06278d6e5 100644 Binary files a/examples/starting-3d-driving/assets/StartingWall.png and b/examples/starting-3d-driving/assets/StartingWall.png differ diff --git a/examples/starting-3d-driving/starting-3d-driving.json b/examples/starting-3d-driving/starting-3d-driving.json index 78c28fbc1..8d2b0d827 100644 --- a/examples/starting-3d-driving/starting-3d-driving.json +++ b/examples/starting-3d-driving/starting-3d-driving.json @@ -1,7 +1,7 @@ { "firstLayout": "", "gdVersion": { - "build": 283, + "build": 284, "major": 5, "minor": 6, "revision": 0 @@ -206,6 +206,22 @@ "name": "assets/Flat dark joystick border horizontal.png", "smoothed": true, "userAdded": false + }, + { + "file": "assets/StartingCheckeredPattern.png", + "kind": "image", + "metadata": "", + "name": "assets/StartingCheckeredPattern.png", + "smoothed": true, + "userAdded": false + }, + { + "file": "assets/StartingGround.png", + "kind": "image", + "metadata": "", + "name": "assets/StartingGround.png", + "smoothed": true, + "userAdded": false } ] }, @@ -222,12 +238,13 @@ "mangledName": "Game_32Scene", "name": "Game Scene", "r": 255, + "renderer3DWorldScale": 100, "standardSortMethod": true, "stopSoundsOnStartup": true, "title": "", "v": 255, "uiSettings": { - "grid": false, + "grid": true, "gridType": "rectangular", "gridWidth": 64, "gridHeight": 64, @@ -237,7 +254,7 @@ "gridOffsetZ": 0, "gridColor": 10401023, "gridAlpha": 0.8, - "snap": false, + "snap": true, "zoomFactor": 0.4633111824514118, "windowMask": false, "selectedLayer": "", @@ -246,24 +263,6 @@ "objectsGroups": [], "variables": [], "instances": [ - { - "angle": 0, - "customSize": true, - "depth": 10, - "height": 704, - "layer": "", - "locked": true, - "name": "Road_3D", - "persistentUuid": "46a0d799-55c4-4f44-b430-120b5b0bc11e", - "width": 5312, - "x": -128, - "y": 0, - "z": -10, - "zOrder": 1, - "numberProperties": [], - "stringProperties": [], - "initialVariables": [] - }, { "angle": 0, "customSize": true, @@ -283,24 +282,6 @@ "stringProperties": [], "initialVariables": [] }, - { - "angle": 0, - "customSize": true, - "depth": 10, - "height": 576, - "layer": "", - "locked": true, - "name": "Grass_3D", - "persistentUuid": "0ded717a-a950-4abf-8393-0a554a43b2f2", - "width": 5312, - "x": -128, - "y": -576, - "z": -10, - "zOrder": 7, - "numberProperties": [], - "stringProperties": [], - "initialVariables": [] - }, { "angle": 0, "customSize": false, @@ -316,24 +297,6 @@ "stringProperties": [], "initialVariables": [] }, - { - "angle": 0, - "customSize": true, - "depth": 10, - "height": 576, - "layer": "", - "locked": true, - "name": "Grass_3D", - "persistentUuid": "f9ea97fc-7ed3-421a-8559-e7f829e15f39", - "width": 5312, - "x": -128, - "y": 704, - "z": -10, - "zOrder": 7, - "numberProperties": [], - "stringProperties": [], - "initialVariables": [] - }, { "angle": 0, "customSize": false, @@ -699,6 +662,22 @@ "numberProperties": [], "stringProperties": [], "initialVariables": [] + }, + { + "angle": 0, + "customSize": true, + "height": 1856, + "layer": "", + "locked": true, + "name": "Terrain", + "persistentUuid": "770e6034-1084-4988-ac7c-c23e98e9cacd", + "width": 5312, + "x": -128, + "y": -576, + "zOrder": 0, + "numberProperties": [], + "stringProperties": [], + "initialVariables": [] } ], "objects": [ @@ -771,75 +750,6 @@ "tileScale": 0.2 } }, - { - "assetStoreId": "", - "name": "Grass_3D", - "persistentUuid": "ab1470f2-49de-49b2-b6a0-1be8f321724e", - "type": "Scene3D::Cube3DObject", - "variables": [], - "effects": [], - "behaviors": [ - { - "name": "Physics3D", - "type": "Physics3D::Physics3DBehavior", - "object3D": "Object3D", - "bodyType": "Static", - "bullet": false, - "fixedRotation": false, - "shape": "Box", - "shapeOrientation": "Z", - "shapeDimensionA": 0, - "shapeDimensionB": 0, - "shapeDimensionC": 0, - "density": 1, - "friction": 5, - "restitution": 0.1, - "linearDamping": 0.1, - "angularDamping": 0.1, - "gravityScale": 1, - "layers": 17, - "masks": 17, - "shapeOffsetX": 0, - "shapeOffsetY": 0, - "shapeOffsetZ": 0, - "massCenterOffsetX": 0, - "massCenterOffsetY": 0, - "massCenterOffsetZ": 0, - "massOverride": 0 - } - ], - "content": { - "width": 128, - "height": 128, - "depth": 128, - "enableTextureTransparency": false, - "facesOrientation": "Y", - "frontFaceResourceName": "assets\\StartingGround.png", - "backFaceResourceName": "assets\\StartingGround.png", - "backFaceUpThroughWhichAxisRotation": "X", - "leftFaceResourceName": "assets\\StartingGround.png", - "rightFaceResourceName": "assets\\StartingGround.png", - "topFaceResourceName": "assets\\StartingGround.png", - "bottomFaceResourceName": "assets\\StartingGround.png", - "frontFaceVisible": true, - "backFaceVisible": true, - "leftFaceVisible": true, - "rightFaceVisible": true, - "topFaceVisible": true, - "bottomFaceVisible": true, - "frontFaceResourceRepeat": true, - "backFaceResourceRepeat": true, - "leftFaceResourceRepeat": true, - "rightFaceResourceRepeat": true, - "topFaceResourceRepeat": true, - "bottomFaceResourceRepeat": true, - "materialType": "StandardWithoutMetalness", - "tint": "255;255;255", - "isCastingShadow": true, - "isReceivingShadow": true, - "tileScale": 0.2 - } - }, { "assetStoreId": "cddab55130dbd5a0b36f763200e876fb514aa608e1b057a136ebdb5f80006e2c", "name": "PlayerCar", @@ -1262,6 +1172,60 @@ "isReceivingShadow": true, "tileScale": 0.2 } + }, + { + "assetStoreId": "", + "name": "Terrain", + "persistentUuid": "0380bd3e-0023-4906-a893-c930d50f78d1", + "type": "Terrain3D::Terrain3D", + "variant": "", + "variables": [], + "effects": [], + "behaviors": [ + { + "name": "Physics3D", + "type": "Physics3D::Physics3DBehavior", + "object3D": "Object3D", + "bodyType": "Static", + "bullet": false, + "fixedRotation": false, + "shape": "Box", + "meshShapeResourceName": "", + "shapeOrientation": "Z", + "shapeDimensionA": 0, + "shapeDimensionB": 0, + "shapeDimensionC": 0, + "shapeOffsetX": 0, + "shapeOffsetY": 0, + "shapeOffsetZ": 0, + "massCenterOffsetX": 0, + "massCenterOffsetY": 0, + "massCenterOffsetZ": 0, + "massOverride": 0, + "density": 1, + "friction": 7, + "restitution": 0.1, + "linearDamping": 0.1, + "angularDamping": 0.1, + "gravityScale": 1, + "layers": 17, + "masks": 17 + } + ], + "content": { + "SculptData": "2;256;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", + "Layer1Texture": "assets/StartingGround.png", + "Layer1Color": "255;255;255", + "HeightBlending": 0.5, + "TextureVariation": 0, + "TextureSize": 128, + "CliffLayer": "3", + "Layer2Texture": "assets/StartingCheckeredPattern.png", + "Layer3Texture": "", + "CliffAngle": 45, + "CastShadow": false, + "Layer2Color": "255;255;255" + } } ], "objectsFolderStructure": { @@ -1274,7 +1238,7 @@ "objectName": "Walls_3D" }, { - "objectName": "Grass_3D" + "objectName": "Terrain" }, { "objectName": "PlayerCar" @@ -2205,7 +2169,6 @@ "name": "ThirdPersonCamera", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -4149,7 +4112,6 @@ "unit": "Second", "label": "Halfway time for rotation", "group": "Catch-up speed", - "quickCustomizationVisibility": "hidden", "name": "RotationHalfwayDuration" }, { @@ -4158,7 +4120,6 @@ "unit": "Second", "label": "Halfway time for elevation rotation", "group": "Catch-up speed", - "quickCustomizationVisibility": "hidden", "name": "ElevationHalfwayDuration" }, { @@ -4167,7 +4128,6 @@ "unit": "Second", "label": "Halfway time on Z axis", "group": "Catch-up speed", - "quickCustomizationVisibility": "hidden", "name": "TranslationZHalfwayDuration" }, { @@ -4225,7 +4185,6 @@ "label": "Follow free area top border on Z axis", "group": "Position", "advanced": true, - "quickCustomizationVisibility": "hidden", "name": "FollowFreeAreaZMax" }, { @@ -4235,7 +4194,6 @@ "label": "Follow free area bottom border on Z axis", "group": "Position", "advanced": true, - "quickCustomizationVisibility": "hidden", "name": "FollowFreeAreaZMin" }, { @@ -4285,14 +4243,12 @@ "value": "true", "type": "Boolean", "label": "Automatically rotate the camera with the object", - "quickCustomizationVisibility": "hidden", "name": "IsRotatingWithObject" }, { "value": "false", "type": "Boolean", "label": "Automatically rotate the camera with the object (elevation)", - "quickCustomizationVisibility": "hidden", "name": "IsElevatingWithObject" }, { @@ -8373,7 +8329,6 @@ "name": "PlatformerMultitouchMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -8627,7 +8582,6 @@ "name": "Platformer3DMultitouchMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -8802,7 +8756,6 @@ "name": "Shooter3DMultitouchMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -8966,7 +8919,6 @@ "name": "FirstPersonMultitouchMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -10171,7 +10123,6 @@ "name": "PhysicsCar3DMultitouchMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -10384,7 +10335,6 @@ "name": "TopDownMultitouchMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -12463,7 +12413,6 @@ "name": "PhysicsCar3DKeyboardMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -12689,6 +12638,7802 @@ } ], "eventsBasedObjects": [] + }, + { + "author": "", + "category": "General", + "dimension": "3D", + "extensionNamespace": "", + "fullName": "3D terrain", + "gdevelopVersion": ">=5.5.222", + "helpPath": "", + "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLXRlcnJhaW4iIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTQsNkwxMC4yNSwxMUwxMy4xLDE0LjhMMTEuNSwxNkM5LjgxLDEzLjc1IDcsMTAgNywxMEwxLDE4SDIzTDE0LDZaIiAvPjwvc3ZnPg==", + "name": "Terrain3D", + "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", + "shortDescription": "A ground with hills and valleys that can be sculpted and painted in the editor, with collisions.", + "version": "0.1.0", + "description": [ + "A 3D terrain object for open worlds, adventure or racing games: hills, mountains, islands or valleys, with painted layers of grass, dirt, rock or sand.", + "", + "- **Sculpt and paint in the scene editor**: select a terrain to show the tools (raise, lower, smooth, flatten and paint 4 layers).", + "- **Start from a relief** (hills, mountains, island) or **from a heightmap image**.", + "- **Steep slopes automatically show rock** (or any layer).", + "- **Natural borders between layers**: stones and tufts of a texture cover the gaps of the other where layers meet (height blending), and the repetition of textures is made less visible (texture variation).", + "- **Physics**: add the **3D physics** behavior to the terrain. Characters and objects collide with the ground exactly. The terrain is always static.", + "- **Pathfinding**: add the **floor/obstacle for pathfinding (navmesh based)** behavior to the terrain, so that characters with the navmesh pathfinding behavior walk on its hills and around its cliffs (in recent GDevelop versions).", + "- **Shape it with the Edits property**: a list of raise, flatten, smooth and paint strokes written in JSON, shown in the scene editor. It's readable and can be changed by hand or by an AI agent, like the relief, the heightmap and the other properties. Actions only change the terrain during the game.", + "- **Change it during the game** with actions (raise, lower, flatten, smooth, paint, along a line for paths and rivers) and read the ground height, slope or layer with expressions.", + "- **Grass**: add a **terrain grass** object over the terrain. Thousands of blades sway in the wind, grow where a layer (like grass) is painted and where the ground is not too steep, and are pushed aside by characters. Sculpt or paint the terrain and the grass follows.", + "- **Forests, rocks and bushes**: add a **scattered models** object over the terrain. It places hundreds or thousands of copies of a 3D model (trees, rocks, bushes, flowers...) on the ground and draws them all at once, which is much faster than an object for each. Copies grow where a layer is painted, away from steep slopes, and follow the ground when it's sculpted. Add or erase them with the brushes of the scene editor or with the Edits property, give trees trunk collisions, and cut them down with actions. Ready-made forests, rocks and bushes are in the asset store.", + "- **Tunnels and caves**: make holes in the ground (and fill them again) with the brushes, the Edits property or actions, and put a 3D model of the tunnel or the cave (with a mesh collision shape) under the ground. Nothing is drawn in holes, and characters, cars and rays go through them.", + "- **Made for large worlds**: the terrain is split in chunks that are hidden when off-screen and simplified when far away, and collisions are updated only where the ground changes." + ], + "origin": { + "identifier": "Terrain3D", + "name": "gdevelop-extension-store" + }, + "tags": [ + "3d", + "terrain", + "ground", + "heightmap", + "landscape", + "open world", + "grass", + "foliage", + "forest", + "trees", + "tunnel", + "cave" + ], + "authorIds": [], + "dependencies": [], + "globalVariables": [], + "sceneVariables": [], + "eventsFunctions": [ + { + "description": "Define helper classes JavaScript code.", + "fullName": "Define helper classes", + "functionType": "Action", + "name": "DefineHelperClasses", + "private": true, + "sentence": "Define helper classes JavaScript code", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "if (gdjs.__terrain3DExtension) {", + " return;", + "}", + "", + "// Tested by the gameplay tests of the \"3D island terrain\" example (GDevelop-examples repository).", + "", + "const groundsByScene = new WeakMap();", + "/**", + " * The grounds of a scene (the terrains), followed by the terrain grass.", + " *", + " * A ground has: `getVersion()` (changing when the ground changes),", + " * `containsPoint(x, y)`, `getHeightAt(x, y)` (Z position of the ground),", + " * `getSlopeAt(x, y)` (in degrees), `getLayerWeightAt(x, y, layer)` (how", + " * much a painted layer, from 1 to 4, is there, from 0 to 1) and optionally", + " * `getTintAt(x, y)` (the painted tint, as red, green and blue from 0 to 255).", + " * @param {gdjs.RuntimeScene} runtimeScene", + " * @returns {Set}", + " */", + "const getGrounds = (runtimeScene) => {", + " let grounds = groundsByScene.get(runtimeScene);", + " if (!grounds) {", + " grounds = new Set();", + " groundsByScene.set(runtimeScene, grounds);", + " }", + " return grounds;", + "};", + "", + "/**", + " * @param {number} seed", + " * @returns {() => number} Random numbers between 0 (included) and 1 (excluded), the same for the same seed.", + " */", + "const createRandom = (seed) => {", + " let state = seed >>> 0 || 1;", + " return () => {", + " state ^= state << 13;", + " state ^= state >>> 17;", + " state ^= state << 5;", + " return (state >>> 0) / 4294967296;", + " };", + "};", + "", + "/**", + " * Find the highest ground at a position: grounds can overlap (like an island over a sea floor).", + " * @param {Iterable} grounds", + " * @param {{ground: object | null, z: number}} result Set to the ground (null if there is none) and its Z position.", + " */", + "const findHighestGround = (grounds, x, y, result) => {", + " result.ground = null;", + " for (const ground of grounds) {", + " if (!ground.containsPoint(x, y)) continue;", + " const z = ground.getHeightAt(x, y);", + " if (!result.ground || z > result.z) {", + " result.ground = ground;", + " result.z = z;", + " }", + " }", + " return result;", + "};", + "", + "// The grounds or the objects placed on them can change every frame (while", + "// sculpting or moving them in the editor): things are placed again at most this often.", + "const PLACEMENT_DELAY = 0.3;", + "", + "/**", + " * Tells when things placed on the grounds of a scene (grass blades, scattered", + " * models) must be placed again: after the grounds or the given properties", + " * changed, at most every `PLACEMENT_DELAY` seconds.", + " */", + "class GroundPlacement {", + " /** @param {gdjs.RuntimeScene} runtimeScene */", + " constructor(runtimeScene) {", + " this.grounds = getGrounds(runtimeScene);", + " this.placedFrom = '';", + " this.timeSincePlacement = 0;", + " }", + "", + " /**", + " * @param {number} elapsedSeconds", + " * @param {string} propertiesKey Changes when the things must be placed again.", + " */", + " shouldPlaceAgain(elapsedSeconds, propertiesKey) {", + " this.timeSincePlacement += elapsedSeconds;", + " if (this.placedFrom && this.timeSincePlacement < PLACEMENT_DELAY) return false;", + " let placementKey = propertiesKey;", + " for (const ground of this.grounds) placementKey += '|' + ground.getVersion();", + " if (placementKey === this.placedFrom) return false;", + " this.placedFrom = placementKey;", + " this.timeSincePlacement = 0;", + " return true;", + " }", + "", + " placeAgain() {", + " this.placedFrom = '';", + " }", + "}", + "", + "/**", + " * Replace parts of the shader code of a Three.js material, with an error if", + " * a part is missing (for example after an update of Three.js).", + " * @param {string} shaderCode", + " * @param {Array<[string, string]>} replacements The searched code and its replacement.", + " * @param {string} materialName", + " */", + "const patchShaderCode = (shaderCode, replacements, materialName) => {", + " for (const [searchedCode, newCode] of replacements) {", + " if (!shaderCode.includes(searchedCode)) {", + " console.error(`${materialName}: \"${searchedCode}\" was not found in the shader, which won't look as expected.`);", + " }", + " shaderCode = shaderCode.replace(searchedCode, newCode);", + " }", + " return shaderCode;", + "};", + "", + "// Size of the object inner area. Instances are scaled from it, so these only", + "// define the default size of a new terrain.", + "const AREA_SIZE = 4096;", + "const AREA_DEPTH = 600;", + "", + "// Terrains have at most 8 x 8 chunks (so at most 64 draw calls), of at least", + "// 32 x 32 cells.", + "const MIN_CHUNK_CELLS = 32;", + "const MAX_CHUNKS_PER_SIDE = 8;", + "/** @param {number} resolution */", + "const getChunkCells = (resolution) => Math.max(MIN_CHUNK_CELLS, resolution / MAX_CHUNKS_PER_SIDE);", + "// Grid steps of each level of detail, and the distance (in chunk sizes)", + "// from which each one is used.", + "const LOD_STEPS = [1, 2, 4];", + "const LOD_DISTANCES = [0, 3, 6];", + "", + "const LAYER_COUNT = 4;", + "const RESOLUTIONS = [64, 128, 256, 512, 1024];", + "const DEFAULT_RESOLUTION = 256;", + "// Each undo step is a copy of the data: fewer are kept for big terrains.", + "const UNDO_STEPS_SAMPLES = 20 * 257 * 257;", + "", + "const clamp01 = (value) => (value < 0 ? 0 : value > 1 ? 1 : value);", + "const clampInteger = (value, min, max) =>", + " value < min ? min : value > max ? max : Math.floor(value);", + "", + "/**", + " * A rectangle of grid samples, inclusive. Empty when `minI > maxI`.", + " */", + "class SampleRectangle {", + " constructor() {", + " this.clear();", + " }", + "", + " clear() {", + " this.minI = Number.MAX_SAFE_INTEGER;", + " this.minJ = Number.MAX_SAFE_INTEGER;", + " this.maxI = -1;", + " this.maxJ = -1;", + " }", + "", + " isEmpty() {", + " return this.minI > this.maxI;", + " }", + "", + " /** @param {SampleRectangle} other */", + " add(other) {", + " if (other.isEmpty()) return;", + " this.minI = Math.min(this.minI, other.minI);", + " this.minJ = Math.min(this.minJ, other.minJ);", + " this.maxI = Math.max(this.maxI, other.maxI);", + " this.maxJ = Math.max(this.maxJ, other.maxJ);", + " }", + "", + " set(minI, minJ, maxI, maxJ) {", + " this.minI = minI;", + " this.minJ = minJ;", + " this.maxI = maxI;", + " this.maxJ = maxJ;", + " return this;", + " }", + "}", + "", + "/**", + " * The heights (between 0 and 1) and the painted layers weights of a square", + " * grid of `size * size` samples.", + " */", + "class TerrainData {", + " /** @param {number} resolution The number of cells on each side. */", + " constructor(resolution) {", + " this.resolution = resolution;", + " this.size = resolution + 1;", + " this.heights = new Float32Array(this.size * this.size);", + " /** 4 weights (0-255) per sample, one for each layer. */", + " this.splat = new Uint8Array(this.size * this.size * LAYER_COUNT);", + " this.fillLayer(0);", + " /**", + " * 4 bytes per sample: a tint multiplying the layers (red, green and", + " * blue, white for none) and the wetness (0 for dry).", + " */", + " this.colors = new Uint8Array(this.size * this.size * 4);", + " this.clearColors();", + " }", + "", + " clearColors() {", + " this.colors.fill(255);", + " for (let index = 3; index < this.colors.length; index += 4) {", + " this.colors[index] = 0;", + " }", + " }", + "", + " /** @param {number} layer */", + " fillLayer(layer) {", + " this.splat.fill(0);", + " for (let index = layer; index < this.splat.length; index += LAYER_COUNT) {", + " this.splat[index] = 255;", + " }", + " }", + "", + " getHeight(i, j) {", + " const last = this.resolution;", + " const clampedI = i < 0 ? 0 : i > last ? last : i;", + " const clampedJ = j < 0 ? 0 : j > last ? last : j;", + " return this.heights[clampedJ * this.size + clampedI];", + " }", + "", + " /**", + " * @param {number} gridX", + " * @param {number} gridY", + " * @returns {number} The height interpolated on the triangles that are drawn.", + " */", + " sampleHeight(gridX, gridY) {", + " const x = Math.min(Math.max(gridX, 0), this.resolution);", + " const y = Math.min(Math.max(gridY, 0), this.resolution);", + " const i = Math.min(Math.floor(x), this.resolution - 1);", + " const j = Math.min(Math.floor(y), this.resolution - 1);", + " const fx = x - i;", + " const fy = y - j;", + " const topRight = this.getHeight(i + 1, j);", + " const bottomLeft = this.getHeight(i, j + 1);", + " if (fx + fy <= 1) {", + " const topLeft = this.getHeight(i, j);", + " return topLeft + fx * (topRight - topLeft) + fy * (bottomLeft - topLeft);", + " }", + " const bottomRight = this.getHeight(i + 1, j + 1);", + " return (", + " bottomRight +", + " (1 - fx) * (bottomLeft - bottomRight) +", + " (1 - fy) * (topRight - bottomRight)", + " );", + " }", + "", + " /** @returns {number} The weight (0-1) of a layer, interpolated between samples. */", + " sampleLayerWeight(gridX, gridY, layer) {", + " const x = Math.min(Math.max(gridX, 0), this.resolution);", + " const y = Math.min(Math.max(gridY, 0), this.resolution);", + " const i = Math.min(Math.floor(x), this.resolution - 1);", + " const j = Math.min(Math.floor(y), this.resolution - 1);", + " const fx = x - i;", + " const fy = y - j;", + " const weight = (i, j) => this.splat[(j * this.size + i) * LAYER_COUNT + layer];", + " const top = weight(i, j) + fx * (weight(i + 1, j) - weight(i, j));", + " const bottom = weight(i, j + 1) + fx * (weight(i + 1, j + 1) - weight(i, j + 1));", + " return (top + fy * (bottom - top)) / 255;", + " }", + "", + " /**", + " * @param {number} channel 0, 1 and 2 for the tint, 3 for the wetness.", + " * @returns {number} The channel (0-1), interpolated between samples.", + " */", + " sampleColor(gridX, gridY, channel) {", + " const x = Math.min(Math.max(gridX, 0), this.resolution);", + " const y = Math.min(Math.max(gridY, 0), this.resolution);", + " const i = Math.min(Math.floor(x), this.resolution - 1);", + " const j = Math.min(Math.floor(y), this.resolution - 1);", + " const fx = x - i;", + " const fy = y - j;", + " const value = (i, j) => this.colors[(j * this.size + i) * 4 + channel];", + " const top = value(i, j) + fx * (value(i + 1, j) - value(i, j));", + " const bottom = value(i, j + 1) + fx * (value(i + 1, j + 1) - value(i, j + 1));", + " return (top + fy * (bottom - top)) / 255;", + " }", + "", + " /**", + " * @param {number} index The index of a sample.", + " * @returns {boolean} true if no layer is painted on the sample: it's a hole.", + " */", + " isHole(index) {", + " const offset = index * LAYER_COUNT;", + " return (this.splat[offset] | this.splat[offset + 1] | this.splat[offset + 2] | this.splat[offset + 3]) === 0;", + " }", + "", + " /**", + " * @returns {number} The sum of the layer weights (0-1), interpolated between samples: there is a", + " * hole where it's under 0.5, like what is drawn.", + " */", + " sampleSolidity(gridX, gridY) {", + " let solidity = 0;", + " for (let layer = 0; layer < LAYER_COUNT; layer++) solidity += this.sampleLayerWeight(gridX, gridY, layer);", + " return solidity;", + " }", + "", + " /** @returns {number} The index (from 0) of the layer painted the most. */", + " getDominantLayer(gridX, gridY) {", + " const i = clampInteger(Math.round(gridX), 0, this.resolution);", + " const j = clampInteger(Math.round(gridY), 0, this.resolution);", + " const offset = (j * this.size + i) * LAYER_COUNT;", + " let dominantLayer = 0;", + " for (let layer = 1; layer < LAYER_COUNT; layer++) {", + " if (this.splat[offset + layer] > this.splat[offset + dominantLayer]) {", + " dominantLayer = layer;", + " }", + " }", + " return dominantLayer;", + " }", + "", + " /** @param {TerrainData} other */", + " copyFrom(other) {", + " this.heights.set(other.heights);", + " this.splat.set(other.splat);", + " this.colors.set(other.colors);", + " }", + "", + " /**", + " * @param {number} resolution", + " * @returns {TerrainData} A copy of this data with another resolution.", + " */", + " resampled(resolution) {", + " const data = new TerrainData(resolution);", + " const ratio = this.resolution / resolution;", + " for (let j = 0; j < data.size; j++) {", + " for (let i = 0; i < data.size; i++) {", + " const index = j * data.size + i;", + " data.heights[index] = this.sampleHeight(i * ratio, j * ratio);", + " const sourceOffset =", + " (Math.round(j * ratio) * this.size + Math.round(i * ratio)) * LAYER_COUNT;", + " for (let layer = 0; layer < LAYER_COUNT; layer++) {", + " data.splat[index * LAYER_COUNT + layer] = this.splat[sourceOffset + layer];", + " }", + " for (let channel = 0; channel < 4; channel++) {", + " data.colors[index * 4 + channel] = this.colors[sourceOffset + channel];", + " }", + " }", + " }", + " return data;", + " }", + "}", + "", + "/**", + " * Brushes change the samples around a segment, with an effect going smoothly", + " * from full on the segment to none at `radius`. A point is a segment of", + " * length 0. Positions are in scene units relative to the terrain origin.", + " */", + "const brushes = {", + " /**", + " * @param {TerrainData} data", + " * @param {{cellWidth: number, cellHeight: number, ax: number, ay: number, bx: number, by: number, radius: number}} stroke", + " * @param {(sampleIndex: number, weight: number, t: number) => void} apply Called with the weight of", + " * the brush and the position (from 0 to 1) of the nearest point of the segment.", + " * @param {SampleRectangle} changedSamples", + " */", + " forEachSample(data, stroke, apply, changedSamples) {", + " const { cellWidth, cellHeight, ax, ay, bx, by, radius } = stroke;", + " const last = data.resolution;", + " const minI = clampInteger(Math.floor((Math.min(ax, bx) - radius) / cellWidth), 0, last);", + " const maxI = clampInteger(Math.ceil((Math.max(ax, bx) + radius) / cellWidth), 0, last);", + " const minJ = clampInteger(Math.floor((Math.min(ay, by) - radius) / cellHeight), 0, last);", + " const maxJ = clampInteger(Math.ceil((Math.max(ay, by) + radius) / cellHeight), 0, last);", + " changedSamples.set(minI, minJ, maxI, maxJ);", + " if (radius <= 0) return;", + "", + " const abX = bx - ax;", + " const abY = by - ay;", + " const abLengthSquared = abX * abX + abY * abY;", + " const radiusSquared = radius * radius;", + " for (let j = minJ; j <= maxJ; j++) {", + " const y = j * cellHeight;", + " for (let i = minI; i <= maxI; i++) {", + " const x = i * cellWidth;", + " const t =", + " abLengthSquared > 0", + " ? clamp01(((x - ax) * abX + (y - ay) * abY) / abLengthSquared)", + " : 0;", + " const dx = x - (ax + t * abX);", + " const dy = y - (ay + t * abY);", + " const distanceSquared = dx * dx + dy * dy;", + " if (distanceSquared >= radiusSquared) continue;", + " const falloff = 1 - distanceSquared / radiusSquared;", + " apply(j * data.size + i, falloff * falloff, t);", + " }", + " }", + " },", + "", + " raise(data, stroke, amount, changedSamples) {", + " const { heights } = data;", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index, weight) => {", + " heights[index] = clamp01(heights[index] + amount * weight);", + " },", + " changedSamples", + " );", + " },", + "", + " /**", + " * @param {number} targetHeight", + " * @param {number} [endTargetHeight] The target at the end of the segment, for a slope (like a road", + " * going up). The same target as at its start by default.", + " */", + " flatten(data, stroke, targetHeight, strength, changedSamples, endTargetHeight = targetHeight) {", + " const { heights } = data;", + " const startTarget = clamp01(targetHeight);", + " const endTarget = clamp01(endTargetHeight);", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index, weight, t) => {", + " const target = startTarget + (endTarget - startTarget) * t;", + " heights[index] += (target - heights[index]) * clamp01(weight * strength);", + " },", + " changedSamples", + " );", + " },", + "", + " smooth(data, stroke, strength, changedSamples) {", + " const { heights, size } = data;", + " const changes = [];", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index, weight) => {", + " const i = index % size;", + " const j = (index - i) / size;", + " let sum = 0;", + " for (let dj = -1; dj <= 1; dj++) {", + " for (let di = -1; di <= 1; di++) {", + " sum += data.getHeight(i + di, j + dj);", + " }", + " }", + " changes.push(index, heights[index] + (sum / 9 - heights[index]) * clamp01(weight * strength));", + " },", + " changedSamples", + " );", + " // Written after all averages are computed, so that the result", + " // doesn't depend on the order in which samples are visited.", + " for (let index = 0; index < changes.length; index += 2) {", + " heights[changes[index]] = changes[index + 1];", + " }", + " },", + "", + " paint(data, stroke, layer, strength, changedSamples) {", + " const { splat } = data;", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index, weight) => {", + " const amount = clamp01(weight * strength);", + " const offset = index * LAYER_COUNT;", + " for (let otherLayer = 0; otherLayer < LAYER_COUNT; otherLayer++) {", + " const target = otherLayer === layer ? 255 : 0;", + " const value = splat[offset + otherLayer];", + " // Rounded towards the target, so that light strokes still paint.", + " const change = (target - value) * amount;", + " splat[offset + otherLayer] = value + (change > 0 ? Math.ceil(change) : Math.floor(change));", + " }", + " },", + " changedSamples", + " );", + " },", + "", + " /** Remove all the layers, making a hole (`fill` or painting a layer fills it again). */", + " hole(data, stroke, changedSamples) {", + " const { splat } = data;", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index) => {", + " splat.fill(0, index * LAYER_COUNT, (index + 1) * LAYER_COUNT);", + " },", + " changedSamples", + " );", + " },", + "", + " /**", + " * Fill the holes in the radius with the layers of the ground around them:", + " * each pass fills the holes next to the ground with the average of their", + " * neighbors, from the edges of the holes inward.", + " */", + " fill(data, stroke, changedSamples) {", + " const { splat, size, resolution } = data;", + " /** @type {number[]} */", + " let holes = [];", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index) => {", + " if (data.isHole(index)) holes.push(index);", + " },", + " changedSamples", + " );", + " const filledWeights = new Float32Array(holes.length * LAYER_COUNT);", + " while (holes.length > 0) {", + " /** @type {number[]} */", + " const remainingHoles = [];", + " /** @type {number[]} */", + " const filledHoles = [];", + " for (const index of holes) {", + " const i = index % size;", + " const j = (index - i) / size;", + " const weights = filledWeights.subarray(filledHoles.length * LAYER_COUNT, (filledHoles.length + 1) * LAYER_COUNT);", + " weights.fill(0);", + " for (let neighborJ = Math.max(j - 1, 0); neighborJ <= Math.min(j + 1, resolution); neighborJ++) {", + " for (let neighborI = Math.max(i - 1, 0); neighborI <= Math.min(i + 1, resolution); neighborI++) {", + " const neighbor = neighborJ * size + neighborI;", + " if (data.isHole(neighbor)) continue;", + " for (let layer = 0; layer < LAYER_COUNT; layer++) weights[layer] += splat[neighbor * LAYER_COUNT + layer];", + " }", + " }", + " if (weights[0] + weights[1] + weights[2] + weights[3] > 0) filledHoles.push(index);", + " else remainingHoles.push(index);", + " }", + " if (filledHoles.length === 0) {", + " // No ground around (a hole bigger than the brush): the first layer is used.", + " for (const index of remainingHoles) splat[index * LAYER_COUNT] = 255;", + " break;", + " }", + " // Filled after the pass, so that the holes are filled from the edges inward.", + " filledHoles.forEach((index, filledIndex) => {", + " const weights = filledWeights.subarray(filledIndex * LAYER_COUNT, (filledIndex + 1) * LAYER_COUNT);", + " const total = weights[0] + weights[1] + weights[2] + weights[3];", + " for (let layer = 0; layer < LAYER_COUNT; layer++) {", + " splat[index * LAYER_COUNT + layer] = Math.round((weights[layer] / total) * 255);", + " }", + " });", + " holes = remainingHoles;", + " }", + " },", + "", + " /** @param {number[]} color The red, green and blue (0-255) of the tint: white removes it. */", + " tint(data, stroke, color, strength, changedSamples) {", + " brushes.paintColors(data, stroke, 0, color, strength, changedSamples);", + " },", + "", + " /** @param {number} wetness From 0 (dry) to 1. */", + " wet(data, stroke, wetness, strength, changedSamples) {", + " brushes.paintColors(data, stroke, 3, [Math.round(clamp01(wetness) * 255)], strength, changedSamples);", + " },", + "", + " /**", + " * Bring channels of the colors (see `TerrainData.colors`) towards targets.", + " * @param {number} firstChannel", + " * @param {number[]} targets From 0 to 255, for the channels from the first one.", + " */", + " paintColors(data, stroke, firstChannel, targets, strength, changedSamples) {", + " const { colors } = data;", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index, weight) => {", + " const amount = clamp01(weight * strength);", + " for (let channel = 0; channel < targets.length; channel++) {", + " const offset = index * 4 + firstChannel + channel;", + " // Rounded towards the target, so that light strokes still paint.", + " const change = (targets[channel] - colors[offset]) * amount;", + " colors[offset] += change > 0 ? Math.ceil(change) : Math.floor(change);", + " }", + " },", + " changedSamples", + " );", + " },", + "};", + "", + "const relief = (() => {", + " const hash = (x, y, seed) => {", + " let h = Math.imul(x, 374761393) + Math.imul(y, 668265263) + Math.imul(seed, 982451653);", + " h = Math.imul(h ^ (h >>> 13), 1274126177);", + " return ((h ^ (h >>> 16)) >>> 0) / 4294967296;", + " };", + " const valueNoise = (x, y, seed) => {", + " const x0 = Math.floor(x);", + " const y0 = Math.floor(y);", + " const fx = x - x0;", + " const fy = y - y0;", + " const sx = fx * fx * (3 - 2 * fx);", + " const sy = fy * fy * (3 - 2 * fy);", + " const top = hash(x0, y0, seed) + sx * (hash(x0 + 1, y0, seed) - hash(x0, y0, seed));", + " const bottom =", + " hash(x0, y0 + 1, seed) + sx * (hash(x0 + 1, y0 + 1, seed) - hash(x0, y0 + 1, seed));", + " return top + sy * (bottom - top);", + " };", + " /** Fractal noise between 0 and 1. */", + " const fractalNoise = (x, y, seed) => {", + " let value = 0;", + " let amplitude = 0.5;", + " let frequency = 1;", + " for (let octave = 0; octave < 5; octave++) {", + " value += amplitude * valueNoise(x * frequency, y * frequency, seed + octave);", + " amplitude *= 0.5;", + " frequency *= 2;", + " }", + " return value / 0.96875;", + " };", + " const smoothstep = (edge0, edge1, x) => {", + " const t = clamp01((x - edge0) / (edge1 - edge0));", + " return t * t * (3 - 2 * t);", + " };", + " const reliefHeights = {", + " Flat: () => 0,", + " Hills: (u, v, seed) => 0.05 + 0.5 * Math.pow(fractalNoise(u * 3, v * 3, seed), 2),", + " Mountains: (u, v, seed) => {", + " const ridge = 1 - Math.abs(2 * fractalNoise(u * 2.5, v * 2.5, seed) - 1);", + " return 0.9 * Math.pow(ridge, 2.5);", + " },", + " Island: (u, v, seed) => {", + " const distanceToCenter = 2 * Math.hypot(u - 0.5, v - 0.5);", + " const coast = 0.2 * (fractalNoise(u * 4, v * 4, seed + 7) - 0.5);", + " const land = smoothstep(0.95, 0.55, distanceToCenter + coast);", + " return land * (0.12 + 0.6 * Math.pow(fractalNoise(u * 3, v * 3, seed), 1.5));", + " },", + " };", + " const beachLayer = 3;", + "", + " return {", + " /**", + " * @param {TerrainData} data", + " * @param {string} reliefName Flat, Hills, Mountains or Island.", + " * @param {number} seed", + " */", + " generate(data, reliefName, seed) {", + " const getHeight = reliefHeights[reliefName] || reliefHeights.Flat;", + " const integerSeed = Math.floor(seed) | 0;", + " data.fillLayer(0);", + " for (let j = 0; j < data.size; j++) {", + " for (let i = 0; i < data.size; i++) {", + " const index = j * data.size + i;", + " const height = clamp01(", + " getHeight(i / data.resolution, j / data.resolution, integerSeed)", + " );", + " data.heights[index] = height;", + " if (reliefName === 'Island') {", + " const sand = Math.round(255 * smoothstep(0.16, 0.1, height));", + " const offset = index * LAYER_COUNT;", + " data.splat[offset] = 255 - sand;", + " data.splat[offset + beachLayer] = sand;", + " }", + " }", + " }", + " },", + " };", + "})();", + "", + "/**", + " * Edits written in the Edits property: a JSON list of brush strokes, like", + " * `[{\"tool\": \"raise\", \"x\": 0.5, \"y\": 0.5, \"radius\": 0.2, \"height\": 0.3}]`,", + " * applied on the relief or the heightmap. Positions and radius are fractions", + " * of the terrain size, heights and Z positions fractions of its depth: they", + " * are the same for all instances, whatever their size. They are made to be", + " * read and written by people and by AI agents.", + " */", + "const edits = {", + " /**", + " * @param {TerrainData} data", + " * @param {string} text", + " */", + " apply(data, text) {", + " if (!text.trim()) return;", + " let editList;", + " try {", + " editList = JSON.parse(text);", + " } catch (error) {", + " console.warn('The edits of a terrain are not valid JSON: ' + error.message);", + " return;", + " }", + " if (!Array.isArray(editList)) {", + " console.warn('The edits of a terrain must be a JSON list (like [{\"tool\": \"raise\", ...}]).');", + " return;", + " }", + " const numberOr = (value, defaultValue) => (typeof value === 'number' && isFinite(value) ? value : defaultValue);", + " const changedSamples = new SampleRectangle();", + " for (const edit of editList) {", + " if (!edit || typeof edit !== 'object') continue;", + " const x = numberOr(edit.x, 0.5);", + " const y = numberOr(edit.y, 0.5);", + " const stroke = {", + " cellWidth: 1 / data.resolution,", + " cellHeight: 1 / data.resolution,", + " ax: x,", + " ay: y,", + " bx: numberOr(edit.toX, x),", + " by: numberOr(edit.toY, y),", + " radius: numberOr(edit.radius, 0.1),", + " };", + " const strength = clamp01(numberOr(edit.strength, 1));", + " if (edit.tool === 'raise') {", + " brushes.raise(data, stroke, numberOr(edit.height, 0.1), changedSamples);", + " } else if (edit.tool === 'flatten') {", + " const z = numberOr(edit.z, 0);", + " brushes.flatten(data, stroke, z, strength, changedSamples, numberOr(edit.toZ, z));", + " } else if (edit.tool === 'smooth') {", + " // Smoothing the same fraction of the terrain at any resolution.", + " const passes = Math.max(Math.round(((1 + 9 * strength) * data.resolution) / 256), 1);", + " for (let pass = 0; pass < passes; pass++) brushes.smooth(data, stroke, 1, changedSamples);", + " } else if (edit.tool === 'paint') {", + " const layer = clampInteger(numberOr(edit.layer, 1) - 1, 0, LAYER_COUNT - 1);", + " brushes.paint(data, stroke, layer, strength, changedSamples);", + " } else if (edit.tool === 'tint') {", + " const color = gdjs.rgbOrHexToRGBColor(typeof edit.color === 'string' ? edit.color : '255;255;255');", + " brushes.tint(data, stroke, color, strength, changedSamples);", + " } else if (edit.tool === 'wetness') {", + " brushes.wet(data, stroke, numberOr(edit.amount, 1), strength, changedSamples);", + " } else if (edit.tool === 'hole') {", + " brushes.hole(data, stroke, changedSamples);", + " } else if (edit.tool === 'fill') {", + " brushes.fill(data, stroke, changedSamples);", + " } else {", + " console.warn('Unknown tool in the edits of a terrain: \"' + edit.tool + '\" (use raise, flatten, smooth, paint, tint, wetness, hole or fill).');", + " }", + " }", + " },", + "};", + "", + "/**", + " * @param {gdjs.RuntimeGame} game", + " * @param {string} imageResourceName", + " * @returns {CanvasImageSource | null} The image, or null if it's not loaded (yet).", + " */", + "const getLoadedImage = (game, imageResourceName) => {", + " const imageManager = game.getImageManager();", + " if (typeof imageManager.getImageSource === 'function') return imageManager.getImageSource(imageResourceName);", + " // Older GDevelop versions: a 192 x 192 placeholder is given for images that are not loaded.", + " const texture = imageManager.getPIXITexture(imageResourceName);", + " return texture && texture !== imageManager.getInvalidPIXITexture() ? texture.baseTexture.getDrawableSource() : null;", + "};", + "", + "/**", + " * Loads heights from an image: black is the lowest, white the highest.", + " * Heights have 16 bits of precision when the red channel holds the most", + " * significant byte and the green one the least significant (a gray image", + " * works as expected too).", + " * @param {TerrainData} data", + " * @param {gdjs.RuntimeGame} game", + " * @param {string} imageResourceName", + " * @returns {boolean} true if the image could be read.", + " */", + "const loadHeightsFromImage = (data, game, imageResourceName) => {", + " const source = getLoadedImage(game, imageResourceName);", + " if (!source) return false;", + "", + " const canvas = document.createElement('canvas');", + " canvas.width = source.width;", + " canvas.height = source.height;", + " const context = canvas.getContext('2d', { willReadFrequently: true });", + " if (!context) return false;", + " context.drawImage(source, 0, 0);", + " const pixels = context.getImageData(0, 0, source.width, source.height).data;", + " // Pixels are decoded before being interpolated, to keep the 16 bits.", + " const pixelHeight = (x, y) => {", + " const offset = (y * source.width + x) * 4;", + " return (256 * pixels[offset] + pixels[offset + 1]) / 65535;", + " };", + " for (let j = 0; j < data.size; j++) {", + " const y = (j / data.resolution) * (source.height - 1);", + " const y0 = Math.floor(y);", + " const y1 = Math.min(y0 + 1, source.height - 1);", + " for (let i = 0; i < data.size; i++) {", + " const x = (i / data.resolution) * (source.width - 1);", + " const x0 = Math.floor(x);", + " const x1 = Math.min(x0 + 1, source.width - 1);", + " const top = pixelHeight(x0, y0) + (x - x0) * (pixelHeight(x1, y0) - pixelHeight(x0, y0));", + " const bottom = pixelHeight(x0, y1) + (x - x0) * (pixelHeight(x1, y1) - pixelHeight(x0, y1));", + " data.heights[j * data.size + i] = top + (y - y0) * (bottom - top);", + " }", + " }", + " return true;", + "};", + "", + "/**", + " * Stores the data in a string. Only the differences with the base data (from", + " * the relief or the heightmap) are stored: heights on 16 bits, layer weights", + " * on 8 bits, then (since format 2) the tint and wetness on 8 bits. Each value is predicted from its neighbors and the", + " * prediction errors are written as variable length integers, with runs of", + " * zeros collapsed. Untouched or smoothly changed areas take almost no space.", + " */", + "const codec = (() => {", + " const formatVersion = '2';", + " // Format 1 has no tint and wetness.", + " const readableFormatVersions = ['1', '2'];", + "", + " class ByteWriter {", + " constructor() {", + " this.bytes = new Uint8Array(1024);", + " this.length = 0;", + " }", + " writeVarUint(value) {", + " if (this.length + 5 > this.bytes.length) {", + " const bytes = new Uint8Array(this.bytes.length * 2);", + " bytes.set(this.bytes);", + " this.bytes = bytes;", + " }", + " while (value >= 0x80) {", + " this.bytes[this.length++] = (value & 0x7f) | 0x80;", + " value >>>= 7;", + " }", + " this.bytes[this.length++] = value;", + " }", + " }", + "", + " class ByteReader {", + " /** @param {Uint8Array} bytes */", + " constructor(bytes) {", + " this.bytes = bytes;", + " this.position = 0;", + " }", + " readVarUint() {", + " let value = 0;", + " let shift = 0;", + " let byte;", + " do {", + " if (this.position >= this.bytes.length) throw new Error('Truncated terrain data.');", + " byte = this.bytes[this.position++];", + " value += (byte & 0x7f) * Math.pow(2, shift);", + " shift += 7;", + " } while (byte & 0x80);", + " return value;", + " }", + " }", + "", + " /** Predicts a value from the left, top and top-left ones. */", + " const predict = (values, size, index) => {", + " const i = index % size;", + " if (index < size) return i === 0 ? 0 : values[index - 1];", + " if (i === 0) return values[index - size];", + " return values[index - 1] + values[index - size] - values[index - size - 1];", + " };", + "", + " /**", + " * @param {ByteWriter} writer", + " * @param {Int32Array} values A grid of `size * size` values.", + " */", + " const writeGrid = (writer, values, size) => {", + " let zeroRun = 0;", + " for (let index = 0; index < values.length; index++) {", + " const error = values[index] - predict(values, size, index);", + " if (error === 0) {", + " zeroRun++;", + " continue;", + " }", + " if (zeroRun > 0) {", + " writer.writeVarUint(0);", + " writer.writeVarUint(zeroRun);", + " zeroRun = 0;", + " }", + " // Zigzag: small negative and positive numbers get small codes.", + " writer.writeVarUint(error > 0 ? error * 2 : -error * 2 - 1);", + " }", + " if (zeroRun > 0) {", + " writer.writeVarUint(0);", + " writer.writeVarUint(zeroRun);", + " }", + " };", + "", + " /** @param {ByteReader} reader */", + " const readGrid = (reader, size) => {", + " const values = new Int32Array(size * size);", + " let zeroRun = 0;", + " for (let index = 0; index < values.length; index++) {", + " let error = 0;", + " if (zeroRun > 0) {", + " zeroRun--;", + " } else {", + " const code = reader.readVarUint();", + " if (code === 0) {", + " zeroRun = reader.readVarUint() - 1;", + " } else {", + " error = code % 2 === 0 ? code / 2 : -(code + 1) / 2;", + " }", + " }", + " values[index] = predict(values, size, index) + error;", + " }", + " return values;", + " };", + "", + " const toBase64 = (bytes, length) => {", + " let binary = '';", + " for (let offset = 0; offset < length; offset += 0x8000) {", + " binary += String.fromCharCode.apply(null, bytes.subarray(offset, Math.min(offset + 0x8000, length)));", + " }", + " return btoa(binary);", + " };", + "", + " const fromBase64 = (base64) => {", + " const binary = atob(base64);", + " const bytes = new Uint8Array(binary.length);", + " for (let index = 0; index < binary.length; index++) {", + " bytes[index] = binary.charCodeAt(index);", + " }", + " return bytes;", + " };", + "", + " return {", + " /**", + " * @param {TerrainData} data", + " * @param {TerrainData} baseData The data without changes, with the same resolution.", + " */", + " encode(data, baseData) {", + " const writer = new ByteWriter();", + " const values = new Int32Array(data.heights.length);", + " for (let index = 0; index < values.length; index++) {", + " values[index] = Math.round((data.heights[index] - baseData.heights[index]) * 65535);", + " }", + " writeGrid(writer, values, data.size);", + " for (let layer = 0; layer < LAYER_COUNT; layer++) {", + " for (let index = 0; index < values.length; index++) {", + " const offset = index * LAYER_COUNT + layer;", + " values[index] = data.splat[offset] - baseData.splat[offset];", + " }", + " writeGrid(writer, values, data.size);", + " }", + " // Not painted, these grids are zeros: a few bytes each.", + " for (let channel = 0; channel < 4; channel++) {", + " for (let index = 0; index < values.length; index++) {", + " const offset = index * 4 + channel;", + " values[index] = data.colors[offset] - baseData.colors[offset];", + " }", + " writeGrid(writer, values, data.size);", + " }", + " return formatVersion + ';' + data.resolution + ';' + toBase64(writer.bytes, writer.length);", + " },", + "", + " /**", + " * Store grids of integers in a string, like the terrain data. Used by other objects (the scattered models).", + " * @param {Int32Array[]} grids Grids of `size * size` values.", + " * @param {number} size", + " */", + " encodeGrids(grids, size) {", + " const writer = new ByteWriter();", + " for (const grid of grids) writeGrid(writer, grid, size);", + " return toBase64(writer.bytes, writer.length);", + " },", + "", + " /**", + " * @param {string} base64 A string from `encodeGrids`.", + " * @param {number} size", + " * @param {number} count The number of grids.", + " * @returns {Int32Array[]} The grids (an error is thrown if the string is not valid).", + " */", + " decodeGrids(base64, size, count) {", + " const reader = new ByteReader(fromBase64(base64));", + " return Array.from({ length: count }, () => readGrid(reader, size));", + " },", + "", + " /**", + " * @param {string} text", + " * @param {(resolution: number) => TerrainData} createBaseData", + " * @returns {TerrainData | null} null if the text is empty or not valid.", + " */", + " decode(text, createBaseData) {", + " if (!text) return null;", + " const parts = text.split(';');", + " const resolution = parseInt(parts[1], 10);", + " if (readableFormatVersions.indexOf(parts[0]) === -1 || RESOLUTIONS.indexOf(resolution) === -1) {", + " return null;", + " }", + " try {", + " const reader = new ByteReader(fromBase64(parts[2]));", + " const data = createBaseData(resolution);", + " const heightChanges = readGrid(reader, data.size);", + " for (let index = 0; index < heightChanges.length; index++) {", + " data.heights[index] = clamp01(data.heights[index] + heightChanges[index] / 65535);", + " }", + " for (let layer = 0; layer < LAYER_COUNT; layer++) {", + " const weightChanges = readGrid(reader, data.size);", + " for (let index = 0; index < weightChanges.length; index++) {", + " const offset = index * LAYER_COUNT + layer;", + " // The base may have changed since the data was saved.", + " data.splat[offset] = Math.min(Math.max(data.splat[offset] + weightChanges[index], 0), 255);", + " }", + " }", + " if (parts[0] !== '1') {", + " for (let channel = 0; channel < 4; channel++) {", + " const colorChanges = readGrid(reader, data.size);", + " for (let index = 0; index < colorChanges.length; index++) {", + " const offset = index * 4 + channel;", + " data.colors[offset] = Math.min(Math.max(data.colors[offset] + colorChanges[index], 0), 255);", + " }", + " }", + " }", + " return data;", + " } catch (error) {", + " console.error('Terrain data could not be read:', error);", + " return null;", + " }", + " },", + " };", + "})();", + "", + "const lodIndices = new Map();", + "/**", + " * The triangles of a chunk, shared by all chunks of the same size. Chunk", + " * borders have a skirt going down, hiding the gaps between chunks drawn", + " * with different levels of detail.", + " * @param {number} chunkCells", + " * @param {number} step", + " */", + "const getLodIndex = (chunkCells, step) => {", + " const key = chunkCells + ';' + step;", + " const existingIndex = lodIndices.get(key);", + " if (existingIndex) return existingIndex;", + "", + " const sideVertices = chunkCells + 1;", + " const gridVertices = sideVertices * sideVertices;", + " const indices = [];", + " const grid = (i, j) => j * sideVertices + i;", + " for (let j = 0; j < chunkCells; j += step) {", + " for (let i = 0; i < chunkCells; i += step) {", + " const topLeft = grid(i, j);", + " const topRight = grid(i + step, j);", + " const bottomLeft = grid(i, j + step);", + " const bottomRight = grid(i + step, j + step);", + " indices.push(topLeft, topRight, bottomLeft, topRight, bottomRight, bottomLeft);", + " }", + " }", + " const borders = [", + " (k) => grid(k, 0),", + " (k) => grid(k, chunkCells),", + " (k) => grid(0, k),", + " (k) => grid(chunkCells, k),", + " ];", + " borders.forEach((borderVertex, border) => {", + " const skirtVertex = (k) => gridVertices + border * sideVertices + k;", + " for (let k = 0; k < chunkCells; k += step) {", + " const a = borderVertex(k);", + " const b = borderVertex(k + step);", + " const skirtA = skirtVertex(k);", + " const skirtB = skirtVertex(k + step);", + " // Both faces, as a skirt can be seen from either side.", + " indices.push(a, b, skirtB, a, skirtB, skirtA, a, skirtB, b, a, skirtA, skirtB);", + " }", + " });", + " const index = new THREE.BufferAttribute(new Uint16Array(indices), 1);", + " lodIndices.set(key, index);", + " return index;", + "};", + "", + "const vertexShaderDeclarations = `", + "varying vec2 vTerrainLocalXY;", + "varying float vTerrainLocalZ;", + "uniform float terrainWorldScale;", + "varying vec2 vTerrainWorldXY;", + "varying float vTerrainUp;", + "`;", + "const vertexShaderCode = `", + "vTerrainLocalXY = position.xy;", + "vTerrainLocalZ = position.z;", + "vTerrainWorldXY = (modelMatrix * vec4(transformed, 1.0)).xy * terrainWorldScale;", + "vTerrainUp = normalize(transpose(inverse(mat3(modelMatrix))) * objectNormal).z;", + "`;", + "const fragmentShaderDeclarations = `", + "uniform sampler2D terrainSplatMap;", + "uniform vec2 terrainSplatUvScaleAndOffset;", + "uniform vec3 terrainLayerColors[4];", + "uniform sampler2D terrainLayerMap0;", + "uniform sampler2D terrainLayerMap1;", + "uniform sampler2D terrainLayerMap2;", + "uniform sampler2D terrainLayerMap3;", + "uniform sampler2D terrainLayerNormalMap0;", + "uniform sampler2D terrainLayerNormalMap1;", + "uniform sampler2D terrainLayerNormalMap2;", + "uniform sampler2D terrainLayerNormalMap3;", + "uniform float terrainHasNormalMaps;", + "uniform float terrainNormalMapStrength;", + "uniform float terrainTextureSize;", + "uniform float terrainHeightBlending;", + "uniform float terrainTextureVariation;", + "uniform vec4 terrainCliffLayer;", + "uniform vec2 terrainCliffCosines;", + "uniform vec4 terrainBrush;", + "uniform sampler2D terrainColorMap;", + "uniform vec3 terrainWetShore;", + "varying vec2 vTerrainLocalXY;", + "varying float vTerrainLocalZ;", + "varying vec2 vTerrainWorldXY;", + "varying float vTerrainUp;", + "", + "float terrainHash(vec2 p) {", + " return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);", + "}", + "// The rotation of the copies of textures used to make repetition less visible.", + "const mat2 terrainCopyRotation = mat2(0.8, -0.6, 0.6, 0.8);", + "/** A bump of a normal map (from -1 to 1), mixed with its rotated copy in patches. */", + "vec3 terrainSampleNormal(sampler2D normalMap, vec2 uv, vec2 uv2, float patches) {", + " vec3 bump = texture2D(normalMap, uv).xyz * 2.0 - 1.0;", + " if (patches <= 0.0) return bump;", + " vec3 copyBump = texture2D(normalMap, uv2).xyz * 2.0 - 1.0;", + " // The bumps of the copy are rotated back with the inverse rotation.", + " copyBump.xy = transpose(terrainCopyRotation) * copyBump.xy;", + " return mix(bump, copyBump, patches);", + "}", + "float terrainNoise(vec2 p) {", + " vec2 i = floor(p);", + " vec2 f = fract(p);", + " vec2 u = f * f * (3.0 - 2.0 * f);", + " return mix(", + " mix(terrainHash(i), terrainHash(i + vec2(1.0, 0.0)), u.x),", + " mix(terrainHash(i + vec2(0.0, 1.0)), terrainHash(i + vec2(1.0, 1.0)), u.x),", + " u.y", + " );", + "}", + "`;", + "// Holes are samples without any painted layer: the interpolated sum of the", + "// weights gives smooth edges. Only compiled for terrains having holes, as", + "// discard makes drawing slower on some GPUs.", + "const holeShaderCode = `", + "#ifdef TERRAIN_HOLES", + "if (dot(texture2D(terrainSplatMap, vTerrainLocalXY * terrainSplatUvScaleAndOffset.x + terrainSplatUvScaleAndOffset.y), vec4(1.0)) < 0.5) discard;", + "#endif", + "`;", + "// For the shadows: the depth of holes is not drawn.", + "const holeDepthShaderDeclarations = `", + "uniform sampler2D terrainSplatMap;", + "uniform vec2 terrainSplatUvScaleAndOffset;", + "varying vec2 vTerrainLocalXY;", + "`;", + "", + "const fragmentShaderCode = holeShaderCode + `", + "// Borders between layers are made irregular and sharper, to look painted.", + "vec2 terrainNoisePosition = vTerrainWorldXY / 24.0;", + "vec4 terrainJitter = vec4(", + " terrainNoise(terrainNoisePosition),", + " terrainNoise(terrainNoisePosition + 19.1),", + " terrainNoise(terrainNoisePosition + 37.7),", + " terrainNoise(terrainNoisePosition + 53.3)", + ");", + "// Layer weights are in a texture, so that painted details stay the same", + "// whatever the level of detail of the ground.", + "vec2 terrainSplatUv = vTerrainLocalXY * terrainSplatUvScaleAndOffset.x + terrainSplatUvScaleAndOffset.y;", + "vec4 terrainWeights = texture2D(terrainSplatMap, terrainSplatUv) * (0.7 + 0.6 * terrainJitter);", + "terrainWeights = terrainWeights * terrainWeights;", + "terrainWeights /= max(dot(terrainWeights, vec4(1.0)), 0.00001);", + "float terrainCliff = 1.0 - smoothstep(terrainCliffCosines.x, terrainCliffCosines.y, vTerrainUp);", + "terrainWeights = mix(terrainWeights, terrainCliffLayer, terrainCliff * dot(terrainCliffLayer, vec4(1.0)));", + "", + "vec2 terrainUv = vTerrainWorldXY / terrainTextureSize;", + "vec3 terrainTexture0 = texture2D(terrainLayerMap0, terrainUv).rgb;", + "vec3 terrainTexture1 = texture2D(terrainLayerMap1, terrainUv).rgb;", + "vec3 terrainTexture2 = texture2D(terrainLayerMap2, terrainUv).rgb;", + "vec3 terrainTexture3 = texture2D(terrainLayerMap3, terrainUv).rgb;", + "", + "// Less visible repetition: in irregular patches, textures are replaced by a", + "// rotated and shifted copy of themselves, and colors slowly vary.", + "float terrainPatches = 0.0;", + "vec2 terrainUv2 = terrainUv;", + "float terrainColorVariation = 1.0;", + "if (terrainTextureVariation > 0.0) {", + " // Up to half of the ground is covered by the copies, each patch showing", + " // only one of them (mixing them would make them blurry).", + " float terrainPatchThreshold = 1.0 - 0.5 * terrainTextureVariation;", + " terrainPatches = smoothstep(terrainPatchThreshold - 0.05, terrainPatchThreshold + 0.05, terrainNoise(terrainUv * 0.23 + 7.3));", + " terrainUv2 = terrainCopyRotation * terrainUv + vec2(0.31, 0.73);", + " terrainTexture0 = mix(terrainTexture0, texture2D(terrainLayerMap0, terrainUv2).rgb, terrainPatches);", + " terrainTexture1 = mix(terrainTexture1, texture2D(terrainLayerMap1, terrainUv2).rgb, terrainPatches);", + " terrainTexture2 = mix(terrainTexture2, texture2D(terrainLayerMap2, terrainUv2).rgb, terrainPatches);", + " terrainTexture3 = mix(terrainTexture3, texture2D(terrainLayerMap3, terrainUv2).rgb, terrainPatches);", + " terrainColorVariation += terrainTextureVariation * 0.3 *", + " (terrainNoise(terrainUv * 0.11) + 0.5 * terrainNoise(terrainUv * 0.37 + 3.1) - 0.75);", + "}", + "", + "// Height-based blending: the brightness of a texture, compared to its", + "// average, is used as its height. Where layers meet, the high parts (stones,", + "// tufts) cover the low parts (sand, gaps) instead of fading into each other.", + "if (terrainHeightBlending > 0.0) {", + " const vec3 terrainLuminance = vec3(0.299, 0.587, 0.114);", + " vec4 terrainHeights = vec4(", + " dot(terrainTexture0 - textureLod(terrainLayerMap0, vec2(0.5), 16.0).rgb, terrainLuminance),", + " dot(terrainTexture1 - textureLod(terrainLayerMap1, vec2(0.5), 16.0).rgb, terrainLuminance),", + " dot(terrainTexture2 - textureLod(terrainLayerMap2, vec2(0.5), 16.0).rgb, terrainLuminance),", + " dot(terrainTexture3 - textureLod(terrainLayerMap3, vec2(0.5), 16.0).rgb, terrainLuminance)", + " );", + " vec4 terrainHeightWeights = terrainWeights + terrainHeights;", + " float terrainTop = max(max(terrainHeightWeights.x, terrainHeightWeights.y), max(terrainHeightWeights.z, terrainHeightWeights.w));", + " // Only painted layers (and the cliff layer) can show.", + " vec4 terrainHeightBlend = max(terrainHeightWeights - terrainTop + 0.1, 0.0) * step(0.004, terrainWeights);", + " terrainHeightBlend /= max(dot(terrainHeightBlend, vec4(1.0)), 0.00001);", + " terrainWeights = mix(terrainWeights, terrainHeightBlend, terrainHeightBlending);", + "}", + "", + "vec3 terrainColor = terrainColorVariation * (", + " terrainWeights.x * terrainLayerColors[0] * terrainTexture0 +", + " terrainWeights.y * terrainLayerColors[1] * terrainTexture1 +", + " terrainWeights.z * terrainLayerColors[2] * terrainTexture2 +", + " terrainWeights.w * terrainLayerColors[3] * terrainTexture3);", + "", + "// The painted tint multiplies all layers. Wet ground is darker (and", + "// shinier, see the roughness).", + "vec4 terrainTintAndWetness = texture2D(terrainColorMap, terrainSplatUv);", + "float terrainWetness = max(", + " terrainTintAndWetness.a,", + " terrainWetShore.z * (1.0 - smoothstep(terrainWetShore.x, terrainWetShore.x + terrainWetShore.y, vTerrainLocalZ))", + ");", + "terrainColor *= terrainTintAndWetness.rgb * (1.0 - 0.4 * terrainWetness);", + "diffuseColor.rgb *= terrainColor;", + "", + "if (terrainBrush.w > 0.5) {", + " float brushDistance = distance(vTerrainWorldXY, terrainBrush.xy);", + " float ringWidth = max(terrainBrush.z * 0.04, 1.5);", + " float ring = 1.0 - smoothstep(0.0, ringWidth, abs(brushDistance - terrainBrush.z));", + " float disc = brushDistance < terrainBrush.z ? 0.12 : 0.0;", + " diffuseColor.rgb = mix(diffuseColor.rgb, vec3(1.0), max(ring * 0.85, disc));", + "}", + "`;", + "", + "const fragmentShaderRoughnessCode = `", + "roughnessFactor = mix(roughnessFactor, 0.3, terrainWetness);", + "`;", + "", + "const fragmentShaderNormalCode = `", + "if (terrainHasNormalMaps > 0.5) {", + " vec3 terrainMapNormal =", + " terrainWeights.x * terrainSampleNormal(terrainLayerNormalMap0, terrainUv, terrainUv2, terrainPatches) +", + " terrainWeights.y * terrainSampleNormal(terrainLayerNormalMap1, terrainUv, terrainUv2, terrainPatches) +", + " terrainWeights.z * terrainSampleNormal(terrainLayerNormalMap2, terrainUv, terrainUv2, terrainPatches) +", + " terrainWeights.w * terrainSampleNormal(terrainLayerNormalMap3, terrainUv, terrainUv2, terrainPatches);", + " terrainMapNormal.xy *= terrainNormalMapStrength;", + " // The X and Y of textures follow the X and Y of the 3D world, projected on", + " // the ground (the camera space of the normal is used).", + " vec3 terrainTangent = (viewMatrix * vec4(1.0, 0.0, 0.0, 0.0)).xyz;", + " terrainTangent -= normal * dot(normal, terrainTangent);", + " // A ground facing the X axis (a rotated terrain) uses the Y axis instead.", + " if (dot(terrainTangent, terrainTangent) < 0.000001) {", + " terrainTangent = (viewMatrix * vec4(0.0, 1.0, 0.0, 0.0)).xyz;", + " terrainTangent -= normal * dot(normal, terrainTangent);", + " }", + " terrainTangent = normalize(terrainTangent);", + " vec3 terrainBitangent = cross(normal, terrainTangent);", + " normal = normalize(mat3(terrainTangent, terrainBitangent, normal) * terrainMapNormal);", + "}", + "`;", + "", + "let whiteTexture = null;", + "const getWhiteTexture = () => {", + " if (!whiteTexture) {", + " whiteTexture = new THREE.DataTexture(new Uint8Array([255, 255, 255, 255]), 1, 1);", + " whiteTexture.needsUpdate = true;", + " }", + " return whiteTexture;", + "};", + "", + "let flatNormalTexture = null;", + "const getFlatNormalTexture = () => {", + " if (!flatNormalTexture) {", + " flatNormalTexture = new THREE.DataTexture(new Uint8Array([128, 128, 255, 255]), 1, 1);", + " flatNormalTexture.needsUpdate = true;", + " }", + " return flatNormalTexture;", + "};", + "", + "const layerTextures = new WeakMap();", + "const layerNormalMaps = new WeakMap();", + "/**", + " * Textures of the image manager have no mipmaps: they would shimmer when", + " * repeated in the distance.", + " * @param {gdjs.RuntimeGame} game", + " * @param {string} resourceName", + " * @param {boolean} isNormalMap Normal maps are directions, not colors.", + " */", + "const getLayerTexture = (game, resourceName, isNormalMap) => {", + " if (!resourceName) return isNormalMap ? getFlatNormalTexture() : getWhiteTexture();", + " const imageTexture = game.getImageManager().getThreeTexture(resourceName);", + " const textures = isNormalMap ? layerNormalMaps : layerTextures;", + " let texture = textures.get(imageTexture);", + " if (!texture) {", + " texture = imageTexture.clone();", + " if (isNormalMap) texture.colorSpace = THREE.NoColorSpace;", + " texture.wrapS = THREE.RepeatWrapping;", + " texture.wrapT = THREE.RepeatWrapping;", + " texture.minFilter = THREE.LinearMipmapLinearFilter;", + " texture.generateMipmaps = true;", + " texture.anisotropy = 4;", + " texture.needsUpdate = true;", + " textures.set(imageTexture, texture);", + " }", + " return texture;", + "};", + "", + "/**", + " * A texture of 4 bytes per sample of a grid of `size * size` samples (layer", + " * weights, tint and wetness...), interpolated between samples.", + " * @param {Uint8Array} bytes", + " * @param {number} size", + " */", + "const createGridTexture = (bytes, size) => {", + " const texture = new THREE.DataTexture(bytes, size, size);", + " texture.magFilter = THREE.LinearFilter;", + " texture.minFilter = THREE.LinearFilter;", + " texture.userData.isUploaded = false;", + " texture.onUpdate = () => {", + " texture.userData.isUploaded = true;", + " };", + " return texture;", + "};", + "", + "/**", + " * Send the rows of the changed samples to the GPU, or the whole texture", + " * before its first upload (and in older GDevelop versions).", + " * @param {THREE.DataTexture} texture", + " * @param {SampleRectangle} samples", + " */", + "const uploadGridTextureRows = (texture, samples) => {", + " if (texture.userData.isUploaded && typeof texture.addUpdateRange === 'function') {", + " const size = texture.image.width;", + " for (let j = samples.minJ; j <= samples.maxJ; j++) {", + " texture.addUpdateRange((j * size + samples.minI) * 4, (samples.maxI - samples.minI + 1) * 4);", + " }", + " }", + " texture.needsUpdate = true;", + "};", + "", + "class TerrainRenderer {", + " /** @param {Terrain} terrain */", + " constructor(terrain) {", + " this.terrain = terrain;", + " this.group = new THREE.Group();", + " terrain.object.get3DRendererObject().add(this.group);", + " /** @type {Array<{mesh: THREE.Mesh, geometry: THREE.BufferGeometry, firstI: number, firstJ: number, center: THREE.Vector3, lod: number}>} */", + " this.chunks = [];", + " this.chunksPerSide = 0;", + " this.chunkCells = 0;", + " this.chunkSideVertices = 0;", + " this.chunkGridVertices = 0;", + " this.chunkVertices = 0;", + " /** @type {THREE.DataTexture | null} */", + " this.splatTexture = null;", + " /** @type {THREE.DataTexture | null} */", + " this.colorTexture = null;", + " this.uniforms = {", + " terrainSplatMap: { value: getWhiteTexture() },", + " terrainSplatUvScaleAndOffset: { value: new THREE.Vector2() },", + " terrainLayerColors: { value: [new THREE.Color(), new THREE.Color(), new THREE.Color(), new THREE.Color()] },", + " terrainLayerMap0: { value: getWhiteTexture() },", + " terrainLayerMap1: { value: getWhiteTexture() },", + " terrainLayerMap2: { value: getWhiteTexture() },", + " terrainLayerMap3: { value: getWhiteTexture() },", + " terrainLayerNormalMap0: { value: getFlatNormalTexture() },", + " terrainLayerNormalMap1: { value: getFlatNormalTexture() },", + " terrainLayerNormalMap2: { value: getFlatNormalTexture() },", + " terrainLayerNormalMap3: { value: getFlatNormalTexture() },", + " terrainHasNormalMaps: { value: 0 },", + " terrainNormalMapStrength: { value: 1 },", + " terrainTextureSize: { value: 256 },", + " terrainHeightBlending: { value: 0 },", + " terrainTextureVariation: { value: 0 },", + " terrainCliffLayer: { value: new THREE.Vector4() },", + " terrainCliffCosines: { value: new THREE.Vector2() },", + " terrainBrush: { value: new THREE.Vector4() },", + " terrainColorMap: { value: getWhiteTexture() },", + " // Z (in the terrain group) under which the ground is wet, fade height, and 1 if it's used.", + " terrainWetShore: { value: new THREE.Vector3() },", + " // Scene units by 3D world unit.", + " terrainWorldScale: { value: 1 },", + " };", + " this.material = new THREE.MeshStandardMaterial({ roughness: 1, metalness: 0 });", + " this.material.onBeforeCompile = (shader) => {", + " Object.assign(shader.uniforms, this.uniforms);", + " shader.vertexShader = patchShaderCode(shader.vertexShader, [", + " ['#include ', '#include \\n' + vertexShaderDeclarations],", + " ['#include ', '#include \\n' + vertexShaderCode],", + " ], 'Terrain3D');", + " shader.fragmentShader = patchShaderCode(shader.fragmentShader, [", + " ['#include ', '#include \\n' + fragmentShaderDeclarations],", + " ['#include ', fragmentShaderCode],", + " ['#include ', '#include \\n' + fragmentShaderRoughnessCode],", + " ['#include ', '#include \\n' + fragmentShaderNormalCode],", + " ], 'Terrain3D');", + " };", + " this.material.customProgramCacheKey = () => (this.hasHoles ? 'Terrain3DWithHoles' : 'Terrain3D');", + " this.hasHoles = false;", + " // Without them, holes would cast shadows.", + " this.depthMaterial = this._createHoleDepthMaterial(new THREE.MeshDepthMaterial());", + " this.distanceMaterial = this._createHoleDepthMaterial(new THREE.MeshDistanceMaterial());", + " this._cameraPosition = new THREE.Vector3();", + " this._chunkPosition = new THREE.Vector3();", + " this._scale = new THREE.Vector3();", + " }", + "", + " /**", + " * @param {THREE.MeshDepthMaterial | THREE.MeshDistanceMaterial} material", + " * @returns The material, drawing nothing in holes.", + " */", + " _createHoleDepthMaterial(material) {", + " material.defines = { TERRAIN_HOLES: '' };", + " material.onBeforeCompile = (shader) => {", + " Object.assign(shader.uniforms, this.uniforms);", + " shader.vertexShader = patchShaderCode(shader.vertexShader, [", + " ['#include ', '#include \\nvarying vec2 vTerrainLocalXY;'],", + " ['#include ', '#include \\nvTerrainLocalXY = position.xy;'],", + " ], 'Terrain3D shadows');", + " shader.fragmentShader = patchShaderCode(shader.fragmentShader, [", + " ['#include ', '#include \\n' + holeDepthShaderDeclarations],", + " ['#include ', holeShaderCode + '#include '],", + " ], 'Terrain3D shadows');", + " };", + " material.customProgramCacheKey = () => 'Terrain3DHoles';", + " return material;", + " }", + "", + " /**", + " * Use the shaders drawing holes, or the faster ones without them.", + " * @param {boolean} hasHoles", + " */", + " setHasHoles(hasHoles) {", + " if (hasHoles === this.hasHoles) return;", + " this.hasHoles = hasHoles;", + " // The material has its own defines (STANDARD).", + " if (hasHoles) this.material.defines.TERRAIN_HOLES = '';", + " else delete this.material.defines.TERRAIN_HOLES;", + " this.material.needsUpdate = true;", + " for (const chunk of this.chunks) this._setChunkShadowMaterials(chunk.mesh);", + " }", + "", + " /** @param {THREE.Mesh} mesh */", + " _setChunkShadowMaterials(mesh) {", + " mesh.customDepthMaterial = this.hasHoles ? this.depthMaterial : undefined;", + " mesh.customDistanceMaterial = this.hasHoles ? this.distanceMaterial : undefined;", + " }", + "", + " /** Recreate all the chunks, for example after the resolution changed. */", + " rebuild() {", + " this.disposeChunks();", + " const { data } = this.terrain;", + " this.chunkCells = getChunkCells(data.resolution);", + " this.chunkSideVertices = this.chunkCells + 1;", + " this.chunkGridVertices = this.chunkSideVertices * this.chunkSideVertices;", + " this.chunkVertices = this.chunkGridVertices + 4 * this.chunkSideVertices;", + " this.chunksPerSide = data.resolution / this.chunkCells;", + " const cellSize = AREA_SIZE / data.resolution;", + " // Sample centers are at the texel centers.", + " this.splatTexture = createGridTexture(data.splat, data.size);", + " this.uniforms.terrainSplatMap.value = this.splatTexture;", + " this.colorTexture = createGridTexture(data.colors, data.size);", + " // Tints are colors chosen like the layer colors.", + " this.colorTexture.colorSpace = THREE.SRGBColorSpace;", + " this.uniforms.terrainColorMap.value = this.colorTexture;", + " this.uniforms.terrainSplatUvScaleAndOffset.value.set(1 / (cellSize * data.size), 0.5 / data.size);", + " for (let chunkJ = 0; chunkJ < this.chunksPerSide; chunkJ++) {", + " for (let chunkI = 0; chunkI < this.chunksPerSide; chunkI++) {", + " const firstI = chunkI * this.chunkCells;", + " const firstJ = chunkJ * this.chunkCells;", + " const positions = new Float32Array(this.chunkVertices * 3);", + " for (let j = 0; j < this.chunkSideVertices; j++) {", + " for (let i = 0; i < this.chunkSideVertices; i++) {", + " const vertex = j * this.chunkSideVertices + i;", + " positions[vertex * 3] = (firstI + i) * cellSize;", + " positions[vertex * 3 + 1] = (firstJ + j) * cellSize;", + " }", + " }", + " // Skirt vertices are below border vertices.", + " this._forEachSkirtVertex((skirtVertex, borderVertex) => {", + " positions[skirtVertex * 3] = positions[borderVertex * 3];", + " positions[skirtVertex * 3 + 1] = positions[borderVertex * 3 + 1];", + " });", + " const geometry = new THREE.BufferGeometry();", + " geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));", + " geometry.setAttribute('normal', new THREE.BufferAttribute(new Float32Array(this.chunkVertices * 3), 3));", + " geometry.setIndex(getLodIndex(this.chunkCells, LOD_STEPS[0]));", + " geometry.boundingBox = new THREE.Box3();", + " geometry.boundingSphere = new THREE.Sphere();", + " const mesh = new THREE.Mesh(geometry, this.material);", + " mesh.matrixAutoUpdate = false;", + " // Rays hit the ground at its full resolution, whatever the level of", + " // detail drawn, and go through holes.", + " const chunkCells = this.chunkCells;", + " const terrainData = () => this.terrain.data;", + " const localPoint = new THREE.Vector3();", + " mesh.raycast = function (raycaster, intersects) {", + " const drawnIndex = geometry.index;", + " const firstIntersection = intersects.length;", + " geometry.setIndex(getLodIndex(chunkCells, LOD_STEPS[0]));", + " THREE.Mesh.prototype.raycast.call(this, raycaster, intersects);", + " geometry.setIndex(drawnIndex);", + " const data = terrainData();", + " const cellSize = AREA_SIZE / data.resolution;", + " for (let index = intersects.length - 1; index >= firstIntersection; index--) {", + " this.worldToLocal(localPoint.copy(intersects[index].point));", + " if (data.sampleSolidity(localPoint.x / cellSize, localPoint.y / cellSize) < 0.5) {", + " intersects.splice(index, 1);", + " }", + " }", + " };", + " this._setChunkShadowMaterials(mesh);", + " this.group.add(mesh);", + " this.chunks.push({", + " mesh,", + " geometry,", + " firstI,", + " firstJ,", + " center: new THREE.Vector3(", + " (firstI + this.chunkCells / 2) * cellSize,", + " (firstJ + this.chunkCells / 2) * cellSize,", + " 0", + " ),", + " lod: 0,", + " });", + " }", + " }", + " this.updateSamples(new SampleRectangle().set(0, 0, data.resolution, data.resolution), true);", + " this.updateMaterial();", + " }", + "", + " _forEachSkirtVertex(callback) {", + " for (let k = 0; k < this.chunkSideVertices; k++) {", + " callback(this.chunkGridVertices + k, k);", + " callback(this.chunkGridVertices + this.chunkSideVertices + k, this.chunkCells * this.chunkSideVertices + k);", + " callback(this.chunkGridVertices + 2 * this.chunkSideVertices + k, k * this.chunkSideVertices);", + " callback(", + " this.chunkGridVertices + 3 * this.chunkSideVertices + k,", + " k * this.chunkSideVertices + this.chunkCells", + " );", + " }", + " }", + "", + " /**", + " * Skirts go down only to the lowest point a coarser neighbor chunk can", + " * draw along the border, so that they can't be seen through holes.", + " * @param {Float32Array} positions", + " */", + " _updateSkirtHeights(positions) {", + " const coarsestStep = LOD_STEPS[LOD_STEPS.length - 1];", + " const borderVertices = [", + " (k) => k,", + " (k) => this.chunkCells * this.chunkSideVertices + k,", + " (k) => k * this.chunkSideVertices,", + " (k) => k * this.chunkSideVertices + this.chunkCells,", + " ];", + " borderVertices.forEach((borderVertex, border) => {", + " for (let k = 0; k < this.chunkSideVertices; k++) {", + " let minZ = Infinity;", + " const from = Math.max(k - coarsestStep, 0);", + " const to = Math.min(k + coarsestStep, this.chunkCells);", + " for (let other = from; other <= to; other++) {", + " minZ = Math.min(minZ, positions[borderVertex(other) * 3 + 2]);", + " }", + " const skirtVertex = this.chunkGridVertices + border * this.chunkSideVertices + k;", + " positions[skirtVertex * 3 + 2] = minZ;", + " }", + " });", + " }", + "", + " /**", + " * Update the vertices of the chunks sharing the given samples.", + " * @param {SampleRectangle} samples", + " * @param {boolean} hasPaintChanged", + " */", + " updateSamples(samples, hasPaintChanged) {", + " if (samples.isEmpty()) return;", + " const { data } = this.terrain;", + " const cellSize = AREA_SIZE / data.resolution;", + " const slopeFactor = AREA_DEPTH / cellSize;", + " // Normals of samples next to changed ones change too.", + " const minI = Math.max(samples.minI - 1, 0);", + " const minJ = Math.max(samples.minJ - 1, 0);", + " const maxI = Math.min(samples.maxI + 1, data.resolution);", + " const maxJ = Math.min(samples.maxJ + 1, data.resolution);", + " const firstChunkI = Math.max(Math.ceil(minI / this.chunkCells) - 1, 0);", + " const firstChunkJ = Math.max(Math.ceil(minJ / this.chunkCells) - 1, 0);", + " const lastChunkI = Math.min(Math.floor(maxI / this.chunkCells), this.chunksPerSide - 1);", + " const lastChunkJ = Math.min(Math.floor(maxJ / this.chunkCells), this.chunksPerSide - 1);", + "", + " for (let chunkJ = firstChunkJ; chunkJ <= lastChunkJ; chunkJ++) {", + " for (let chunkI = firstChunkI; chunkI <= lastChunkI; chunkI++) {", + " const chunk = this.chunks[chunkJ * this.chunksPerSide + chunkI];", + " const positions = chunk.geometry.attributes.position.array;", + " const normals = chunk.geometry.attributes.normal.array;", + " const fromI = Math.max(minI - chunk.firstI, 0);", + " const toI = Math.min(maxI - chunk.firstI, this.chunkCells);", + " const fromJ = Math.max(minJ - chunk.firstJ, 0);", + " const toJ = Math.min(maxJ - chunk.firstJ, this.chunkCells);", + " for (let j = fromJ; j <= toJ; j++) {", + " const gridJ = chunk.firstJ + j;", + " for (let i = fromI; i <= toI; i++) {", + " const gridI = chunk.firstI + i;", + " const vertex = j * this.chunkSideVertices + i;", + " const sample = gridJ * data.size + gridI;", + " positions[vertex * 3 + 2] = data.heights[sample] * AREA_DEPTH;", + "", + " const leftI = Math.max(gridI - 1, 0);", + " const rightI = Math.min(gridI + 1, data.resolution);", + " const upJ = Math.max(gridJ - 1, 0);", + " const downJ = Math.min(gridJ + 1, data.resolution);", + " const nx =", + " ((data.getHeight(leftI, gridJ) - data.getHeight(rightI, gridJ)) * slopeFactor) /", + " (rightI - leftI);", + " const ny =", + " ((data.getHeight(gridI, upJ) - data.getHeight(gridI, downJ)) * slopeFactor) /", + " (downJ - upJ);", + " const inverseLength = 1 / Math.sqrt(nx * nx + ny * ny + 1);", + " normals[vertex * 3] = nx * inverseLength;", + " normals[vertex * 3 + 1] = ny * inverseLength;", + " normals[vertex * 3 + 2] = inverseLength;", + " }", + " }", + " this._forEachSkirtVertex((skirtVertex, borderVertex) => {", + " for (let axis = 0; axis < 3; axis++) {", + " normals[skirtVertex * 3 + axis] = normals[borderVertex * 3 + axis];", + " }", + " });", + " this._updateSkirtHeights(positions);", + " let maxZ = 0;", + " for (let vertex = 0; vertex < this.chunkGridVertices; vertex++) {", + " maxZ = Math.max(maxZ, positions[vertex * 3 + 2]);", + " }", + " const chunkSize = this.chunkCells * cellSize;", + " chunk.geometry.boundingBox.min.set(chunk.firstI * cellSize, chunk.firstJ * cellSize, 0);", + " chunk.geometry.boundingBox.max.set(", + " chunk.firstI * cellSize + chunkSize,", + " chunk.firstJ * cellSize + chunkSize,", + " maxZ", + " );", + " chunk.geometry.boundingBox.getBoundingSphere(chunk.geometry.boundingSphere);", + " chunk.center.z = maxZ / 2;", + " chunk.geometry.attributes.position.needsUpdate = true;", + " chunk.geometry.attributes.normal.needsUpdate = true;", + " }", + " }", + " if (hasPaintChanged) {", + " // The data may have been replaced by one with the same resolution.", + " this.splatTexture.image.data = data.splat;", + " uploadGridTextureRows(this.splatTexture, samples);", + " this.colorTexture.image.data = data.colors;", + " uploadGridTextureRows(this.colorTexture, samples);", + " }", + " }", + "", + " updateMaterial() {", + " const { object } = this.terrain;", + " const game = object.getRuntimeScene().getGame();", + " const colors = [object._getLayer1Color(), object._getLayer2Color(), object._getLayer3Color(), object._getLayer4Color()];", + " const textures = [", + " object._getLayer1Texture(),", + " object._getLayer2Texture(),", + " object._getLayer3Texture(),", + " object._getLayer4Texture(),", + " ];", + " const normalMaps = [", + " object._getLayer1NormalMap(),", + " object._getLayer2NormalMap(),", + " object._getLayer3NormalMap(),", + " object._getLayer4NormalMap(),", + " ];", + " for (let layer = 0; layer < LAYER_COUNT; layer++) {", + " this.uniforms.terrainLayerColors.value[layer].set(gdjs.rgbOrHexStringToNumber(colors[layer]));", + " this.uniforms['terrainLayerMap' + layer].value = getLayerTexture(game, textures[layer], false);", + " this.uniforms['terrainLayerNormalMap' + layer].value = getLayerTexture(game, normalMaps[layer], true);", + " }", + " this.uniforms.terrainHasNormalMaps.value = normalMaps.some((normalMap) => !!normalMap) ? 1 : 0;", + " this.uniforms.terrainNormalMapStrength.value = Math.max(object._getNormalMapStrength(), 0);", + " this.uniforms.terrainTextureSize.value = Math.max(object._getTextureSize(), 1);", + " this.uniforms.terrainHeightBlending.value = clamp01(object._getHeightBlending());", + " this.uniforms.terrainTextureVariation.value = clamp01(object._getTextureVariation());", + "", + " const cliffLayer = parseInt(object._getCliffLayer(), 10);", + " const cliffAngle = gdjs.toRad(object._getCliffAngle());", + " const cliffBlendAngle = gdjs.toRad(4);", + " this.uniforms.terrainCliffLayer.value.set(", + " cliffLayer === 1 ? 1 : 0,", + " cliffLayer === 2 ? 1 : 0,", + " cliffLayer === 3 ? 1 : 0,", + " cliffLayer === 4 ? 1 : 0", + " );", + " this.uniforms.terrainCliffCosines.value.set(", + " Math.cos(cliffAngle + cliffBlendAngle),", + " Math.cos(cliffAngle - cliffBlendAngle)", + " );", + "", + " const castShadow = object._getCastShadow();", + " const receiveShadow = object._getReceiveShadow();", + " for (const chunk of this.chunks) {", + " chunk.mesh.castShadow = castShadow;", + " chunk.mesh.receiveShadow = receiveShadow;", + " }", + " }", + "", + " /**", + " * Show the brush circle of the editor.", + " * @param {THREE.Vector3 | null} localPosition", + " * @param {number} radius In scene units.", + " */", + " setBrush(localPosition, radius) {", + " const brush = this.uniforms.terrainBrush.value;", + " if (!localPosition) {", + " brush.w = 0;", + " return;", + " }", + " this.group.updateWorldMatrix(true, false);", + " const worldPosition = this._chunkPosition.copy(localPosition).applyMatrix4(this.group.matrixWorld);", + " const worldScale = this.uniforms.terrainWorldScale.value;", + " brush.set(worldPosition.x * worldScale, worldPosition.y * worldScale, radius, 1);", + " }", + "", + " /** @param {THREE.Camera | null} camera */", + " updateLevelsOfDetail(camera) {", + " if (!camera || !this.group.visible) return;", + " const matrixWorld = this.group.matrixWorld;", + " this._cameraPosition.setFromMatrixPosition(camera.matrixWorld);", + " this._scale.setFromMatrixScale(matrixWorld);", + " const cellSize = AREA_SIZE / this.terrain.data.resolution;", + " const chunkWorldSize = this.chunkCells * cellSize * Math.max(this._scale.x, this._scale.y);", + " for (const chunk of this.chunks) {", + " const distance =", + " this._chunkPosition.copy(chunk.center).applyMatrix4(matrixWorld).distanceTo(this._cameraPosition) /", + " chunkWorldSize;", + " let lod = LOD_DISTANCES.length - 1;", + " while (lod > 0 && distance < LOD_DISTANCES[lod]) lod--;", + " if (lod !== chunk.lod) {", + " chunk.lod = lod;", + " chunk.geometry.setIndex(getLodIndex(this.chunkCells, LOD_STEPS[lod]));", + " }", + " }", + " }", + "", + " disposeChunks() {", + " for (const chunk of this.chunks) {", + " chunk.mesh.removeFromParent();", + " // The index is shared by all chunks: it must stay on the GPU.", + " chunk.geometry.setIndex(null);", + " chunk.geometry.dispose();", + " }", + " this.chunks.length = 0;", + " if (this.splatTexture) this.splatTexture.dispose();", + " this.splatTexture = null;", + " if (this.colorTexture) this.colorTexture.dispose();", + " this.colorTexture = null;", + " }", + "", + " dispose() {", + " this.disposeChunks();", + " this.material.dispose();", + " this.depthMaterial.dispose();", + " this.distanceMaterial.dispose();", + " this.group.removeFromParent();", + " }", + "}", + "", + "/**", + " * Makes a Physics3D behavior use a height field shape following the terrain.", + " * Changes of heights are applied to the shape without recreating it.", + " * @implements {gdjs.Physics3DRuntimeBehavior.BodyUpdater}", + " */", + "class TerrainBodyUpdater {", + " /**", + " * @param {Terrain} terrain", + " * @param {gdjs.Physics3DRuntimeBehavior} physics", + " */", + " constructor(terrain, physics) {", + " this.terrain = terrain;", + " this.physics = physics;", + " this.defaultBodyUpdater = physics.bodyUpdater;", + " this.changedSamples = new SampleRectangle();", + " /** @type {Jolt.HeightFieldShape | null} */", + " this.heightField = null;", + " this.heightScale = 1;", + " this.heightsBufferPointer = 0;", + " this.heightsBufferLength = 0;", + " this.bodyX = 0;", + " this.bodyY = 0;", + " this.bodyZ = 0;", + " this.bodyHeight = 0;", + " physics.bodyType = 'Static';", + " physics.bodyUpdater = this;", + " }", + "", + " createShape() {", + " const { data, object } = this.terrain;", + " const worldInvScale = this.physics._sharedData.worldInvScale;", + " const { size } = data;", + " this.heightScale = object.getDepth() * worldInvScale;", + "", + " // Changes of heights (SetHeights) break collisions unless the sample", + " // count is a power of 2: extra samples have no collision.", + " const sampleCount = Math.pow(2, Math.ceil(Math.log2(size)));", + " const settings = new Jolt.HeightFieldShapeSettings();", + " settings.mScale.Set(", + " (object.getWidth() / data.resolution) * worldInvScale,", + " 1,", + " (object.getHeight() / data.resolution) * worldInvScale", + " );", + " settings.mSampleCount = sampleCount;", + " settings.mBlockSize = 4;", + " // Allows later changes of heights on the whole depth of the object.", + " settings.mMinHeightValue = 0;", + " settings.mMaxHeightValue = this.heightScale;", + " settings.mHeightSamples.resize(sampleCount * sampleCount);", + " const samples = new Float32Array(", + " Jolt.HEAPF32.buffer,", + " Jolt.getPointer(settings.mHeightSamples.data()),", + " sampleCount * sampleCount", + " );", + " const noCollision = Jolt.HeightFieldShapeConstantValues.prototype.cNoCollisionValue;", + " samples.fill(noCollision);", + " // Height fields go along X and Z: rows are flipped so that, once", + " // rotated to have heights along Z, rows go along Y.", + " for (let j = 0; j < size; j++) {", + " const row = (size - 1 - j) * sampleCount;", + " for (let i = 0; i < size; i++) {", + " samples[row + i] = this._getSampleHeight(j * size + i, noCollision);", + " }", + " }", + " const shape = settings.Create().Get();", + " Jolt.destroy(settings);", + " this.heightField = Jolt.castObject(shape, Jolt.HeightFieldShape);", + " this.changedSamples.clear();", + " return shape;", + " }", + "", + " /** @returns {number} The height of a sample in the shape, or no collision in a hole. */", + " _getSampleHeight(index, noCollision) {", + " const height = this.terrain.surfaceHeights[index];", + " return Number.isNaN(height) ? noCollision : height * this.heightScale;", + " }", + "", + " _getBodyPosition() {", + " const { object } = this.terrain;", + " const worldInvScale = this.physics._sharedData.worldInvScale;", + " this.bodyX = object.getX();", + " this.bodyY = object.getY();", + " this.bodyZ = object.getZ();", + " // The origin of the height field is at the bottom of the terrain (see createShape).", + " this.bodyHeight = object.getHeight();", + " return this.physics._sharedData.getRVec3(", + " object.getX() * worldInvScale,", + " (object.getY() + object.getHeight()) * worldInvScale,", + " object.getZ() * worldInvScale", + " );", + " }", + "", + " _getBodyRotation() {", + " // A quarter turn around X: heights go along Z.", + " return this.physics._sharedData.getQuat(Math.SQRT1_2, 0, 0, Math.SQRT1_2);", + " }", + "", + " createAndAddBody() {", + " const { physics } = this;", + " const { bodyInterface } = physics._sharedData;", + " const shape = this.createShape();", + " const settings = new Jolt.BodyCreationSettings(", + " shape,", + " this._getBodyPosition(),", + " this._getBodyRotation(),", + " Jolt.EMotionType_Static,", + " physics.getBodyLayer()", + " );", + " settings.mFriction = physics.friction;", + " settings.mRestitution = physics.restitution;", + " const body = bodyInterface.CreateBody(settings);", + " Jolt.destroy(settings);", + " bodyInterface.AddBody(body.GetID(), Jolt.EActivation_DontActivate);", + " return body;", + " }", + "", + " updateObjectFromBody() {", + " // The terrain is static: the body never moves it.", + " }", + "", + " updateBodyFromObject() {", + " const { physics } = this;", + " const body = physics._body;", + " if (!body) return;", + " const { object } = this.terrain;", + " const { bodyInterface } = physics._sharedData;", + " if (", + " object.getX() !== this.bodyX ||", + " object.getY() !== this.bodyY ||", + " object.getZ() !== this.bodyZ ||", + " object.getHeight() !== this.bodyHeight", + " ) {", + " bodyInterface.SetPositionAndRotation(", + " body.GetID(),", + " this._getBodyPosition(),", + " this._getBodyRotation(),", + " Jolt.EActivation_DontActivate", + " );", + " }", + " if (!this.changedSamples.isEmpty()) {", + " this._applyChangedHeights(body);", + " }", + " }", + "", + " _applyChangedHeights(body) {", + " const { data } = this.terrain;", + " const { size } = data;", + " const { minI, minJ, maxI, maxJ } = this.changedSamples;", + " // Jolt only changes blocks of samples. Rows are flipped and extra", + " // samples have no collision (see createShape).", + " const blockSize = this.heightField.GetBlockSize();", + " const sampleCount = this.heightField.GetSampleCount();", + " const alignDown = (value) => Math.floor(value / blockSize) * blockSize;", + " const alignUp = (value) => Math.min(Math.ceil(value / blockSize) * blockSize, sampleCount);", + " const firstX = alignDown(minI);", + " const firstRow = alignDown(size - 1 - maxJ);", + " const width = alignUp(maxI + 1) - firstX;", + " const height = alignUp(size - minJ) - firstRow;", + " if (width * height > this.heightsBufferLength) {", + " if (this.heightsBufferPointer) Jolt._webidl_free(this.heightsBufferPointer);", + " this.heightsBufferLength = width * height;", + " this.heightsBufferPointer = Jolt._webidl_malloc(this.heightsBufferLength * 4);", + " }", + " const heights = new Float32Array(Jolt.HEAPF32.buffer, this.heightsBufferPointer, width * height);", + " const noCollision = Jolt.HeightFieldShapeConstantValues.prototype.cNoCollisionValue;", + " for (let row = 0; row < height; row++) {", + " const j = size - 1 - (firstRow + row);", + " for (let column = 0; column < width; column++) {", + " const i = firstX + column;", + " heights[row * width + column] =", + " i < size && j >= 0 ? this._getSampleHeight(j * size + i, noCollision) : noCollision;", + " }", + " }", + " const sharedData = this.physics._sharedData;", + " this.heightField.SetHeights(", + " firstX,", + " firstRow,", + " width,", + " height,", + " this.heightsBufferPointer,", + " width,", + " sharedData.jolt.GetTempAllocator()", + " );", + " sharedData.bodyInterface.NotifyShapeChanged(", + " body.GetID(),", + " sharedData.getVec3(0, 0, 0),", + " false,", + " Jolt.EActivation_DontActivate", + " );", + " this._wakeUpBodiesOnChangedSamples();", + " this.changedSamples.clear();", + " }", + "", + " /** Bodies sleeping on the changed area would otherwise float or sink. */", + " _wakeUpBodiesOnChangedSamples() {", + " const { object, data } = this.terrain;", + " const worldInvScale = this.physics._sharedData.worldInvScale;", + " const cellWidth = object.getWidth() / data.resolution;", + " const cellHeight = object.getHeight() / data.resolution;", + " const { minI, minJ, maxI, maxJ } = this.changedSamples;", + " const min = new Jolt.Vec3(", + " (object.getX() + (minI - 1) * cellWidth) * worldInvScale,", + " (object.getY() + (minJ - 1) * cellHeight) * worldInvScale,", + " (object.getZ() - 1) * worldInvScale", + " );", + " const max = new Jolt.Vec3(", + " (object.getX() + (maxI + 1) * cellWidth) * worldInvScale,", + " (object.getY() + (maxJ + 1) * cellHeight) * worldInvScale,", + " (object.getZ() + object.getDepth() + 1) * worldInvScale", + " );", + " const box = new Jolt.AABox(min, max);", + " const broadPhaseLayerFilter = new Jolt.BroadPhaseLayerFilter();", + " const objectLayerFilter = new Jolt.ObjectLayerFilter();", + " this.physics._sharedData.bodyInterface.ActivateBodiesInAABox(box, broadPhaseLayerFilter, objectLayerFilter);", + " Jolt.destroy(min);", + " Jolt.destroy(max);", + " Jolt.destroy(box);", + " Jolt.destroy(broadPhaseLayerFilter);", + " Jolt.destroy(objectLayerFilter);", + " }", + "", + " recreateShape() {", + " const body = this.physics._body;", + " if (!body) return;", + " this.physics._sharedData.bodyInterface.SetShape(", + " body.GetID(),", + " this.createShape(),", + " false,", + " Jolt.EActivation_DontActivate", + " );", + " const { resolution } = this.terrain.data;", + " this.changedSamples.set(0, 0, resolution, resolution);", + " this._wakeUpBodiesOnChangedSamples();", + " this.changedSamples.clear();", + " }", + "", + " destroyBody() {", + " this.defaultBodyUpdater.destroyBody();", + " this.heightField = null;", + " }", + "", + " dispose() {", + " if (this.heightsBufferPointer) Jolt._webidl_free(this.heightsBufferPointer);", + " this.heightsBufferPointer = 0;", + " this.heightsBufferLength = 0;", + " }", + "}", + "", + "/** @param {TerrainData} data */", + "const copyTerrainData = (data) => {", + " const copy = new TerrainData(data.resolution);", + " copy.copyFrom(data);", + " return copy;", + "};", + "", + "// Generating a relief or reading an image is long for big terrains, and done", + "// for every instance and every hot-reload: the results are kept.", + "const baseDataCache = new Map();", + "const MAX_CACHED_BASE_DATA = 8;", + "// Changes of the shape of a terrain kept for consumers of its surface that", + "// missed some versions: older changes update the whole surface.", + "const MAX_SURFACE_CHANGES = 64;", + "// Unique among all terrains: a terrain replaced by another one never has its version.", + "let lastGroundVersion = 0;", + "", + "/**", + " * The terrain of a Terrain3D object: its data, how it's drawn and its", + " * surface (for collisions and navigation meshes). Positions are in the scene.", + " */", + "class Terrain {", + " /** @param {gdjs.CustomRuntimeObject3D} object */", + " constructor(object) {", + " this.object = object;", + " /** @type {TerrainData} */", + " this.data = new TerrainData(DEFAULT_RESOLUTION);", + " /** The data before any sculpting or painting, from the relief or heightmap. */", + " this.baseData = this.data;", + " this.renderer = new TerrainRenderer(this);", + " /** @type {TerrainBodyUpdater[]} */", + " this.bodyUpdaters = [];", + " this.changedSamples = new SampleRectangle();", + " this.hasPaintChanged = false;", + " /** Increased each time the ground changes, for objects following it (like grass). */", + " this.version = 0;", + " /** Increased each time the shape of the ground changes (not its paint or position). */", + " this.surfaceVersion = 0;", + " /**", + " * The last changes of the shape of the ground, for `getChangedArea`.", + " * @type {Array<{version: number, samples: SampleRectangle}>}", + " */", + " this.surfaceChanges = [];", + " /** The changes since this version are all in `surfaceChanges`. */", + " this.surfaceChangesStartVersion = 0;", + " this._stroke = { cellWidth: 1, cellHeight: 1, ax: 0, ay: 0, bx: 0, by: 0, radius: 0 };", + " this._strokeChangedSamples = new SampleRectangle();", + " /** The properties the data was loaded from, to reload it only if they change. */", + " this.loadedFrom = '';", + " /** The heightmap image being loaded: the relief is used until it is. */", + " this.loadingHeightmapImage = '';", + " this.isDisposed = false;", + " /** The last value of the sculpt data property read or saved. */", + " this.sculptData = '';", + " /**", + " * Values saved by the editor tools: the editor sends them back with a", + " * hot-reload, possibly after newer ones were saved.", + " * @type {string[]}", + " */", + " this.savedSculptData = [];", + " /** The heights of the surface: the heights of the data, and NaN in holes. */", + " this.surfaceHeights = new Float32Array(0);", + " this.loadFromProperties();", + " this._transform = this._getTransform();", + " getGrounds(object.getRuntimeScene()).add(this);", + " /**", + " * The shape of the ground, for the physics engine and navigation meshes.", + " * @type {gdjs.Surface}", + " */", + " this.surface = {", + " getVersion: () => this.surfaceVersion,", + " getChangedArea: (sinceVersion) => this.getChangedArea(sinceVersion),", + " getTriangles: () => null,", + " getHeightField: () => ({ columns: this.data.size, rows: this.data.size, heights: this.surfaceHeights }),", + " };", + " if (typeof object.setSurface === 'function') {", + " object.setSurface(this.surface);", + " } else if (gdjs.Physics3DRuntimeBehavior) {", + " // Older GDevelop versions don't use surfaces.", + " for (const behavior of object._behaviors) {", + " if (behavior instanceof gdjs.Physics3DRuntimeBehavior) {", + " this.bodyUpdaters.push(new TerrainBodyUpdater(this, behavior));", + " }", + " }", + " }", + " }", + "", + " _getSourceProperties() {", + " const { object } = this;", + " return [", + " object._getResolution(),", + " object._getRelief(),", + " object._getSeed(),", + " object._getHeightmapImage(),", + " object._getEdits(),", + " ].join('|');", + " }", + "", + " loadFromProperties() {", + " const { object } = this;", + " const resolution = parseInt(object._getResolution(), 10);", + " const validResolution = RESOLUTIONS.indexOf(resolution) !== -1 ? resolution : DEFAULT_RESOLUTION;", + " this.sculptData = object._getSculptData();", + " this.loadedFrom = this._getSourceProperties();", + "", + " this.baseData = this._getBaseData(validResolution);", + " let data = codec.decode(this.sculptData, (resolution) => copyTerrainData(this._getBaseData(resolution)));", + " if (data && data.resolution !== validResolution) {", + " data = data.resampled(validResolution);", + " }", + " this.setData(data || copyTerrainData(this.baseData));", + " }", + "", + " /**", + " * @param {number} resolution", + " * @returns {TerrainData} The data from the relief or the heightmap, shared: it must not be changed.", + " */", + " _getBaseData(resolution) {", + " const { object } = this;", + " const key = [resolution, object._getRelief(), object._getSeed(), object._getHeightmapImage(), object._getEdits()].join('|');", + " const cachedData = baseDataCache.get(key);", + " if (cachedData) return cachedData;", + "", + " const data = new TerrainData(resolution);", + " const heightmapImage = object._getHeightmapImage();", + " const isHeightmapLoaded =", + " !!heightmapImage && loadHeightsFromImage(data, object.getRuntimeScene().getGame(), heightmapImage);", + " if (!isHeightmapLoaded) {", + " relief.generate(data, object._getRelief(), object._getSeed());", + " if (heightmapImage) this._reloadOnceLoaded(heightmapImage);", + " }", + " edits.apply(data, object._getEdits());", + " // Not kept if the image is missing: it may be loaded later.", + " if (isHeightmapLoaded || !heightmapImage) baseDataCache.set(key, data);", + " if (baseDataCache.size > MAX_CACHED_BASE_DATA) {", + " baseDataCache.delete(baseDataCache.keys().next().value);", + " }", + " return data;", + " }", + "", + " /** @param {TerrainData} data */", + " setData(data) {", + " this.version = ++lastGroundVersion;", + " this.surfaceVersion = this.version;", + " this.surfaceChanges.length = 0;", + " this.surfaceChangesStartVersion = this.version;", + " const hasSameResolution = data.resolution === this.data.resolution && this.renderer.chunks.length > 0;", + " this.data = data;", + " const allSamples = new SampleRectangle().set(0, 0, data.resolution, data.resolution);", + " this.surfaceHeights = new Float32Array(data.heights.length);", + " this.renderer.setHasHoles(false);", + " this._updateSurfaceHeights(allSamples);", + " if (hasSameResolution) {", + " this.renderer.updateSamples(allSamples, true);", + " } else {", + " this.renderer.rebuild();", + " }", + " for (const bodyUpdater of this.bodyUpdaters) {", + " bodyUpdater.recreateShape();", + " }", + " }", + "", + " onHotReloading() {", + " const sculptData = this.object._getSculptData();", + " const savedIndex = this.savedSculptData.indexOf(sculptData);", + " if (savedIndex !== -1 && this._getSourceProperties() === this.loadedFrom) {", + " // The terrain already has this data, or newer changes.", + " this.savedSculptData.splice(0, savedIndex + 1);", + " } else if (sculptData !== this.sculptData || this._getSourceProperties() !== this.loadedFrom) {", + " this.savedSculptData.length = 0;", + " this.loadFromProperties();", + " }", + " this.renderer.updateMaterial();", + " }", + "", + " /** @returns {string} The data to save in the sculpt data property. */", + " save() {", + " this.sculptData = codec.encode(this.data, this.baseData);", + " this.savedSculptData.push(this.sculptData);", + " return this.sculptData;", + " }", + "", + " getCellWidth() {", + " return this.object.getWidth() / this.data.resolution;", + " }", + "", + " getCellHeight() {", + " return this.object.getHeight() / this.data.resolution;", + " }", + "", + " _toGridX(x) {", + " return (x - this.object.getX()) / this.getCellWidth();", + " }", + "", + " _toGridY(y) {", + " return (y - this.object.getY()) / this.getCellHeight();", + " }", + "", + " /** @returns {number} The Z position of the ground. */", + " getHeightAt(x, y) {", + " return this.object.getZ() + this.data.sampleHeight(this._toGridX(x), this._toGridY(y)) * this.object.getDepth();", + " }", + "", + " /** @returns {number} The angle of the ground with the horizontal, in degrees. */", + " getSlopeAt(x, y) {", + " const gridX = this._toGridX(x);", + " const gridY = this._toGridY(y);", + " const { data } = this;", + " const depth = this.object.getDepth();", + " const slopeX = ((data.sampleHeight(gridX + 0.5, gridY) - data.sampleHeight(gridX - 0.5, gridY)) * depth) / this.getCellWidth();", + " const slopeY = ((data.sampleHeight(gridX, gridY + 0.5) - data.sampleHeight(gridX, gridY - 0.5)) * depth) / this.getCellHeight();", + " return gdjs.toDegrees(Math.atan(Math.hypot(slopeX, slopeY)));", + " }", + "", + " /** @returns {number} The layer painted the most, from 1 to 4. */", + " getLayerAt(x, y) {", + " return this.data.getDominantLayer(this._toGridX(x), this._toGridY(y)) + 1;", + " }", + "", + " /**", + " * @param {number} layer From 1 to 4.", + " * @returns {number} How much the layer is painted, from 0 to 1.", + " */", + " getLayerWeightAt(x, y, layer) {", + " const layerIndex = clampInteger(layer - 1, 0, LAYER_COUNT - 1);", + " return this.data.sampleLayerWeight(this._toGridX(x), this._toGridY(y), layerIndex);", + " }", + "", + " /** @returns {number[]} The painted tint: red, green and blue, from 0 to 255 (white when there is none). */", + " getTintAt(x, y) {", + " const gridX = this._toGridX(x);", + " const gridY = this._toGridY(y);", + " return [0, 1, 2].map((channel) => Math.round(this.data.sampleColor(gridX, gridY, channel) * 255));", + " }", + "", + " /** @returns {number} How wet the ground is, from 0 (dry) to 1, painted or near the water (see the wet shore). */", + " getWetnessAt(x, y) {", + " const paintedWetness = this.data.sampleColor(this._toGridX(x), this._toGridY(y), 3);", + " const { object } = this;", + " if (!object._getWetShore()) return paintedWetness;", + " const fade = Math.max(object._getWetShoreFade(), 0.001);", + " const shoreWetness = clamp01((object._getWetShoreZ() + fade - this.getHeightAt(x, y)) / fade);", + " // Like the smoothstep of the shader.", + " return Math.max(paintedWetness, shoreWetness * shoreWetness * (3 - 2 * shoreWetness));", + " }", + "", + " _isInArea(x, y) {", + " const { object } = this;", + " return (", + " x >= object.getX() &&", + " x <= object.getX() + object.getWidth() &&", + " y >= object.getY() &&", + " y <= object.getY() + object.getHeight()", + " );", + " }", + "", + " /** @returns {boolean} true if the position is above or under the terrain, and not in a hole. */", + " containsPoint(x, y) {", + " return this._isInArea(x, y) && this.data.sampleSolidity(this._toGridX(x), this._toGridY(y)) >= 0.5;", + " }", + "", + " /** @returns {boolean} true if there is a hole in the terrain at the position, like what is drawn. */", + " isHoleAt(x, y) {", + " return this._isInArea(x, y) && this.data.sampleSolidity(this._toGridX(x), this._toGridY(y)) < 0.5;", + " }", + "", + " _getStroke(x1, y1, x2, y2, radius) {", + " const stroke = this._stroke;", + " stroke.cellWidth = this.getCellWidth();", + " stroke.cellHeight = this.getCellHeight();", + " stroke.ax = x1 - this.object.getX();", + " stroke.ay = y1 - this.object.getY();", + " stroke.bx = x2 - this.object.getX();", + " stroke.by = y2 - this.object.getY();", + " stroke.radius = Math.max(radius, 0);", + " return stroke;", + " }", + "", + " /**", + " * @param {SampleRectangle} samples", + " * @param {boolean} haveHeightsChanged false when only the paint changed.", + " */", + " _onSamplesChanged(samples, haveHeightsChanged) {", + " if (samples.isEmpty()) return;", + " this.version = ++lastGroundVersion;", + " this.changedSamples.add(samples);", + " // Painting changes the surface when it makes holes or fills them.", + " const hasSurfaceChanged = this._updateSurfaceHeights(samples);", + " if (!haveHeightsChanged && !hasSurfaceChanged) return;", + " this.surfaceVersion = this.version;", + " this.surfaceChanges.push({ version: this.version, samples: new SampleRectangle().set(samples.minI, samples.minJ, samples.maxI, samples.maxJ) });", + " if (this.surfaceChanges.length > MAX_SURFACE_CHANGES) {", + " this.surfaceChangesStartVersion = this.surfaceChanges.shift().version;", + " }", + " for (const bodyUpdater of this.bodyUpdaters) {", + " bodyUpdater.changedSamples.add(samples);", + " }", + " }", + "", + " /**", + " * @param {SampleRectangle} samples", + " * @returns {boolean} true if heights of the surface changed, or holes were made or filled.", + " */", + " _updateSurfaceHeights(samples) {", + " const { data, surfaceHeights } = this;", + " let hasChanged = false;", + " let hasHoles = false;", + " for (let j = samples.minJ; j <= samples.maxJ; j++) {", + " for (let i = samples.minI; i <= samples.maxI; i++) {", + " const index = j * data.size + i;", + " const isHole = data.isHole(index);", + " const height = isHole ? NaN : data.heights[index];", + " const previousHeight = surfaceHeights[index];", + " if (isHole) {", + " hasHoles = true;", + " if (Number.isNaN(previousHeight)) continue;", + " } else if (height === previousHeight) {", + " continue;", + " }", + " surfaceHeights[index] = height;", + " hasChanged = true;", + " }", + " }", + " // Filled holes keep the shaders drawing holes until the data is loaded again.", + " if (hasHoles) this.renderer.setHasHoles(true);", + " return hasChanged;", + " }", + "", + " /** @param {number} height In scene units, negative to lower the ground. */", + " raise(x1, y1, x2, y2, radius, height) {", + " const samples = this._strokeChangedSamples;", + " brushes.raise(this.data, this._getStroke(x1, y1, x2, y2, radius), height / this.object.getDepth(), samples);", + " this._onSamplesChanged(samples, true);", + " }", + "", + " /** @param {number} z The Z position to flatten the ground to. */", + " /** @param {number} [toZ] The Z position at the end of the segment, for a slope. The same as z by default. */", + " flatten(x1, y1, x2, y2, radius, z, strength, toZ = z) {", + " const samples = this._strokeChangedSamples;", + " const toHeight = (value) => (value - this.object.getZ()) / this.object.getDepth();", + " brushes.flatten(this.data, this._getStroke(x1, y1, x2, y2, radius), toHeight(z), strength, samples, toHeight(toZ));", + " this._onSamplesChanged(samples, true);", + " }", + "", + " smooth(x1, y1, x2, y2, radius, strength) {", + " const samples = this._strokeChangedSamples;", + " brushes.smooth(this.data, this._getStroke(x1, y1, x2, y2, radius), strength, samples);", + " this._onSamplesChanged(samples, true);", + " }", + "", + " /** @param {number} layer From 1 to 4. */", + " paint(x1, y1, x2, y2, radius, layer, strength) {", + " const layerIndex = clampInteger(layer - 1, 0, LAYER_COUNT - 1);", + " const samples = this._strokeChangedSamples;", + " brushes.paint(this.data, this._getStroke(x1, y1, x2, y2, radius), layerIndex, strength, samples);", + " this.hasPaintChanged = true;", + " this._onSamplesChanged(samples, false);", + " }", + "", + " /** @param {number[]} color The red, green and blue (0-255) of the tint: white removes it. */", + " tint(x1, y1, x2, y2, radius, color, strength) {", + " const samples = this._strokeChangedSamples;", + " brushes.tint(this.data, this._getStroke(x1, y1, x2, y2, radius), color, strength, samples);", + " this.hasPaintChanged = true;", + " this._onSamplesChanged(samples, false);", + " }", + "", + " /** @param {number} wetness From 0 (dry) to 1. */", + " wet(x1, y1, x2, y2, radius, wetness, strength) {", + " const samples = this._strokeChangedSamples;", + " brushes.wet(this.data, this._getStroke(x1, y1, x2, y2, radius), wetness, strength, samples);", + " this.hasPaintChanged = true;", + " this._onSamplesChanged(samples, false);", + " }", + "", + " /** Make a hole around a segment (`fillHoles` fills it again). */", + " makeHole(x1, y1, x2, y2, radius) {", + " const samples = this._strokeChangedSamples;", + " brushes.hole(this.data, this._getStroke(x1, y1, x2, y2, radius), samples);", + " this.hasPaintChanged = true;", + " this._onSamplesChanged(samples, false);", + " }", + "", + " /** Fill the holes around a segment with the layers of the ground around them. */", + " fillHoles(x1, y1, x2, y2, radius) {", + " const samples = this._strokeChangedSamples;", + " brushes.fill(this.data, this._getStroke(x1, y1, x2, y2, radius), samples);", + " this.hasPaintChanged = true;", + " this._onSamplesChanged(samples, false);", + " }", + "", + " /**", + " * Make the entrance of a tunnel or a cave: a hole, with the ground in", + " * front of it flattened to the floor of the tunnel.", + " * @param {number} angle The direction going into the ground, in degrees.", + " * @param {number} floorZ", + " * @param {number} length The length of the flat ground in front of the entrance.", + " */", + " carveEntrance(x, y, radius, angle, floorZ, length) {", + " const directionX = Math.cos(gdjs.toRad(angle));", + " const directionY = Math.sin(gdjs.toRad(angle));", + " this.flatten(x - directionX * length, y - directionY * length, x, y, radius * 1.5, floorZ, 1);", + " this.makeHole(x, y, x + directionX * radius, y + directionY * radius, radius);", + " }", + "", + " generate(reliefName, seed) {", + " this.baseData = new TerrainData(this.data.resolution);", + " relief.generate(this.baseData, reliefName, seed);", + " this.setData(copyTerrainData(this.baseData));", + " }", + "", + " loadHeightmap(imageResourceName) {", + " const baseData = new TerrainData(this.data.resolution);", + " if (loadHeightsFromImage(baseData, this.object.getRuntimeScene().getGame(), imageResourceName)) {", + " baseData.splat.set(this.data.splat);", + " baseData.colors.set(this.data.colors);", + " this.baseData = baseData;", + " this.setData(copyTerrainData(baseData));", + " }", + " }", + "", + " /** @param {gdjs.RuntimeObject[]} objects */", + " placeOnGround(objects) {", + " for (const object of objects) {", + " if (object === this.object || !gdjs.Base3DHandler.is3D(object)) continue;", + " const groundZ = this.getHeightAt(object.getCenterXInScene(), object.getCenterYInScene());", + " // The origin of the object is not always at its bottom.", + " object.setZ(groundZ + object.getZ() - object.getUnrotatedAABBMinZ());", + " }", + " }", + "", + " /**", + " * @returns {THREE.Vector3 | null} The point of the ground under the cursor,", + " * in the terrain group coordinates. Holes are hit too, so that brushes can fill them.", + " */", + " raycastFromCursor(game, raycaster, ndc) {", + " const layer = this.object.getInstanceContainer().getLayer(this.object.getLayer());", + " const camera = layer.getRenderer().getThreeCamera();", + " if (!camera) return null;", + " const inputManager = game.getInputManager();", + " ndc.set(", + " (inputManager.getCursorX() / game.getGameResolutionWidth()) * 2 - 1,", + " -(inputManager.getCursorY() / game.getGameResolutionHeight()) * 2 + 1", + " );", + " raycaster.setFromCamera(ndc, camera);", + " this.renderer.group.updateWorldMatrix(true, false);", + " const ray = raycaster.ray.clone().applyMatrix4(this.renderer.group.matrixWorld.clone().invert());", + " ray.direction.normalize();", + "", + " const box = new THREE.Box3(new THREE.Vector3(0, 0, 0), new THREE.Vector3(AREA_SIZE, AREA_SIZE, AREA_DEPTH));", + " const entry = box.containsPoint(ray.origin) ? ray.origin.clone() : ray.intersectBox(box, new THREE.Vector3());", + " if (!entry) return null;", + " const cellSize = AREA_SIZE / this.data.resolution;", + " const groundZAt = (point) => this.data.sampleHeight(point.x / cellSize, point.y / cellSize) * AREA_DEPTH;", + " const step = cellSize / 2;", + " const point = entry.clone();", + " const previousPoint = entry.clone();", + " for (let distance = 0; distance < AREA_SIZE * 2; distance += step) {", + " if (point.z <= groundZAt(point)) {", + " // Refine between the last point above the ground and this one.", + " for (let iteration = 0; iteration < 8; iteration++) {", + " const middle = previousPoint.clone().lerp(point, 0.5);", + " if (middle.z <= groundZAt(middle)) point.copy(middle);", + " else previousPoint.copy(middle);", + " }", + " return point;", + " }", + " if (!box.containsPoint(point)) return null;", + " previousPoint.copy(point);", + " point.addScaledVector(ray.direction, step);", + " }", + " return null;", + " }", + "", + " /**", + " * @param {THREE.Vector3} localPosition In the terrain group coordinates (like from `raycastFromCursor`).", + " * @returns {{x: number, y: number, z: number}} The position in the scene.", + " */", + " toScenePosition(localPosition) {", + " const { object } = this;", + " return {", + " x: object.getX() + (localPosition.x / AREA_SIZE) * object.getWidth(),", + " y: object.getY() + (localPosition.y / AREA_SIZE) * object.getHeight(),", + " z: object.getZ() + (localPosition.z / AREA_DEPTH) * object.getDepth(),", + " };", + " }", + "", + " /** Draw the changes of the ground (collisions are updated by the physics engine). */", + " applyChanges() {", + " this.renderer.updateSamples(this.changedSamples, this.hasPaintChanged);", + " this.changedSamples.clear();", + " this.hasPaintChanged = false;", + " }", + "", + " /** @returns {number} A number changing each time the ground changes (see `getGrounds`). */", + " getVersion() {", + " return this.version;", + " }", + "", + " /**", + " * @param {number} sinceVersion", + " * @returns {gdjs.SurfaceArea | null} The part of the ground whose shape changed since the version (see `gdjs.Surface`).", + " */", + " getChangedArea(sinceVersion) {", + " if (sinceVersion < this.surfaceChangesStartVersion) return null;", + " const samples = new SampleRectangle();", + " for (const change of this.surfaceChanges) {", + " if (change.version > sinceVersion) samples.add(change.samples);", + " }", + " if (samples.isEmpty()) return { minX: 0, minY: 0, maxX: 0, maxY: 0 };", + " const { resolution } = this.data;", + " return {", + " minX: samples.minI / resolution,", + " minY: samples.minJ / resolution,", + " maxX: samples.maxI / resolution,", + " maxY: samples.maxJ / resolution,", + " };", + " }", + "", + " _getTransform() {", + " const { object } = this;", + " return [object.getX(), object.getY(), object.getZ(), object.getWidth(), object.getHeight(), object.getDepth()].join(',');", + " }", + "", + " /** @param {string} imageResourceName A heightmap image that is not loaded yet. */", + " _reloadOnceLoaded(imageResourceName) {", + " if (this.loadingHeightmapImage === imageResourceName) return;", + " this.loadingHeightmapImage = imageResourceName;", + " const game = this.object.getRuntimeScene().getGame();", + " game.getImageManager()", + " .loadResource(imageResourceName)", + " .then(() => {", + " if (this.isDisposed || this.loadingHeightmapImage !== imageResourceName) return;", + " this.loadingHeightmapImage = '';", + " if (getLoadedImage(game, imageResourceName)) this.loadFromProperties();", + " })", + " .catch(() => {});", + " }", + "", + " update() {", + " const transform = this._getTransform();", + " if (transform !== this._transform) {", + " this._transform = transform;", + " this.version = ++lastGroundVersion;", + " }", + " this.applyChanges();", + " this.renderer.uniforms.terrainWorldScale.value = getWorldScale(this.object);", + " this._updateWetShore();", + " const layer = this.object.getInstanceContainer().getLayer(this.object.getLayer());", + " this.renderer.updateLevelsOfDetail(layer.getRenderer().getThreeCamera());", + " }", + "", + " _updateWetShore() {", + " const { object } = this;", + " // In the coordinates of the terrain group, like the vertices.", + " const depthScale = AREA_DEPTH / Math.max(object.getDepth(), 0.0001);", + " this.renderer.uniforms.terrainWetShore.value.set(", + " (object._getWetShoreZ() - object.getZ()) * depthScale,", + " Math.max(object._getWetShoreFade(), 0.001) * depthScale,", + " object._getWetShore() ? 1 : 0", + " );", + " }", + "", + " dispose() {", + " this.isDisposed = true;", + " getGrounds(this.object.getRuntimeScene()).delete(this);", + " if (typeof this.object.setSurface === 'function' && this.object.getSurface() === this.surface) {", + " this.object.setSurface(null);", + " }", + " this.renderer.dispose();", + " for (const bodyUpdater of this.bodyUpdaters) {", + " bodyUpdater.dispose();", + " }", + " }", + "}", + "", + "/**", + " * @param {gdjs.RuntimeObject} object", + " * @returns {number} The number of scene units by 3D world unit (1 in GDevelop versions before the world scale).", + " */", + "const getWorldScale = (object) => {", + " const scene = object.getRuntimeScene();", + " return typeof scene.getRenderer3DWorldScale === 'function' ? scene.getRenderer3DWorldScale() : 1;", + "};", + "", + "const averageColors = new WeakMap();", + "let white = null;", + "/**", + " * @param {THREE.Texture} texture", + " * @returns {THREE.Color} The average color of the texture (white when it's not loaded).", + " */", + "const getAverageColor = (texture) => {", + " const cachedColor = averageColors.get(texture);", + " if (cachedColor) return cachedColor;", + " const image = texture.image;", + " const isDrawable =", + " typeof document !== 'undefined' &&", + " ((image instanceof HTMLImageElement && image.complete && image.naturalWidth > 0) ||", + " image instanceof HTMLCanvasElement ||", + " (typeof ImageBitmap !== 'undefined' && image instanceof ImageBitmap));", + " if (!isDrawable) return white || (white = new THREE.Color(1, 1, 1));", + " const size = 8;", + " const canvas = document.createElement('canvas');", + " canvas.width = size;", + " canvas.height = size;", + " const context = canvas.getContext('2d');", + " context.drawImage(image, 0, 0, size, size);", + " const pixels = context.getImageData(0, 0, size, size).data;", + " let red = 0;", + " let green = 0;", + " let blue = 0;", + " for (let index = 0; index < pixels.length; index += 4) {", + " red += pixels[index];", + " green += pixels[index + 1];", + " blue += pixels[index + 2];", + " }", + " const count = size * size * 255;", + " const color = new THREE.Color().setRGB(red / count, green / count, blue / count, THREE.SRGBColorSpace);", + " averageColors.set(texture, color);", + " return color;", + "};", + "", + "const svgIcon = (content) =>", + " 'data:image/svg+xml,' +", + " encodeURIComponent('' + content + '');", + "const strokes = 'fill=\"none\" stroke=\"#000\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"';", + "const toolIcons = {", + " Raise: svgIcon(''),", + " Lower: svgIcon(''),", + " Smooth: svgIcon(''),", + " Flatten: svgIcon(''),", + " Size: svgIcon(''),", + " Strength: svgIcon(''),", + " Undo: svgIcon(''),", + " Hole: svgIcon(''),", + " FillHoles: svgIcon(''),", + " Wetness: svgIcon(''),", + "};", + "// Tints for the color brush when the editor has no color picker (older GDevelop versions).", + "const tintPresets = ['#e2c879', '#a5c45a', '#8a6a48', '#c98a62', '#9a9a92', '#ffffff'];", + "", + "/**", + " * The sculpt and paint tools shown in the scene editor when a terrain is", + " * selected. Without the needed editor functions (older GDevelop versions),", + " * nothing is shown: the terrain can still be changed with events.", + " */", + "class TerrainEditorTools {", + " constructor() {", + " /** '' when no brush is used. */", + " this.toolName = '';", + " this.sizePercent = 30;", + " this.strengthPercent = 50;", + " /** @type {Terrain | null} */", + " this.terrain = null;", + " /** @type {gdjs.InGameEditor | null} */", + " this.editor = null;", + " this.isStroking = false;", + " this.flattenHeight = 0;", + " /** The color of the \"Tint\" brush, as \"#rrggbb\". */", + " this.tintColor = tintPresets[0];", + " this.wetnessPercent = 100;", + " this.lastStrokeTime = 0;", + " /** @type {{data: TerrainData, baseData: TerrainData}[]} */", + " this.undoSteps = [];", + " this.raycaster = new THREE.Raycaster();", + " this.ndc = new THREE.Vector2();", + " this._swatchColor = new THREE.Color();", + " }", + "", + " /** @param {gdjs.InGameEditor} editor */", + " update(editor) {", + " if (typeof editor.showToolbar !== 'function') {", + " // Older GDevelop versions: the terrain can only be changed with events.", + " return;", + " }", + " this.editor = editor;", + " const selectedObjects = editor.getSelectedObjects();", + " const terrain =", + " selectedObjects.length === 1 && selectedObjects[0].__terrain3D ? selectedObjects[0].__terrain3D : null;", + " if (terrain !== this.terrain) {", + " this._endStroke();", + " if (this.terrain) this.terrain.renderer.setBrush(null, 0);", + " this.terrain = terrain;", + " this.undoSteps.length = 0;", + " }", + " if (terrain) editor.showToolbar('Terrain3D', this._getToolbarItems(terrain));", + " const game = editor.getRuntimeGame();", + " if (!terrain || !this.toolName) {", + " if (terrain) terrain.renderer.setBrush(null, 0);", + " return;", + " }", + "", + " const inputManager = game.getInputManager();", + " if (inputManager.wasKeyJustPressed(gdjs.evtTools.input.keysNameToCode.Escape)) {", + " this._endStroke();", + " this.toolName = '';", + " terrain.renderer.setBrush(null, 0);", + " return;", + " }", + " editor.captureLeftMouseButton();", + " const hit = terrain.raycastFromCursor(game, this.raycaster, this.ndc);", + " const radius = this._getRadius();", + " terrain.renderer.setBrush(hit, radius);", + "", + " // Older GDevelop versions don't have it.", + " const isPressedForTool =", + " typeof editor.isCapturedLeftMouseButtonPressed === 'function'", + " ? editor.isCapturedLeftMouseButtonPressed()", + " : inputManager.isMouseButtonPressed(0);", + " // The button is released outside the canvas when the mouse is on a toolbar.", + " const isPressed = isPressedForTool && inputManager.isMouseInsideCanvas();", + " if (isPressed && hit && !this.isStroking) {", + " this._startStroke(terrain, hit);", + " }", + " if (this.isStroking && isPressed && hit) {", + " this._applyBrush(terrain, hit, radius);", + " }", + " if (!isPressed) {", + " this._endStroke();", + " }", + " }", + "", + " _getRadius() {", + " const { object } = this.terrain;", + " const maxRadius = Math.max(object.getWidth(), object.getHeight()) / 4;", + " return Math.max(maxRadius * Math.pow(this.sizePercent / 100, 2), this.terrain.getCellWidth());", + " }", + "", + " _startStroke(terrain, hit) {", + " this.isStroking = true;", + " this.lastStrokeTime = performance.now();", + " this.flattenHeight = terrain.toScenePosition(hit).z;", + " this.undoSteps.push({ data: copyTerrainData(terrain.data), baseData: terrain.baseData });", + " const maxUndoSteps = Math.max(Math.floor(UNDO_STEPS_SAMPLES / terrain.data.heights.length), 2);", + " while (this.undoSteps.length > maxUndoSteps) this.undoSteps.shift();", + " }", + "", + " _endStroke() {", + " if (!this.isStroking) return;", + " this.isStroking = false;", + " this._save();", + " }", + "", + " _applyBrush(terrain, hit, radius) {", + " const now = performance.now();", + " const elapsedSeconds = Math.min((now - this.lastStrokeTime) / 1000, 0.1);", + " this.lastStrokeTime = now;", + " const { x, y } = terrain.toScenePosition(hit);", + " const amount = (this.strengthPercent / 100) * elapsedSeconds;", + " const depth = terrain.object.getDepth();", + " if (this.toolName === 'Raise') terrain.raise(x, y, x, y, radius, amount * depth * 0.5);", + " else if (this.toolName === 'Lower') terrain.raise(x, y, x, y, radius, -amount * depth * 0.5);", + " else if (this.toolName === 'Smooth') terrain.smooth(x, y, x, y, radius, amount * 10);", + " else if (this.toolName === 'Flatten') terrain.flatten(x, y, x, y, radius, this.flattenHeight, amount * 10);", + " else if (this.toolName.startsWith('Paint')) {", + " terrain.paint(x, y, x, y, radius, parseInt(this.toolName.slice(5), 10), amount * 8);", + " } else if (this.toolName === 'Hole') {", + " terrain.makeHole(x, y, x, y, radius);", + " } else if (this.toolName === 'FillHoles') {", + " terrain.fillHoles(x, y, x, y, radius);", + " } else if (this.toolName === 'Tint') {", + " terrain.tint(x, y, x, y, radius, gdjs.rgbOrHexToRGBColor(this.tintColor), amount * 8);", + " } else if (this.toolName === 'Wetness') {", + " terrain.wet(x, y, x, y, radius, this.wetnessPercent / 100, amount * 8);", + " }", + " // Drawn now, as the terrain was updated earlier in this frame.", + " terrain.applyChanges();", + " }", + "", + " _save() {", + " const { terrain, editor } = this;", + " if (!terrain || !editor) return;", + " editor.updateObjectProperties(terrain.object.getName(), { SculptData: terrain.save() });", + " }", + "", + " _undo() {", + " const undoStep = this.undoSteps.pop();", + " // The relief, the edits or the resolution may have changed since the stroke.", + " if (!this.terrain || !undoStep || undoStep.baseData !== this.terrain.baseData) {", + " this.undoSteps.length = 0;", + " return;", + " }", + " this.terrain.setData(undoStep.data);", + " this._save();", + " }", + "", + " /** @returns {Array} The settings of the tint or wetness brush, when it's used. */", + " _getColorToolItems() {", + " const { editor } = this;", + " if (this.toolName === 'Tint') {", + " if (editor && typeof editor.isToolbarItemTypeSupported === 'function' && editor.isToolbarItemTypeSupported('color')) {", + " return [", + " {", + " type: 'color',", + " id: 'TintColor',", + " tooltip: 'Color of the tint',", + " value: this.tintColor,", + " onChange: (color) => (this.tintColor = color),", + " },", + " ];", + " }", + " return tintPresets.map((color) => ({", + " type: 'button',", + " id: 'TintPreset' + color,", + " tooltip: color === '#ffffff' ? 'No tint' : 'Tint with this color',", + " color,", + " isActive: this.tintColor === color,", + " onClick: () => (this.tintColor = color),", + " }));", + " }", + " if (this.toolName === 'Wetness') {", + " return [", + " {", + " type: 'slider',", + " id: 'WetnessAmount',", + " tooltip: 'Wetness',", + " iconUrl: toolIcons.Wetness,", + " min: 0,", + " max: 100,", + " value: this.wetnessPercent,", + " onChange: (value) => (this.wetnessPercent = value),", + " },", + " ];", + " }", + " return [];", + " }", + "", + " /**", + " * @param {Terrain} terrain", + " * @returns {Array} The toolbar for the current state (the editor only", + " * updates what changed).", + " */", + " _getToolbarItems(terrain) {", + " const { object } = terrain;", + " const toolButton = (toolName, tooltip, color) => ({", + " type: 'button',", + " id: toolName,", + " tooltip,", + " iconUrl: toolIcons[toolName],", + " color,", + " isActive: this.toolName === toolName,", + " onClick: () => (this.toolName = this.toolName === toolName ? '' : toolName),", + " });", + " const layerColors = [object._getLayer1Color(), object._getLayer2Color(), object._getLayer3Color(), object._getLayer4Color()];", + " // Layers are shown with their color, tinting their texture.", + " const swatchColors = layerColors.map((color, index) =>", + " this._swatchColor", + " .set(gdjs.rgbOrHexStringToNumber(color))", + " .multiply(getAverageColor(terrain.renderer.uniforms['terrainLayerMap' + index].value))", + " .getHexString()", + " );", + " return [", + " toolButton('Raise', 'Raise the ground (drag on the terrain)'),", + " toolButton('Lower', 'Lower the ground'),", + " toolButton('Smooth', 'Smooth the ground'),", + " toolButton('Flatten', 'Flatten the ground to the height where the drag starts'),", + " { type: 'divider', id: 'PaintDivider' },", + " ...swatchColors.map((swatchColor, index) =>", + " toolButton('Paint' + (index + 1), 'Paint layer ' + (index + 1), '#' + swatchColor)", + " ),", + " toolButton('Hole', 'Make a hole, for the entrance of a tunnel or a cave'),", + " toolButton('FillHoles', 'Fill holes, with the layers of the ground around them'),", + " { type: 'divider', id: 'ColorDivider' },", + " toolButton('Tint', 'Tint the ground (white removes the tint)', this.tintColor),", + " toolButton('Wetness', 'Make the ground wet, or dry with a wetness of 0'),", + " ...this._getColorToolItems(),", + " { type: 'divider', id: 'BrushDivider' },", + " {", + " type: 'slider',", + " id: 'Size',", + " tooltip: 'Brush size',", + " iconUrl: toolIcons.Size,", + " min: 1,", + " max: 100,", + " value: this.sizePercent,", + " onChange: (value) => (this.sizePercent = value),", + " },", + " {", + " type: 'slider',", + " id: 'Strength',", + " tooltip: 'Brush strength',", + " iconUrl: toolIcons.Strength,", + " min: 5,", + " max: 100,", + " value: this.strengthPercent,", + " onChange: (value) => (this.strengthPercent = value),", + " },", + " { type: 'button', id: 'Undo', tooltip: 'Undo the last stroke on this terrain', iconUrl: toolIcons.Undo, onClick: () => this._undo() },", + " ];", + " }", + "}", + "", + "const editorTools = new TerrainEditorTools();", + "if (gdjs.registerInGameEditorPostStepCallback) {", + " gdjs.registerInGameEditorPostStepCallback((editor) => editorTools.update(editor));", + "}", + "", + "gdjs.__terrain3DExtension = {", + " Terrain,", + " // Used by the terrain grass and the scattered models.", + " getWorldScale,", + " getGrounds,", + " patchShaderCode,", + " createRandom,", + " findHighestGround,", + " GroundPlacement,", + " svgIcon,", + " toolIcons,", + " // Exposed for tests.", + " TerrainBodyUpdater,", + " TerrainData,", + " SampleRectangle,", + " brushes,", + " edits,", + " codec,", + " relief,", + "};" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [], + "objectGroups": [] + }, + { + "description": "Define grass helper classes JavaScript code.", + "fullName": "Define grass helper classes", + "functionType": "Action", + "name": "DefineGrassHelperClasses", + "private": true, + "sentence": "Define grass helper classes JavaScript code", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "if (gdjs.__terrainGrassExtension) {", + " return;", + "}", + "// Blades grow on the grounds of the scene: the 3D terrains (see `getGrounds`", + "// in the terrain helper).", + "", + "// Size of the object inner area: instances are scaled from it.", + "const AREA_SIZE = 1000;", + "const AREA_DEPTH = 40;", + "const CHUNK_SIZE = 500;", + "const MAX_BENDING_OBJECTS = 8;", + "", + "/**", + " * A blade: a thin, tapered strip. Its width and height are 1: blades are", + " * scaled by the shader.", + " */", + "const createBladeGeometry = () => {", + " const halfWidths = [0.5, 0.42, 0.28, 0];", + " const heights = [0, 0.35, 0.7, 1];", + " const positions = [];", + " halfWidths.forEach((halfWidth, level) => {", + " positions.push(-halfWidth, 0, heights[level]);", + " if (halfWidth > 0) positions.push(halfWidth, 0, heights[level]);", + " });", + " const indices = [0, 1, 2, 1, 3, 2, 2, 3, 4, 3, 5, 4, 4, 5, 6];", + " const geometry = new THREE.InstancedBufferGeometry();", + " geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));", + " geometry.setAttribute('normal', new THREE.Float32BufferAttribute(new Array(positions.length).fill(0), 3));", + " geometry.setIndex(indices);", + " return geometry;", + "};", + "", + "const vertexShaderDeclarations = `", + "attribute vec4 grassRoot;", + "attribute vec2 grassShape;", + "// The tint of the ground at the root (linear, from 0 to 1).", + "attribute vec3 grassGroundTint;", + "uniform mat3 grassWorldToLocal;", + "uniform float grassWorldScale;", + "uniform float grassBladeWidth;", + "uniform float grassTime;", + "uniform float grassWindStrength;", + "uniform float grassWindSpeed;", + "uniform vec4 grassBenders[${MAX_BENDING_OBJECTS}];", + "uniform vec3 grassBaseColor;", + "uniform vec3 grassTipColor;", + "varying vec3 vGrassColor;", + "`;", + "const vertexShaderPosition = `", + "// Blades are shaped in scene units (with Y flipped like the 3D world), then", + "// moved back in the (scaled) object space.", + "float bladeHeight = position.z;", + "float grassAngle = grassRoot.w;", + "float bladeWidth = position.x * grassBladeWidth;", + "vec3 blade = vec3(", + " bladeWidth * cos(grassAngle),", + " bladeWidth * sin(grassAngle),", + " bladeHeight * grassShape.x", + ");", + "vec3 rootWorld = (modelMatrix * vec4(grassRoot.xyz, 1.0)).xyz * grassWorldScale;", + "// Tips bend more than the base.", + "float bendFactor = bladeHeight * bladeHeight * grassShape.x;", + "float gust = sin(grassTime * grassWindSpeed + rootWorld.x * 0.013 + rootWorld.y * 0.011);", + "float flutter = sin(grassTime * grassWindSpeed * 2.7 + grassShape.y * 40.0) * 0.3;", + "blade.xy += vec2(0.8, 0.5) * (gust + flutter + 0.6) * grassWindStrength * 0.35 * bendFactor;", + "for (int i = 0; i < ${MAX_BENDING_OBJECTS}; i++) {", + " vec4 bender = grassBenders[i];", + " if (bender.z <= 0.0) continue;", + " vec2 away = rootWorld.xy - bender.xy;", + " float distanceToBender = length(away);", + " float push = 1.0 - smoothstep(bender.z * 0.3, bender.z, distanceToBender);", + " if (push > 0.0) {", + " blade.xy += normalize(away + vec2(0.001)) * push * bendFactor * 0.9;", + " blade.z *= 1.0 - push * 0.5;", + " }", + "}", + "vec3 transformed = grassRoot.xyz + grassWorldToLocal * (blade / grassWorldScale);", + "vGrassColor = mix(grassBaseColor, grassTipColor, bladeHeight) * (0.85 + 0.3 * grassShape.y) * grassGroundTint;", + "`;", + "const vertexShaderNormal = `", + "// Blades are lit like the ground below them.", + "transformedNormal = normalize((viewMatrix * vec4(0.0, 0.0, 1.0, 0.0)).xyz);", + "`;", + "const fragmentShaderDeclarations = `", + "varying vec3 vGrassColor;", + "`;", + "const fragmentShaderColor = `", + "diffuseColor.rgb *= vGrassColor;", + "`;", + "// Both faces of blades are lit like the ground (not flipped for back faces).", + "const fragmentShaderNormal = `", + "#include ", + "normal = normalize(vNormal);", + "nonPerturbedNormal = normal;", + "`;", + "", + "const { getWorldScale, patchShaderCode, createRandom, findHighestGround, GroundPlacement } = gdjs.__terrain3DExtension;", + "", + "/**", + " * The grass of a terrain grass object: blades drawn with GPU instancing, in chunks", + " * that are hidden when off-screen and thinned out with the distance. They grow", + " * on the grounds below, like 3D terrains (only where a given layer is painted).", + " */", + "class Grass {", + " /** @param {gdjs.CustomRuntimeObject3D} object */", + " constructor(object) {", + " this.object = object;", + " this.group = new THREE.Group();", + " object.get3DRendererObject().add(this.group);", + " this.bladeGeometry = createBladeGeometry();", + " /** @type {Array<{mesh: THREE.Mesh, blades: number, center: THREE.Vector3}>} */", + " this.chunks = [];", + " this.placement = new GroundPlacement(object.getRuntimeScene());", + " this._groundAtBlade = { ground: null, z: 0 };", + " this.uniforms = {", + " grassWorldToLocal: { value: new THREE.Matrix3() },", + " // Scene units by 3D world unit.", + " grassWorldScale: { value: 1 },", + " grassBladeWidth: { value: 1 },", + " grassTime: { value: 0 },", + " grassWindStrength: { value: 1 },", + " grassWindSpeed: { value: 1 },", + " grassBenders: { value: Array.from({ length: MAX_BENDING_OBJECTS }, () => new THREE.Vector4()) },", + " grassBaseColor: { value: new THREE.Color() },", + " grassTipColor: { value: new THREE.Color() },", + " };", + " this.material = new THREE.MeshStandardMaterial({ roughness: 1, metalness: 0, side: THREE.DoubleSide });", + " this.material.onBeforeCompile = (shader) => {", + " Object.assign(shader.uniforms, this.uniforms);", + " shader.vertexShader = patchShaderCode(shader.vertexShader, [", + " ['#include ', '#include \\n' + vertexShaderDeclarations],", + " ['#include ', vertexShaderPosition],", + " ['#include ', '#include \\n' + vertexShaderNormal],", + " ], 'TerrainGrass');", + " shader.fragmentShader = patchShaderCode(shader.fragmentShader, [", + " ['#include ', '#include \\n' + fragmentShaderDeclarations],", + " ['#include ', '#include \\n' + fragmentShaderColor],", + " ['#include ', fragmentShaderNormal],", + " ], 'TerrainGrass');", + " };", + " this.material.customProgramCacheKey = () => 'TerrainGrass';", + "", + " this._matrix = new THREE.Matrix4();", + " this._cameraPosition = new THREE.Vector3();", + " this._chunkPosition = new THREE.Vector3();", + " this._scale = new THREE.Vector3();", + " this.updateFromProperties();", + " }", + "", + " updateFromProperties() {", + " const { object } = this;", + " this.uniforms.grassBaseColor.value.set(gdjs.rgbOrHexStringToNumber(object._getBaseColor()));", + " this.uniforms.grassTipColor.value.set(gdjs.rgbOrHexStringToNumber(object._getTipColor()));", + " this.uniforms.grassWindStrength.value = object._getWindStrength();", + " this.uniforms.grassWindSpeed.value = object._getWindSpeed();", + " this.uniforms.grassBladeWidth.value = Math.max(object._getBladeWidth(), 0);", + " this.material.needsUpdate = true;", + " this.placement.placeAgain();", + " }", + "", + " _getPlacementKey() {", + " const { object } = this;", + " return [", + " object.getX(), object.getY(), object.getZ(), object.getWidth(), object.getHeight(), object.getDepth(),", + " object._getDensity(), object._getBladeHeight(), object._getGroundLayer(), object._getMaxSlope(),", + " object._getSeed(),", + " ].join('|');", + " }", + "", + " /** Place the blades again, for example after the ground or the object changed. */", + " rebuild() {", + " const grounds = Array.from(this.placement.grounds);", + " for (const chunk of this.chunks) {", + " chunk.mesh.removeFromParent();", + " chunk.mesh.geometry.dispose();", + " }", + " this.chunks.length = 0;", + "", + " const { object } = this;", + " const width = object.getWidth();", + " const height = object.getHeight();", + " const scaleX = width / AREA_SIZE;", + " const scaleY = height / AREA_SIZE;", + " const scaleZ = Math.max(object.getDepth() / AREA_DEPTH, 0.0001);", + " const bladesPerChunk = Math.round((Math.max(object._getDensity(), 0) * CHUNK_SIZE * CHUNK_SIZE) / 10000);", + " const groundLayer = parseInt(object._getGroundLayer(), 10) || 0;", + " const maxSlope = object._getMaxSlope();", + " const chunksX = Math.max(Math.ceil(width / CHUNK_SIZE), 1);", + " const chunksY = Math.max(Math.ceil(height / CHUNK_SIZE), 1);", + " const nextRandom = createRandom(object._getSeed() * 7919 + 1);", + " const bladeHeight = Math.max(object._getBladeHeight(), 0);", + "", + " for (let chunkY = 0; chunkY < chunksY; chunkY++) {", + " for (let chunkX = 0; chunkX < chunksX; chunkX++) {", + " const roots = new Float32Array(bladesPerChunk * 4);", + " const shapes = new Float32Array(bladesPerChunk * 2);", + " const groundTints = new Uint8Array(bladesPerChunk * 3).fill(255);", + " let blades = 0;", + " let minZ = Infinity;", + " let maxZ = -Infinity;", + " for (let attempt = 0; attempt < bladesPerChunk; attempt++) {", + " // In scene units, relative to the object.", + " const x = (chunkX + nextRandom()) * CHUNK_SIZE;", + " const y = (chunkY + nextRandom()) * CHUNK_SIZE;", + " const angle = nextRandom() * Math.PI * 2;", + " const heightFactor = 0.6 + 0.8 * nextRandom();", + " const tint = nextRandom();", + " const growth = nextRandom();", + " if (x > width || y > height) continue;", + " const sceneX = object.getX() + x;", + " const sceneY = object.getY() + y;", + " const { ground, z } = findHighestGround(grounds, sceneX, sceneY, this._groundAtBlade);", + " const groundZ = ground ? z : object.getZ();", + " if (ground) {", + " if (groundLayer > 0 && growth > ground.getLayerWeightAt(sceneX, sceneY, groundLayer)) continue;", + " if (ground.getSlopeAt(sceneX, sceneY) > maxSlope) continue;", + " if (typeof ground.getTintAt === 'function') {", + " const tint = ground.getTintAt(sceneX, sceneY);", + " for (let channel = 0; channel < 3; channel++) {", + " groundTints[blades * 3 + channel] = Math.round(255 * Math.pow(tint[channel] / 255, 2.2));", + " }", + " }", + " }", + " const localZ = (groundZ - object.getZ()) / scaleZ;", + " roots[blades * 4] = x / scaleX;", + " roots[blades * 4 + 1] = y / scaleY;", + " roots[blades * 4 + 2] = localZ;", + " roots[blades * 4 + 3] = angle;", + " shapes[blades * 2] = heightFactor * bladeHeight;", + " shapes[blades * 2 + 1] = tint;", + " minZ = Math.min(minZ, localZ);", + " maxZ = Math.max(maxZ, localZ);", + " blades++;", + " }", + " if (blades === 0) continue;", + "", + " const geometry = new THREE.InstancedBufferGeometry();", + " geometry.index = this.bladeGeometry.index;", + " geometry.setAttribute('position', this.bladeGeometry.attributes.position);", + " geometry.setAttribute('normal', this.bladeGeometry.attributes.normal);", + " geometry.setAttribute('grassRoot', new THREE.InstancedBufferAttribute(roots.subarray(0, blades * 4), 4));", + " geometry.setAttribute('grassShape', new THREE.InstancedBufferAttribute(shapes.subarray(0, blades * 2), 2));", + " geometry.setAttribute('grassGroundTint', new THREE.InstancedBufferAttribute(groundTints.subarray(0, blades * 3), 3, true));", + " geometry.instanceCount = blades;", + " // The furthest tips can go with the wind and the bending objects (see the shader).", + " const maxBladeHeight = bladeHeight * 1.4;", + " const lean =", + " (0.35 * 1.9 * 0.943 * Math.max(object._getWindStrength(), 0) + 0.9) * maxBladeHeight +", + " Math.max(object._getBladeWidth(), 0) / 2;", + " const leanX = lean / scaleX;", + " const leanY = lean / scaleY;", + " geometry.boundingBox = new THREE.Box3(", + " new THREE.Vector3((chunkX * CHUNK_SIZE) / scaleX - leanX, (chunkY * CHUNK_SIZE) / scaleY - leanY, minZ),", + " new THREE.Vector3(", + " Math.min((chunkX + 1) * CHUNK_SIZE, width) / scaleX + leanX,", + " Math.min((chunkY + 1) * CHUNK_SIZE, height) / scaleY + leanY,", + " maxZ + (bladeHeight * 1.4) / scaleZ", + " )", + " );", + " geometry.boundingSphere = geometry.boundingBox.getBoundingSphere(new THREE.Sphere());", + " const mesh = new THREE.Mesh(geometry, this.material);", + " mesh.receiveShadow = true;", + " // Raycasts would only test one blade: the selection plane is used instead.", + " mesh.raycast = () => {};", + " this.group.add(mesh);", + " this.chunks.push({ mesh, blades, center: geometry.boundingSphere.center.clone() });", + " }", + " }", + " }", + "", + " _updateBendingObjects() {", + " const benders = this.uniforms.grassBenders.value;", + " const objectName = this.object._getBendingObject();", + " const bendingObjects = objectName ? this.object.getInstanceContainer().getObjects(objectName) || [] : [];", + " for (let index = 0; index < MAX_BENDING_OBJECTS; index++) {", + " const bendingObject = bendingObjects[index];", + " if (!bendingObject) {", + " benders[index].set(0, 0, 0, 0);", + " continue;", + " }", + " // Blades are pushed in the 3D scene, which is flipped on Y.", + " const radius = Math.max(bendingObject.getWidth(), bendingObject.getHeight()) * 1.2;", + " benders[index].set(bendingObject.getCenterXInScene(), -bendingObject.getCenterYInScene(), radius, 0);", + " }", + " }", + "", + " /**", + " * @param {number} elapsedSeconds", + " * @param {number} time In seconds, the same for all grass objects.", + " */", + " update(elapsedSeconds, time, camera) {", + " if (this.placement.shouldPlaceAgain(elapsedSeconds, this._getPlacementKey())) {", + " this.rebuild();", + " }", + "", + " this.uniforms.grassTime.value = time;", + " this.uniforms.grassWorldScale.value = getWorldScale(this.object);", + " // The object transformation is applied just before rendering, it's needed now.", + " this.object.getRenderer().ensureUpToDate();", + " this.group.updateWorldMatrix(true, false);", + " this.uniforms.grassWorldToLocal.value.setFromMatrix4(this._matrix.copy(this.group.matrixWorld).invert());", + " this._updateBendingObjects();", + " if (!camera) return;", + "", + " // Far chunks are hidden, and thinned out before (in 3D world units).", + " const fadeDistance = Math.max(this.object._getFadeDistance(), 1) / getWorldScale(this.object);", + " this._cameraPosition.setFromMatrixPosition(camera.matrixWorld);", + " for (const chunk of this.chunks) {", + " const distance = this._chunkPosition", + " .copy(chunk.center)", + " .applyMatrix4(this.group.matrixWorld)", + " .distanceTo(this._cameraPosition);", + " const density = gdjs.evtTools.common.clamp((fadeDistance - distance) / (fadeDistance * 0.5), 0, 1);", + " chunk.mesh.visible = density > 0;", + " chunk.mesh.geometry.instanceCount = Math.ceil(chunk.blades * density);", + " }", + " }", + "", + " dispose() {", + " for (const chunk of this.chunks) {", + " chunk.mesh.geometry.dispose();", + " }", + " this.bladeGeometry.dispose();", + " this.material.dispose();", + " this.group.removeFromParent();", + " }", + "}", + "", + "gdjs.__terrainGrassExtension = { Grass };" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [], + "objectGroups": [] + }, + { + "description": "Define scattered models helper classes JavaScript code.", + "fullName": "Define scattered models helper classes", + "functionType": "Action", + "name": "DefineScatterHelperClasses", + "private": true, + "sentence": "Define scattered models helper classes JavaScript code", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "if (gdjs.__terrainScatterExtension) {", + " return;", + "}", + "// Copies grow on the grounds of the scene, like the terrain grass (see", + "// `getGrounds` in the terrain helper).", + "", + "const {", + " getWorldScale,", + " getGrounds,", + " patchShaderCode,", + " createRandom,", + " findHighestGround,", + " GroundPlacement,", + " SampleRectangle,", + " brushes,", + " codec,", + " svgIcon,", + " toolIcons,", + "} = gdjs.__terrain3DExtension;", + "", + "// Size of the object inner area: instances are scaled from it.", + "const AREA_SIZE = 2000;", + "const AREA_DEPTH = 200;", + "// The painted density is stored in a grid of this many cells on each side,", + "// whatever the size of the object.", + "const MASK_RESOLUTION = 256;", + "const MASK_FORMAT_VERSION = '1';", + "// Copies are hidden in chunks: big chunks mean fewer draw calls, small ones", + "// hide more copies out of the screen.", + "const MIN_CHUNK_SIZE = 500;", + "const MAX_CHUNK_SIZE = 2000;", + "// Copies shrink to nothing over this part of the draw distance, instead of popping out.", + "const FADE_PART = 0.15;", + "const MAX_COPIES = 200000;", + "// Prisms of the trunk collisions.", + "const TRUNK_SIDES = 6;", + "", + "const clamp01 = (value) => (value < 0 ? 0 : value > 1 ? 1 : value);", + "", + "/**", + " * @returns {number} A number for a cell of the copies grid, changing with the seed.", + " */", + "const hashCell = (seed, i, j) => {", + " let h = Math.imul(i, 374761393) + Math.imul(j, 668265263) + Math.imul(seed, 982451653);", + " h = Math.imul(h ^ (h >>> 13), 1274126177);", + " return (h ^ (h >>> 16)) >>> 0;", + "};", + "", + "/**", + " * How dense copies are, from 0 to 1, in a grid covering the object: painted", + " * with the editor brushes, the edits or events.", + " */", + "class DensityMask {", + " /** @param {number} fillValue From 0 to 255. */", + " constructor(fillValue) {", + " // `resolution` and `size` let terrain brushes paint it.", + " this.resolution = MASK_RESOLUTION;", + " this.size = MASK_RESOLUTION + 1;", + " this.values = new Uint8Array(this.size * this.size).fill(fillValue);", + " }", + "", + " /**", + " * @param {number} u From 0 to 1, along the object width.", + " * @param {number} v From 0 to 1, along the object height.", + " * @returns {number} The density, from 0 to 1, interpolated between samples.", + " */", + " sample(u, v) {", + " const x = clamp01(u) * this.resolution;", + " const y = clamp01(v) * this.resolution;", + " const i = Math.min(Math.floor(x), this.resolution - 1);", + " const j = Math.min(Math.floor(y), this.resolution - 1);", + " const fx = x - i;", + " const fy = y - j;", + " const value = (i, j) => this.values[j * this.size + i];", + " const top = value(i, j) + fx * (value(i + 1, j) - value(i, j));", + " const bottom = value(i, j + 1) + fx * (value(i + 1, j + 1) - value(i, j + 1));", + " return (top + fy * (bottom - top)) / 255;", + " }", + "", + " /**", + " * Bring the density towards a target around a segment: fully up to 70%", + " * of the radius, then fading out (forests have dense centers and soft edges).", + " * @param {{cellWidth: number, cellHeight: number, ax: number, ay: number, bx: number, by: number, radius: number}} stroke", + " * @param {number} density The target, from 0 to 1.", + " * @param {number} strength From 0 to 1.", + " * @param {SampleRectangle} changedSamples", + " */", + " paint(stroke, density, strength, changedSamples) {", + " const { values } = this;", + " const target = Math.round(clamp01(density) * 255);", + " brushes.forEachSample(", + " this,", + " stroke,", + " (index, falloff) => {", + " // The falloff of terrain brushes is (1 - distance² / radius²)².", + " const weight = clamp01(2 * Math.sqrt(falloff));", + " // Rounded towards the target, so that light strokes still paint.", + " const change = (target - values[index]) * clamp01(weight * strength);", + " values[index] += change > 0 ? Math.ceil(change) : Math.floor(change);", + " },", + " changedSamples", + " );", + " }", + "", + " /** @returns {DensityMask} */", + " clone() {", + " const mask = new DensityMask(0);", + " mask.values.set(this.values);", + " return mask;", + " }", + "}", + "", + "/**", + " * Edits written in the Edits property: a JSON list of brush strokes, like", + " * `[{\"tool\": \"add\", \"x\": 0.5, \"y\": 0.5, \"radius\": 0.2}]`. Positions and", + " * radius are fractions of the object size, like the terrain edits.", + " */", + "const applyEdits = (mask, text) => {", + " if (!text.trim()) return;", + " let editList;", + " try {", + " editList = JSON.parse(text);", + " } catch (error) {", + " console.warn('The edits of scattered models are not valid JSON: ' + error.message);", + " return;", + " }", + " if (!Array.isArray(editList)) {", + " console.warn('The edits of scattered models must be a JSON list (like [{\"tool\": \"add\", ...}]).');", + " return;", + " }", + " const numberOr = (value, defaultValue) => (typeof value === 'number' && isFinite(value) ? value : defaultValue);", + " const changedSamples = new SampleRectangle();", + " for (const edit of editList) {", + " if (!edit || typeof edit !== 'object') continue;", + " const x = numberOr(edit.x, 0.5);", + " const y = numberOr(edit.y, 0.5);", + " const stroke = {", + " cellWidth: 1 / mask.resolution,", + " cellHeight: 1 / mask.resolution,", + " ax: x,", + " ay: y,", + " bx: numberOr(edit.toX, x),", + " by: numberOr(edit.toY, y),", + " radius: numberOr(edit.radius, 0.1),", + " };", + " const strength = clamp01(numberOr(edit.strength, 1));", + " if (edit.tool === 'add') {", + " mask.paint(stroke, numberOr(edit.density, 1), strength, changedSamples);", + " } else if (edit.tool === 'erase') {", + " mask.paint(stroke, 0, strength, changedSamples);", + " } else {", + " console.warn('Unknown tool in the edits of scattered models: \"' + edit.tool + '\" (use add or erase).');", + " }", + " }", + "};", + "", + "/**", + " * The density painted in the editor, stored as its difference with the", + " * density from the properties (the coverage and the edits), like the terrain", + " * sculpt data.", + " */", + "const maskCodec = {", + " /**", + " * @param {DensityMask} mask", + " * @param {DensityMask} baseMask", + " */", + " encode(mask, baseMask) {", + " const changes = new Int32Array(mask.values.length);", + " let hasChanges = false;", + " for (let index = 0; index < changes.length; index++) {", + " changes[index] = mask.values[index] - baseMask.values[index];", + " if (changes[index] !== 0) hasChanges = true;", + " }", + " if (!hasChanges) return '';", + " return MASK_FORMAT_VERSION + ';' + MASK_RESOLUTION + ';' + codec.encodeGrids([changes], mask.size);", + " },", + "", + " /**", + " * @param {string} text", + " * @param {DensityMask} baseMask", + " * @returns {DensityMask | null} null if the text is empty or not valid.", + " */", + " decode(text, baseMask) {", + " if (!text) return null;", + " const parts = text.split(';');", + " if (parts[0] !== MASK_FORMAT_VERSION || parseInt(parts[1], 10) !== MASK_RESOLUTION) return null;", + " try {", + " const mask = baseMask.clone();", + " const [changes] = codec.decodeGrids(parts[2], mask.size, 1);", + " for (let index = 0; index < changes.length; index++) {", + " // The base may have changed since the data was saved.", + " mask.values[index] = Math.min(Math.max(mask.values[index] + changes[index], 0), 255);", + " }", + " return mask;", + " } catch (error) {", + " console.error('The painted data of scattered models could not be read:', error);", + " return null;", + " }", + " },", + "};", + "", + "const fadeShaderDeclarations = `", + "uniform float scatterDrawDistance;", + "`;", + "// Instance matrices are applied after this: copies shrink towards their base.", + "const fadeShaderCode = `", + "#ifdef USE_INSTANCING", + "vec3 scatterCopyPosition = (modelMatrix * instanceMatrix * vec4(0.0, 0.0, 0.0, 1.0)).xyz;", + "transformed *= clamp(", + " (scatterDrawDistance - distance(scatterCopyPosition, cameraPosition)) / (scatterDrawDistance * ${FADE_PART}),", + " 0.0,", + " 1.0", + ");", + "#endif", + "`;", + "", + "const flipTriangles = (geometry) => {", + " const index = geometry.index;", + " for (let offset = 0; offset + 2 < index.count; offset += 3) {", + " const second = index.getX(offset + 1);", + " index.setX(offset + 1, index.getX(offset + 2));", + " index.setX(offset + 2, second);", + " }", + "};", + "", + "/**", + " * The parts of a model, ready to be drawn with instancing: each glTF", + " * primitive with its material. Geometries are moved so that the model base", + " * is at the origin, its height is 1 and it stands along Z (like 3D model", + " * objects with a 90° rotation around X).", + " * @param {THREE.Object3D} modelScene", + " * @param {object} uniforms Uniforms added to the materials.", + " * @returns {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}}", + " */", + "const createModelParts = (modelScene, uniforms) => {", + " modelScene.updateMatrixWorld(true);", + " const rootInverse = modelScene.matrixWorld.clone().invert();", + " const standUp = new THREE.Matrix4().makeRotationX(Math.PI / 2);", + " /** @type {Array<{mesh: THREE.Mesh, matrix: THREE.Matrix4}>} */", + " const meshes = [];", + " const box = new THREE.Box3();", + " modelScene.traverse((node) => {", + " if (!node.isMesh || !node.geometry || !node.geometry.attributes.position) return;", + " const matrix = standUp.clone().multiply(rootInverse).multiply(node.matrixWorld);", + " if (!node.geometry.boundingBox) node.geometry.computeBoundingBox();", + " box.union(node.geometry.boundingBox.clone().applyMatrix4(matrix));", + " meshes.push({ mesh: node, matrix });", + " });", + " if (box.isEmpty()) return { parts: [], width: 0, depth: 0 };", + "", + " const height = Math.max(box.max.z - box.min.z, 0.000001);", + " // The Y axis of the scene goes down: the model is flipped, like 3D model objects.", + " const normalization = new THREE.Matrix4()", + " .makeScale(1 / height, -1 / height, 1 / height)", + " .multiply(", + " new THREE.Matrix4().makeTranslation(", + " -(box.min.x + box.max.x) / 2,", + " -(box.min.y + box.max.y) / 2,", + " -box.min.z", + " )", + " );", + " const parts = meshes.map(({ mesh, matrix }) => {", + " let geometry = mesh.geometry.clone();", + " if (!geometry.index) {", + " const indices = Array.from({ length: geometry.attributes.position.count }, (_, index) => index);", + " geometry.setIndex(indices);", + " }", + " geometry.applyMatrix4(normalization.clone().multiply(matrix));", + " // The flip turned the faces inside out.", + " flipTriangles(geometry);", + " geometry.computeBoundingSphere();", + " geometry.computeBoundingBox();", + " const patchMaterial = (originalMaterial) => {", + " const material = originalMaterial.clone();", + " // Like 3D model objects (\"Standard without metalness\"), which look better without an environment map.", + " if (material.metalness) material.metalness = 0;", + " material.onBeforeCompile = (shader) => {", + " Object.assign(shader.uniforms, uniforms);", + " shader.vertexShader = patchShaderCode(shader.vertexShader, [", + " ['#include ', '#include \\n' + fadeShaderDeclarations],", + " ['#include ', '#include \\n' + fadeShaderCode],", + " ], 'ScatteredModels');", + " };", + " material.customProgramCacheKey = () => 'ScatteredModels';", + " return material;", + " };", + " const material = Array.isArray(mesh.material) ? mesh.material.map(patchMaterial) : patchMaterial(mesh.material);", + " return { geometry, material };", + " });", + " return {", + " parts,", + " width: (box.max.x - box.min.x) / height,", + " depth: (box.max.y - box.min.y) / height,", + " };", + "};", + "", + "/**", + " * A copy of the model: its base position in the scene, its scale (the", + " * height of the copy) and the key of its cell in the grid of copies.", + " * @typedef {{x: number, y: number, z: number, angle: number, height: number, key: number}} Copy", + " */", + "", + "/**", + " * The scattered models of an object: copies of a 3D model placed on the", + " * grounds of the scene, drawn with GPU instancing in chunks that are hidden", + " * when far or off-screen.", + " */", + "class Scatter {", + " /** @param {gdjs.CustomRuntimeObject3D} object */", + " constructor(object) {", + " this.object = object;", + " this.group = new THREE.Group();", + " object.get3DRendererObject().add(this.group);", + " /** @type {Array<{firstCellI: number, firstCellJ: number, meshes: THREE.InstancedMesh[], copies: Copy[], center: THREE.Vector3, radius: number}>} */", + " this.chunks = [];", + " /**", + " * The grid of copies: one copy at most in each cell, cells grouped in chunks.", + " * @type {{spacing: number, cellsPerChunk: number, cellsX: number, cellsY: number} | null}", + " */", + " this.grid = null;", + " this.placement = new GroundPlacement(object.getRuntimeScene());", + " this.uniforms = {", + " // In 3D world units.", + " scatterDrawDistance: { value: 1 },", + " };", + " /** @type {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}} */", + " this.model = { parts: [], width: 0, depth: 0 };", + " this.modelResourceName = '';", + " this.loadingModelResourceName = '';", + " /**", + " * Copies removed by events (for example, cut trees), by key.", + " * @type {Map}", + " */", + " this.removedCopies = new Map();", + " /** Increased each time copies change, for the collisions. */", + " this.version = 0;", + " this.isDisposed = false;", + " /** The last value of the painted data property read or saved. */", + " this.paintData = '';", + " /**", + " * Values saved by the editor tools: the editor sends them back with a", + " * hot-reload, possibly after newer ones were saved.", + " * @type {string[]}", + " */", + " this.savedPaintData = [];", + " this.maskSource = '';", + " /** @type {DensityMask} */", + " this.baseMask = new DensityMask(255);", + " /** @type {DensityMask} */", + " this.mask = this.baseMask;", + " this.changedSamples = new SampleRectangle();", + " this._stroke = { cellWidth: 1, cellHeight: 1, ax: 0, ay: 0, bx: 0, by: 0, radius: 0 };", + " this._groundAtCopy = { ground: null, z: 0 };", + " this._matrix = new THREE.Matrix4();", + " this._raycastSphere = new THREE.Sphere();", + " this._raycastRay = new THREE.Ray();", + " this._raycastBox = new THREE.Box3();", + " this._raycastInverseMatrix = new THREE.Matrix4();", + " this._raycastScale = new THREE.Vector3();", + " /** Placed on a copy to test the triangles of the model. */", + " this._raycastMesh = new THREE.Mesh();", + " /** @type {THREE.Intersection[]} */", + " this._raycastHits = [];", + " this._color = new THREE.Color();", + " this._tint = new THREE.Color();", + " this._cameraPosition = new THREE.Vector3();", + " this._chunkPosition = new THREE.Vector3();", + " /**", + " * Trunks, for the 3D physics behavior and navigation meshes. Always given,", + " * empty without trunk collisions: otherwise these behaviors would use the", + " * object box, covering the whole area.", + " * @type {gdjs.Surface}", + " */", + " this.surface = {", + " getVersion: () => this.version,", + " getChangedArea: () => null,", + " getTriangles: () => this._getTrunkTriangles(),", + " getHeightField: () => null,", + " };", + " /** The trunk collision properties, changing the surface. */", + " this.trunkKey = '';", + " this.loadMaskFromProperties();", + " this.updateFromProperties();", + " // Before the behaviors create their shapes.", + " this._updateSurface();", + " }", + "", + " _getMaskSource() {", + " return this.object._getCoverage() + '|' + this.object._getEdits();", + " }", + "", + " loadMaskFromProperties() {", + " const { object } = this;", + " this.maskSource = this._getMaskSource();", + " this.paintData = object._getPaintData();", + " this.baseMask = new DensityMask(object._getCoverage() === 'Painted' ? 0 : 255);", + " applyEdits(this.baseMask, object._getEdits());", + " this.mask = maskCodec.decode(this.paintData, this.baseMask) || this.baseMask.clone();", + " this.placement.placeAgain();", + " }", + "", + " onHotReloading() {", + " const paintData = this.object._getPaintData();", + " const savedIndex = this.savedPaintData.indexOf(paintData);", + " if (savedIndex !== -1 && this._getMaskSource() === this.maskSource) {", + " // The object already has this data, or newer changes.", + " this.savedPaintData.splice(0, savedIndex + 1);", + " } else if (paintData !== this.paintData || this._getMaskSource() !== this.maskSource) {", + " this.savedPaintData.length = 0;", + " this.loadMaskFromProperties();", + " }", + " this.updateFromProperties();", + " }", + "", + " /** @returns {string} The data to save in the painted data property. */", + " save() {", + " this.paintData = maskCodec.encode(this.mask, this.baseMask);", + " this.savedPaintData.push(this.paintData);", + " return this.paintData;", + " }", + "", + " updateFromProperties() {", + " const { object } = this;", + " const modelResourceName = object._getModel();", + " if (modelResourceName !== this.modelResourceName) {", + " this.modelResourceName = modelResourceName;", + " this._loadModel();", + " }", + " this.placement.placeAgain();", + " }", + "", + " _disposeModel() {", + " for (const part of this.model.parts) {", + " part.geometry.dispose();", + " for (const material of Array.isArray(part.material) ? part.material : [part.material]) {", + " material.dispose();", + " }", + " }", + " this.model = { parts: [], width: 0, depth: 0 };", + " }", + "", + " _loadModel() {", + " this._disposeChunks(this.chunks);", + " this._disposeModel();", + " const resourceName = this.modelResourceName;", + " if (!resourceName) return;", + " const game = this.object.getRuntimeScene().getGame();", + " const modelManager = game.getModel3DManager();", + " const gltf = modelManager.getModel(resourceName);", + " if (gltf === modelManager._invalidModel) {", + " // Not loaded yet, for example a model just chosen in the editor.", + " if (this.loadingModelResourceName === resourceName) return;", + " this.loadingModelResourceName = resourceName;", + " game.getResourceLoader()", + " .loadResource(resourceName)", + " .then(() => {", + " if (this.isDisposed || this.modelResourceName !== resourceName) return;", + " this.loadingModelResourceName = '';", + " if (modelManager.getModel(resourceName) !== modelManager._invalidModel) {", + " this._loadModel();", + " this.placement.placeAgain();", + " }", + " })", + " .catch(() => {});", + " return;", + " }", + " this.model = createModelParts(gltf.scene, this.uniforms);", + " }", + "", + " _getPlacementKey() {", + " const { object } = this;", + " return [", + " object.getX(), object.getY(), object.getZ(), object.getWidth(), object.getHeight(), object.getDepth(),", + " object._getDensity(), object._getSize(), object._getMinScale(), object._getMaxScale(),", + " object._getRandomRotation(), object._getRandomTint(), object._getGroundLayer(), object._getMaxSlope(),", + " object._getMinGroundZ(), object._getMaxGroundZ(), object._getSeed(), object._getDrawDistance(),", + " object._getCastShadow(), object._getReceiveShadow(), this.model.parts.length,", + " ].join('|');", + " }", + "", + " _getChunkSize() {", + " return Math.min(Math.max(this.object._getDrawDistance() / 4, MIN_CHUNK_SIZE), MAX_CHUNK_SIZE);", + " }", + "", + " /** @returns {number} The distance between copies (one copy per cell of this size at most). */", + " _getSpacing() {", + " const { object } = this;", + " const density = Math.max(object._getDensity(), 0);", + " if (density <= 0) return 0;", + " const spacing = 100 / Math.sqrt(density);", + " // Too many cells would freeze the game: the density is lowered.", + " const cellCount = (object.getWidth() / spacing) * (object.getHeight() / spacing);", + " return cellCount > MAX_COPIES ? spacing * Math.sqrt(cellCount / MAX_COPIES) : spacing;", + " }", + "", + " /**", + " * Find the copy of a cell of the grid, if there is one.", + " * @returns {Copy | null}", + " */", + " _placeCopy(cellI, cellJ, spacing, grounds) {", + " const { object } = this;", + " const width = object.getWidth();", + " const height = object.getHeight();", + " const nextRandom = createRandom(hashCell(Math.floor(object._getSeed()), cellI, cellJ));", + " // Always drawn in the same order, so that a copy stays the same when properties change.", + " const x = (cellI + nextRandom()) * spacing;", + " const y = (cellJ + nextRandom()) * spacing;", + " const presence = nextRandom();", + " const growth = nextRandom();", + " const scaleFactor = nextRandom();", + " const angle = nextRandom() * Math.PI * 2;", + " if (x > width || y > height) return null;", + " if (presence >= this.mask.sample(x / width, y / height)) return null;", + " const key = cellJ * 65536 + cellI;", + " if (this.removedCopies.has(key)) return null;", + "", + " const sceneX = object.getX() + x;", + " const sceneY = object.getY() + y;", + " const { ground, z } = findHighestGround(grounds, sceneX, sceneY, this._groundAtCopy);", + " const groundZ = ground ? z : object.getZ();", + " if (groundZ < object._getMinGroundZ() || groundZ > object._getMaxGroundZ()) return null;", + " const copyHeight =", + " Math.max(object._getSize(), 0) *", + " (object._getMinScale() + (object._getMaxScale() - object._getMinScale()) * scaleFactor);", + " if (ground) {", + " if (ground.getSlopeAt(sceneX, sceneY) > object._getMaxSlope()) return null;", + " const groundLayer = parseInt(object._getGroundLayer(), 10) || 0;", + " if (groundLayer > 0) {", + " if (growth > ground.getLayerWeightAt(sceneX, sceneY, groundLayer)) return null;", + " // Copies don't stand at the border of their layer, so that trees don't cover roads.", + " const margin = (copyHeight * Math.max(this.model.width, this.model.depth)) / 4;", + " for (const [offsetX, offsetY] of [[margin, 0], [-margin, 0], [0, margin], [0, -margin]]) {", + " if (ground.getLayerWeightAt(sceneX + offsetX, sceneY + offsetY, groundLayer) < 0.5) return null;", + " }", + " }", + " }", + " return {", + " x: sceneX,", + " y: sceneY,", + " z: groundZ,", + " angle: object._getRandomRotation() ? angle : 0,", + " height: copyHeight,", + " key,", + " };", + " }", + "", + " _disposeChunks(chunks) {", + " for (const chunk of chunks) {", + " for (const mesh of chunk.meshes) {", + " mesh.removeFromParent();", + " // Geometries and materials are the ones of the model.", + " mesh.dispose();", + " }", + " }", + " if (chunks === this.chunks) this.chunks = [];", + " }", + "", + " /** Place the copies again, for example after the ground or the object changed. */", + " rebuild() {", + " this._disposeChunks(this.chunks);", + " this.version++;", + " const { object } = this;", + " const spacing = this._getSpacing();", + " if (spacing <= 0 || this.model.parts.length === 0) {", + " this.grid = null;", + " return;", + " }", + " const cellsPerChunk = Math.max(Math.round(this._getChunkSize() / spacing), 1);", + " const grid = {", + " spacing,", + " cellsPerChunk,", + " cellsX: Math.ceil(object.getWidth() / spacing),", + " cellsY: Math.ceil(object.getHeight() / spacing),", + " };", + " this.grid = grid;", + " for (let firstCellJ = 0; firstCellJ < grid.cellsY; firstCellJ += cellsPerChunk) {", + " for (let firstCellI = 0; firstCellI < grid.cellsX; firstCellI += cellsPerChunk) {", + " this._createChunk(firstCellI, firstCellJ);", + " }", + " }", + " }", + "", + " /**", + " * Place again the copies of the chunks touching a rectangle of the scene,", + " * for example after painting.", + " */", + " _rebuildArea(minX, minY, maxX, maxY) {", + " const { grid, object } = this;", + " if (!grid) return;", + " const chunkSize = grid.spacing * grid.cellsPerChunk;", + " const firstChunkI = Math.max(Math.floor((minX - object.getX()) / chunkSize), 0);", + " const firstChunkJ = Math.max(Math.floor((minY - object.getY()) / chunkSize), 0);", + " const lastChunkI = Math.min(Math.floor((maxX - object.getX()) / chunkSize), Math.ceil(grid.cellsX / grid.cellsPerChunk) - 1);", + " const lastChunkJ = Math.min(Math.floor((maxY - object.getY()) / chunkSize), Math.ceil(grid.cellsY / grid.cellsPerChunk) - 1);", + " if (firstChunkI > lastChunkI || firstChunkJ > lastChunkJ) return;", + " const isInArea = (chunk) => {", + " const chunkI = chunk.firstCellI / grid.cellsPerChunk;", + " const chunkJ = chunk.firstCellJ / grid.cellsPerChunk;", + " return chunkI >= firstChunkI && chunkI <= lastChunkI && chunkJ >= firstChunkJ && chunkJ <= lastChunkJ;", + " };", + " this._disposeChunks(this.chunks.filter(isInArea));", + " this.chunks = this.chunks.filter((chunk) => !isInArea(chunk));", + " for (let chunkJ = firstChunkJ; chunkJ <= lastChunkJ; chunkJ++) {", + " for (let chunkI = firstChunkI; chunkI <= lastChunkI; chunkI++) {", + " this._createChunk(chunkI * grid.cellsPerChunk, chunkJ * grid.cellsPerChunk);", + " }", + " }", + " this.version++;", + " }", + "", + " _createChunk(firstCellI, firstCellJ) {", + " const { grid } = this;", + " const grounds = Array.from(this.placement.grounds);", + " /** @type {Copy[]} */", + " const copies = [];", + " for (let cellJ = firstCellJ; cellJ < Math.min(firstCellJ + grid.cellsPerChunk, grid.cellsY); cellJ++) {", + " for (let cellI = firstCellI; cellI < Math.min(firstCellI + grid.cellsPerChunk, grid.cellsX); cellI++) {", + " const copy = this._placeCopy(cellI, cellJ, grid.spacing, grounds);", + " if (copy) copies.push(copy);", + " }", + " }", + " if (copies.length === 0) return;", + "", + " const { object } = this;", + " // Copies are placed in scene units, and the object group is scaled", + " // from its inner area: they are scaled back to keep their proportions.", + " const scaleX = Math.max(object.getWidth() / AREA_SIZE, 0.0001);", + " const scaleY = Math.max(object.getHeight() / AREA_SIZE, 0.0001);", + " const scaleZ = Math.max(object.getDepth() / AREA_DEPTH, 0.0001);", + " const matrices = new Float32Array(copies.length * 16);", + " const tintColor = gdjs.rgbOrHexStringToNumber(object._getRandomTint());", + " const hasTint = tintColor !== 0xffffff;", + " const colors = hasTint ? new Float32Array(copies.length * 3) : null;", + " this._tint.setHex(tintColor);", + " const min = new THREE.Vector3(Infinity, Infinity, Infinity);", + " const max = new THREE.Vector3(-Infinity, -Infinity, -Infinity);", + " copies.forEach((copy, index) => {", + " const cos = Math.cos(copy.angle) * copy.height;", + " const sin = Math.sin(copy.angle) * copy.height;", + " const localX = (copy.x - object.getX()) / scaleX;", + " const localY = (copy.y - object.getY()) / scaleY;", + " const localZ = (copy.z - object.getZ()) / scaleZ;", + " this._matrix.set(", + " cos / scaleX, -sin / scaleX, 0, localX,", + " sin / scaleY, cos / scaleY, 0, localY,", + " 0, 0, copy.height / scaleZ, localZ,", + " 0, 0, 0, 1", + " );", + " this._matrix.toArray(matrices, index * 16);", + " if (colors) {", + " // The tint of each copy is between white and the tint color.", + " this._color.setRGB(1, 1, 1).lerp(this._tint, createRandom(copy.key + 1)());", + " this._color.toArray(colors, index * 3);", + " }", + " const reach = (copy.height * Math.max(this.model.width, this.model.depth)) / 2;", + " min.min(this._chunkPosition.set(copy.x - reach, copy.y - reach, copy.z));", + " max.max(this._chunkPosition.set(copy.x + reach, copy.y + reach, copy.z + copy.height));", + " });", + " const instanceMatrix = new THREE.InstancedBufferAttribute(matrices, 16);", + " const instanceColor = colors ? new THREE.InstancedBufferAttribute(colors, 3) : null;", + " const castShadow = object._getCastShadow();", + " const receiveShadow = object._getReceiveShadow();", + " const meshes = this.model.parts.map((part) => {", + " const mesh = new THREE.InstancedMesh(part.geometry, part.material, 0);", + " // All the parts of the model share the positions of the copies.", + " mesh.instanceMatrix = instanceMatrix;", + " mesh.instanceColor = instanceColor;", + " mesh.count = copies.length;", + " mesh.computeBoundingSphere();", + " mesh.castShadow = castShadow;", + " mesh.receiveShadow = receiveShadow;", + " // The first mesh tests all the parts of the copies.", + " mesh.raycast = () => {};", + " this.group.add(mesh);", + " return mesh;", + " });", + " const center = new THREE.Vector3(", + " ((min.x + max.x) / 2 - object.getX()) / scaleX,", + " ((min.y + max.y) / 2 - object.getY()) / scaleY,", + " ((min.z + max.z) / 2 - object.getZ()) / scaleZ", + " );", + " const chunk = { firstCellI, firstCellJ, meshes, copies, center, radius: min.distanceTo(max) / 2 };", + " // Used by the scene editor to select the object when a copy is clicked.", + " meshes[0].raycast = (raycaster, intersects) => this._raycastChunk(chunk, raycaster, intersects);", + " this.chunks.push(chunk);", + " }", + "", + " /**", + " * Add the hits of a ray on the copies of a chunk, like they are drawn. There", + " * can be thousands of copies: only the ones whose box is crossed by the ray", + " * are tested with the triangles of the model.", + " * @param {{meshes: THREE.InstancedMesh[], copies: Copy[], center: THREE.Vector3, radius: number}} chunk", + " * @param {THREE.Raycaster} raycaster", + " * @param {THREE.Intersection[]} intersects", + " */", + " _raycastChunk(chunk, raycaster, intersects) {", + " const { object, group } = this;", + " this._raycastSphere.center.copy(chunk.center).applyMatrix4(group.matrixWorld);", + " this._raycastSphere.radius = chunk.radius / getWorldScale(object);", + " if (!raycaster.ray.intersectsSphere(this._raycastSphere)) return;", + "", + " // The ray in the scene coordinates, like the copies.", + " const scale = this._raycastScale.set(", + " Math.max(object.getWidth() / AREA_SIZE, 0.0001),", + " Math.max(object.getHeight() / AREA_SIZE, 0.0001),", + " Math.max(object.getDepth() / AREA_DEPTH, 0.0001)", + " );", + " const ray = this._raycastRay.copy(raycaster.ray).applyMatrix4(this._raycastInverseMatrix.copy(group.matrixWorld).invert());", + " ray.origin.multiply(scale).add(this._chunkPosition.set(object.getX(), object.getY(), object.getZ()));", + " ray.direction.multiply(scale);", + "", + " const reachPerHeight = Math.max(this.model.width, this.model.depth) / 2;", + " const instanceMatrices = chunk.meshes[0].instanceMatrix.array;", + " chunk.copies.forEach((copy, index) => {", + " const reach = copy.height * reachPerHeight;", + " this._raycastBox.min.set(copy.x - reach, copy.y - reach, copy.z);", + " this._raycastBox.max.set(copy.x + reach, copy.y + reach, copy.z + copy.height);", + " if (!ray.intersectsBox(this._raycastBox)) return;", + " this._raycastMesh.matrixWorld.fromArray(instanceMatrices, index * 16).premultiply(group.matrixWorld);", + " for (const mesh of chunk.meshes) {", + " this._raycastMesh.geometry = mesh.geometry;", + " this._raycastMesh.material = mesh.material;", + " this._raycastHits.length = 0;", + " this._raycastMesh.raycast(raycaster, this._raycastHits);", + " for (const hit of this._raycastHits) {", + " hit.object = mesh;", + " hit.instanceId = index;", + " intersects.push(hit);", + " }", + " }", + " });", + " }", + "", + " /** @param {THREE.Camera | null} camera */", + " _hideFarChunks(camera) {", + " if (!camera) return;", + " const worldScale = getWorldScale(this.object);", + " const drawDistance = Math.max(this.object._getDrawDistance(), 1) / worldScale;", + " this.uniforms.scatterDrawDistance.value = drawDistance;", + " this._cameraPosition.setFromMatrixPosition(camera.matrixWorld);", + " for (const chunk of this.chunks) {", + " const distance = this._chunkPosition", + " .copy(chunk.center)", + " .applyMatrix4(this.group.matrixWorld)", + " .distanceTo(this._cameraPosition);", + " const isVisible = distance - chunk.radius / worldScale < drawDistance;", + " for (const mesh of chunk.meshes) mesh.visible = isVisible;", + " }", + " }", + "", + " /**", + " * @param {number} elapsedSeconds", + " * @param {THREE.Camera | null} camera", + " */", + " update(elapsedSeconds, camera) {", + " if (this.placement.shouldPlaceAgain(elapsedSeconds, this._getPlacementKey())) {", + " this.rebuild();", + " }", + " if (!this.changedSamples.isEmpty()) {", + " const { object } = this;", + " const sampleWidth = object.getWidth() / MASK_RESOLUTION;", + " const sampleHeight = object.getHeight() / MASK_RESOLUTION;", + " const { minI, minJ, maxI, maxJ } = this.changedSamples;", + " // Copies between samples use the density of the samples around them.", + " this._rebuildArea(", + " object.getX() + (minI - 1) * sampleWidth,", + " object.getY() + (minJ - 1) * sampleHeight,", + " object.getX() + (maxI + 1) * sampleWidth,", + " object.getY() + (maxJ + 1) * sampleHeight", + " );", + " this.changedSamples.clear();", + " }", + " // The object transformation is applied just before rendering, it's needed now.", + " this.object.getRenderer().ensureUpToDate();", + " this.group.updateWorldMatrix(true, false);", + " this._hideFarChunks(camera);", + " this._updateSurface();", + " }", + "", + " _updateSurface() {", + " const { object } = this;", + " if (typeof object.setSurface !== 'function') return;", + " const trunkKey = object._getTrunkCollisions() + '|' + object._getTrunkDiameter();", + " if (trunkKey !== this.trunkKey) {", + " this.trunkKey = trunkKey;", + " this.version++;", + " }", + " if (object.getSurface() !== this.surface) object.setSurface(this.surface);", + " }", + "", + " /** @returns {Copy[]} */", + " getCopies() {", + " const copies = [];", + " for (const chunk of this.chunks) copies.push(...chunk.copies);", + " return copies;", + " }", + "", + " /**", + " * @returns {Copy | null} The copy whose base is the nearest to a position (on X and Y), if any.", + " */", + " findNearestCopy(x, y) {", + " let nearestCopy = null;", + " let nearestDistanceSquared = Infinity;", + " for (const chunk of this.chunks) {", + " for (const copy of chunk.copies) {", + " const distanceSquared = (copy.x - x) * (copy.x - x) + (copy.y - y) * (copy.y - y);", + " if (distanceSquared < nearestDistanceSquared) {", + " nearestDistanceSquared = distanceSquared;", + " nearestCopy = copy;", + " }", + " }", + " }", + " return nearestCopy;", + " }", + "", + " getCopyCount() {", + " let count = 0;", + " for (const chunk of this.chunks) count += chunk.copies.length;", + " return count;", + " }", + "", + " /** Remove the copies whose base is in a circle (they come back when the scene is restarted). */", + " removeCopies(x, y, radius) {", + " let hasRemovedCopies = false;", + " for (const chunk of this.chunks) {", + " for (const copy of chunk.copies) {", + " if ((copy.x - x) * (copy.x - x) + (copy.y - y) * (copy.y - y) <= radius * radius) {", + " this.removedCopies.set(copy.key, copy);", + " hasRemovedCopies = true;", + " }", + " }", + " }", + " if (hasRemovedCopies) this._rebuildArea(x - radius, y - radius, x + radius, y + radius);", + " }", + "", + " /** Paint the full density around a position, and bring back the copies removed there. */", + " addCopies(x, y, radius) {", + " for (const [key, copy] of this.removedCopies) {", + " if ((copy.x - x) * (copy.x - x) + (copy.y - y) * (copy.y - y) <= radius * radius) {", + " this.removedCopies.delete(key);", + " }", + " }", + " this.paint(x, y, x, y, radius, 1, 1);", + " }", + "", + " _getStroke(x1, y1, x2, y2, radius) {", + " const { object } = this;", + " const stroke = this._stroke;", + " stroke.cellWidth = object.getWidth() / MASK_RESOLUTION;", + " stroke.cellHeight = object.getHeight() / MASK_RESOLUTION;", + " stroke.ax = x1 - object.getX();", + " stroke.ay = y1 - object.getY();", + " stroke.bx = x2 - object.getX();", + " stroke.by = y2 - object.getY();", + " stroke.radius = Math.max(radius, 0);", + " return stroke;", + " }", + "", + " /**", + " * Paint the density around a segment (copies are placed again at the next frame).", + " * @param {number} density From 0 (erase) to 1 (as dense as the Density property).", + " * @param {number} strength From 0 to 1.", + " */", + " paint(x1, y1, x2, y2, radius, density, strength) {", + " const changedSamples = new SampleRectangle();", + " this.mask.paint(this._getStroke(x1, y1, x2, y2, radius), density, strength, changedSamples);", + " this.changedSamples.add(changedSamples);", + " }", + "", + " /** @param {DensityMask} mask */", + " setMask(mask) {", + " this.mask = mask;", + " this.rebuild();", + " }", + "", + " /**", + " * Prisms around the trunks, from 0 to 1 in the object box (see `gdjs.Surface`).", + " * @returns {gdjs.SurfaceTriangles}", + " */", + " _getTrunkTriangles() {", + " const { object } = this;", + " const copies = object._getTrunkCollisions() ? this.getCopies() : [];", + " const verticesPerCopy = TRUNK_SIDES * 2 + 1;", + " const positions = new Float32Array(copies.length * verticesPerCopy * 3);", + " const indices = new Uint32Array(copies.length * TRUNK_SIDES * 3 * 3);", + " const width = object.getWidth() || 1;", + " const height = object.getHeight() || 1;", + " const depth = object.getDepth() || 1;", + " let indexOffset = 0;", + " copies.forEach((copy, copyIndex) => {", + " const radius = (Math.max(object._getTrunkDiameter(), 0) / 2) * (copy.height / Math.max(object._getSize(), 1));", + " const top = copy.z + copy.height * 0.8;", + " const firstVertex = copyIndex * verticesPerCopy;", + " const setVertex = (vertex, x, y, z) => {", + " positions[vertex * 3] = (x - object.getX()) / width;", + " positions[vertex * 3 + 1] = (y - object.getY()) / height;", + " positions[vertex * 3 + 2] = (z - object.getZ()) / depth;", + " };", + " for (let side = 0; side < TRUNK_SIDES; side++) {", + " const angle = ((side + 0.5) / TRUNK_SIDES) * Math.PI * 2;", + " const x = copy.x + Math.cos(angle) * radius;", + " const y = copy.y + Math.sin(angle) * radius;", + " // Starting a bit under the ground, which can be sloped.", + " setVertex(firstVertex + side * 2, x, y, copy.z - radius);", + " setVertex(firstVertex + side * 2 + 1, x, y, top);", + " }", + " const center = firstVertex + TRUNK_SIDES * 2;", + " setVertex(center, copy.x, copy.y, top);", + " for (let side = 0; side < TRUNK_SIDES; side++) {", + " const bottom = firstVertex + side * 2;", + " const nextBottom = firstVertex + ((side + 1) % TRUNK_SIDES) * 2;", + " // Triangles face out of the prism (vertices at odd indices are at the top).", + " indices.set([bottom, nextBottom, bottom + 1, nextBottom, nextBottom + 1, bottom + 1], indexOffset);", + " indices.set([center, bottom + 1, nextBottom + 1], indexOffset + 6);", + " indexOffset += 9;", + " }", + " });", + " if (copies.length === 0) {", + " // Without triangles, the physics engine and navigation meshes would use", + " // the object box: a tiny triangle under the object is used instead.", + " return { positions: new Float32Array([0, 0, -1, 0.0001, 0, -1, 0, 0.0001, -1]), indices: new Uint32Array([0, 2, 1]) };", + " }", + " return { positions, indices };", + " }", + "", + " dispose() {", + " this.isDisposed = true;", + " if (typeof this.object.setSurface === 'function' && this.object.getSurface() === this.surface) {", + " this.object.setSurface(null);", + " }", + " this._disposeChunks(this.chunks);", + " this._disposeModel();", + " this.group.removeFromParent();", + " }", + "}", + "", + "const scatterToolIcons = {", + " Add: svgIcon(", + " ''", + " ),", + " Erase: svgIcon(", + " ''", + " ),", + "};", + "", + "/**", + " * The brushes shown in the scene editor when scattered models are selected,", + " * to add or erase copies. Without the needed editor functions (older GDevelop", + " * versions), nothing is shown: copies can still be painted with events.", + " */", + "class ScatterEditorTools {", + " constructor() {", + " /** '' when no brush is used. */", + " this.toolName = '';", + " this.sizePercent = 30;", + " this.strengthPercent = 50;", + " /** @type {Scatter | null} */", + " this.scatter = null;", + " /** @type {gdjs.InGameEditor | null} */", + " this.editor = null;", + " this.isStroking = false;", + " this.lastStrokeTime = 0;", + " /** @type {Array<{mask: DensityMask, baseMask: DensityMask}>} */", + " this.undoSteps = [];", + " this.raycaster = new THREE.Raycaster();", + " this.ndc = new THREE.Vector2();", + " this._groundAtCursor = { ground: null, z: 0 };", + " /** A circle following the ground under the cursor. */", + " this.brushRing = new THREE.LineLoop(", + " new THREE.BufferGeometry().setAttribute('position', new THREE.BufferAttribute(new Float32Array(64 * 3), 3)),", + " new THREE.LineBasicMaterial({ color: 0xffffff, depthTest: false, transparent: true })", + " );", + " this.brushRing.renderOrder = 1000;", + " this.brushRing.frustumCulled = false;", + " this.brushRing.raycast = () => {};", + " }", + "", + " /** @param {gdjs.InGameEditor} editor */", + " update(editor) {", + " if (typeof editor.showToolbar !== 'function') return;", + " this.editor = editor;", + " const selectedObjects = editor.getSelectedObjects();", + " const scatter =", + " selectedObjects.length === 1 && selectedObjects[0].__terrainScatter ? selectedObjects[0].__terrainScatter : null;", + " if (scatter !== this.scatter) {", + " this._endStroke();", + " this.scatter = scatter;", + " this.undoSteps.length = 0;", + " }", + " if (scatter) editor.showToolbar('ScatteredModels', this._getToolbarItems());", + " const game = editor.getRuntimeGame();", + " if (!scatter || !this.toolName) {", + " this.brushRing.removeFromParent();", + " return;", + " }", + " const inputManager = game.getInputManager();", + " if (inputManager.wasKeyJustPressed(gdjs.evtTools.input.keysNameToCode.Escape)) {", + " this._endStroke();", + " this.toolName = '';", + " this.brushRing.removeFromParent();", + " return;", + " }", + " editor.captureLeftMouseButton();", + " const hit = this._getPositionUnderCursor(scatter, game);", + " const radius = this._getRadius();", + " this._showBrushRing(scatter, hit, radius);", + "", + " const isPressedForTool =", + " typeof editor.isCapturedLeftMouseButtonPressed === 'function'", + " ? editor.isCapturedLeftMouseButtonPressed()", + " : inputManager.isMouseButtonPressed(0);", + " // The button is released outside the canvas when the mouse is on a toolbar.", + " const isPressed = isPressedForTool && inputManager.isMouseInsideCanvas();", + " if (isPressed && hit && !this.isStroking) {", + " this._startStroke(scatter);", + " }", + " if (this.isStroking && isPressed && hit) {", + " this._applyBrush(scatter, hit, radius);", + " }", + " if (!isPressed) {", + " this._endStroke();", + " }", + " }", + "", + " _getRadius() {", + " const { object } = this.scatter;", + " const maxRadius = Math.max(object.getWidth(), object.getHeight()) / 4;", + " return Math.max(maxRadius * Math.pow(this.sizePercent / 100, 2), 10);", + " }", + "", + " /**", + " * @returns {{x: number, y: number} | null} The position of the scene under the cursor: on the", + " * highest ground hit, or on the plane at the object Z.", + " */", + " _getPositionUnderCursor(scatter, game) {", + " let hit = null;", + " for (const ground of getGrounds(scatter.object.getRuntimeScene())) {", + " if (typeof ground.raycastFromCursor !== 'function') continue;", + " const localHit = ground.raycastFromCursor(game, this.raycaster, this.ndc);", + " if (!localHit) continue;", + " const sceneHit = ground.toScenePosition(localHit);", + " if (!hit || sceneHit.z > hit.z) hit = sceneHit;", + " }", + " if (hit) return hit;", + "", + " const { object } = scatter;", + " const layer = object.getInstanceContainer().getLayer(object.getLayer());", + " const camera = layer.getRenderer().getThreeCamera();", + " if (!camera) return null;", + " const inputManager = game.getInputManager();", + " this.ndc.set(", + " (inputManager.getCursorX() / game.getGameResolutionWidth()) * 2 - 1,", + " -(inputManager.getCursorY() / game.getGameResolutionHeight()) * 2 + 1", + " );", + " this.raycaster.setFromCamera(this.ndc, camera);", + " scatter.group.updateWorldMatrix(true, false);", + " const ray = this.raycaster.ray.clone().applyMatrix4(scatter.group.matrixWorld.clone().invert());", + " const point = ray.intersectPlane(new THREE.Plane(new THREE.Vector3(0, 0, 1), 0), new THREE.Vector3());", + " if (!point) return null;", + " return {", + " x: object.getX() + (point.x / AREA_SIZE) * object.getWidth(),", + " y: object.getY() + (point.y / AREA_SIZE) * object.getHeight(),", + " };", + " }", + "", + " _showBrushRing(scatter, hit, radius) {", + " if (!hit) {", + " this.brushRing.removeFromParent();", + " return;", + " }", + " const { object } = scatter;", + " if (this.brushRing.parent !== scatter.group) scatter.group.add(this.brushRing);", + " const grounds = getGrounds(object.getRuntimeScene());", + " const positions = this.brushRing.geometry.attributes.position;", + " for (let index = 0; index < positions.count; index++) {", + " const angle = (index / positions.count) * Math.PI * 2;", + " const x = hit.x + Math.cos(angle) * radius;", + " const y = hit.y + Math.sin(angle) * radius;", + " const { ground, z } = findHighestGround(grounds, x, y, this._groundAtCursor);", + " positions.setXYZ(", + " index,", + " ((x - object.getX()) / object.getWidth()) * AREA_SIZE,", + " ((y - object.getY()) / object.getHeight()) * AREA_SIZE,", + " (((ground ? z : object.getZ()) + 2 - object.getZ()) / Math.max(object.getDepth(), 0.0001)) * AREA_DEPTH", + " );", + " }", + " positions.needsUpdate = true;", + " }", + "", + " _startStroke(scatter) {", + " this.isStroking = true;", + " this.lastStrokeTime = performance.now();", + " this.undoSteps.push({ mask: scatter.mask.clone(), baseMask: scatter.baseMask });", + " while (this.undoSteps.length > 30) this.undoSteps.shift();", + " }", + "", + " _endStroke() {", + " if (!this.isStroking) return;", + " this.isStroking = false;", + " this._save();", + " }", + "", + " _applyBrush(scatter, hit, radius) {", + " const now = performance.now();", + " const elapsedSeconds = Math.min((now - this.lastStrokeTime) / 1000, 0.1);", + " this.lastStrokeTime = now;", + " const strength = (this.strengthPercent / 100) * elapsedSeconds * 8;", + " scatter.paint(hit.x, hit.y, hit.x, hit.y, radius, this.toolName === 'Add' ? 1 : 0, strength);", + " }", + "", + " _save() {", + " const { scatter, editor } = this;", + " if (!scatter || !editor) return;", + " editor.updateObjectProperties(scatter.object.getName(), { PaintData: scatter.save() });", + " }", + "", + " _undo() {", + " const undoStep = this.undoSteps.pop();", + " // The coverage or the edits may have changed since the stroke.", + " if (!this.scatter || !undoStep || undoStep.baseMask !== this.scatter.baseMask) {", + " this.undoSteps.length = 0;", + " return;", + " }", + " this.scatter.setMask(undoStep.mask);", + " this._save();", + " }", + "", + " /** @returns {Array} */", + " _getToolbarItems() {", + " const toolButton = (toolName, tooltip) => ({", + " type: 'button',", + " id: toolName,", + " tooltip,", + " iconUrl: scatterToolIcons[toolName],", + " isActive: this.toolName === toolName,", + " onClick: () => (this.toolName = this.toolName === toolName ? '' : toolName),", + " });", + " return [", + " toolButton('Add', 'Add copies (drag on the ground)'),", + " toolButton('Erase', 'Erase copies'),", + " { type: 'divider', id: 'BrushDivider' },", + " {", + " type: 'slider',", + " id: 'Size',", + " tooltip: 'Brush size',", + " iconUrl: toolIcons.Size,", + " min: 1,", + " max: 100,", + " value: this.sizePercent,", + " onChange: (value) => (this.sizePercent = value),", + " },", + " {", + " type: 'slider',", + " id: 'Strength',", + " tooltip: 'Brush strength',", + " iconUrl: toolIcons.Strength,", + " min: 5,", + " max: 100,", + " value: this.strengthPercent,", + " onChange: (value) => (this.strengthPercent = value),", + " },", + " {", + " type: 'button',", + " id: 'Undo',", + " tooltip: 'Undo the last stroke on these scattered models',", + " iconUrl: toolIcons.Undo,", + " onClick: () => this._undo(),", + " },", + " ];", + " }", + "}", + "", + "const editorTools = new ScatterEditorTools();", + "if (gdjs.registerInGameEditorPostStepCallback) {", + " gdjs.registerInGameEditorPostStepCallback((editor) => editorTools.update(editor));", + "}", + "", + "gdjs.__terrainScatterExtension = {", + " Scatter,", + " // Exposed for tests.", + " DensityMask,", + " maskCodec,", + " applyEdits,", + "};", + "" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [], + "objectGroups": [] + } + ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "DefineHelperClasses" + }, + { + "functionName": "DefineGrassHelperClasses" + }, + { + "functionName": "DefineScatterHelperClasses" + } + ] + }, + "eventsBasedBehaviors": [], + "eventsBasedObjects": [ + { + "areaMaxX": 4096, + "areaMaxY": 4096, + "areaMaxZ": 600, + "areaMinX": 0, + "areaMinY": 0, + "areaMinZ": 0, + "assetStoreTag": "3d terrains", + "defaultName": "Terrain", + "description": "A ground with hills, mountains or valleys that can be sculpted and painted in the scene editor, or changed with events. Add the 3D physics behavior to it to make objects walk or roll on it.", + "fullName": "3D terrain", + "helpPath": "", + "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLXRlcnJhaW4iIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTQsNkwxMC4yNSwxMUwxMy4xLDE0LjhMMTEuNSwxNkM5LjgxLDEzLjc1IDcsMTAgNywxMEwxLDE4SDIzTDE0LDZaIiAvPjwvc3ZnPg==", + "is3D": true, + "isUsingLegacyInstancesRenderer": true, + "name": "Terrain3D", + "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", + "objects": [ + { + "assetStoreId": "", + "name": "AreaPlaceholder", + "type": "Scene3D::Cube3DObject", + "variables": [], + "effects": [], + "behaviors": [], + "content": { + "width": 4096, + "height": 4096, + "depth": 600, + "enableTextureTransparency": false, + "facesOrientation": "Y", + "backFaceUpThroughWhichAxisRotation": "X", + "materialType": "Basic", + "tint": "255;255;255", + "frontFaceResourceName": "", + "frontFaceVisible": false, + "frontFaceResourceRepeat": false, + "backFaceResourceName": "", + "backFaceVisible": false, + "backFaceResourceRepeat": false, + "leftFaceResourceName": "", + "leftFaceVisible": false, + "leftFaceResourceRepeat": false, + "rightFaceResourceName": "", + "rightFaceVisible": false, + "rightFaceResourceRepeat": false, + "topFaceResourceName": "", + "topFaceVisible": false, + "topFaceResourceRepeat": false, + "bottomFaceResourceName": "", + "bottomFaceVisible": false, + "bottomFaceResourceRepeat": false + } + } + ], + "objectsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "objectName": "AreaPlaceholder" + } + ] + }, + "objectsGroups": [], + "layers": [ + { + "ambientLightColorB": 200, + "ambientLightColorG": 200, + "ambientLightColorR": 200, + "camera2DPlaneMaxDrawingDistance": 5000, + "camera3DFarPlaneDistance": 10000, + "camera3DFieldOfView": 45, + "camera3DNearPlaneDistance": 3, + "cameraType": "", + "followBaseLayerCamera": false, + "isLightingLayer": false, + "isLocked": false, + "name": "", + "renderingType": "", + "visibility": true, + "cameras": [ + { + "defaultSize": true, + "defaultViewport": true, + "height": 0, + "viewportBottom": 1, + "viewportLeft": 0, + "viewportRight": 1, + "viewportTop": 0, + "width": 0 + } + ], + "effects": [] + } + ], + "instances": [ + { + "angle": 0, + "customSize": true, + "depth": 600, + "height": 4096, + "keepRatio": true, + "layer": "", + "name": "AreaPlaceholder", + "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-004096004096", + "width": 4096, + "x": 0, + "y": 0, + "zOrder": 1, + "numberProperties": [], + "stringProperties": [], + "initialVariables": [] + } + ], + "editionSettings": {}, + "eventsFunctions": [ + { + "fullName": "", + "functionType": "Action", + "name": "onCreated", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Terrain3D::DefineHelperClasses" + }, + "parameters": [ + "", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "for (const child of object.getChildrenContainer().getAdhocListOfAllInstances()) {", + " child.deleteFromScene();", + "}", + "if (object.__terrain3D) object.__terrain3D.dispose();", + "object.__terrain3D = new gdjs.__terrain3DExtension.Terrain(object);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "onDestroy", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrain3D.dispose();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "doStepPostEvents", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrain3D.update();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "onHotReloading", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrain3D.onHotReloading();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "description": "Raise the ground around a position (or lower it with a negative height). It's raised the most at the center and less and less up to the radius.", + "fullName": "Raise or lower the ground", + "functionType": "Action", + "group": "Sculpt", + "name": "RaiseGround", + "sentence": "Raise the ground of _PARAM0_ by _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const height = eventsFunctionContext.getArgument(\"Height\");", + "object.__terrain3D.raise(x, y, x, y, radius, height);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Height to add (negative to lower the ground)", + "name": "Height", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Raise the ground along a line, for example for a ridge, or lower it with a negative height to dig a river bed or a valley.", + "fullName": "Raise or lower the ground along a line", + "functionType": "Action", + "group": "Sculpt", + "name": "RaiseGroundAlongLine", + "sentence": "Raise the ground of _PARAM0_ by _PARAM6_ from _PARAM1_; _PARAM2_ to _PARAM3_; _PARAM4_ with a radius of _PARAM5_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x1 = eventsFunctionContext.getArgument(\"X1\");", + "const y1 = eventsFunctionContext.getArgument(\"Y1\");", + "const x2 = eventsFunctionContext.getArgument(\"X2\");", + "const y2 = eventsFunctionContext.getArgument(\"Y2\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const height = eventsFunctionContext.getArgument(\"Height\");", + "object.__terrain3D.raise(x1, y1, x2, y2, radius, height);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the start", + "name": "X1", + "type": "expression" + }, + { + "description": "Y position of the start", + "name": "Y1", + "type": "expression" + }, + { + "description": "X position of the end", + "name": "X2", + "type": "expression" + }, + { + "description": "Y position of the end", + "name": "Y2", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance from the line.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Height to add (negative to lower the ground)", + "name": "Height", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Flatten the ground around a position to a given height, for example to place a building.", + "fullName": "Flatten the ground", + "functionType": "Action", + "group": "Sculpt", + "name": "FlattenGround", + "sentence": "Flatten the ground of _PARAM0_ to a Z of _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const z = eventsFunctionContext.getArgument(\"Z\");", + "object.__terrain3D.flatten(x, y, x, y, radius, z, 1);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Z position of the flat ground", + "name": "Z", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Smooth the ground around a position, softening bumps and cliffs.", + "fullName": "Smooth the ground", + "functionType": "Action", + "group": "Sculpt", + "name": "SmoothGround", + "sentence": "Smooth the ground of _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_ (strength: _PARAM4_)", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const strength = eventsFunctionContext.getArgument(\"Strength\");", + "object.__terrain3D.smooth(x, y, x, y, radius, strength);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Strength (between 0 and 1)", + "name": "Strength", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Paint a layer (like grass, dirt, rock or sand) on the ground around a position.", + "fullName": "Paint a layer", + "functionType": "Action", + "group": "Paint", + "name": "PaintLayer", + "sentence": "Paint layer _PARAM4_ on _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const layer = eventsFunctionContext.getArgument(\"Layer\");", + "object.__terrain3D.paint(x, y, x, y, radius, layer, 1);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Layer (1 to 4)", + "name": "Layer", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Paint a layer on the ground along a line, for example for a path or a road.", + "fullName": "Paint a layer along a line", + "functionType": "Action", + "group": "Paint", + "name": "PaintLayerAlongLine", + "sentence": "Paint layer _PARAM6_ on _PARAM0_ from _PARAM1_; _PARAM2_ to _PARAM3_; _PARAM4_ with a radius of _PARAM5_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x1 = eventsFunctionContext.getArgument(\"X1\");", + "const y1 = eventsFunctionContext.getArgument(\"Y1\");", + "const x2 = eventsFunctionContext.getArgument(\"X2\");", + "const y2 = eventsFunctionContext.getArgument(\"Y2\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const layer = eventsFunctionContext.getArgument(\"Layer\");", + "object.__terrain3D.paint(x1, y1, x2, y2, radius, layer, 1);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the start", + "name": "X1", + "type": "expression" + }, + { + "description": "Y position of the start", + "name": "Y1", + "type": "expression" + }, + { + "description": "X position of the end", + "name": "X2", + "type": "expression" + }, + { + "description": "Y position of the end", + "name": "Y2", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance from the line.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Layer (1 to 4)", + "name": "Layer", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Tint the ground around a position with a color multiplying its layers, like dry grass or darker mud. White removes the tint.", + "fullName": "Tint the ground", + "functionType": "Action", + "group": "Paint", + "name": "TintGround", + "sentence": "Tint the ground of _PARAM0_ with _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const color = gdjs.rgbOrHexToRGBColor(eventsFunctionContext.getArgument(\"Color\"));", + "object.__terrain3D.tint(x, y, x, y, radius, color, 1);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Color", + "name": "Color", + "type": "color" + } + ], + "objectGroups": [] + }, + { + "description": "Make the ground around a position wet (darker and shiny, like after rain or near water) or dry.", + "fullName": "Change the wetness of the ground", + "functionType": "Action", + "group": "Paint", + "name": "WetGround", + "sentence": "Change the wetness of the ground of _PARAM0_ to _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const wetness = eventsFunctionContext.getArgument(\"Wetness\");", + "object.__terrain3D.wet(x, y, x, y, radius, wetness, 1);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Wetness (from 0 for dry to 1)", + "name": "Wetness", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "the wetness of the ground at a position, from 0 (dry) to 1, painted or near the water (wet shore).", + "fullName": "Ground wetness", + "functionType": "ExpressionAndCondition", + "group": "Paint", + "name": "GroundWetness", + "sentence": "the wetness of the ground at _PARAM1_; _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "eventsFunctionContext.returnValue = object.__terrain3D.getWetnessAt(x, y);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "The tint painted on the ground at a position, like \"255;255;255\" (white when there is none).", + "fullName": "Ground tint", + "functionType": "StringExpression", + "group": "Paint", + "name": "GroundTint", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "eventsFunctionContext.returnValue = object.__terrain3D.getTintAt(x, y).join(\";\");" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "string" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Remove the ground in a circle, for the entrance of a tunnel, a cave or a well: there is nothing drawn and no collision there. Fill it again with the \"Fill holes\" action. A height field can't go under itself: put a 3D model (with a collision shape) for the tunnel or the cave under the ground. The physics engine removes the triangles touching a hole, so the hole in the collisions can be up to a cell larger than the one drawn: make the model go a bit further than the hole, or put rocks around the entrance.", + "fullName": "Make a hole", + "functionType": "Action", + "group": "Holes", + "name": "MakeHole", + "sentence": "Make a hole in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "object.__terrain3D.makeHole(x, y, x, y, radius);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "name": "Radius", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Fill the holes in a circle with the layers of the ground around them: the ground is drawn and has collisions again.", + "fullName": "Fill holes", + "functionType": "Action", + "group": "Holes", + "name": "FillHoles", + "sentence": "Fill the holes in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "object.__terrain3D.fillHoles(x, y, x, y, radius);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "name": "Radius", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Make the entrance of a tunnel or a cave on a slope: a hole, with the ground in front of it flattened to the floor of the tunnel.", + "fullName": "Carve an entrance", + "functionType": "Action", + "group": "Holes", + "name": "CarveEntrance", + "sentence": "Carve an entrance in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_, going in at _PARAM4_° with its floor at Z _PARAM5_ and _PARAM6_ pixels of flat ground in front", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const angle = eventsFunctionContext.getArgument(\"Angle\");", + "const floorZ = eventsFunctionContext.getArgument(\"FloorZ\");", + "const length = eventsFunctionContext.getArgument(\"Length\");", + "object.__terrain3D.carveEntrance(x, y, radius, angle, floorZ, length);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "name": "Radius", + "type": "expression" + }, + { + "description": "Direction going into the ground (in degrees)", + "name": "Angle", + "type": "expression" + }, + { + "description": "Z position of the floor", + "name": "FloorZ", + "type": "expression" + }, + { + "description": "Length of the flat ground in front of the entrance (in pixels)", + "name": "Length", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Check if there is a hole in the ground at a position.", + "fullName": "Hole", + "functionType": "Condition", + "group": "Holes", + "name": "IsHole", + "sentence": "There is a hole in _PARAM0_ at _PARAM1_; _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "eventsFunctionContext.returnValue = object.__terrain3D.isHoleAt(x, y);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "The Z position of the ground at a position, or the given value where there is a hole.", + "fullName": "Ground Z position, with holes", + "functionType": "Expression", + "group": "Holes", + "name": "GroundZWithHoles", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const terrain = object.__terrain3D;", + "eventsFunctionContext.returnValue = terrain.isHoleAt(x, y) ? eventsFunctionContext.getArgument(\"ZInHoles\") : terrain.getHeightAt(x, y);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + }, + { + "description": "Value in holes", + "longDescription": "For example, a Z position far under the ground.", + "name": "ZInHoles", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Replace the ground with a generated relief. Painted layers are reset.", + "fullName": "Generate a relief", + "functionType": "Action", + "group": "Generation", + "name": "GenerateRelief", + "sentence": "Generate a relief of _PARAM1_ on _PARAM0_ with seed _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const relief = eventsFunctionContext.getArgument(\"Relief\");", + "const seed = eventsFunctionContext.getArgument(\"Seed\");", + "object.__terrain3D.generate(relief, seed);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "Relief", + "name": "Relief", + "supplementaryInformation": "[\"Flat\", \"Hills\", \"Mountains\", \"Island\"]", + "type": "stringWithSelector" + }, + { + "description": "Seed", + "longDescription": "Each seed gives a different terrain with the same relief.", + "name": "Seed", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Replace the ground with the heights of an image. Painted layers are kept.", + "fullName": "Load a heightmap", + "functionType": "Action", + "group": "Generation", + "name": "LoadHeightmap", + "sentence": "Load the heights of _PARAM0_ from _PARAM1_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const image = eventsFunctionContext.getArgument(\"Image\");", + "object.__terrain3D.loadHeightmap(image);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "Heightmap image", + "longDescription": "Black is the lowest, white the highest. For more precision, store the height on 16 bits in the red (most significant byte) and green channels.", + "name": "Image", + "type": "imageResource" + } + ], + "objectGroups": [] + }, + { + "description": "Move objects up or down so that their bottom is on the ground, under their center. Useful to place trees, rocks or characters on a terrain.", + "fullName": "Put objects on the ground", + "functionType": "Action", + "group": "Position", + "name": "PutOnGround", + "sentence": "Put _PARAM1_ on the ground of _PARAM0_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrain3D.placeOnGround(eventsFunctionContext.getObjects(\"Objects\"));" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "Objects to put on the ground", + "name": "Objects", + "type": "objectList" + } + ], + "objectGroups": [] + }, + { + "description": "the Z position of the ground at a position of the scene.", + "fullName": "Ground Z position", + "functionType": "ExpressionAndCondition", + "group": "Position", + "name": "GroundZ", + "sentence": "the ground Z position at _PARAM1_; _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "eventsFunctionContext.returnValue = object.__terrain3D.getHeightAt(x, y);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "the slope of the ground (angle with the horizontal, in degrees) at a position of the scene.", + "fullName": "Ground slope", + "functionType": "ExpressionAndCondition", + "group": "Position", + "name": "GroundSlope", + "sentence": "the ground slope at _PARAM1_; _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "eventsFunctionContext.returnValue = object.__terrain3D.getSlopeAt(x, y);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "the layer (from 1 to 4) painted the most at a position of the scene. Useful to play footstep sounds or to slow down characters in water or mud.", + "fullName": "Ground layer", + "functionType": "ExpressionAndCondition", + "group": "Paint", + "name": "GroundLayer", + "sentence": "the ground layer at _PARAM1_; _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "eventsFunctionContext.returnValue = object.__terrain3D.getLayerAt(x, y);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + } + ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + }, + { + "folderName": "Sculpt", + "children": [ + { + "functionName": "RaiseGround" + }, + { + "functionName": "RaiseGroundAlongLine" + }, + { + "functionName": "FlattenGround" + }, + { + "functionName": "SmoothGround" + } + ] + }, + { + "folderName": "Paint", + "children": [ + { + "functionName": "PaintLayer" + }, + { + "functionName": "PaintLayerAlongLine" + }, + { + "functionName": "TintGround" + }, + { + "functionName": "WetGround" + }, + { + "functionName": "GroundWetness" + }, + { + "functionName": "GroundTint" + }, + { + "functionName": "GroundLayer" + } + ] + }, + { + "folderName": "Holes", + "children": [ + { + "functionName": "MakeHole" + }, + { + "functionName": "FillHoles" + }, + { + "functionName": "CarveEntrance" + }, + { + "functionName": "IsHole" + }, + { + "functionName": "GroundZWithHoles" + } + ] + }, + { + "folderName": "Generation", + "children": [ + { + "functionName": "GenerateRelief" + }, + { + "functionName": "LoadHeightmap" + } + ] + }, + { + "folderName": "Position", + "children": [ + { + "functionName": "PutOnGround" + }, + { + "functionName": "GroundZ" + }, + { + "functionName": "GroundSlope" + } + ] + } + ] + }, + "propertyDescriptors": [ + { + "value": "Hills", + "type": "Choice", + "label": "Relief", + "description": "The initial shape, before sculpting.", + "group": "Shape", + "choices": [ + { + "label": "Flat", + "value": "Flat" + }, + { + "label": "Hills", + "value": "Hills" + }, + { + "label": "Mountains", + "value": "Mountains" + }, + { + "label": "Island", + "value": "Island" + } + ], + "name": "Relief" + }, + { + "value": "1", + "type": "Number", + "label": "Seed", + "description": "Each seed gives a different terrain with the same relief.", + "group": "Shape", + "name": "Seed" + }, + { + "value": "", + "type": "Resource", + "label": "Heightmap image", + "description": "Optional. Replaces the relief: black is the lowest, white the highest (the object depth).", + "group": "Shape", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "HeightmapImage" + }, + { + "value": "", + "type": "MultilineString", + "label": "Edits", + "description": "Changes of the relief or heightmap, applied in order, as a JSON list, for example [{\"tool\": \"raise\", \"x\": 0.3, \"y\": 0.4, \"radius\": 0.15, \"height\": 0.4}, {\"tool\": \"paint\", \"x\": 0.1, \"y\": 0.9, \"toX\": 0.8, \"toY\": 0.2, \"radius\": 0.02, \"layer\": 2}]. Tools: raise (height added to the ground, negative to lower), flatten (z: the ground is brought to this height, fully at the center, cutting what is above it and filling what is below; with toX and toY, toZ gives the height at the end of the line, for a road going up), smooth (strength), paint (layer, from 1 to 4, and strength), tint (color, like \"120;140;60\", multiplying the layers, white removing the tint, and strength), wetness (amount, from 0 for dry to 1 for wet, and strength), hole (no ground in the radius, for the entrances of tunnels and caves) and fill (the holes in the radius are filled with the layers of the ground around them). x, y and radius are fractions (0 to 1) of the terrain size (x is (scene X - terrain X) / terrain width). height and z are fractions of its depth: the ground at z is at Z = terrain Z + z * terrain depth. Add toX and toY to change the ground along a line (paths, rivers, ridges). Shown in the scene editor.", + "group": "Shape", + "advanced": true, + "name": "Edits" + }, + { + "value": "256", + "type": "Choice", + "label": "Resolution", + "description": "The number of cells on each side of the terrain. Higher is more detailed but slower.", + "group": "Shape", + "choices": [ + { + "label": "64 x 64", + "value": "64" + }, + { + "label": "128 x 128", + "value": "128" + }, + { + "label": "256 x 256", + "value": "256" + }, + { + "label": "512 x 512", + "value": "512" + }, + { + "label": "1024 x 1024", + "value": "1024" + } + ], + "name": "Resolution" + }, + { + "value": "96;160;64", + "type": "Color", + "label": "Layer 1 color (grass)", + "group": "Layers", + "name": "Layer1Color" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 1 texture", + "description": "Optional. Tinted by the layer color.", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer1Texture" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 1 normal map", + "description": "Optional. The bumps of the texture, lit by the lights (an OpenGL normal map, like the textures of ambientCG).", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer1NormalMap" + }, + { + "value": "150;112;72", + "type": "Color", + "label": "Layer 2 color (dirt)", + "group": "Layers", + "name": "Layer2Color" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 2 texture", + "description": "Optional. Tinted by the layer color.", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer2Texture" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 2 normal map", + "description": "Optional. The bumps of the texture, lit by the lights (an OpenGL normal map, like the textures of ambientCG).", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer2NormalMap" + }, + { + "value": "124;118;112", + "type": "Color", + "label": "Layer 3 color (rock)", + "group": "Layers", + "name": "Layer3Color" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 3 texture", + "description": "Optional. Tinted by the layer color.", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer3Texture" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 3 normal map", + "description": "Optional. The bumps of the texture, lit by the lights (an OpenGL normal map, like the textures of ambientCG).", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer3NormalMap" + }, + { + "value": "224;204;148", + "type": "Color", + "label": "Layer 4 color (sand)", + "group": "Layers", + "name": "Layer4Color" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 4 texture", + "description": "Optional. Tinted by the layer color.", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer4Texture" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 4 normal map", + "description": "Optional. The bumps of the texture, lit by the lights (an OpenGL normal map, like the textures of ambientCG).", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer4NormalMap" + }, + { + "value": "1", + "type": "Number", + "label": "Normal map strength", + "description": "How much the normal maps bump the ground: 0 for none, 1 as they are, more to exaggerate them.", + "group": "Layers", + "name": "NormalMapStrength" + }, + { + "value": "256", + "type": "Number", + "unit": "Pixel", + "label": "Texture size", + "description": "The size, in the scene, of one repetition of the layer textures.", + "group": "Layers", + "name": "TextureSize" + }, + { + "value": "0.5", + "type": "Number", + "label": "Height blending", + "description": "Where layers meet, the bright parts of textures (stones, tufts) cover the dark ones (gaps, sand) instead of fading into each other: 0 for none, 1 for the most.", + "group": "Layers", + "name": "HeightBlending" + }, + { + "value": "0.5", + "type": "Number", + "label": "Texture variation", + "description": "Makes the repetition of textures less visible, with patches of bigger and rotated textures and colors slowly varying: 0 for none, 1 for the most.", + "group": "Layers", + "name": "TextureVariation" + }, + { + "value": "false", + "type": "Boolean", + "label": "Wet shore", + "description": "Make the ground wet (darker and shiny) under a Z position, like the sand of a beach near the water.", + "group": "Wetness", + "name": "WetShore" + }, + { + "value": "0", + "type": "Number", + "unit": "Pixel", + "label": "Wet below Z", + "description": "The ground is wet under this Z position (for example, the Z of a 3D water object).", + "group": "Wetness", + "name": "WetShoreZ" + }, + { + "value": "60", + "type": "Number", + "unit": "Pixel", + "label": "Wet shore fade", + "description": "The ground becomes dry over this height above \"Wet below Z\".", + "group": "Wetness", + "name": "WetShoreFade" + }, + { + "value": "3", + "type": "Choice", + "label": "Layer on steep slopes", + "description": "This layer is shown where the ground is steep, whatever is painted.", + "group": "Layers", + "choices": [ + { + "label": "None", + "value": "0" + }, + { + "label": "Layer 1", + "value": "1" + }, + { + "label": "Layer 2", + "value": "2" + }, + { + "label": "Layer 3", + "value": "3" + }, + { + "label": "Layer 4", + "value": "4" + } + ], + "name": "CliffLayer" + }, + { + "value": "45", + "type": "Number", + "unit": "DegreeAngle", + "label": "Steep slope angle", + "group": "Layers", + "name": "CliffAngle" + }, + { + "value": "true", + "type": "Boolean", + "label": "Cast shadows", + "group": "Rendering", + "name": "CastShadow" + }, + { + "value": "true", + "type": "Boolean", + "label": "Receive shadows", + "group": "Rendering", + "name": "ReceiveShadow" + }, + { + "value": "", + "type": "String", + "label": "Sculpted and painted data", + "description": "Written by the sculpt and paint tools of the scene editor, in a compact format not meant to be read or written by hand: use Edits instead. Clear it to go back to the relief, heightmap and edits.", + "advanced": true, + "name": "SculptData" + } + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Shape", + "children": [ + { + "propertyName": "Relief" + }, + { + "propertyName": "Seed" + }, + { + "propertyName": "HeightmapImage" + }, + { + "propertyName": "Edits" + }, + { + "propertyName": "Resolution" + } + ] + }, + { + "folderName": "Layers", + "children": [ + { + "propertyName": "Layer1Color" + }, + { + "propertyName": "Layer1Texture" + }, + { + "propertyName": "Layer1NormalMap" + }, + { + "propertyName": "Layer2Color" + }, + { + "propertyName": "Layer2Texture" + }, + { + "propertyName": "Layer2NormalMap" + }, + { + "propertyName": "Layer3Color" + }, + { + "propertyName": "Layer3Texture" + }, + { + "propertyName": "Layer3NormalMap" + }, + { + "propertyName": "Layer4Color" + }, + { + "propertyName": "Layer4Texture" + }, + { + "propertyName": "Layer4NormalMap" + }, + { + "propertyName": "NormalMapStrength" + }, + { + "propertyName": "TextureSize" + }, + { + "propertyName": "HeightBlending" + }, + { + "propertyName": "TextureVariation" + }, + { + "propertyName": "CliffLayer" + }, + { + "propertyName": "CliffAngle" + } + ] + }, + { + "folderName": "Wetness", + "children": [ + { + "propertyName": "WetShore" + }, + { + "propertyName": "WetShoreZ" + }, + { + "propertyName": "WetShoreFade" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "CastShadow" + }, + { + "propertyName": "ReceiveShadow" + } + ] + }, + { + "propertyName": "SculptData" + } + ] + }, + "variants": [] + }, + { + "areaMaxX": 1000, + "areaMaxY": 1000, + "areaMaxZ": 40, + "areaMinX": 0, + "areaMinY": 0, + "areaMinZ": 0, + "defaultName": "Grass", + "description": "A field of grass blades swaying in the wind and pushed aside by characters. They grow in the area of the object, on the highest 3D terrain at each point (one field can cover several terrains), or at the Z position of the object where there is no terrain.", + "fullName": "Terrain grass", + "helpPath": "", + "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGQ9Ik0zIDIxYzEtNSAxLTkgMC0xMyAyIDMgMyA3IDMgMTN6bTQgMGMwLTYgMS0xMSA0LTE2LTEgNS0xIDEwIDAgMTZ6bTUgMGMwLTUgMi05IDUtMTItMiA0LTIgOC0xIDEyem01IDBjMS00IDItNiA0LTgtMSAzLTEgNS0xIDh6Ii8+PC9zdmc+", + "is3D": true, + "isUsingLegacyInstancesRenderer": true, + "name": "TerrainGrass", + "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/732ef90b7fcf5dd9171fe95d0cf262e09159b487304915a4693baa893d9ce16c_grass.svg", + "objects": [ + { + "assetStoreId": "", + "name": "AreaPlaceholder", + "type": "Scene3D::Cube3DObject", + "variables": [], + "effects": [], + "behaviors": [], + "content": { + "width": 1000, + "height": 1000, + "depth": 40, + "enableTextureTransparency": false, + "facesOrientation": "Y", + "backFaceUpThroughWhichAxisRotation": "X", + "materialType": "Basic", + "tint": "255;255;255", + "frontFaceResourceName": "", + "frontFaceVisible": false, + "frontFaceResourceRepeat": false, + "backFaceResourceName": "", + "backFaceVisible": false, + "backFaceResourceRepeat": false, + "leftFaceResourceName": "", + "leftFaceVisible": false, + "leftFaceResourceRepeat": false, + "rightFaceResourceName": "", + "rightFaceVisible": false, + "rightFaceResourceRepeat": false, + "topFaceResourceName": "", + "topFaceVisible": false, + "topFaceResourceRepeat": false, + "bottomFaceResourceName": "", + "bottomFaceVisible": false, + "bottomFaceResourceRepeat": false + } + } + ], + "objectsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "objectName": "AreaPlaceholder" + } + ] + }, + "objectsGroups": [], + "layers": [ + { + "ambientLightColorB": 200, + "ambientLightColorG": 200, + "ambientLightColorR": 200, + "camera2DPlaneMaxDrawingDistance": 5000, + "camera3DFarPlaneDistance": 10000, + "camera3DFieldOfView": 45, + "camera3DNearPlaneDistance": 3, + "cameraType": "", + "followBaseLayerCamera": false, + "isLightingLayer": false, + "isLocked": false, + "name": "", + "renderingType": "", + "visibility": true, + "cameras": [ + { + "defaultSize": true, + "defaultViewport": true, + "height": 0, + "viewportBottom": 1, + "viewportLeft": 0, + "viewportRight": 1, + "viewportTop": 0, + "width": 0 + } + ], + "effects": [] + } + ], + "instances": [ + { + "angle": 0, + "customSize": true, + "depth": 40, + "height": 1000, + "keepRatio": true, + "layer": "", + "name": "AreaPlaceholder", + "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-001000001000", + "width": 1000, + "x": 0, + "y": 0, + "zOrder": 1, + "numberProperties": [], + "stringProperties": [], + "initialVariables": [] + } + ], + "editionSettings": {}, + "eventsFunctions": [ + { + "fullName": "", + "functionType": "Action", + "name": "onCreated", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Terrain3D::DefineHelperClasses" + }, + "parameters": [ + "", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Terrain3D::DefineGrassHelperClasses" + }, + "parameters": [ + "", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "for (const child of object.getChildrenContainer().getAdhocListOfAllInstances()) {", + " child.deleteFromScene();", + "}", + "if (object.__terrainGrass) object.__terrainGrass.dispose();", + "object.__terrainGrass = new gdjs.__terrainGrassExtension.Grass(object);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "onDestroy", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrainGrass.dispose();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "doStepPostEvents", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const game = runtimeScene.getGame();", + "// Time doesn't pass in the scene editor: real time is used to show the wind.", + "const isInGameEdition = game.isInGameEdition && game.isInGameEdition();", + "const elapsedTime = isInGameEdition ? 1 / 60 : object.getElapsedTime() / 1000;", + "const time = isInGameEdition", + " ? performance.now() / 1000", + " : object.getRuntimeScene().getTimeManager().getTimeFromStart() / 1000;", + "const layer = object.getInstanceContainer().getLayer(object.getLayer());", + "object.__terrainGrass.update(elapsedTime, time, layer.getRenderer().getThreeCamera());" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "onHotReloading", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrainGrass.updateFromProperties();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" + } + ], + "objectGroups": [] + } + ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + } + ] + }, + "propertyDescriptors": [ + { + "value": "60", + "type": "Number", + "label": "Density", + "description": "The number of blades in a square of 100 x 100 pixels.", + "group": "Blades", + "name": "Density" + }, + { + "value": "36", + "type": "Number", + "unit": "Pixel", + "label": "Blade height", + "description": "The average height of blades (each blade is between 60% and 140% of it).", + "group": "Blades", + "name": "BladeHeight" + }, + { + "value": "5", + "type": "Number", + "unit": "Pixel", + "label": "Blade width", + "group": "Blades", + "name": "BladeWidth" + }, + { + "value": "52;104;36", + "type": "Color", + "label": "Base color", + "group": "Blades", + "name": "BaseColor" + }, + { + "value": "150;196;82", + "type": "Color", + "label": "Tip color", + "group": "Blades", + "name": "TipColor" + }, + { + "value": "1", + "type": "Number", + "label": "Seed", + "description": "Each seed places blades differently.", + "group": "Blades", + "name": "Seed" + }, + { + "value": "1", + "type": "Choice", + "label": "Grows on terrain layer", + "description": "On a 3D terrain, blades only grow where this layer is painted. A new terrain has layer 1 painted everywhere.", + "group": "Ground", + "choices": [ + { + "label": "Any", + "value": "0" + }, + { + "label": "Layer 1", + "value": "1" + }, + { + "label": "Layer 2", + "value": "2" + }, + { + "label": "Layer 3", + "value": "3" + }, + { + "label": "Layer 4", + "value": "4" + } + ], + "name": "GroundLayer" + }, + { + "value": "40", + "type": "Number", + "unit": "DegreeAngle", + "label": "Maximum slope", + "description": "No blades grow on steeper ground.", + "group": "Ground", + "name": "MaxSlope" + }, + { + "value": "1", + "type": "Number", + "label": "Wind strength", + "description": "0 for no wind.", + "group": "Wind", + "name": "WindStrength" + }, + { + "value": "2", + "type": "Number", + "label": "Wind speed", + "group": "Wind", + "name": "WindSpeed" + }, + { + "value": "", + "type": "String", + "label": "Object bending the grass", + "description": "The name of an object pushing blades aside when walking through the grass, like the player. The 8 first instances are used.", + "group": "Wind", + "name": "BendingObject" + }, + { + "value": "2500", + "type": "Number", + "unit": "Pixel", + "label": "Fade distance", + "description": "Blades are less and less dense up to this distance from the camera, and hidden after.", + "group": "Rendering", + "name": "FadeDistance" + } + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Blades", + "children": [ + { + "propertyName": "Density" + }, + { + "propertyName": "BladeHeight" + }, + { + "propertyName": "BladeWidth" + }, + { + "propertyName": "BaseColor" + }, + { + "propertyName": "TipColor" + }, + { + "propertyName": "Seed" + } + ] + }, + { + "folderName": "Ground", + "children": [ + { + "propertyName": "GroundLayer" + }, + { + "propertyName": "MaxSlope" + } + ] + }, + { + "folderName": "Wind", + "children": [ + { + "propertyName": "WindStrength" + }, + { + "propertyName": "WindSpeed" + }, + { + "propertyName": "BendingObject" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "FadeDistance" + } + ] + } + ] + }, + "variants": [] + }, + { + "areaMaxX": 2000, + "areaMaxY": 2000, + "areaMaxZ": 200, + "areaMinX": 0, + "areaMinY": 0, + "areaMinZ": 0, + "assetStoreTag": "3d scattered models", + "defaultName": "ScatteredModels", + "description": "Many copies of a 3D model (trees, rocks, bushes, flowers...) placed in the area of the object and drawn all at once, which is much faster than an object for each one. They stand on the highest 3D terrain at each point, or at the Z position of the object where there is no terrain. Paint where they grow with the brushes of the scene editor.", + "fullName": "Scattered models (forests, rocks...)", + "helpPath": "", + "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGQ9Ik0xMCAyMXYtM0gzbDUtNUg1bDUtNUg3bDUtNSA1IDVoLTNsNSA1aC0zbDUgNWgtN3YzeiIvPjwvc3ZnPg==", + "is3D": true, + "isUsingLegacyInstancesRenderer": true, + "name": "ScatteredModels", + "previewIconUrl": "", + "objects": [ + { + "assetStoreId": "", + "name": "AreaPlaceholder", + "type": "Scene3D::Cube3DObject", + "variables": [], + "effects": [], + "behaviors": [], + "content": { + "width": 2000, + "height": 2000, + "depth": 200, + "enableTextureTransparency": false, + "facesOrientation": "Y", + "backFaceUpThroughWhichAxisRotation": "X", + "materialType": "Basic", + "tint": "255;255;255", + "frontFaceResourceName": "", + "frontFaceVisible": false, + "frontFaceResourceRepeat": false, + "backFaceResourceName": "", + "backFaceVisible": false, + "backFaceResourceRepeat": false, + "leftFaceResourceName": "", + "leftFaceVisible": false, + "leftFaceResourceRepeat": false, + "rightFaceResourceName": "", + "rightFaceVisible": false, + "rightFaceResourceRepeat": false, + "topFaceResourceName": "", + "topFaceVisible": false, + "topFaceResourceRepeat": false, + "bottomFaceResourceName": "", + "bottomFaceVisible": false, + "bottomFaceResourceRepeat": false + } + } + ], + "objectsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "objectName": "AreaPlaceholder" + } + ] + }, + "objectsGroups": [], + "layers": [ + { + "ambientLightColorB": 200, + "ambientLightColorG": 200, + "ambientLightColorR": 200, + "camera2DPlaneMaxDrawingDistance": 5000, + "camera3DFarPlaneDistance": 10000, + "camera3DFieldOfView": 45, + "camera3DNearPlaneDistance": 3, + "cameraType": "", + "followBaseLayerCamera": false, + "isLightingLayer": false, + "isLocked": false, + "name": "", + "renderingType": "", + "visibility": true, + "cameras": [ + { + "defaultSize": true, + "defaultViewport": true, + "height": 0, + "viewportBottom": 1, + "viewportLeft": 0, + "viewportRight": 1, + "viewportTop": 0, + "width": 0 + } + ], + "effects": [] + } + ], + "instances": [ + { + "angle": 0, + "customSize": true, + "depth": 200, + "height": 2000, + "keepRatio": true, + "layer": "", + "name": "AreaPlaceholder", + "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-002000002000", + "width": 2000, + "x": 0, + "y": 0, + "zOrder": 1, + "numberProperties": [], + "stringProperties": [], + "initialVariables": [] + } + ], + "editionSettings": {}, + "eventsFunctions": [ + { + "fullName": "", + "functionType": "Action", + "name": "onCreated", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Terrain3D::DefineHelperClasses" + }, + "parameters": [ + "", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Terrain3D::DefineScatterHelperClasses" + }, + "parameters": [ + "", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "for (const child of object.getChildrenContainer().getAdhocListOfAllInstances()) {", + " child.deleteFromScene();", + "}", + "if (object.__terrainScatter) object.__terrainScatter.dispose();", + "object.__terrainScatter = new gdjs.__terrainScatterExtension.Scatter(object);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "onDestroy", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrainScatter.dispose();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "doStepPostEvents", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const game = runtimeScene.getGame();", + "const isInGameEdition = game.isInGameEdition && game.isInGameEdition();", + "const elapsedTime = isInGameEdition ? 1 / 60 : object.getElapsedTime() / 1000;", + "const layer = object.getInstanceContainer().getLayer(object.getLayer());", + "object.__terrainScatter.update(elapsedTime, layer.getRenderer().getThreeCamera());" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "onHotReloading", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrainScatter.onHotReloading();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "description": "Paint the density of copies around a position, up to the density of the object (copies removed with \"Erase copies\" come back). The effect fades out up to the radius.", + "fullName": "Add copies", + "functionType": "Action", + "group": "Paint", + "name": "AddCopies", + "sentence": "Add copies of _PARAM0_ around _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "object.__terrainScatter.addCopies(x, y, radius);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "name": "Radius", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Remove the copies standing in a circle, for example a tree cut by the player (use the position of the nearest copy and a small radius) or a forest cleared for a building. They come back when the scene is restarted.", + "fullName": "Erase copies", + "functionType": "Action", + "group": "Paint", + "name": "EraseCopies", + "sentence": "Erase the copies of _PARAM0_ in a circle at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "object.__terrainScatter.removeCopies(x, y, radius);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "name": "Radius", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "the number of copies placed in the area.", + "fullName": "Number of copies", + "functionType": "ExpressionAndCondition", + "group": "Copies", + "name": "CopyCount", + "sentence": "the number of copies", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "eventsFunctionContext.returnValue = object.__terrainScatter.getCopyCount();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "description": "The X position of the base of the copy nearest to a position (the position itself if there is no copy).", + "fullName": "X position of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyX", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? copy.x : x;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "The Y position of the base of the copy nearest to a position (the position itself if there is no copy).", + "fullName": "Y position of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyY", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? copy.y : y;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "The Z position of the base of the copy nearest to a position (on the ground).", + "fullName": "Z position of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyZ", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? copy.z : object.getZ();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "the distance (on X and Y) from a position to the base of the nearest copy. It's a very big number when there is no copy.", + "fullName": "Distance to the nearest copy", + "functionType": "ExpressionAndCondition", + "group": "Copies", + "name": "NearestCopyDistance", + "sentence": "the distance from _PARAM1_; _PARAM2_ to the nearest copy", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? Math.hypot(copy.x - x, copy.y - y) : Number.MAX_VALUE;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + } + ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + }, + { + "folderName": "Paint", + "children": [ + { + "functionName": "AddCopies" + }, + { + "functionName": "EraseCopies" + } + ] + }, + { + "folderName": "Copies", + "children": [ + { + "functionName": "CopyCount" + }, + { + "functionName": "NearestCopyX" + }, + { + "functionName": "NearestCopyY" + }, + { + "functionName": "NearestCopyZ" + }, + { + "functionName": "NearestCopyDistance" + } + ] + } + ] + }, + "propertyDescriptors": [ + { + "value": "", + "type": "Resource", + "label": "3D model", + "description": "The model copied everywhere (a .glb file). Models standing on the Y axis, like most models of the asset store, are stood up along Z.", + "group": "Model", + "extraInformation": [ + "model3D" + ], + "choices": [], + "name": "Model" + }, + { + "value": "300", + "type": "Number", + "unit": "Pixel", + "label": "Height of a copy", + "description": "Copies keep the proportions of the model.", + "group": "Model", + "name": "Size" + }, + { + "value": "0.8", + "type": "Number", + "label": "Smallest random scale", + "description": "Each copy is scaled at random between the smallest and the largest scale.", + "group": "Model", + "name": "MinScale" + }, + { + "value": "1.2", + "type": "Number", + "label": "Largest random scale", + "group": "Model", + "name": "MaxScale" + }, + { + "value": "true", + "type": "Boolean", + "label": "Random rotation", + "description": "Turn each copy around the Z axis at random.", + "group": "Model", + "name": "RandomRotation" + }, + { + "value": "255;255;255", + "type": "Color", + "label": "Random tint", + "description": "Each copy is tinted between white and this color, at random. White for no tint.", + "group": "Model", + "name": "RandomTint" + }, + { + "value": "0.3", + "type": "Number", + "label": "Density", + "description": "The number of copies in a square of 100 x 100 pixels where the density is fully painted (0.3 is about one copy every 180 pixels). Copies never overlap their neighbors much: they are placed at random in a grid.", + "group": "Placement", + "name": "Density" + }, + { + "value": "1", + "type": "Number", + "label": "Seed", + "description": "Each seed places copies differently.", + "group": "Placement", + "name": "Seed" + }, + { + "value": "Everywhere", + "type": "Choice", + "label": "Copies grow", + "description": "Everywhere in the area, or only where they are painted with the \"Add copies\" brush (or the edits).", + "group": "Placement", + "choices": [ + { + "label": "Everywhere (erase them with the brush)", + "value": "Everywhere" + }, + { + "label": "Only where painted", + "value": "Painted" + } + ], + "name": "Coverage" + }, + { + "value": "1", + "type": "Choice", + "label": "Grows on terrain layer", + "description": "On a 3D terrain, copies only grow where this layer is painted, away from its borders (so that trees don't stand on roads painted with another layer). A new terrain has layer 1 painted everywhere.", + "group": "Ground", + "choices": [ + { + "label": "Any", + "value": "0" + }, + { + "label": "Layer 1", + "value": "1" + }, + { + "label": "Layer 2", + "value": "2" + }, + { + "label": "Layer 3", + "value": "3" + }, + { + "label": "Layer 4", + "value": "4" + } + ], + "name": "GroundLayer" + }, + { + "value": "30", + "type": "Number", + "unit": "DegreeAngle", + "label": "Maximum slope", + "description": "No copies grow on steeper ground.", + "group": "Ground", + "name": "MaxSlope" + }, + { + "value": "-100000", + "type": "Number", + "unit": "Pixel", + "label": "Minimum ground Z", + "description": "No copies grow where the ground is lower (for example, under the water level).", + "group": "Ground", + "name": "MinGroundZ" + }, + { + "value": "100000", + "type": "Number", + "unit": "Pixel", + "label": "Maximum ground Z", + "description": "No copies grow where the ground is higher (for example, above the tree line).", + "group": "Ground", + "name": "MaxGroundZ" + }, + { + "value": "false", + "type": "Boolean", + "label": "Trunk collisions", + "description": "Give each copy a static collision shape around its trunk, for the 3D physics behavior (add it to this object, with the \"Static\" type) and navigation meshes. Without them, these behaviors have no effect on this object.", + "group": "Collisions", + "name": "TrunkCollisions" + }, + { + "value": "30", + "type": "Number", + "unit": "Pixel", + "label": "Trunk diameter", + "description": "The diameter of the collision shape of a copy at the height given in \"Height of a copy\" (it is scaled with the copy).", + "group": "Collisions", + "name": "TrunkDiameter" + }, + { + "value": "8000", + "type": "Number", + "unit": "Pixel", + "label": "Draw distance", + "description": "Copies shrink and are hidden at this distance from the camera.", + "group": "Rendering", + "name": "DrawDistance" + }, + { + "value": "true", + "type": "Boolean", + "label": "Cast shadows", + "group": "Rendering", + "name": "CastShadow" + }, + { + "value": "true", + "type": "Boolean", + "label": "Receive shadows", + "group": "Rendering", + "name": "ReceiveShadow" + }, + { + "value": "", + "type": "MultilineString", + "label": "Edits", + "description": "Where copies are added or erased, applied in order, as a JSON list, for example [{\"tool\": \"add\", \"x\": 0.3, \"y\": 0.4, \"radius\": 0.15}, {\"tool\": \"erase\", \"x\": 0.1, \"y\": 0.9, \"toX\": 0.8, \"toY\": 0.2, \"radius\": 0.02}]. Tools: add (density: from 0 to 1, 1 by default, how dense copies are compared to the Density property) and erase. strength (from 0 to 1) makes a stroke lighter. x, y and radius are fractions (0 to 1) of the object size (x is (scene X - object X) / object width). Add toX and toY to paint along a line (a row of trees, a path through a forest). With \"Copies grow: only where painted\", only the added areas have copies. Shown in the scene editor.", + "group": "Placement", + "advanced": true, + "name": "Edits" + }, + { + "value": "", + "type": "String", + "label": "Painted data", + "description": "Written by the brushes of the scene editor, in a compact format not meant to be read or written by hand: use Edits instead. Clear it to go back to the coverage and the edits.", + "advanced": true, + "name": "PaintData" + } + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Model", + "children": [ + { + "propertyName": "Model" + }, + { + "propertyName": "Size" + }, + { + "propertyName": "MinScale" + }, + { + "propertyName": "MaxScale" + }, + { + "propertyName": "RandomRotation" + }, + { + "propertyName": "RandomTint" + } + ] + }, + { + "folderName": "Placement", + "children": [ + { + "propertyName": "Density" + }, + { + "propertyName": "Seed" + }, + { + "propertyName": "Coverage" + }, + { + "propertyName": "Edits" + } + ] + }, + { + "folderName": "Ground", + "children": [ + { + "propertyName": "GroundLayer" + }, + { + "propertyName": "MaxSlope" + }, + { + "propertyName": "MinGroundZ" + }, + { + "propertyName": "MaxGroundZ" + } + ] + }, + { + "folderName": "Collisions", + "children": [ + { + "propertyName": "TrunkCollisions" + }, + { + "propertyName": "TrunkDiameter" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "DrawDistance" + }, + { + "propertyName": "CastShadow" + }, + { + "propertyName": "ReceiveShadow" + } + ] + }, + { + "propertyName": "PaintData" + } + ] + }, + "variants": [] + } + ] } ], "externalLayouts": [] diff --git a/examples/starting-3d-endless-runner/assets/StartingWall.png b/examples/starting-3d-endless-runner/assets/StartingWall.png index b3a097887..06278d6e5 100644 Binary files a/examples/starting-3d-endless-runner/assets/StartingWall.png and b/examples/starting-3d-endless-runner/assets/StartingWall.png differ diff --git a/examples/starting-3d-fnaf-five-nights-at-freddys/assets/StartingWall.png b/examples/starting-3d-fnaf-five-nights-at-freddys/assets/StartingWall.png index b3a097887..06278d6e5 100644 Binary files a/examples/starting-3d-fnaf-five-nights-at-freddys/assets/StartingWall.png and b/examples/starting-3d-fnaf-five-nights-at-freddys/assets/StartingWall.png differ diff --git a/examples/starting-3d-grand-theft-auto/assets/StartingWall.png b/examples/starting-3d-grand-theft-auto/assets/StartingWall.png index b3a097887..06278d6e5 100644 Binary files a/examples/starting-3d-grand-theft-auto/assets/StartingWall.png and b/examples/starting-3d-grand-theft-auto/assets/StartingWall.png differ diff --git a/examples/starting-3d-grand-theft-auto/starting-3d-grand-theft-auto.json b/examples/starting-3d-grand-theft-auto/starting-3d-grand-theft-auto.json index 6352a1a8a..136cb142d 100644 --- a/examples/starting-3d-grand-theft-auto/starting-3d-grand-theft-auto.json +++ b/examples/starting-3d-grand-theft-auto/starting-3d-grand-theft-auto.json @@ -1,7 +1,7 @@ { "firstLayout": "", "gdVersion": { - "build": 283, + "build": 284, "major": 5, "minor": 6, "revision": 0 @@ -262,6 +262,14 @@ "name": "assets/StartingWall.png", "smoothed": true, "userAdded": false + }, + { + "file": "assets/StartingGround.png", + "kind": "image", + "metadata": "", + "name": "assets/StartingGround.png", + "smoothed": true, + "userAdded": false } ] }, @@ -278,6 +286,7 @@ "mangledName": "Game_32Scene", "name": "Game Scene", "r": 255, + "renderer3DWorldScale": 100, "standardSortMethod": true, "stopSoundsOnStartup": true, "title": "", @@ -332,24 +341,6 @@ } ], "instances": [ - { - "angle": 0, - "customSize": true, - "depth": 20, - "height": 1920, - "layer": "", - "locked": true, - "name": "Ground", - "persistentUuid": "e0e7cbb7-baa3-44b9-b3ff-5e98f24aa0e5", - "width": 3840, - "x": -1280, - "y": -448, - "z": -20, - "zOrder": 11, - "numberProperties": [], - "stringProperties": [], - "initialVariables": [] - }, { "angle": 0, "customSize": true, @@ -918,6 +909,22 @@ "numberProperties": [], "stringProperties": [], "initialVariables": [] + }, + { + "angle": 0, + "customSize": true, + "height": 1920, + "layer": "", + "locked": true, + "name": "Terrain", + "persistentUuid": "de32d185-b6ec-454b-a22d-843fba65a40b", + "width": 3840, + "x": -1280, + "y": -448, + "zOrder": 0, + "numberProperties": [], + "stringProperties": [], + "initialVariables": [] } ], "objects": [ @@ -1597,75 +1604,6 @@ "tileScale": 0.2 } }, - { - "assetStoreId": "", - "name": "Ground", - "persistentUuid": "52b2bedf-3947-4c9b-82c7-bb19fc929c7b", - "type": "Scene3D::Cube3DObject", - "variables": [], - "effects": [], - "behaviors": [ - { - "name": "Physics3D", - "type": "Physics3D::Physics3DBehavior", - "object3D": "Object3D", - "bodyType": "Static", - "bullet": false, - "fixedRotation": false, - "shape": "Box", - "meshShapeResourceName": "", - "shapeOrientation": "Z", - "shapeDimensionA": 0, - "shapeDimensionB": 0, - "shapeDimensionC": 0, - "shapeOffsetX": 0, - "shapeOffsetY": 0, - "shapeOffsetZ": 0, - "massCenterOffsetX": 0, - "massCenterOffsetY": 0, - "massCenterOffsetZ": 0, - "massOverride": 0, - "density": 1, - "friction": 0.3, - "restitution": 0.1, - "linearDamping": 0.1, - "angularDamping": 0.1, - "gravityScale": 1, - "layers": 17, - "masks": 17 - } - ], - "content": { - "width": 1000, - "height": 1000, - "depth": 40, - "enableTextureTransparency": false, - "facesOrientation": "Y", - "frontFaceResourceName": "assets\\StartingGround.png", - "backFaceResourceName": "", - "backFaceUpThroughWhichAxisRotation": "X", - "leftFaceResourceName": "assets\\StartingWall.png", - "rightFaceResourceName": "assets\\StartingWall.png", - "topFaceResourceName": "assets\\StartingWall.png", - "bottomFaceResourceName": "assets\\StartingWall.png", - "frontFaceVisible": true, - "backFaceVisible": false, - "leftFaceVisible": false, - "rightFaceVisible": false, - "topFaceVisible": false, - "bottomFaceVisible": false, - "frontFaceResourceRepeat": true, - "backFaceResourceRepeat": false, - "leftFaceResourceRepeat": false, - "rightFaceResourceRepeat": false, - "topFaceResourceRepeat": false, - "bottomFaceResourceRepeat": false, - "materialType": "StandardWithoutMetalness", - "isCastingShadow": true, - "isReceivingShadow": true, - "tileScale": 0.2 - } - }, { "assetStoreId": "", "name": "ControlsToggle", @@ -1901,6 +1839,59 @@ ] } ] + }, + { + "assetStoreId": "", + "name": "Terrain", + "persistentUuid": "5fa0e966-0e9a-4a61-8fa8-8c45812cbd68", + "type": "Terrain3D::Terrain3D", + "variant": "", + "variables": [], + "effects": [], + "behaviors": [ + { + "name": "Physics3D", + "type": "Physics3D::Physics3DBehavior", + "object3D": "Object3D", + "bodyType": "Static", + "bullet": false, + "fixedRotation": false, + "shape": "Box", + "meshShapeResourceName": "", + "shapeOrientation": "Z", + "shapeDimensionA": 0, + "shapeDimensionB": 0, + "shapeDimensionC": 0, + "shapeOffsetX": 0, + "shapeOffsetY": 0, + "shapeOffsetZ": 0, + "massCenterOffsetX": 0, + "massCenterOffsetY": 0, + "massCenterOffsetZ": 0, + "massOverride": 0, + "density": 1, + "friction": 0.3, + "restitution": 0.1, + "linearDamping": 0.1, + "angularDamping": 0.1, + "gravityScale": 1, + "layers": 17, + "masks": 17 + } + ], + "content": { + "SculptData": "2;256;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AQABAQIBAgIAAQEAAQEAAQIBAAICAQEEAQEAVQIAAQIAAQIBAgIDAgIAAQIAARAAAQEAAQECAQABAQIAAQECAQEFBAACAgABAQADAQABAQACAQAFAgAEAQIAAQICAgEDAgIAAQECBgABAwABCAMAAQMCBAMGAwACAQEMAQMHAAICAAEBGhkEAwIABAECAQYDAQAEAQIABAEGAwABAgMCAAECAwIAAgEAAgQFAgQAAQEDAgAEAgMEAAEDAgIAAQUCAggHAAIBAgEAAgQBAQEAVwICAAEBAgIAAgQFAgIAAQ4AAQEAAwEAAQQDAAIFAQICAAECAAEBAQQDAQEAFgIEAQADAgEGAgcCCAEBAAEBBgECAAECBQACDAEJAAEDAgACAR4ZAgUCAgEBAAEBAgADAgECAQQAAQUEAQAEAQABBAMCAAEBAAMBBAEBBgMAAgIAAQMEAQEEAwIBAgABAgMAAQQDAgAFAwIAAgEAXAQAAwEGAwQBAAEEDgECAQABAwQCBQQHAQIGAwIBAQADAwAdAgIAAgEGBgcAAQYBAAEBAAECAgABAQABBAUAAQoDBQEAAwIAAQEEAQABAwIAAQIBAAMBAgACAgMAAQIBAgABAQEEAAQBAgMEAQIAAgEAAwECAgABAQACAQABBAMAAQIAAQMCAgEABgIAAQEBAF8EAgACAQABAgACAgwAAgECAgMAAQEFAgIAAwEAAgEBACICAgABAgQFAgYFBAEAAgIAAQIDAAIEAgMBAwABAgACHBsBAgACAgMEAAIDAAECAAEBAgACAgMAAQICAQACAwIBBgMEAwQFAgACAQABAgEEAQABAQABBgMBAgIDAAECAAEBAgECAAEBAAMBAGICAAIEAgEBAgACDgABBQQBAQQHAQQAAQIAAQMCAQEBACgECAUAAQYDAAEDCAMAAQQDCAUDDAEFAAEDAQIBAhgVAwQBAgEBAAICAAEBAgEAAQIBAAICAQIBAAICAwIBBAMAAQIAAgECAAECAgEDAgEEAwABAgEBBAEABwQFAGcCAgECAAECAAEBAgoBBAABAwIFAAECAAIBAAIDAC0GAQABCAUGBwQCAQYJBAEAAQwCDRAFBQUCBAEBBAwCBAkJAQQCBgcBAgMCAw4PAgECAQICAAIDBgABAQMCAQABAgABAQABAgECAgMCAAEBAgEAAQQDBgMCBQIBAGkCAAECAQQBAAEEAQgEAQABAQMAAQIMDQIAAQMBAC8EAgICAgYJBBQbCAABDg8IFAEkPygBKzoHKwMOHQEEJBcGAw02AysIEhECAgMCEBIjHAgFHx4GHxgBFRgbHBUOERYVEA0KDxQDDQwJCA0KAQMAAQQFAGkEAQIAAgIAAQIMAwIFAQQAAgUIAwMAMgIBBgUIAwABBQoUGQUKBgkDAgQWChkAARliRwUAAQoFEBEFEAsRDhYGIQwLNCMFEgcOCwcGHCMBAAEGAgMAAQIFAQACAQIBBAIKDwIKCwACBAMAAQQAAQMAawIAAQIBAAMIAgUBBAABAQABAwA3AgYHEgABBxQRHgMIDwABFDE+OQNAOzw3QhMaDQgpKAMREhsHDgsOCAUrJgQjCxIRAAICAQABIB8AARoZFhUWERATEA8MCwIBAAEEAwgHAgEAAQIBAGwEAQECAgECCgUBBAQFAgEDAPIBBAACAQIAAQQBBAIFAgABAQEA8gECAgIAAQEAAQIEAgMAAgMA9QECAAICAAEGAAEDAgABAwD2AQIAAQECAgYBAAIDAPgBBAEAAgICAgUA+gEEAAEDBAEBAPoBAgACAgQAAQMA+wECAAICAQD8AQIBAgIBAPwBAgIBAP8BAgCBAgIA/wECAQD/AQIAgQICAIACAgCBBAIAjBoBAIIGAQCABAECAP8BAgCCBAEA/wEBAgD/AQIDAIACAQD/AQMEAP8BAQD/AQEAggICAP0BAQEEAQCAAgEA/gECAQUA/gEBAwgA/AEBAAEDBgD9AQIDAgEBAPwBAQICAwD9AQMEAwYA/AEBBAMCAAECAPsBAgMCAgABBwD7AQECAgIHBgD7AQICAwUEAQD8AQEDBgEBAP0BBAkEBAD7AQEAAQEIAwD8AQMAAgICAgD6AQUEAAEEAwABAQD6AQIBAAEDAgEBAPoBAgIDAAEDAAEEAPoBAwABBAABAQYDAPoBAgMDAggBAwD6AQMAAQYCBwEGAPoBAgQDBwYBAPsBBAUCAgECAwD6AQECAQYCBQEA+gEDBAQDBwgGAPoBBAEFAAEIAQD8AQMEAgEBAPsBAwYAAQEAAgMA/gEBAAEEAPoBBAEDAAECBAEA+gEDAAEEAgIFAQD9AQEBAgD+AQEBAgQDAgIA+AEBAwIEAAIHAgYA9wEBAAEEAAMCAAEBAAECAPYBAQQAAgIBAQECAwQA9gEEAAEBAgEAAgECAQMGAPUBAgECAAEBBAMAAQECAAEDBAD0AQIAAQIDBAMEAwIAAQIBAAECAPQBAgECAQIAAQQDAQIAAQEAAQIA8gECAAIBAgEAAQIBAAEBAgD4AQIDAgEAAQECAwYBAPQBAgEBAgAEAwQAAQMEAAEBAgDzAQIAAQEAAwQDAAEBAgD2AQIDBAABAgABAgEAAQIDAgD0AQEAAQIAAQICAgABAQIBAPcBAQICAQEA+wECAAIDAgIBAgIAAQECAQD1AQIAAQIBAgACAgEEAQEA9AEEBQICAgMAAQIBAgABAQIAAQEA8gEBBgABAQECAAECAAIBAAEBAgDzAQICAwABAgEAAQMAAQYBAAECAQD1AQICAQECAAIFBAIBAPYBAgMCAgIAAwEBAgD3AQIAAgIBAgABAQIBAPYBAgADBAEAAQEA+wECAAICAQABAgEA+AECAQQCAAEBAgEA+QECAAEBAAICAQD7AQIDAgABAQD9AQIAgAIBAIACAgCAAgIAgAICAP8BAgMAgAICAIEEAgCDCAIA5hoDAAICAQABAgIA+QEBAAMCAgEBAAECAAEDBAD0AQQAAgMBAwACAgABAgIBAAICAPUBAgABAQABAwEBAgABBgABAQIA8AEBAgECAQQCAQACAQACBwABAgQAAQIBAgDtAQMCAgIFAAECBAMCAAECAgEDAwABBAQBAOwBAwABBAUEBgEAAQEEAAEBAQABAgABAwABBQABCADrAQcCBAEEAQUEAAICAQYDAgECAgABAQMFAgYCAOgBAwIBAQABAQYCAAIBAgMEAwQCAQACAgMBAwIGAOgBAgEAAwEBAAEBAgQAAQMCAAEBBAEEAAECAQMDAQYCAOYBBAACAQACAQABAgEFAAEEAAEBAAICAAECAAECAAEBAQMGAAEBAgDjAQECAAEEAQAFAgEBAAQBAQABAgABAgIDAQMCBgECAOYBAgABAQAEAgEABAIBAQACAgABAgEDAgMGAQIA4QEBAAECAwICAAYCAQACAQICAQAFAgEBAgcIAQIA4gEBBAEAAQIABAECAgABAQABAQABBAMABAICAwEGCQgBAOABAgACAwADAgABAgECAQACAgEAAgECAAMCAQADAwQHCADfAQIAAQEAAQIBAAIDAgQEAQIAAgICAAECAAMBAAEBAAICAAIBAAEHAN8BCgABAgACAgEAAQIBBQECAQACAQACAQDinQEBAIACAQD/AQUA/wEDAAEBAP0BAQMAAQMA/AEBAwACAgECAPkBAwABAwQAAgECAPkBAwECAQABAQMEAPcBAQMCAQIBAwQDAQYA9QEBAQEAAQIDBAEBAwABBAD1AQUAAgIDBAMBAAEDAAECAgDyAQEFBAACAQABAwMEBQUEAQQA8QEDAQQAAQEBAAEHAgADAwEBAgYA7wEDAgABAwEDAAIEAQIBAAEJBAICAgDtAQEBAAEBAAEBAQIAAQUGAQEAAQIFAAQGAOwBAQMBAAEBAQQBAgIHAgIBAwcCAAEDBgICAOgBAQEDAAEBAgABAQYDAgcCBAEBAQEJAAEEAgMCBAIA5gEBAwEAAgIAAQEEAwEDBgEAAgUAAQoA6aECBAACAQIAAQEBAPkBAgADAQECAgABAQABBAMA9AEDAAIEAgQAAgEAAQEBAgACAQD1AQEAAQIAAQQCAgEAAQUAAQIBAPABAgECAQIDAQIAAgIAAggAAQEDAAEBAgEA7QEEAQEBBgABAQMEAQABAQECBAQAAQMDAgDsAQQAAQMGAwUCAAECAwABAgIAAQEAAQQAAQYAAQcA6wEIAQMCAwIGAwACAQIFBAECAQEAAQIEBgEFAQDoAQQBAgIAAQIFAQACAgEEAwQDAQIAAgEEAgQBBQDoAQECAAMCAgABAgEDAAEEAQABAgMCAwABAQIEBAIFAQDmAQMAAgIAAgIAAQECBgABAwABAgACAQABAQABAQABAgIEBQABAgEA4wECAQABAwIABQECAgAEAgIAAQEAAQEBBAIEAQUCAQDmAQEAAQIABAECAAQBAgIAAgEAAQECBAEEBQIBAOEBAgABAQQBAQAGAQIAAgIBAQIABQECAgEIBwIBAOIBAgMCAAEBAAQCAQEAAQIAAQIAAQMEAAQBAQQCBQoHAgDgAQEAAgQAAwEAAQECAQIAAgECAAICAQADAQIAAwQDCAcA3wEBAAECAAEBAgACBAEDAwIBAAIBAQABAQADAgABAgACAQACAgABCADfAQkAAQEAAgECAAEBAgYCAQIAAgIAAgIA9L8BqgERBw8JCQkHCQcJAwUFBQUDBQACAQEBAQEBAQABAQABAQEABAIAAQIAAgIBAgABAgEAAQQEAQIGBAYCAgoECAIGAgoKBAgGAgIODgABBAMDAQEBIAUCAAIHAQcCAQUBBQABAQUBAAIDAAEDAwEDAQEAAQMDAgMAAQMAAQEBAwIFAgUCAwMCAwABBQACAwABAQABBQABAQMBAAEBAwABAgEHAQACAwEBAgABAwIDAQBVAgIEBAYGCggICggKDhATBgUGAQIAAwICAQABAgACBAQBAQACAQADAgECAAICAAYBAAIDBAECAAEEAAEDAwQAAQUAAgQCCQYBBAUEAQUGAQABBAABCwABDAIBAgABAgIlIAECAAECAQQDAgEBBgIDAQQAAQEAAQEEAAEBAAECAAQBAgECAgMCAgMCAQIBAgIDBAACAwIEBQIAAQIBAgABAgECAQEAAQIAAQIAAQECAAIBAgIDAgIAVQEAAQEAAQECAQEEAQEAAQEAAQ8AAQIAAQIBAgABAgEAAQIBAgIGAwACAQABAgADAgABAgACAgAFAQAEAgEAAQEBAQIEAQEAAQIBBQABBAABBwQAAQQBAwQFBAACAgILAgQIAAIBAAECGRoDBAEABAIBAgUEAgAEAgEABAIFBAABAQQBAAEBBAEAAgIAAgMGAQMAAQIEAQAEAQQDAAEEAQEAAQYBAQcIAAICAQIAAgMCAgIAVwEBAAECAQEAAgMGAQEAAQ0AAQIAAwIAAQMEAAIGAgEBAAEBAAECAgMEAgIAFgEDAgADAQIFAQgBBwICAAECBQIBAAEBBgACCwIKAAEEAQACAh0aAQYBAQICAAECAQADAQIBAgMAAQYDAgAEAgABAwQBAAECAAMCAwICBQQAAgEAAQQDAgIDBAECAQABAQQAAQMEAQAFBAEAAgIAXAMAAwIFBAMCAAEDDQIBAgABBAMBBgMIAgEFBAECAgADBAAdAQEAAgIFBQgAAQUCAAECAAEBAQABAgABAwYAAQkEBgIAAwEAAQIDAgABBAEAAQECAAMCAQACAQQAAQECAQABAgIDAAQCAQQDAgEAAgIAAwIBAQABAgACAgABAwQAAQEAAQQBAQIABgEAAQICAF8DAQACAgABAQACAQsAAgIBAQQAAQIGAQEAAwIAAgICACIBAQABAQMGAQUGAwIAAgEAAQEEAAIDAQQCBAABAQACGxwCAQACAQQDAAIEAAEBAAECAQACAQQAAQEBAgACBAECBQQDBAMGAQACAgABAQIDAgABAgABBQQCAQEEAAEBAAECAQIBAAECAAMCAGIBAAIDAQICAQACDQABBgMCAgMIAgMAAQEAAQQBAgICACgDBwYAAQUEAAEEBwQAAQMEBwYECwIGAAEEAgECARcWBAMCAQICAAIBAAECAQIAAQECAAIBAgECAAIBBAECAwQAAQEAAgIBAAEBAQIEAQIDBAABAQICAwIABwMGAGcBAQIBAAEBAAECAQkCAwABBAEGAAEBAAICAAIEAC0FAgABBwYFCAMBAgUKAwIAAQsBDg8GBgYBAwICAwsBAwoKAgMBBQgCAQQBBA0QAQIBAgEBAAIEBQABAgQBAgABAQABAgABAQIBAQQBAAECAQIAAQMEBQQBBgECAGkBAAEBAgMCAAEDAgcDAgABAgQAAQELDgEAAQQCAC8DAQEBAQUKAxMcBwABDRAHEwIjQCcCLDkILAQNHgIDIxgFBA41BCwHERIBAQQBDxEkGwcGIB0FIBcCFhccGxYNEhUWDw4JEBMEDgsKBw4JAgQAAQMGAGkDAgEAAgEAAQELBAEGAgMAAgYHBAQAMgECBQYHBAABBgkTGgYJBQoEAQMVCRoAARphSAYAAQkGDxIGDwwSDRUFIgsMMyQGEQgNDAgFGyQCAAEFAQQAAQEGAgACAgECAwEJEAEJDAACAwQAAQMAAQQAawEAAQECAAMHAQYCAwABAgABBAA3AQUIEQABCBMSHQQHEAABEzI9OgQ/PDs4QRQZDgcqJwQSERwIDQwNBwYsJQMkDBESAAIBAgABHyAAARkaFRYVEg8UDxALDAECAAEDBAcIAQIAAQECAGwDAgIBAQIBCQYCAwMGAQIEAPIBAwACAgEAAQMCAwEGAQABAgIA8gEBAQEAAQIAAQEDAQQAAgQA9QEBAAIBAAEFAAEEAQABBAD2AQEAAQIBAQUCAAIEAPgBAwIAAgEBAQYA+gEDAAEEAwICAPoBAQACAQMAAQQA+wEBAAIBAgD8AQECAQECAPwBAQECAP8BAQCBAgEA/wEBAgD/AQEAgQIBAIACAQCBBAEAjBoCAIIGAgCABAIBAP8BAQCCBAIA/wECAQD/AQEEAIACAgD/AQQDAP8BAgD/AQIAggIBAP0BAgIDAgCAAgIA/gEBAgYA/gECBAcA/AECAAEEBQD9AQEEAQICAPwBAgEBBAD9AQQDBAUA/AECAwQBAAEBAPsBAQQBAQABCAD7AQIBAQEIBQD7AQEBBAYDAgD8AQIEBQICAP0BAwoDAwD7AQIAAQIHBAD8AQQAAgEBAQD6AQYDAAEDBAABAgD6AQECAAEEAQICAPoBAQEEAAEEAAEDAPoBBAABAwABAgUEAPoBAQQEAQcCBAD6AQQAAQUBCAIFAPoBAQMECAUCAPsBAwYBAQIBBAD6AQIBAgUBBgIA+gEEAwMECAcFAPoBAwIGAAEHAgD8AQQDAQICAPsBBAUAAQIAAgQA/gECAAEDAPoBAwIEAAEBAwIA+gEEAAEDAQEGAgD9AQICAQD+AQICAQMEAQEA+AECBAEDAAIIAQUA9wECAAEDAAMBAAECAAEBAPYBAgMAAgECAgIBBAMA9gEDAAECAQIAAgIBAgQFAPUBAQIBAAECAwQAAQIBAAEEAwD0AQEAAQEEAwQDBAEAAQECAAEBAPQBAQIBAgEAAQMEAgEAAQIAAQEA8gEBAAICAQIAAQECAAECAQD4AQEEAQIAAQIBBAUCAPQBAQICAQAEBAMAAQQDAAECAQDzAQEAAQIAAwMEAAECAQD2AQEEAwABAQABAQIAAQEEAQD0AQIAAQEAAQEBAQABAgECAPcBAgEBAgIA+wEBAAIEAQECAQEAAQIBAgD1AQEAAQECAQACAQIDAgIA9AEDBgEBAQQAAQECAQABAgEAAQIA8gECBQABAgIBAAEBAAICAAECAQDzAQEBBAABAQIAAQQAAQUCAAEBAgD1AQEBAgIBAAIGAwECAPYBAQQBAQEAAwICAQD3AQEAAgECAQABAgECAPYBAQADAwIAAQIA+wEBAAIBAgABAQIA+AEBAgMBAAECAQIA+QEBAAECAAIBAgD7AQEEAQABAgD9AQEAgAICAIACAQCAAgEAgAIBAP8BAQQAgAIBAIEEAQCDCAEA7NoBAgCAAgIA/wEGAP8BBAABAgD9AQIEAAEEAPwBAgQAAgECAQD5AQQAAQQDAAICAQD5AQQCAQIAAQIEAwD3AQIEAQIBAgQDBAIFAPUBAgICAAEBBAMCAgQAAQMA9QEGAAIBBAMEAgABBAABAQEA8gECBgMAAgIAAQQEAwYGAwIDAPEBBAIDAAECAgABCAEAAwQCAgEFAO8BBAEAAQQCBAACAwIBAgABCgMBAQEA7QECAgABAgABAgIBAAEGBQICAAEBBgAEBQDsAQIEAgABAgIDAgEBCAEBAgQIAQABBAUBAQDoAQICBAABAgEAAQIFBAEIAQMCAgICCgABAwEEAQMBAOYBAgQCAAIBAAECAwQCBAUCAAIGAAEJAIGEBACBhAQAgYQEAIGEBACBhAQ=", + "Layer1Texture": "assets/StartingGround.png", + "Layer1Color": "255;255;255", + "HeightBlending": 0.5, + "TextureVariation": 0, + "TextureSize": 128, + "CliffLayer": "3", + "Layer2Texture": "", + "Layer3Texture": "", + "CliffAngle": 45, + "CastShadow": false + } } ], "objectsFolderStructure": { @@ -1927,7 +1918,7 @@ "objectName": "PushableBox" }, { - "objectName": "Ground" + "objectName": "Terrain" }, { "objectName": "ControlsToggle" @@ -3858,6 +3849,7 @@ "frontFaceResourceName": "assets/Skybox_Front.png", "leftFaceResourceName": "assets/Skybox_Left.png", "rightFaceResourceName": "assets/Skybox_Right.png", + "top": "Legacy", "topFaceResourceName": "assets/Skybox_Top.png" }, "booleanParameters": {} @@ -4814,7 +4806,6 @@ "name": "ThirdPersonCamera", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -6758,7 +6749,6 @@ "unit": "Second", "label": "Halfway time for rotation", "group": "Catch-up speed", - "quickCustomizationVisibility": "hidden", "name": "RotationHalfwayDuration" }, { @@ -6767,7 +6757,6 @@ "unit": "Second", "label": "Halfway time for elevation rotation", "group": "Catch-up speed", - "quickCustomizationVisibility": "hidden", "name": "ElevationHalfwayDuration" }, { @@ -6776,7 +6765,6 @@ "unit": "Second", "label": "Halfway time on Z axis", "group": "Catch-up speed", - "quickCustomizationVisibility": "hidden", "name": "TranslationZHalfwayDuration" }, { @@ -6834,7 +6822,6 @@ "label": "Follow free area top border on Z axis", "group": "Position", "advanced": true, - "quickCustomizationVisibility": "hidden", "name": "FollowFreeAreaZMax" }, { @@ -6844,7 +6831,6 @@ "label": "Follow free area bottom border on Z axis", "group": "Position", "advanced": true, - "quickCustomizationVisibility": "hidden", "name": "FollowFreeAreaZMin" }, { @@ -6894,14 +6880,12 @@ "value": "true", "type": "Boolean", "label": "Automatically rotate the camera with the object", - "quickCustomizationVisibility": "hidden", "name": "IsRotatingWithObject" }, { "value": "false", "type": "Boolean", "label": "Automatically rotate the camera with the object (elevation)", - "quickCustomizationVisibility": "hidden", "name": "IsElevatingWithObject" }, { @@ -10982,7 +10966,6 @@ "name": "PlatformerMultitouchMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -11236,7 +11219,6 @@ "name": "Platformer3DMultitouchMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -11411,7 +11393,6 @@ "name": "Shooter3DMultitouchMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -11575,7 +11556,6 @@ "name": "FirstPersonMultitouchMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -12780,7 +12760,6 @@ "name": "PhysicsCar3DMultitouchMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -12993,7 +12972,6 @@ "name": "TopDownMultitouchMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -15073,7 +15051,6 @@ "name": "PhysicsCar3DKeyboardMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -28658,6 +28635,7802 @@ } ], "eventsBasedObjects": [] + }, + { + "author": "", + "category": "General", + "dimension": "3D", + "extensionNamespace": "", + "fullName": "3D terrain", + "gdevelopVersion": ">=5.5.222", + "helpPath": "", + "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLXRlcnJhaW4iIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTQsNkwxMC4yNSwxMUwxMy4xLDE0LjhMMTEuNSwxNkM5LjgxLDEzLjc1IDcsMTAgNywxMEwxLDE4SDIzTDE0LDZaIiAvPjwvc3ZnPg==", + "name": "Terrain3D", + "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", + "shortDescription": "A ground with hills and valleys that can be sculpted and painted in the editor, with collisions.", + "version": "0.1.0", + "description": [ + "A 3D terrain object for open worlds, adventure or racing games: hills, mountains, islands or valleys, with painted layers of grass, dirt, rock or sand.", + "", + "- **Sculpt and paint in the scene editor**: select a terrain to show the tools (raise, lower, smooth, flatten and paint 4 layers).", + "- **Start from a relief** (hills, mountains, island) or **from a heightmap image**.", + "- **Steep slopes automatically show rock** (or any layer).", + "- **Natural borders between layers**: stones and tufts of a texture cover the gaps of the other where layers meet (height blending), and the repetition of textures is made less visible (texture variation).", + "- **Physics**: add the **3D physics** behavior to the terrain. Characters and objects collide with the ground exactly. The terrain is always static.", + "- **Pathfinding**: add the **floor/obstacle for pathfinding (navmesh based)** behavior to the terrain, so that characters with the navmesh pathfinding behavior walk on its hills and around its cliffs (in recent GDevelop versions).", + "- **Shape it with the Edits property**: a list of raise, flatten, smooth and paint strokes written in JSON, shown in the scene editor. It's readable and can be changed by hand or by an AI agent, like the relief, the heightmap and the other properties. Actions only change the terrain during the game.", + "- **Change it during the game** with actions (raise, lower, flatten, smooth, paint, along a line for paths and rivers) and read the ground height, slope or layer with expressions.", + "- **Grass**: add a **terrain grass** object over the terrain. Thousands of blades sway in the wind, grow where a layer (like grass) is painted and where the ground is not too steep, and are pushed aside by characters. Sculpt or paint the terrain and the grass follows.", + "- **Forests, rocks and bushes**: add a **scattered models** object over the terrain. It places hundreds or thousands of copies of a 3D model (trees, rocks, bushes, flowers...) on the ground and draws them all at once, which is much faster than an object for each. Copies grow where a layer is painted, away from steep slopes, and follow the ground when it's sculpted. Add or erase them with the brushes of the scene editor or with the Edits property, give trees trunk collisions, and cut them down with actions. Ready-made forests, rocks and bushes are in the asset store.", + "- **Tunnels and caves**: make holes in the ground (and fill them again) with the brushes, the Edits property or actions, and put a 3D model of the tunnel or the cave (with a mesh collision shape) under the ground. Nothing is drawn in holes, and characters, cars and rays go through them.", + "- **Made for large worlds**: the terrain is split in chunks that are hidden when off-screen and simplified when far away, and collisions are updated only where the ground changes." + ], + "origin": { + "identifier": "Terrain3D", + "name": "gdevelop-extension-store" + }, + "tags": [ + "3d", + "terrain", + "ground", + "heightmap", + "landscape", + "open world", + "grass", + "foliage", + "forest", + "trees", + "tunnel", + "cave" + ], + "authorIds": [], + "dependencies": [], + "globalVariables": [], + "sceneVariables": [], + "eventsFunctions": [ + { + "description": "Define helper classes JavaScript code.", + "fullName": "Define helper classes", + "functionType": "Action", + "name": "DefineHelperClasses", + "private": true, + "sentence": "Define helper classes JavaScript code", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "if (gdjs.__terrain3DExtension) {", + " return;", + "}", + "", + "// Tested by the gameplay tests of the \"3D island terrain\" example (GDevelop-examples repository).", + "", + "const groundsByScene = new WeakMap();", + "/**", + " * The grounds of a scene (the terrains), followed by the terrain grass.", + " *", + " * A ground has: `getVersion()` (changing when the ground changes),", + " * `containsPoint(x, y)`, `getHeightAt(x, y)` (Z position of the ground),", + " * `getSlopeAt(x, y)` (in degrees), `getLayerWeightAt(x, y, layer)` (how", + " * much a painted layer, from 1 to 4, is there, from 0 to 1) and optionally", + " * `getTintAt(x, y)` (the painted tint, as red, green and blue from 0 to 255).", + " * @param {gdjs.RuntimeScene} runtimeScene", + " * @returns {Set}", + " */", + "const getGrounds = (runtimeScene) => {", + " let grounds = groundsByScene.get(runtimeScene);", + " if (!grounds) {", + " grounds = new Set();", + " groundsByScene.set(runtimeScene, grounds);", + " }", + " return grounds;", + "};", + "", + "/**", + " * @param {number} seed", + " * @returns {() => number} Random numbers between 0 (included) and 1 (excluded), the same for the same seed.", + " */", + "const createRandom = (seed) => {", + " let state = seed >>> 0 || 1;", + " return () => {", + " state ^= state << 13;", + " state ^= state >>> 17;", + " state ^= state << 5;", + " return (state >>> 0) / 4294967296;", + " };", + "};", + "", + "/**", + " * Find the highest ground at a position: grounds can overlap (like an island over a sea floor).", + " * @param {Iterable} grounds", + " * @param {{ground: object | null, z: number}} result Set to the ground (null if there is none) and its Z position.", + " */", + "const findHighestGround = (grounds, x, y, result) => {", + " result.ground = null;", + " for (const ground of grounds) {", + " if (!ground.containsPoint(x, y)) continue;", + " const z = ground.getHeightAt(x, y);", + " if (!result.ground || z > result.z) {", + " result.ground = ground;", + " result.z = z;", + " }", + " }", + " return result;", + "};", + "", + "// The grounds or the objects placed on them can change every frame (while", + "// sculpting or moving them in the editor): things are placed again at most this often.", + "const PLACEMENT_DELAY = 0.3;", + "", + "/**", + " * Tells when things placed on the grounds of a scene (grass blades, scattered", + " * models) must be placed again: after the grounds or the given properties", + " * changed, at most every `PLACEMENT_DELAY` seconds.", + " */", + "class GroundPlacement {", + " /** @param {gdjs.RuntimeScene} runtimeScene */", + " constructor(runtimeScene) {", + " this.grounds = getGrounds(runtimeScene);", + " this.placedFrom = '';", + " this.timeSincePlacement = 0;", + " }", + "", + " /**", + " * @param {number} elapsedSeconds", + " * @param {string} propertiesKey Changes when the things must be placed again.", + " */", + " shouldPlaceAgain(elapsedSeconds, propertiesKey) {", + " this.timeSincePlacement += elapsedSeconds;", + " if (this.placedFrom && this.timeSincePlacement < PLACEMENT_DELAY) return false;", + " let placementKey = propertiesKey;", + " for (const ground of this.grounds) placementKey += '|' + ground.getVersion();", + " if (placementKey === this.placedFrom) return false;", + " this.placedFrom = placementKey;", + " this.timeSincePlacement = 0;", + " return true;", + " }", + "", + " placeAgain() {", + " this.placedFrom = '';", + " }", + "}", + "", + "/**", + " * Replace parts of the shader code of a Three.js material, with an error if", + " * a part is missing (for example after an update of Three.js).", + " * @param {string} shaderCode", + " * @param {Array<[string, string]>} replacements The searched code and its replacement.", + " * @param {string} materialName", + " */", + "const patchShaderCode = (shaderCode, replacements, materialName) => {", + " for (const [searchedCode, newCode] of replacements) {", + " if (!shaderCode.includes(searchedCode)) {", + " console.error(`${materialName}: \"${searchedCode}\" was not found in the shader, which won't look as expected.`);", + " }", + " shaderCode = shaderCode.replace(searchedCode, newCode);", + " }", + " return shaderCode;", + "};", + "", + "// Size of the object inner area. Instances are scaled from it, so these only", + "// define the default size of a new terrain.", + "const AREA_SIZE = 4096;", + "const AREA_DEPTH = 600;", + "", + "// Terrains have at most 8 x 8 chunks (so at most 64 draw calls), of at least", + "// 32 x 32 cells.", + "const MIN_CHUNK_CELLS = 32;", + "const MAX_CHUNKS_PER_SIDE = 8;", + "/** @param {number} resolution */", + "const getChunkCells = (resolution) => Math.max(MIN_CHUNK_CELLS, resolution / MAX_CHUNKS_PER_SIDE);", + "// Grid steps of each level of detail, and the distance (in chunk sizes)", + "// from which each one is used.", + "const LOD_STEPS = [1, 2, 4];", + "const LOD_DISTANCES = [0, 3, 6];", + "", + "const LAYER_COUNT = 4;", + "const RESOLUTIONS = [64, 128, 256, 512, 1024];", + "const DEFAULT_RESOLUTION = 256;", + "// Each undo step is a copy of the data: fewer are kept for big terrains.", + "const UNDO_STEPS_SAMPLES = 20 * 257 * 257;", + "", + "const clamp01 = (value) => (value < 0 ? 0 : value > 1 ? 1 : value);", + "const clampInteger = (value, min, max) =>", + " value < min ? min : value > max ? max : Math.floor(value);", + "", + "/**", + " * A rectangle of grid samples, inclusive. Empty when `minI > maxI`.", + " */", + "class SampleRectangle {", + " constructor() {", + " this.clear();", + " }", + "", + " clear() {", + " this.minI = Number.MAX_SAFE_INTEGER;", + " this.minJ = Number.MAX_SAFE_INTEGER;", + " this.maxI = -1;", + " this.maxJ = -1;", + " }", + "", + " isEmpty() {", + " return this.minI > this.maxI;", + " }", + "", + " /** @param {SampleRectangle} other */", + " add(other) {", + " if (other.isEmpty()) return;", + " this.minI = Math.min(this.minI, other.minI);", + " this.minJ = Math.min(this.minJ, other.minJ);", + " this.maxI = Math.max(this.maxI, other.maxI);", + " this.maxJ = Math.max(this.maxJ, other.maxJ);", + " }", + "", + " set(minI, minJ, maxI, maxJ) {", + " this.minI = minI;", + " this.minJ = minJ;", + " this.maxI = maxI;", + " this.maxJ = maxJ;", + " return this;", + " }", + "}", + "", + "/**", + " * The heights (between 0 and 1) and the painted layers weights of a square", + " * grid of `size * size` samples.", + " */", + "class TerrainData {", + " /** @param {number} resolution The number of cells on each side. */", + " constructor(resolution) {", + " this.resolution = resolution;", + " this.size = resolution + 1;", + " this.heights = new Float32Array(this.size * this.size);", + " /** 4 weights (0-255) per sample, one for each layer. */", + " this.splat = new Uint8Array(this.size * this.size * LAYER_COUNT);", + " this.fillLayer(0);", + " /**", + " * 4 bytes per sample: a tint multiplying the layers (red, green and", + " * blue, white for none) and the wetness (0 for dry).", + " */", + " this.colors = new Uint8Array(this.size * this.size * 4);", + " this.clearColors();", + " }", + "", + " clearColors() {", + " this.colors.fill(255);", + " for (let index = 3; index < this.colors.length; index += 4) {", + " this.colors[index] = 0;", + " }", + " }", + "", + " /** @param {number} layer */", + " fillLayer(layer) {", + " this.splat.fill(0);", + " for (let index = layer; index < this.splat.length; index += LAYER_COUNT) {", + " this.splat[index] = 255;", + " }", + " }", + "", + " getHeight(i, j) {", + " const last = this.resolution;", + " const clampedI = i < 0 ? 0 : i > last ? last : i;", + " const clampedJ = j < 0 ? 0 : j > last ? last : j;", + " return this.heights[clampedJ * this.size + clampedI];", + " }", + "", + " /**", + " * @param {number} gridX", + " * @param {number} gridY", + " * @returns {number} The height interpolated on the triangles that are drawn.", + " */", + " sampleHeight(gridX, gridY) {", + " const x = Math.min(Math.max(gridX, 0), this.resolution);", + " const y = Math.min(Math.max(gridY, 0), this.resolution);", + " const i = Math.min(Math.floor(x), this.resolution - 1);", + " const j = Math.min(Math.floor(y), this.resolution - 1);", + " const fx = x - i;", + " const fy = y - j;", + " const topRight = this.getHeight(i + 1, j);", + " const bottomLeft = this.getHeight(i, j + 1);", + " if (fx + fy <= 1) {", + " const topLeft = this.getHeight(i, j);", + " return topLeft + fx * (topRight - topLeft) + fy * (bottomLeft - topLeft);", + " }", + " const bottomRight = this.getHeight(i + 1, j + 1);", + " return (", + " bottomRight +", + " (1 - fx) * (bottomLeft - bottomRight) +", + " (1 - fy) * (topRight - bottomRight)", + " );", + " }", + "", + " /** @returns {number} The weight (0-1) of a layer, interpolated between samples. */", + " sampleLayerWeight(gridX, gridY, layer) {", + " const x = Math.min(Math.max(gridX, 0), this.resolution);", + " const y = Math.min(Math.max(gridY, 0), this.resolution);", + " const i = Math.min(Math.floor(x), this.resolution - 1);", + " const j = Math.min(Math.floor(y), this.resolution - 1);", + " const fx = x - i;", + " const fy = y - j;", + " const weight = (i, j) => this.splat[(j * this.size + i) * LAYER_COUNT + layer];", + " const top = weight(i, j) + fx * (weight(i + 1, j) - weight(i, j));", + " const bottom = weight(i, j + 1) + fx * (weight(i + 1, j + 1) - weight(i, j + 1));", + " return (top + fy * (bottom - top)) / 255;", + " }", + "", + " /**", + " * @param {number} channel 0, 1 and 2 for the tint, 3 for the wetness.", + " * @returns {number} The channel (0-1), interpolated between samples.", + " */", + " sampleColor(gridX, gridY, channel) {", + " const x = Math.min(Math.max(gridX, 0), this.resolution);", + " const y = Math.min(Math.max(gridY, 0), this.resolution);", + " const i = Math.min(Math.floor(x), this.resolution - 1);", + " const j = Math.min(Math.floor(y), this.resolution - 1);", + " const fx = x - i;", + " const fy = y - j;", + " const value = (i, j) => this.colors[(j * this.size + i) * 4 + channel];", + " const top = value(i, j) + fx * (value(i + 1, j) - value(i, j));", + " const bottom = value(i, j + 1) + fx * (value(i + 1, j + 1) - value(i, j + 1));", + " return (top + fy * (bottom - top)) / 255;", + " }", + "", + " /**", + " * @param {number} index The index of a sample.", + " * @returns {boolean} true if no layer is painted on the sample: it's a hole.", + " */", + " isHole(index) {", + " const offset = index * LAYER_COUNT;", + " return (this.splat[offset] | this.splat[offset + 1] | this.splat[offset + 2] | this.splat[offset + 3]) === 0;", + " }", + "", + " /**", + " * @returns {number} The sum of the layer weights (0-1), interpolated between samples: there is a", + " * hole where it's under 0.5, like what is drawn.", + " */", + " sampleSolidity(gridX, gridY) {", + " let solidity = 0;", + " for (let layer = 0; layer < LAYER_COUNT; layer++) solidity += this.sampleLayerWeight(gridX, gridY, layer);", + " return solidity;", + " }", + "", + " /** @returns {number} The index (from 0) of the layer painted the most. */", + " getDominantLayer(gridX, gridY) {", + " const i = clampInteger(Math.round(gridX), 0, this.resolution);", + " const j = clampInteger(Math.round(gridY), 0, this.resolution);", + " const offset = (j * this.size + i) * LAYER_COUNT;", + " let dominantLayer = 0;", + " for (let layer = 1; layer < LAYER_COUNT; layer++) {", + " if (this.splat[offset + layer] > this.splat[offset + dominantLayer]) {", + " dominantLayer = layer;", + " }", + " }", + " return dominantLayer;", + " }", + "", + " /** @param {TerrainData} other */", + " copyFrom(other) {", + " this.heights.set(other.heights);", + " this.splat.set(other.splat);", + " this.colors.set(other.colors);", + " }", + "", + " /**", + " * @param {number} resolution", + " * @returns {TerrainData} A copy of this data with another resolution.", + " */", + " resampled(resolution) {", + " const data = new TerrainData(resolution);", + " const ratio = this.resolution / resolution;", + " for (let j = 0; j < data.size; j++) {", + " for (let i = 0; i < data.size; i++) {", + " const index = j * data.size + i;", + " data.heights[index] = this.sampleHeight(i * ratio, j * ratio);", + " const sourceOffset =", + " (Math.round(j * ratio) * this.size + Math.round(i * ratio)) * LAYER_COUNT;", + " for (let layer = 0; layer < LAYER_COUNT; layer++) {", + " data.splat[index * LAYER_COUNT + layer] = this.splat[sourceOffset + layer];", + " }", + " for (let channel = 0; channel < 4; channel++) {", + " data.colors[index * 4 + channel] = this.colors[sourceOffset + channel];", + " }", + " }", + " }", + " return data;", + " }", + "}", + "", + "/**", + " * Brushes change the samples around a segment, with an effect going smoothly", + " * from full on the segment to none at `radius`. A point is a segment of", + " * length 0. Positions are in scene units relative to the terrain origin.", + " */", + "const brushes = {", + " /**", + " * @param {TerrainData} data", + " * @param {{cellWidth: number, cellHeight: number, ax: number, ay: number, bx: number, by: number, radius: number}} stroke", + " * @param {(sampleIndex: number, weight: number, t: number) => void} apply Called with the weight of", + " * the brush and the position (from 0 to 1) of the nearest point of the segment.", + " * @param {SampleRectangle} changedSamples", + " */", + " forEachSample(data, stroke, apply, changedSamples) {", + " const { cellWidth, cellHeight, ax, ay, bx, by, radius } = stroke;", + " const last = data.resolution;", + " const minI = clampInteger(Math.floor((Math.min(ax, bx) - radius) / cellWidth), 0, last);", + " const maxI = clampInteger(Math.ceil((Math.max(ax, bx) + radius) / cellWidth), 0, last);", + " const minJ = clampInteger(Math.floor((Math.min(ay, by) - radius) / cellHeight), 0, last);", + " const maxJ = clampInteger(Math.ceil((Math.max(ay, by) + radius) / cellHeight), 0, last);", + " changedSamples.set(minI, minJ, maxI, maxJ);", + " if (radius <= 0) return;", + "", + " const abX = bx - ax;", + " const abY = by - ay;", + " const abLengthSquared = abX * abX + abY * abY;", + " const radiusSquared = radius * radius;", + " for (let j = minJ; j <= maxJ; j++) {", + " const y = j * cellHeight;", + " for (let i = minI; i <= maxI; i++) {", + " const x = i * cellWidth;", + " const t =", + " abLengthSquared > 0", + " ? clamp01(((x - ax) * abX + (y - ay) * abY) / abLengthSquared)", + " : 0;", + " const dx = x - (ax + t * abX);", + " const dy = y - (ay + t * abY);", + " const distanceSquared = dx * dx + dy * dy;", + " if (distanceSquared >= radiusSquared) continue;", + " const falloff = 1 - distanceSquared / radiusSquared;", + " apply(j * data.size + i, falloff * falloff, t);", + " }", + " }", + " },", + "", + " raise(data, stroke, amount, changedSamples) {", + " const { heights } = data;", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index, weight) => {", + " heights[index] = clamp01(heights[index] + amount * weight);", + " },", + " changedSamples", + " );", + " },", + "", + " /**", + " * @param {number} targetHeight", + " * @param {number} [endTargetHeight] The target at the end of the segment, for a slope (like a road", + " * going up). The same target as at its start by default.", + " */", + " flatten(data, stroke, targetHeight, strength, changedSamples, endTargetHeight = targetHeight) {", + " const { heights } = data;", + " const startTarget = clamp01(targetHeight);", + " const endTarget = clamp01(endTargetHeight);", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index, weight, t) => {", + " const target = startTarget + (endTarget - startTarget) * t;", + " heights[index] += (target - heights[index]) * clamp01(weight * strength);", + " },", + " changedSamples", + " );", + " },", + "", + " smooth(data, stroke, strength, changedSamples) {", + " const { heights, size } = data;", + " const changes = [];", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index, weight) => {", + " const i = index % size;", + " const j = (index - i) / size;", + " let sum = 0;", + " for (let dj = -1; dj <= 1; dj++) {", + " for (let di = -1; di <= 1; di++) {", + " sum += data.getHeight(i + di, j + dj);", + " }", + " }", + " changes.push(index, heights[index] + (sum / 9 - heights[index]) * clamp01(weight * strength));", + " },", + " changedSamples", + " );", + " // Written after all averages are computed, so that the result", + " // doesn't depend on the order in which samples are visited.", + " for (let index = 0; index < changes.length; index += 2) {", + " heights[changes[index]] = changes[index + 1];", + " }", + " },", + "", + " paint(data, stroke, layer, strength, changedSamples) {", + " const { splat } = data;", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index, weight) => {", + " const amount = clamp01(weight * strength);", + " const offset = index * LAYER_COUNT;", + " for (let otherLayer = 0; otherLayer < LAYER_COUNT; otherLayer++) {", + " const target = otherLayer === layer ? 255 : 0;", + " const value = splat[offset + otherLayer];", + " // Rounded towards the target, so that light strokes still paint.", + " const change = (target - value) * amount;", + " splat[offset + otherLayer] = value + (change > 0 ? Math.ceil(change) : Math.floor(change));", + " }", + " },", + " changedSamples", + " );", + " },", + "", + " /** Remove all the layers, making a hole (`fill` or painting a layer fills it again). */", + " hole(data, stroke, changedSamples) {", + " const { splat } = data;", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index) => {", + " splat.fill(0, index * LAYER_COUNT, (index + 1) * LAYER_COUNT);", + " },", + " changedSamples", + " );", + " },", + "", + " /**", + " * Fill the holes in the radius with the layers of the ground around them:", + " * each pass fills the holes next to the ground with the average of their", + " * neighbors, from the edges of the holes inward.", + " */", + " fill(data, stroke, changedSamples) {", + " const { splat, size, resolution } = data;", + " /** @type {number[]} */", + " let holes = [];", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index) => {", + " if (data.isHole(index)) holes.push(index);", + " },", + " changedSamples", + " );", + " const filledWeights = new Float32Array(holes.length * LAYER_COUNT);", + " while (holes.length > 0) {", + " /** @type {number[]} */", + " const remainingHoles = [];", + " /** @type {number[]} */", + " const filledHoles = [];", + " for (const index of holes) {", + " const i = index % size;", + " const j = (index - i) / size;", + " const weights = filledWeights.subarray(filledHoles.length * LAYER_COUNT, (filledHoles.length + 1) * LAYER_COUNT);", + " weights.fill(0);", + " for (let neighborJ = Math.max(j - 1, 0); neighborJ <= Math.min(j + 1, resolution); neighborJ++) {", + " for (let neighborI = Math.max(i - 1, 0); neighborI <= Math.min(i + 1, resolution); neighborI++) {", + " const neighbor = neighborJ * size + neighborI;", + " if (data.isHole(neighbor)) continue;", + " for (let layer = 0; layer < LAYER_COUNT; layer++) weights[layer] += splat[neighbor * LAYER_COUNT + layer];", + " }", + " }", + " if (weights[0] + weights[1] + weights[2] + weights[3] > 0) filledHoles.push(index);", + " else remainingHoles.push(index);", + " }", + " if (filledHoles.length === 0) {", + " // No ground around (a hole bigger than the brush): the first layer is used.", + " for (const index of remainingHoles) splat[index * LAYER_COUNT] = 255;", + " break;", + " }", + " // Filled after the pass, so that the holes are filled from the edges inward.", + " filledHoles.forEach((index, filledIndex) => {", + " const weights = filledWeights.subarray(filledIndex * LAYER_COUNT, (filledIndex + 1) * LAYER_COUNT);", + " const total = weights[0] + weights[1] + weights[2] + weights[3];", + " for (let layer = 0; layer < LAYER_COUNT; layer++) {", + " splat[index * LAYER_COUNT + layer] = Math.round((weights[layer] / total) * 255);", + " }", + " });", + " holes = remainingHoles;", + " }", + " },", + "", + " /** @param {number[]} color The red, green and blue (0-255) of the tint: white removes it. */", + " tint(data, stroke, color, strength, changedSamples) {", + " brushes.paintColors(data, stroke, 0, color, strength, changedSamples);", + " },", + "", + " /** @param {number} wetness From 0 (dry) to 1. */", + " wet(data, stroke, wetness, strength, changedSamples) {", + " brushes.paintColors(data, stroke, 3, [Math.round(clamp01(wetness) * 255)], strength, changedSamples);", + " },", + "", + " /**", + " * Bring channels of the colors (see `TerrainData.colors`) towards targets.", + " * @param {number} firstChannel", + " * @param {number[]} targets From 0 to 255, for the channels from the first one.", + " */", + " paintColors(data, stroke, firstChannel, targets, strength, changedSamples) {", + " const { colors } = data;", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index, weight) => {", + " const amount = clamp01(weight * strength);", + " for (let channel = 0; channel < targets.length; channel++) {", + " const offset = index * 4 + firstChannel + channel;", + " // Rounded towards the target, so that light strokes still paint.", + " const change = (targets[channel] - colors[offset]) * amount;", + " colors[offset] += change > 0 ? Math.ceil(change) : Math.floor(change);", + " }", + " },", + " changedSamples", + " );", + " },", + "};", + "", + "const relief = (() => {", + " const hash = (x, y, seed) => {", + " let h = Math.imul(x, 374761393) + Math.imul(y, 668265263) + Math.imul(seed, 982451653);", + " h = Math.imul(h ^ (h >>> 13), 1274126177);", + " return ((h ^ (h >>> 16)) >>> 0) / 4294967296;", + " };", + " const valueNoise = (x, y, seed) => {", + " const x0 = Math.floor(x);", + " const y0 = Math.floor(y);", + " const fx = x - x0;", + " const fy = y - y0;", + " const sx = fx * fx * (3 - 2 * fx);", + " const sy = fy * fy * (3 - 2 * fy);", + " const top = hash(x0, y0, seed) + sx * (hash(x0 + 1, y0, seed) - hash(x0, y0, seed));", + " const bottom =", + " hash(x0, y0 + 1, seed) + sx * (hash(x0 + 1, y0 + 1, seed) - hash(x0, y0 + 1, seed));", + " return top + sy * (bottom - top);", + " };", + " /** Fractal noise between 0 and 1. */", + " const fractalNoise = (x, y, seed) => {", + " let value = 0;", + " let amplitude = 0.5;", + " let frequency = 1;", + " for (let octave = 0; octave < 5; octave++) {", + " value += amplitude * valueNoise(x * frequency, y * frequency, seed + octave);", + " amplitude *= 0.5;", + " frequency *= 2;", + " }", + " return value / 0.96875;", + " };", + " const smoothstep = (edge0, edge1, x) => {", + " const t = clamp01((x - edge0) / (edge1 - edge0));", + " return t * t * (3 - 2 * t);", + " };", + " const reliefHeights = {", + " Flat: () => 0,", + " Hills: (u, v, seed) => 0.05 + 0.5 * Math.pow(fractalNoise(u * 3, v * 3, seed), 2),", + " Mountains: (u, v, seed) => {", + " const ridge = 1 - Math.abs(2 * fractalNoise(u * 2.5, v * 2.5, seed) - 1);", + " return 0.9 * Math.pow(ridge, 2.5);", + " },", + " Island: (u, v, seed) => {", + " const distanceToCenter = 2 * Math.hypot(u - 0.5, v - 0.5);", + " const coast = 0.2 * (fractalNoise(u * 4, v * 4, seed + 7) - 0.5);", + " const land = smoothstep(0.95, 0.55, distanceToCenter + coast);", + " return land * (0.12 + 0.6 * Math.pow(fractalNoise(u * 3, v * 3, seed), 1.5));", + " },", + " };", + " const beachLayer = 3;", + "", + " return {", + " /**", + " * @param {TerrainData} data", + " * @param {string} reliefName Flat, Hills, Mountains or Island.", + " * @param {number} seed", + " */", + " generate(data, reliefName, seed) {", + " const getHeight = reliefHeights[reliefName] || reliefHeights.Flat;", + " const integerSeed = Math.floor(seed) | 0;", + " data.fillLayer(0);", + " for (let j = 0; j < data.size; j++) {", + " for (let i = 0; i < data.size; i++) {", + " const index = j * data.size + i;", + " const height = clamp01(", + " getHeight(i / data.resolution, j / data.resolution, integerSeed)", + " );", + " data.heights[index] = height;", + " if (reliefName === 'Island') {", + " const sand = Math.round(255 * smoothstep(0.16, 0.1, height));", + " const offset = index * LAYER_COUNT;", + " data.splat[offset] = 255 - sand;", + " data.splat[offset + beachLayer] = sand;", + " }", + " }", + " }", + " },", + " };", + "})();", + "", + "/**", + " * Edits written in the Edits property: a JSON list of brush strokes, like", + " * `[{\"tool\": \"raise\", \"x\": 0.5, \"y\": 0.5, \"radius\": 0.2, \"height\": 0.3}]`,", + " * applied on the relief or the heightmap. Positions and radius are fractions", + " * of the terrain size, heights and Z positions fractions of its depth: they", + " * are the same for all instances, whatever their size. They are made to be", + " * read and written by people and by AI agents.", + " */", + "const edits = {", + " /**", + " * @param {TerrainData} data", + " * @param {string} text", + " */", + " apply(data, text) {", + " if (!text.trim()) return;", + " let editList;", + " try {", + " editList = JSON.parse(text);", + " } catch (error) {", + " console.warn('The edits of a terrain are not valid JSON: ' + error.message);", + " return;", + " }", + " if (!Array.isArray(editList)) {", + " console.warn('The edits of a terrain must be a JSON list (like [{\"tool\": \"raise\", ...}]).');", + " return;", + " }", + " const numberOr = (value, defaultValue) => (typeof value === 'number' && isFinite(value) ? value : defaultValue);", + " const changedSamples = new SampleRectangle();", + " for (const edit of editList) {", + " if (!edit || typeof edit !== 'object') continue;", + " const x = numberOr(edit.x, 0.5);", + " const y = numberOr(edit.y, 0.5);", + " const stroke = {", + " cellWidth: 1 / data.resolution,", + " cellHeight: 1 / data.resolution,", + " ax: x,", + " ay: y,", + " bx: numberOr(edit.toX, x),", + " by: numberOr(edit.toY, y),", + " radius: numberOr(edit.radius, 0.1),", + " };", + " const strength = clamp01(numberOr(edit.strength, 1));", + " if (edit.tool === 'raise') {", + " brushes.raise(data, stroke, numberOr(edit.height, 0.1), changedSamples);", + " } else if (edit.tool === 'flatten') {", + " const z = numberOr(edit.z, 0);", + " brushes.flatten(data, stroke, z, strength, changedSamples, numberOr(edit.toZ, z));", + " } else if (edit.tool === 'smooth') {", + " // Smoothing the same fraction of the terrain at any resolution.", + " const passes = Math.max(Math.round(((1 + 9 * strength) * data.resolution) / 256), 1);", + " for (let pass = 0; pass < passes; pass++) brushes.smooth(data, stroke, 1, changedSamples);", + " } else if (edit.tool === 'paint') {", + " const layer = clampInteger(numberOr(edit.layer, 1) - 1, 0, LAYER_COUNT - 1);", + " brushes.paint(data, stroke, layer, strength, changedSamples);", + " } else if (edit.tool === 'tint') {", + " const color = gdjs.rgbOrHexToRGBColor(typeof edit.color === 'string' ? edit.color : '255;255;255');", + " brushes.tint(data, stroke, color, strength, changedSamples);", + " } else if (edit.tool === 'wetness') {", + " brushes.wet(data, stroke, numberOr(edit.amount, 1), strength, changedSamples);", + " } else if (edit.tool === 'hole') {", + " brushes.hole(data, stroke, changedSamples);", + " } else if (edit.tool === 'fill') {", + " brushes.fill(data, stroke, changedSamples);", + " } else {", + " console.warn('Unknown tool in the edits of a terrain: \"' + edit.tool + '\" (use raise, flatten, smooth, paint, tint, wetness, hole or fill).');", + " }", + " }", + " },", + "};", + "", + "/**", + " * @param {gdjs.RuntimeGame} game", + " * @param {string} imageResourceName", + " * @returns {CanvasImageSource | null} The image, or null if it's not loaded (yet).", + " */", + "const getLoadedImage = (game, imageResourceName) => {", + " const imageManager = game.getImageManager();", + " if (typeof imageManager.getImageSource === 'function') return imageManager.getImageSource(imageResourceName);", + " // Older GDevelop versions: a 192 x 192 placeholder is given for images that are not loaded.", + " const texture = imageManager.getPIXITexture(imageResourceName);", + " return texture && texture !== imageManager.getInvalidPIXITexture() ? texture.baseTexture.getDrawableSource() : null;", + "};", + "", + "/**", + " * Loads heights from an image: black is the lowest, white the highest.", + " * Heights have 16 bits of precision when the red channel holds the most", + " * significant byte and the green one the least significant (a gray image", + " * works as expected too).", + " * @param {TerrainData} data", + " * @param {gdjs.RuntimeGame} game", + " * @param {string} imageResourceName", + " * @returns {boolean} true if the image could be read.", + " */", + "const loadHeightsFromImage = (data, game, imageResourceName) => {", + " const source = getLoadedImage(game, imageResourceName);", + " if (!source) return false;", + "", + " const canvas = document.createElement('canvas');", + " canvas.width = source.width;", + " canvas.height = source.height;", + " const context = canvas.getContext('2d', { willReadFrequently: true });", + " if (!context) return false;", + " context.drawImage(source, 0, 0);", + " const pixels = context.getImageData(0, 0, source.width, source.height).data;", + " // Pixels are decoded before being interpolated, to keep the 16 bits.", + " const pixelHeight = (x, y) => {", + " const offset = (y * source.width + x) * 4;", + " return (256 * pixels[offset] + pixels[offset + 1]) / 65535;", + " };", + " for (let j = 0; j < data.size; j++) {", + " const y = (j / data.resolution) * (source.height - 1);", + " const y0 = Math.floor(y);", + " const y1 = Math.min(y0 + 1, source.height - 1);", + " for (let i = 0; i < data.size; i++) {", + " const x = (i / data.resolution) * (source.width - 1);", + " const x0 = Math.floor(x);", + " const x1 = Math.min(x0 + 1, source.width - 1);", + " const top = pixelHeight(x0, y0) + (x - x0) * (pixelHeight(x1, y0) - pixelHeight(x0, y0));", + " const bottom = pixelHeight(x0, y1) + (x - x0) * (pixelHeight(x1, y1) - pixelHeight(x0, y1));", + " data.heights[j * data.size + i] = top + (y - y0) * (bottom - top);", + " }", + " }", + " return true;", + "};", + "", + "/**", + " * Stores the data in a string. Only the differences with the base data (from", + " * the relief or the heightmap) are stored: heights on 16 bits, layer weights", + " * on 8 bits, then (since format 2) the tint and wetness on 8 bits. Each value is predicted from its neighbors and the", + " * prediction errors are written as variable length integers, with runs of", + " * zeros collapsed. Untouched or smoothly changed areas take almost no space.", + " */", + "const codec = (() => {", + " const formatVersion = '2';", + " // Format 1 has no tint and wetness.", + " const readableFormatVersions = ['1', '2'];", + "", + " class ByteWriter {", + " constructor() {", + " this.bytes = new Uint8Array(1024);", + " this.length = 0;", + " }", + " writeVarUint(value) {", + " if (this.length + 5 > this.bytes.length) {", + " const bytes = new Uint8Array(this.bytes.length * 2);", + " bytes.set(this.bytes);", + " this.bytes = bytes;", + " }", + " while (value >= 0x80) {", + " this.bytes[this.length++] = (value & 0x7f) | 0x80;", + " value >>>= 7;", + " }", + " this.bytes[this.length++] = value;", + " }", + " }", + "", + " class ByteReader {", + " /** @param {Uint8Array} bytes */", + " constructor(bytes) {", + " this.bytes = bytes;", + " this.position = 0;", + " }", + " readVarUint() {", + " let value = 0;", + " let shift = 0;", + " let byte;", + " do {", + " if (this.position >= this.bytes.length) throw new Error('Truncated terrain data.');", + " byte = this.bytes[this.position++];", + " value += (byte & 0x7f) * Math.pow(2, shift);", + " shift += 7;", + " } while (byte & 0x80);", + " return value;", + " }", + " }", + "", + " /** Predicts a value from the left, top and top-left ones. */", + " const predict = (values, size, index) => {", + " const i = index % size;", + " if (index < size) return i === 0 ? 0 : values[index - 1];", + " if (i === 0) return values[index - size];", + " return values[index - 1] + values[index - size] - values[index - size - 1];", + " };", + "", + " /**", + " * @param {ByteWriter} writer", + " * @param {Int32Array} values A grid of `size * size` values.", + " */", + " const writeGrid = (writer, values, size) => {", + " let zeroRun = 0;", + " for (let index = 0; index < values.length; index++) {", + " const error = values[index] - predict(values, size, index);", + " if (error === 0) {", + " zeroRun++;", + " continue;", + " }", + " if (zeroRun > 0) {", + " writer.writeVarUint(0);", + " writer.writeVarUint(zeroRun);", + " zeroRun = 0;", + " }", + " // Zigzag: small negative and positive numbers get small codes.", + " writer.writeVarUint(error > 0 ? error * 2 : -error * 2 - 1);", + " }", + " if (zeroRun > 0) {", + " writer.writeVarUint(0);", + " writer.writeVarUint(zeroRun);", + " }", + " };", + "", + " /** @param {ByteReader} reader */", + " const readGrid = (reader, size) => {", + " const values = new Int32Array(size * size);", + " let zeroRun = 0;", + " for (let index = 0; index < values.length; index++) {", + " let error = 0;", + " if (zeroRun > 0) {", + " zeroRun--;", + " } else {", + " const code = reader.readVarUint();", + " if (code === 0) {", + " zeroRun = reader.readVarUint() - 1;", + " } else {", + " error = code % 2 === 0 ? code / 2 : -(code + 1) / 2;", + " }", + " }", + " values[index] = predict(values, size, index) + error;", + " }", + " return values;", + " };", + "", + " const toBase64 = (bytes, length) => {", + " let binary = '';", + " for (let offset = 0; offset < length; offset += 0x8000) {", + " binary += String.fromCharCode.apply(null, bytes.subarray(offset, Math.min(offset + 0x8000, length)));", + " }", + " return btoa(binary);", + " };", + "", + " const fromBase64 = (base64) => {", + " const binary = atob(base64);", + " const bytes = new Uint8Array(binary.length);", + " for (let index = 0; index < binary.length; index++) {", + " bytes[index] = binary.charCodeAt(index);", + " }", + " return bytes;", + " };", + "", + " return {", + " /**", + " * @param {TerrainData} data", + " * @param {TerrainData} baseData The data without changes, with the same resolution.", + " */", + " encode(data, baseData) {", + " const writer = new ByteWriter();", + " const values = new Int32Array(data.heights.length);", + " for (let index = 0; index < values.length; index++) {", + " values[index] = Math.round((data.heights[index] - baseData.heights[index]) * 65535);", + " }", + " writeGrid(writer, values, data.size);", + " for (let layer = 0; layer < LAYER_COUNT; layer++) {", + " for (let index = 0; index < values.length; index++) {", + " const offset = index * LAYER_COUNT + layer;", + " values[index] = data.splat[offset] - baseData.splat[offset];", + " }", + " writeGrid(writer, values, data.size);", + " }", + " // Not painted, these grids are zeros: a few bytes each.", + " for (let channel = 0; channel < 4; channel++) {", + " for (let index = 0; index < values.length; index++) {", + " const offset = index * 4 + channel;", + " values[index] = data.colors[offset] - baseData.colors[offset];", + " }", + " writeGrid(writer, values, data.size);", + " }", + " return formatVersion + ';' + data.resolution + ';' + toBase64(writer.bytes, writer.length);", + " },", + "", + " /**", + " * Store grids of integers in a string, like the terrain data. Used by other objects (the scattered models).", + " * @param {Int32Array[]} grids Grids of `size * size` values.", + " * @param {number} size", + " */", + " encodeGrids(grids, size) {", + " const writer = new ByteWriter();", + " for (const grid of grids) writeGrid(writer, grid, size);", + " return toBase64(writer.bytes, writer.length);", + " },", + "", + " /**", + " * @param {string} base64 A string from `encodeGrids`.", + " * @param {number} size", + " * @param {number} count The number of grids.", + " * @returns {Int32Array[]} The grids (an error is thrown if the string is not valid).", + " */", + " decodeGrids(base64, size, count) {", + " const reader = new ByteReader(fromBase64(base64));", + " return Array.from({ length: count }, () => readGrid(reader, size));", + " },", + "", + " /**", + " * @param {string} text", + " * @param {(resolution: number) => TerrainData} createBaseData", + " * @returns {TerrainData | null} null if the text is empty or not valid.", + " */", + " decode(text, createBaseData) {", + " if (!text) return null;", + " const parts = text.split(';');", + " const resolution = parseInt(parts[1], 10);", + " if (readableFormatVersions.indexOf(parts[0]) === -1 || RESOLUTIONS.indexOf(resolution) === -1) {", + " return null;", + " }", + " try {", + " const reader = new ByteReader(fromBase64(parts[2]));", + " const data = createBaseData(resolution);", + " const heightChanges = readGrid(reader, data.size);", + " for (let index = 0; index < heightChanges.length; index++) {", + " data.heights[index] = clamp01(data.heights[index] + heightChanges[index] / 65535);", + " }", + " for (let layer = 0; layer < LAYER_COUNT; layer++) {", + " const weightChanges = readGrid(reader, data.size);", + " for (let index = 0; index < weightChanges.length; index++) {", + " const offset = index * LAYER_COUNT + layer;", + " // The base may have changed since the data was saved.", + " data.splat[offset] = Math.min(Math.max(data.splat[offset] + weightChanges[index], 0), 255);", + " }", + " }", + " if (parts[0] !== '1') {", + " for (let channel = 0; channel < 4; channel++) {", + " const colorChanges = readGrid(reader, data.size);", + " for (let index = 0; index < colorChanges.length; index++) {", + " const offset = index * 4 + channel;", + " data.colors[offset] = Math.min(Math.max(data.colors[offset] + colorChanges[index], 0), 255);", + " }", + " }", + " }", + " return data;", + " } catch (error) {", + " console.error('Terrain data could not be read:', error);", + " return null;", + " }", + " },", + " };", + "})();", + "", + "const lodIndices = new Map();", + "/**", + " * The triangles of a chunk, shared by all chunks of the same size. Chunk", + " * borders have a skirt going down, hiding the gaps between chunks drawn", + " * with different levels of detail.", + " * @param {number} chunkCells", + " * @param {number} step", + " */", + "const getLodIndex = (chunkCells, step) => {", + " const key = chunkCells + ';' + step;", + " const existingIndex = lodIndices.get(key);", + " if (existingIndex) return existingIndex;", + "", + " const sideVertices = chunkCells + 1;", + " const gridVertices = sideVertices * sideVertices;", + " const indices = [];", + " const grid = (i, j) => j * sideVertices + i;", + " for (let j = 0; j < chunkCells; j += step) {", + " for (let i = 0; i < chunkCells; i += step) {", + " const topLeft = grid(i, j);", + " const topRight = grid(i + step, j);", + " const bottomLeft = grid(i, j + step);", + " const bottomRight = grid(i + step, j + step);", + " indices.push(topLeft, topRight, bottomLeft, topRight, bottomRight, bottomLeft);", + " }", + " }", + " const borders = [", + " (k) => grid(k, 0),", + " (k) => grid(k, chunkCells),", + " (k) => grid(0, k),", + " (k) => grid(chunkCells, k),", + " ];", + " borders.forEach((borderVertex, border) => {", + " const skirtVertex = (k) => gridVertices + border * sideVertices + k;", + " for (let k = 0; k < chunkCells; k += step) {", + " const a = borderVertex(k);", + " const b = borderVertex(k + step);", + " const skirtA = skirtVertex(k);", + " const skirtB = skirtVertex(k + step);", + " // Both faces, as a skirt can be seen from either side.", + " indices.push(a, b, skirtB, a, skirtB, skirtA, a, skirtB, b, a, skirtA, skirtB);", + " }", + " });", + " const index = new THREE.BufferAttribute(new Uint16Array(indices), 1);", + " lodIndices.set(key, index);", + " return index;", + "};", + "", + "const vertexShaderDeclarations = `", + "varying vec2 vTerrainLocalXY;", + "varying float vTerrainLocalZ;", + "uniform float terrainWorldScale;", + "varying vec2 vTerrainWorldXY;", + "varying float vTerrainUp;", + "`;", + "const vertexShaderCode = `", + "vTerrainLocalXY = position.xy;", + "vTerrainLocalZ = position.z;", + "vTerrainWorldXY = (modelMatrix * vec4(transformed, 1.0)).xy * terrainWorldScale;", + "vTerrainUp = normalize(transpose(inverse(mat3(modelMatrix))) * objectNormal).z;", + "`;", + "const fragmentShaderDeclarations = `", + "uniform sampler2D terrainSplatMap;", + "uniform vec2 terrainSplatUvScaleAndOffset;", + "uniform vec3 terrainLayerColors[4];", + "uniform sampler2D terrainLayerMap0;", + "uniform sampler2D terrainLayerMap1;", + "uniform sampler2D terrainLayerMap2;", + "uniform sampler2D terrainLayerMap3;", + "uniform sampler2D terrainLayerNormalMap0;", + "uniform sampler2D terrainLayerNormalMap1;", + "uniform sampler2D terrainLayerNormalMap2;", + "uniform sampler2D terrainLayerNormalMap3;", + "uniform float terrainHasNormalMaps;", + "uniform float terrainNormalMapStrength;", + "uniform float terrainTextureSize;", + "uniform float terrainHeightBlending;", + "uniform float terrainTextureVariation;", + "uniform vec4 terrainCliffLayer;", + "uniform vec2 terrainCliffCosines;", + "uniform vec4 terrainBrush;", + "uniform sampler2D terrainColorMap;", + "uniform vec3 terrainWetShore;", + "varying vec2 vTerrainLocalXY;", + "varying float vTerrainLocalZ;", + "varying vec2 vTerrainWorldXY;", + "varying float vTerrainUp;", + "", + "float terrainHash(vec2 p) {", + " return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);", + "}", + "// The rotation of the copies of textures used to make repetition less visible.", + "const mat2 terrainCopyRotation = mat2(0.8, -0.6, 0.6, 0.8);", + "/** A bump of a normal map (from -1 to 1), mixed with its rotated copy in patches. */", + "vec3 terrainSampleNormal(sampler2D normalMap, vec2 uv, vec2 uv2, float patches) {", + " vec3 bump = texture2D(normalMap, uv).xyz * 2.0 - 1.0;", + " if (patches <= 0.0) return bump;", + " vec3 copyBump = texture2D(normalMap, uv2).xyz * 2.0 - 1.0;", + " // The bumps of the copy are rotated back with the inverse rotation.", + " copyBump.xy = transpose(terrainCopyRotation) * copyBump.xy;", + " return mix(bump, copyBump, patches);", + "}", + "float terrainNoise(vec2 p) {", + " vec2 i = floor(p);", + " vec2 f = fract(p);", + " vec2 u = f * f * (3.0 - 2.0 * f);", + " return mix(", + " mix(terrainHash(i), terrainHash(i + vec2(1.0, 0.0)), u.x),", + " mix(terrainHash(i + vec2(0.0, 1.0)), terrainHash(i + vec2(1.0, 1.0)), u.x),", + " u.y", + " );", + "}", + "`;", + "// Holes are samples without any painted layer: the interpolated sum of the", + "// weights gives smooth edges. Only compiled for terrains having holes, as", + "// discard makes drawing slower on some GPUs.", + "const holeShaderCode = `", + "#ifdef TERRAIN_HOLES", + "if (dot(texture2D(terrainSplatMap, vTerrainLocalXY * terrainSplatUvScaleAndOffset.x + terrainSplatUvScaleAndOffset.y), vec4(1.0)) < 0.5) discard;", + "#endif", + "`;", + "// For the shadows: the depth of holes is not drawn.", + "const holeDepthShaderDeclarations = `", + "uniform sampler2D terrainSplatMap;", + "uniform vec2 terrainSplatUvScaleAndOffset;", + "varying vec2 vTerrainLocalXY;", + "`;", + "", + "const fragmentShaderCode = holeShaderCode + `", + "// Borders between layers are made irregular and sharper, to look painted.", + "vec2 terrainNoisePosition = vTerrainWorldXY / 24.0;", + "vec4 terrainJitter = vec4(", + " terrainNoise(terrainNoisePosition),", + " terrainNoise(terrainNoisePosition + 19.1),", + " terrainNoise(terrainNoisePosition + 37.7),", + " terrainNoise(terrainNoisePosition + 53.3)", + ");", + "// Layer weights are in a texture, so that painted details stay the same", + "// whatever the level of detail of the ground.", + "vec2 terrainSplatUv = vTerrainLocalXY * terrainSplatUvScaleAndOffset.x + terrainSplatUvScaleAndOffset.y;", + "vec4 terrainWeights = texture2D(terrainSplatMap, terrainSplatUv) * (0.7 + 0.6 * terrainJitter);", + "terrainWeights = terrainWeights * terrainWeights;", + "terrainWeights /= max(dot(terrainWeights, vec4(1.0)), 0.00001);", + "float terrainCliff = 1.0 - smoothstep(terrainCliffCosines.x, terrainCliffCosines.y, vTerrainUp);", + "terrainWeights = mix(terrainWeights, terrainCliffLayer, terrainCliff * dot(terrainCliffLayer, vec4(1.0)));", + "", + "vec2 terrainUv = vTerrainWorldXY / terrainTextureSize;", + "vec3 terrainTexture0 = texture2D(terrainLayerMap0, terrainUv).rgb;", + "vec3 terrainTexture1 = texture2D(terrainLayerMap1, terrainUv).rgb;", + "vec3 terrainTexture2 = texture2D(terrainLayerMap2, terrainUv).rgb;", + "vec3 terrainTexture3 = texture2D(terrainLayerMap3, terrainUv).rgb;", + "", + "// Less visible repetition: in irregular patches, textures are replaced by a", + "// rotated and shifted copy of themselves, and colors slowly vary.", + "float terrainPatches = 0.0;", + "vec2 terrainUv2 = terrainUv;", + "float terrainColorVariation = 1.0;", + "if (terrainTextureVariation > 0.0) {", + " // Up to half of the ground is covered by the copies, each patch showing", + " // only one of them (mixing them would make them blurry).", + " float terrainPatchThreshold = 1.0 - 0.5 * terrainTextureVariation;", + " terrainPatches = smoothstep(terrainPatchThreshold - 0.05, terrainPatchThreshold + 0.05, terrainNoise(terrainUv * 0.23 + 7.3));", + " terrainUv2 = terrainCopyRotation * terrainUv + vec2(0.31, 0.73);", + " terrainTexture0 = mix(terrainTexture0, texture2D(terrainLayerMap0, terrainUv2).rgb, terrainPatches);", + " terrainTexture1 = mix(terrainTexture1, texture2D(terrainLayerMap1, terrainUv2).rgb, terrainPatches);", + " terrainTexture2 = mix(terrainTexture2, texture2D(terrainLayerMap2, terrainUv2).rgb, terrainPatches);", + " terrainTexture3 = mix(terrainTexture3, texture2D(terrainLayerMap3, terrainUv2).rgb, terrainPatches);", + " terrainColorVariation += terrainTextureVariation * 0.3 *", + " (terrainNoise(terrainUv * 0.11) + 0.5 * terrainNoise(terrainUv * 0.37 + 3.1) - 0.75);", + "}", + "", + "// Height-based blending: the brightness of a texture, compared to its", + "// average, is used as its height. Where layers meet, the high parts (stones,", + "// tufts) cover the low parts (sand, gaps) instead of fading into each other.", + "if (terrainHeightBlending > 0.0) {", + " const vec3 terrainLuminance = vec3(0.299, 0.587, 0.114);", + " vec4 terrainHeights = vec4(", + " dot(terrainTexture0 - textureLod(terrainLayerMap0, vec2(0.5), 16.0).rgb, terrainLuminance),", + " dot(terrainTexture1 - textureLod(terrainLayerMap1, vec2(0.5), 16.0).rgb, terrainLuminance),", + " dot(terrainTexture2 - textureLod(terrainLayerMap2, vec2(0.5), 16.0).rgb, terrainLuminance),", + " dot(terrainTexture3 - textureLod(terrainLayerMap3, vec2(0.5), 16.0).rgb, terrainLuminance)", + " );", + " vec4 terrainHeightWeights = terrainWeights + terrainHeights;", + " float terrainTop = max(max(terrainHeightWeights.x, terrainHeightWeights.y), max(terrainHeightWeights.z, terrainHeightWeights.w));", + " // Only painted layers (and the cliff layer) can show.", + " vec4 terrainHeightBlend = max(terrainHeightWeights - terrainTop + 0.1, 0.0) * step(0.004, terrainWeights);", + " terrainHeightBlend /= max(dot(terrainHeightBlend, vec4(1.0)), 0.00001);", + " terrainWeights = mix(terrainWeights, terrainHeightBlend, terrainHeightBlending);", + "}", + "", + "vec3 terrainColor = terrainColorVariation * (", + " terrainWeights.x * terrainLayerColors[0] * terrainTexture0 +", + " terrainWeights.y * terrainLayerColors[1] * terrainTexture1 +", + " terrainWeights.z * terrainLayerColors[2] * terrainTexture2 +", + " terrainWeights.w * terrainLayerColors[3] * terrainTexture3);", + "", + "// The painted tint multiplies all layers. Wet ground is darker (and", + "// shinier, see the roughness).", + "vec4 terrainTintAndWetness = texture2D(terrainColorMap, terrainSplatUv);", + "float terrainWetness = max(", + " terrainTintAndWetness.a,", + " terrainWetShore.z * (1.0 - smoothstep(terrainWetShore.x, terrainWetShore.x + terrainWetShore.y, vTerrainLocalZ))", + ");", + "terrainColor *= terrainTintAndWetness.rgb * (1.0 - 0.4 * terrainWetness);", + "diffuseColor.rgb *= terrainColor;", + "", + "if (terrainBrush.w > 0.5) {", + " float brushDistance = distance(vTerrainWorldXY, terrainBrush.xy);", + " float ringWidth = max(terrainBrush.z * 0.04, 1.5);", + " float ring = 1.0 - smoothstep(0.0, ringWidth, abs(brushDistance - terrainBrush.z));", + " float disc = brushDistance < terrainBrush.z ? 0.12 : 0.0;", + " diffuseColor.rgb = mix(diffuseColor.rgb, vec3(1.0), max(ring * 0.85, disc));", + "}", + "`;", + "", + "const fragmentShaderRoughnessCode = `", + "roughnessFactor = mix(roughnessFactor, 0.3, terrainWetness);", + "`;", + "", + "const fragmentShaderNormalCode = `", + "if (terrainHasNormalMaps > 0.5) {", + " vec3 terrainMapNormal =", + " terrainWeights.x * terrainSampleNormal(terrainLayerNormalMap0, terrainUv, terrainUv2, terrainPatches) +", + " terrainWeights.y * terrainSampleNormal(terrainLayerNormalMap1, terrainUv, terrainUv2, terrainPatches) +", + " terrainWeights.z * terrainSampleNormal(terrainLayerNormalMap2, terrainUv, terrainUv2, terrainPatches) +", + " terrainWeights.w * terrainSampleNormal(terrainLayerNormalMap3, terrainUv, terrainUv2, terrainPatches);", + " terrainMapNormal.xy *= terrainNormalMapStrength;", + " // The X and Y of textures follow the X and Y of the 3D world, projected on", + " // the ground (the camera space of the normal is used).", + " vec3 terrainTangent = (viewMatrix * vec4(1.0, 0.0, 0.0, 0.0)).xyz;", + " terrainTangent -= normal * dot(normal, terrainTangent);", + " // A ground facing the X axis (a rotated terrain) uses the Y axis instead.", + " if (dot(terrainTangent, terrainTangent) < 0.000001) {", + " terrainTangent = (viewMatrix * vec4(0.0, 1.0, 0.0, 0.0)).xyz;", + " terrainTangent -= normal * dot(normal, terrainTangent);", + " }", + " terrainTangent = normalize(terrainTangent);", + " vec3 terrainBitangent = cross(normal, terrainTangent);", + " normal = normalize(mat3(terrainTangent, terrainBitangent, normal) * terrainMapNormal);", + "}", + "`;", + "", + "let whiteTexture = null;", + "const getWhiteTexture = () => {", + " if (!whiteTexture) {", + " whiteTexture = new THREE.DataTexture(new Uint8Array([255, 255, 255, 255]), 1, 1);", + " whiteTexture.needsUpdate = true;", + " }", + " return whiteTexture;", + "};", + "", + "let flatNormalTexture = null;", + "const getFlatNormalTexture = () => {", + " if (!flatNormalTexture) {", + " flatNormalTexture = new THREE.DataTexture(new Uint8Array([128, 128, 255, 255]), 1, 1);", + " flatNormalTexture.needsUpdate = true;", + " }", + " return flatNormalTexture;", + "};", + "", + "const layerTextures = new WeakMap();", + "const layerNormalMaps = new WeakMap();", + "/**", + " * Textures of the image manager have no mipmaps: they would shimmer when", + " * repeated in the distance.", + " * @param {gdjs.RuntimeGame} game", + " * @param {string} resourceName", + " * @param {boolean} isNormalMap Normal maps are directions, not colors.", + " */", + "const getLayerTexture = (game, resourceName, isNormalMap) => {", + " if (!resourceName) return isNormalMap ? getFlatNormalTexture() : getWhiteTexture();", + " const imageTexture = game.getImageManager().getThreeTexture(resourceName);", + " const textures = isNormalMap ? layerNormalMaps : layerTextures;", + " let texture = textures.get(imageTexture);", + " if (!texture) {", + " texture = imageTexture.clone();", + " if (isNormalMap) texture.colorSpace = THREE.NoColorSpace;", + " texture.wrapS = THREE.RepeatWrapping;", + " texture.wrapT = THREE.RepeatWrapping;", + " texture.minFilter = THREE.LinearMipmapLinearFilter;", + " texture.generateMipmaps = true;", + " texture.anisotropy = 4;", + " texture.needsUpdate = true;", + " textures.set(imageTexture, texture);", + " }", + " return texture;", + "};", + "", + "/**", + " * A texture of 4 bytes per sample of a grid of `size * size` samples (layer", + " * weights, tint and wetness...), interpolated between samples.", + " * @param {Uint8Array} bytes", + " * @param {number} size", + " */", + "const createGridTexture = (bytes, size) => {", + " const texture = new THREE.DataTexture(bytes, size, size);", + " texture.magFilter = THREE.LinearFilter;", + " texture.minFilter = THREE.LinearFilter;", + " texture.userData.isUploaded = false;", + " texture.onUpdate = () => {", + " texture.userData.isUploaded = true;", + " };", + " return texture;", + "};", + "", + "/**", + " * Send the rows of the changed samples to the GPU, or the whole texture", + " * before its first upload (and in older GDevelop versions).", + " * @param {THREE.DataTexture} texture", + " * @param {SampleRectangle} samples", + " */", + "const uploadGridTextureRows = (texture, samples) => {", + " if (texture.userData.isUploaded && typeof texture.addUpdateRange === 'function') {", + " const size = texture.image.width;", + " for (let j = samples.minJ; j <= samples.maxJ; j++) {", + " texture.addUpdateRange((j * size + samples.minI) * 4, (samples.maxI - samples.minI + 1) * 4);", + " }", + " }", + " texture.needsUpdate = true;", + "};", + "", + "class TerrainRenderer {", + " /** @param {Terrain} terrain */", + " constructor(terrain) {", + " this.terrain = terrain;", + " this.group = new THREE.Group();", + " terrain.object.get3DRendererObject().add(this.group);", + " /** @type {Array<{mesh: THREE.Mesh, geometry: THREE.BufferGeometry, firstI: number, firstJ: number, center: THREE.Vector3, lod: number}>} */", + " this.chunks = [];", + " this.chunksPerSide = 0;", + " this.chunkCells = 0;", + " this.chunkSideVertices = 0;", + " this.chunkGridVertices = 0;", + " this.chunkVertices = 0;", + " /** @type {THREE.DataTexture | null} */", + " this.splatTexture = null;", + " /** @type {THREE.DataTexture | null} */", + " this.colorTexture = null;", + " this.uniforms = {", + " terrainSplatMap: { value: getWhiteTexture() },", + " terrainSplatUvScaleAndOffset: { value: new THREE.Vector2() },", + " terrainLayerColors: { value: [new THREE.Color(), new THREE.Color(), new THREE.Color(), new THREE.Color()] },", + " terrainLayerMap0: { value: getWhiteTexture() },", + " terrainLayerMap1: { value: getWhiteTexture() },", + " terrainLayerMap2: { value: getWhiteTexture() },", + " terrainLayerMap3: { value: getWhiteTexture() },", + " terrainLayerNormalMap0: { value: getFlatNormalTexture() },", + " terrainLayerNormalMap1: { value: getFlatNormalTexture() },", + " terrainLayerNormalMap2: { value: getFlatNormalTexture() },", + " terrainLayerNormalMap3: { value: getFlatNormalTexture() },", + " terrainHasNormalMaps: { value: 0 },", + " terrainNormalMapStrength: { value: 1 },", + " terrainTextureSize: { value: 256 },", + " terrainHeightBlending: { value: 0 },", + " terrainTextureVariation: { value: 0 },", + " terrainCliffLayer: { value: new THREE.Vector4() },", + " terrainCliffCosines: { value: new THREE.Vector2() },", + " terrainBrush: { value: new THREE.Vector4() },", + " terrainColorMap: { value: getWhiteTexture() },", + " // Z (in the terrain group) under which the ground is wet, fade height, and 1 if it's used.", + " terrainWetShore: { value: new THREE.Vector3() },", + " // Scene units by 3D world unit.", + " terrainWorldScale: { value: 1 },", + " };", + " this.material = new THREE.MeshStandardMaterial({ roughness: 1, metalness: 0 });", + " this.material.onBeforeCompile = (shader) => {", + " Object.assign(shader.uniforms, this.uniforms);", + " shader.vertexShader = patchShaderCode(shader.vertexShader, [", + " ['#include ', '#include \\n' + vertexShaderDeclarations],", + " ['#include ', '#include \\n' + vertexShaderCode],", + " ], 'Terrain3D');", + " shader.fragmentShader = patchShaderCode(shader.fragmentShader, [", + " ['#include ', '#include \\n' + fragmentShaderDeclarations],", + " ['#include ', fragmentShaderCode],", + " ['#include ', '#include \\n' + fragmentShaderRoughnessCode],", + " ['#include ', '#include \\n' + fragmentShaderNormalCode],", + " ], 'Terrain3D');", + " };", + " this.material.customProgramCacheKey = () => (this.hasHoles ? 'Terrain3DWithHoles' : 'Terrain3D');", + " this.hasHoles = false;", + " // Without them, holes would cast shadows.", + " this.depthMaterial = this._createHoleDepthMaterial(new THREE.MeshDepthMaterial());", + " this.distanceMaterial = this._createHoleDepthMaterial(new THREE.MeshDistanceMaterial());", + " this._cameraPosition = new THREE.Vector3();", + " this._chunkPosition = new THREE.Vector3();", + " this._scale = new THREE.Vector3();", + " }", + "", + " /**", + " * @param {THREE.MeshDepthMaterial | THREE.MeshDistanceMaterial} material", + " * @returns The material, drawing nothing in holes.", + " */", + " _createHoleDepthMaterial(material) {", + " material.defines = { TERRAIN_HOLES: '' };", + " material.onBeforeCompile = (shader) => {", + " Object.assign(shader.uniforms, this.uniforms);", + " shader.vertexShader = patchShaderCode(shader.vertexShader, [", + " ['#include ', '#include \\nvarying vec2 vTerrainLocalXY;'],", + " ['#include ', '#include \\nvTerrainLocalXY = position.xy;'],", + " ], 'Terrain3D shadows');", + " shader.fragmentShader = patchShaderCode(shader.fragmentShader, [", + " ['#include ', '#include \\n' + holeDepthShaderDeclarations],", + " ['#include ', holeShaderCode + '#include '],", + " ], 'Terrain3D shadows');", + " };", + " material.customProgramCacheKey = () => 'Terrain3DHoles';", + " return material;", + " }", + "", + " /**", + " * Use the shaders drawing holes, or the faster ones without them.", + " * @param {boolean} hasHoles", + " */", + " setHasHoles(hasHoles) {", + " if (hasHoles === this.hasHoles) return;", + " this.hasHoles = hasHoles;", + " // The material has its own defines (STANDARD).", + " if (hasHoles) this.material.defines.TERRAIN_HOLES = '';", + " else delete this.material.defines.TERRAIN_HOLES;", + " this.material.needsUpdate = true;", + " for (const chunk of this.chunks) this._setChunkShadowMaterials(chunk.mesh);", + " }", + "", + " /** @param {THREE.Mesh} mesh */", + " _setChunkShadowMaterials(mesh) {", + " mesh.customDepthMaterial = this.hasHoles ? this.depthMaterial : undefined;", + " mesh.customDistanceMaterial = this.hasHoles ? this.distanceMaterial : undefined;", + " }", + "", + " /** Recreate all the chunks, for example after the resolution changed. */", + " rebuild() {", + " this.disposeChunks();", + " const { data } = this.terrain;", + " this.chunkCells = getChunkCells(data.resolution);", + " this.chunkSideVertices = this.chunkCells + 1;", + " this.chunkGridVertices = this.chunkSideVertices * this.chunkSideVertices;", + " this.chunkVertices = this.chunkGridVertices + 4 * this.chunkSideVertices;", + " this.chunksPerSide = data.resolution / this.chunkCells;", + " const cellSize = AREA_SIZE / data.resolution;", + " // Sample centers are at the texel centers.", + " this.splatTexture = createGridTexture(data.splat, data.size);", + " this.uniforms.terrainSplatMap.value = this.splatTexture;", + " this.colorTexture = createGridTexture(data.colors, data.size);", + " // Tints are colors chosen like the layer colors.", + " this.colorTexture.colorSpace = THREE.SRGBColorSpace;", + " this.uniforms.terrainColorMap.value = this.colorTexture;", + " this.uniforms.terrainSplatUvScaleAndOffset.value.set(1 / (cellSize * data.size), 0.5 / data.size);", + " for (let chunkJ = 0; chunkJ < this.chunksPerSide; chunkJ++) {", + " for (let chunkI = 0; chunkI < this.chunksPerSide; chunkI++) {", + " const firstI = chunkI * this.chunkCells;", + " const firstJ = chunkJ * this.chunkCells;", + " const positions = new Float32Array(this.chunkVertices * 3);", + " for (let j = 0; j < this.chunkSideVertices; j++) {", + " for (let i = 0; i < this.chunkSideVertices; i++) {", + " const vertex = j * this.chunkSideVertices + i;", + " positions[vertex * 3] = (firstI + i) * cellSize;", + " positions[vertex * 3 + 1] = (firstJ + j) * cellSize;", + " }", + " }", + " // Skirt vertices are below border vertices.", + " this._forEachSkirtVertex((skirtVertex, borderVertex) => {", + " positions[skirtVertex * 3] = positions[borderVertex * 3];", + " positions[skirtVertex * 3 + 1] = positions[borderVertex * 3 + 1];", + " });", + " const geometry = new THREE.BufferGeometry();", + " geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));", + " geometry.setAttribute('normal', new THREE.BufferAttribute(new Float32Array(this.chunkVertices * 3), 3));", + " geometry.setIndex(getLodIndex(this.chunkCells, LOD_STEPS[0]));", + " geometry.boundingBox = new THREE.Box3();", + " geometry.boundingSphere = new THREE.Sphere();", + " const mesh = new THREE.Mesh(geometry, this.material);", + " mesh.matrixAutoUpdate = false;", + " // Rays hit the ground at its full resolution, whatever the level of", + " // detail drawn, and go through holes.", + " const chunkCells = this.chunkCells;", + " const terrainData = () => this.terrain.data;", + " const localPoint = new THREE.Vector3();", + " mesh.raycast = function (raycaster, intersects) {", + " const drawnIndex = geometry.index;", + " const firstIntersection = intersects.length;", + " geometry.setIndex(getLodIndex(chunkCells, LOD_STEPS[0]));", + " THREE.Mesh.prototype.raycast.call(this, raycaster, intersects);", + " geometry.setIndex(drawnIndex);", + " const data = terrainData();", + " const cellSize = AREA_SIZE / data.resolution;", + " for (let index = intersects.length - 1; index >= firstIntersection; index--) {", + " this.worldToLocal(localPoint.copy(intersects[index].point));", + " if (data.sampleSolidity(localPoint.x / cellSize, localPoint.y / cellSize) < 0.5) {", + " intersects.splice(index, 1);", + " }", + " }", + " };", + " this._setChunkShadowMaterials(mesh);", + " this.group.add(mesh);", + " this.chunks.push({", + " mesh,", + " geometry,", + " firstI,", + " firstJ,", + " center: new THREE.Vector3(", + " (firstI + this.chunkCells / 2) * cellSize,", + " (firstJ + this.chunkCells / 2) * cellSize,", + " 0", + " ),", + " lod: 0,", + " });", + " }", + " }", + " this.updateSamples(new SampleRectangle().set(0, 0, data.resolution, data.resolution), true);", + " this.updateMaterial();", + " }", + "", + " _forEachSkirtVertex(callback) {", + " for (let k = 0; k < this.chunkSideVertices; k++) {", + " callback(this.chunkGridVertices + k, k);", + " callback(this.chunkGridVertices + this.chunkSideVertices + k, this.chunkCells * this.chunkSideVertices + k);", + " callback(this.chunkGridVertices + 2 * this.chunkSideVertices + k, k * this.chunkSideVertices);", + " callback(", + " this.chunkGridVertices + 3 * this.chunkSideVertices + k,", + " k * this.chunkSideVertices + this.chunkCells", + " );", + " }", + " }", + "", + " /**", + " * Skirts go down only to the lowest point a coarser neighbor chunk can", + " * draw along the border, so that they can't be seen through holes.", + " * @param {Float32Array} positions", + " */", + " _updateSkirtHeights(positions) {", + " const coarsestStep = LOD_STEPS[LOD_STEPS.length - 1];", + " const borderVertices = [", + " (k) => k,", + " (k) => this.chunkCells * this.chunkSideVertices + k,", + " (k) => k * this.chunkSideVertices,", + " (k) => k * this.chunkSideVertices + this.chunkCells,", + " ];", + " borderVertices.forEach((borderVertex, border) => {", + " for (let k = 0; k < this.chunkSideVertices; k++) {", + " let minZ = Infinity;", + " const from = Math.max(k - coarsestStep, 0);", + " const to = Math.min(k + coarsestStep, this.chunkCells);", + " for (let other = from; other <= to; other++) {", + " minZ = Math.min(minZ, positions[borderVertex(other) * 3 + 2]);", + " }", + " const skirtVertex = this.chunkGridVertices + border * this.chunkSideVertices + k;", + " positions[skirtVertex * 3 + 2] = minZ;", + " }", + " });", + " }", + "", + " /**", + " * Update the vertices of the chunks sharing the given samples.", + " * @param {SampleRectangle} samples", + " * @param {boolean} hasPaintChanged", + " */", + " updateSamples(samples, hasPaintChanged) {", + " if (samples.isEmpty()) return;", + " const { data } = this.terrain;", + " const cellSize = AREA_SIZE / data.resolution;", + " const slopeFactor = AREA_DEPTH / cellSize;", + " // Normals of samples next to changed ones change too.", + " const minI = Math.max(samples.minI - 1, 0);", + " const minJ = Math.max(samples.minJ - 1, 0);", + " const maxI = Math.min(samples.maxI + 1, data.resolution);", + " const maxJ = Math.min(samples.maxJ + 1, data.resolution);", + " const firstChunkI = Math.max(Math.ceil(minI / this.chunkCells) - 1, 0);", + " const firstChunkJ = Math.max(Math.ceil(minJ / this.chunkCells) - 1, 0);", + " const lastChunkI = Math.min(Math.floor(maxI / this.chunkCells), this.chunksPerSide - 1);", + " const lastChunkJ = Math.min(Math.floor(maxJ / this.chunkCells), this.chunksPerSide - 1);", + "", + " for (let chunkJ = firstChunkJ; chunkJ <= lastChunkJ; chunkJ++) {", + " for (let chunkI = firstChunkI; chunkI <= lastChunkI; chunkI++) {", + " const chunk = this.chunks[chunkJ * this.chunksPerSide + chunkI];", + " const positions = chunk.geometry.attributes.position.array;", + " const normals = chunk.geometry.attributes.normal.array;", + " const fromI = Math.max(minI - chunk.firstI, 0);", + " const toI = Math.min(maxI - chunk.firstI, this.chunkCells);", + " const fromJ = Math.max(minJ - chunk.firstJ, 0);", + " const toJ = Math.min(maxJ - chunk.firstJ, this.chunkCells);", + " for (let j = fromJ; j <= toJ; j++) {", + " const gridJ = chunk.firstJ + j;", + " for (let i = fromI; i <= toI; i++) {", + " const gridI = chunk.firstI + i;", + " const vertex = j * this.chunkSideVertices + i;", + " const sample = gridJ * data.size + gridI;", + " positions[vertex * 3 + 2] = data.heights[sample] * AREA_DEPTH;", + "", + " const leftI = Math.max(gridI - 1, 0);", + " const rightI = Math.min(gridI + 1, data.resolution);", + " const upJ = Math.max(gridJ - 1, 0);", + " const downJ = Math.min(gridJ + 1, data.resolution);", + " const nx =", + " ((data.getHeight(leftI, gridJ) - data.getHeight(rightI, gridJ)) * slopeFactor) /", + " (rightI - leftI);", + " const ny =", + " ((data.getHeight(gridI, upJ) - data.getHeight(gridI, downJ)) * slopeFactor) /", + " (downJ - upJ);", + " const inverseLength = 1 / Math.sqrt(nx * nx + ny * ny + 1);", + " normals[vertex * 3] = nx * inverseLength;", + " normals[vertex * 3 + 1] = ny * inverseLength;", + " normals[vertex * 3 + 2] = inverseLength;", + " }", + " }", + " this._forEachSkirtVertex((skirtVertex, borderVertex) => {", + " for (let axis = 0; axis < 3; axis++) {", + " normals[skirtVertex * 3 + axis] = normals[borderVertex * 3 + axis];", + " }", + " });", + " this._updateSkirtHeights(positions);", + " let maxZ = 0;", + " for (let vertex = 0; vertex < this.chunkGridVertices; vertex++) {", + " maxZ = Math.max(maxZ, positions[vertex * 3 + 2]);", + " }", + " const chunkSize = this.chunkCells * cellSize;", + " chunk.geometry.boundingBox.min.set(chunk.firstI * cellSize, chunk.firstJ * cellSize, 0);", + " chunk.geometry.boundingBox.max.set(", + " chunk.firstI * cellSize + chunkSize,", + " chunk.firstJ * cellSize + chunkSize,", + " maxZ", + " );", + " chunk.geometry.boundingBox.getBoundingSphere(chunk.geometry.boundingSphere);", + " chunk.center.z = maxZ / 2;", + " chunk.geometry.attributes.position.needsUpdate = true;", + " chunk.geometry.attributes.normal.needsUpdate = true;", + " }", + " }", + " if (hasPaintChanged) {", + " // The data may have been replaced by one with the same resolution.", + " this.splatTexture.image.data = data.splat;", + " uploadGridTextureRows(this.splatTexture, samples);", + " this.colorTexture.image.data = data.colors;", + " uploadGridTextureRows(this.colorTexture, samples);", + " }", + " }", + "", + " updateMaterial() {", + " const { object } = this.terrain;", + " const game = object.getRuntimeScene().getGame();", + " const colors = [object._getLayer1Color(), object._getLayer2Color(), object._getLayer3Color(), object._getLayer4Color()];", + " const textures = [", + " object._getLayer1Texture(),", + " object._getLayer2Texture(),", + " object._getLayer3Texture(),", + " object._getLayer4Texture(),", + " ];", + " const normalMaps = [", + " object._getLayer1NormalMap(),", + " object._getLayer2NormalMap(),", + " object._getLayer3NormalMap(),", + " object._getLayer4NormalMap(),", + " ];", + " for (let layer = 0; layer < LAYER_COUNT; layer++) {", + " this.uniforms.terrainLayerColors.value[layer].set(gdjs.rgbOrHexStringToNumber(colors[layer]));", + " this.uniforms['terrainLayerMap' + layer].value = getLayerTexture(game, textures[layer], false);", + " this.uniforms['terrainLayerNormalMap' + layer].value = getLayerTexture(game, normalMaps[layer], true);", + " }", + " this.uniforms.terrainHasNormalMaps.value = normalMaps.some((normalMap) => !!normalMap) ? 1 : 0;", + " this.uniforms.terrainNormalMapStrength.value = Math.max(object._getNormalMapStrength(), 0);", + " this.uniforms.terrainTextureSize.value = Math.max(object._getTextureSize(), 1);", + " this.uniforms.terrainHeightBlending.value = clamp01(object._getHeightBlending());", + " this.uniforms.terrainTextureVariation.value = clamp01(object._getTextureVariation());", + "", + " const cliffLayer = parseInt(object._getCliffLayer(), 10);", + " const cliffAngle = gdjs.toRad(object._getCliffAngle());", + " const cliffBlendAngle = gdjs.toRad(4);", + " this.uniforms.terrainCliffLayer.value.set(", + " cliffLayer === 1 ? 1 : 0,", + " cliffLayer === 2 ? 1 : 0,", + " cliffLayer === 3 ? 1 : 0,", + " cliffLayer === 4 ? 1 : 0", + " );", + " this.uniforms.terrainCliffCosines.value.set(", + " Math.cos(cliffAngle + cliffBlendAngle),", + " Math.cos(cliffAngle - cliffBlendAngle)", + " );", + "", + " const castShadow = object._getCastShadow();", + " const receiveShadow = object._getReceiveShadow();", + " for (const chunk of this.chunks) {", + " chunk.mesh.castShadow = castShadow;", + " chunk.mesh.receiveShadow = receiveShadow;", + " }", + " }", + "", + " /**", + " * Show the brush circle of the editor.", + " * @param {THREE.Vector3 | null} localPosition", + " * @param {number} radius In scene units.", + " */", + " setBrush(localPosition, radius) {", + " const brush = this.uniforms.terrainBrush.value;", + " if (!localPosition) {", + " brush.w = 0;", + " return;", + " }", + " this.group.updateWorldMatrix(true, false);", + " const worldPosition = this._chunkPosition.copy(localPosition).applyMatrix4(this.group.matrixWorld);", + " const worldScale = this.uniforms.terrainWorldScale.value;", + " brush.set(worldPosition.x * worldScale, worldPosition.y * worldScale, radius, 1);", + " }", + "", + " /** @param {THREE.Camera | null} camera */", + " updateLevelsOfDetail(camera) {", + " if (!camera || !this.group.visible) return;", + " const matrixWorld = this.group.matrixWorld;", + " this._cameraPosition.setFromMatrixPosition(camera.matrixWorld);", + " this._scale.setFromMatrixScale(matrixWorld);", + " const cellSize = AREA_SIZE / this.terrain.data.resolution;", + " const chunkWorldSize = this.chunkCells * cellSize * Math.max(this._scale.x, this._scale.y);", + " for (const chunk of this.chunks) {", + " const distance =", + " this._chunkPosition.copy(chunk.center).applyMatrix4(matrixWorld).distanceTo(this._cameraPosition) /", + " chunkWorldSize;", + " let lod = LOD_DISTANCES.length - 1;", + " while (lod > 0 && distance < LOD_DISTANCES[lod]) lod--;", + " if (lod !== chunk.lod) {", + " chunk.lod = lod;", + " chunk.geometry.setIndex(getLodIndex(this.chunkCells, LOD_STEPS[lod]));", + " }", + " }", + " }", + "", + " disposeChunks() {", + " for (const chunk of this.chunks) {", + " chunk.mesh.removeFromParent();", + " // The index is shared by all chunks: it must stay on the GPU.", + " chunk.geometry.setIndex(null);", + " chunk.geometry.dispose();", + " }", + " this.chunks.length = 0;", + " if (this.splatTexture) this.splatTexture.dispose();", + " this.splatTexture = null;", + " if (this.colorTexture) this.colorTexture.dispose();", + " this.colorTexture = null;", + " }", + "", + " dispose() {", + " this.disposeChunks();", + " this.material.dispose();", + " this.depthMaterial.dispose();", + " this.distanceMaterial.dispose();", + " this.group.removeFromParent();", + " }", + "}", + "", + "/**", + " * Makes a Physics3D behavior use a height field shape following the terrain.", + " * Changes of heights are applied to the shape without recreating it.", + " * @implements {gdjs.Physics3DRuntimeBehavior.BodyUpdater}", + " */", + "class TerrainBodyUpdater {", + " /**", + " * @param {Terrain} terrain", + " * @param {gdjs.Physics3DRuntimeBehavior} physics", + " */", + " constructor(terrain, physics) {", + " this.terrain = terrain;", + " this.physics = physics;", + " this.defaultBodyUpdater = physics.bodyUpdater;", + " this.changedSamples = new SampleRectangle();", + " /** @type {Jolt.HeightFieldShape | null} */", + " this.heightField = null;", + " this.heightScale = 1;", + " this.heightsBufferPointer = 0;", + " this.heightsBufferLength = 0;", + " this.bodyX = 0;", + " this.bodyY = 0;", + " this.bodyZ = 0;", + " this.bodyHeight = 0;", + " physics.bodyType = 'Static';", + " physics.bodyUpdater = this;", + " }", + "", + " createShape() {", + " const { data, object } = this.terrain;", + " const worldInvScale = this.physics._sharedData.worldInvScale;", + " const { size } = data;", + " this.heightScale = object.getDepth() * worldInvScale;", + "", + " // Changes of heights (SetHeights) break collisions unless the sample", + " // count is a power of 2: extra samples have no collision.", + " const sampleCount = Math.pow(2, Math.ceil(Math.log2(size)));", + " const settings = new Jolt.HeightFieldShapeSettings();", + " settings.mScale.Set(", + " (object.getWidth() / data.resolution) * worldInvScale,", + " 1,", + " (object.getHeight() / data.resolution) * worldInvScale", + " );", + " settings.mSampleCount = sampleCount;", + " settings.mBlockSize = 4;", + " // Allows later changes of heights on the whole depth of the object.", + " settings.mMinHeightValue = 0;", + " settings.mMaxHeightValue = this.heightScale;", + " settings.mHeightSamples.resize(sampleCount * sampleCount);", + " const samples = new Float32Array(", + " Jolt.HEAPF32.buffer,", + " Jolt.getPointer(settings.mHeightSamples.data()),", + " sampleCount * sampleCount", + " );", + " const noCollision = Jolt.HeightFieldShapeConstantValues.prototype.cNoCollisionValue;", + " samples.fill(noCollision);", + " // Height fields go along X and Z: rows are flipped so that, once", + " // rotated to have heights along Z, rows go along Y.", + " for (let j = 0; j < size; j++) {", + " const row = (size - 1 - j) * sampleCount;", + " for (let i = 0; i < size; i++) {", + " samples[row + i] = this._getSampleHeight(j * size + i, noCollision);", + " }", + " }", + " const shape = settings.Create().Get();", + " Jolt.destroy(settings);", + " this.heightField = Jolt.castObject(shape, Jolt.HeightFieldShape);", + " this.changedSamples.clear();", + " return shape;", + " }", + "", + " /** @returns {number} The height of a sample in the shape, or no collision in a hole. */", + " _getSampleHeight(index, noCollision) {", + " const height = this.terrain.surfaceHeights[index];", + " return Number.isNaN(height) ? noCollision : height * this.heightScale;", + " }", + "", + " _getBodyPosition() {", + " const { object } = this.terrain;", + " const worldInvScale = this.physics._sharedData.worldInvScale;", + " this.bodyX = object.getX();", + " this.bodyY = object.getY();", + " this.bodyZ = object.getZ();", + " // The origin of the height field is at the bottom of the terrain (see createShape).", + " this.bodyHeight = object.getHeight();", + " return this.physics._sharedData.getRVec3(", + " object.getX() * worldInvScale,", + " (object.getY() + object.getHeight()) * worldInvScale,", + " object.getZ() * worldInvScale", + " );", + " }", + "", + " _getBodyRotation() {", + " // A quarter turn around X: heights go along Z.", + " return this.physics._sharedData.getQuat(Math.SQRT1_2, 0, 0, Math.SQRT1_2);", + " }", + "", + " createAndAddBody() {", + " const { physics } = this;", + " const { bodyInterface } = physics._sharedData;", + " const shape = this.createShape();", + " const settings = new Jolt.BodyCreationSettings(", + " shape,", + " this._getBodyPosition(),", + " this._getBodyRotation(),", + " Jolt.EMotionType_Static,", + " physics.getBodyLayer()", + " );", + " settings.mFriction = physics.friction;", + " settings.mRestitution = physics.restitution;", + " const body = bodyInterface.CreateBody(settings);", + " Jolt.destroy(settings);", + " bodyInterface.AddBody(body.GetID(), Jolt.EActivation_DontActivate);", + " return body;", + " }", + "", + " updateObjectFromBody() {", + " // The terrain is static: the body never moves it.", + " }", + "", + " updateBodyFromObject() {", + " const { physics } = this;", + " const body = physics._body;", + " if (!body) return;", + " const { object } = this.terrain;", + " const { bodyInterface } = physics._sharedData;", + " if (", + " object.getX() !== this.bodyX ||", + " object.getY() !== this.bodyY ||", + " object.getZ() !== this.bodyZ ||", + " object.getHeight() !== this.bodyHeight", + " ) {", + " bodyInterface.SetPositionAndRotation(", + " body.GetID(),", + " this._getBodyPosition(),", + " this._getBodyRotation(),", + " Jolt.EActivation_DontActivate", + " );", + " }", + " if (!this.changedSamples.isEmpty()) {", + " this._applyChangedHeights(body);", + " }", + " }", + "", + " _applyChangedHeights(body) {", + " const { data } = this.terrain;", + " const { size } = data;", + " const { minI, minJ, maxI, maxJ } = this.changedSamples;", + " // Jolt only changes blocks of samples. Rows are flipped and extra", + " // samples have no collision (see createShape).", + " const blockSize = this.heightField.GetBlockSize();", + " const sampleCount = this.heightField.GetSampleCount();", + " const alignDown = (value) => Math.floor(value / blockSize) * blockSize;", + " const alignUp = (value) => Math.min(Math.ceil(value / blockSize) * blockSize, sampleCount);", + " const firstX = alignDown(minI);", + " const firstRow = alignDown(size - 1 - maxJ);", + " const width = alignUp(maxI + 1) - firstX;", + " const height = alignUp(size - minJ) - firstRow;", + " if (width * height > this.heightsBufferLength) {", + " if (this.heightsBufferPointer) Jolt._webidl_free(this.heightsBufferPointer);", + " this.heightsBufferLength = width * height;", + " this.heightsBufferPointer = Jolt._webidl_malloc(this.heightsBufferLength * 4);", + " }", + " const heights = new Float32Array(Jolt.HEAPF32.buffer, this.heightsBufferPointer, width * height);", + " const noCollision = Jolt.HeightFieldShapeConstantValues.prototype.cNoCollisionValue;", + " for (let row = 0; row < height; row++) {", + " const j = size - 1 - (firstRow + row);", + " for (let column = 0; column < width; column++) {", + " const i = firstX + column;", + " heights[row * width + column] =", + " i < size && j >= 0 ? this._getSampleHeight(j * size + i, noCollision) : noCollision;", + " }", + " }", + " const sharedData = this.physics._sharedData;", + " this.heightField.SetHeights(", + " firstX,", + " firstRow,", + " width,", + " height,", + " this.heightsBufferPointer,", + " width,", + " sharedData.jolt.GetTempAllocator()", + " );", + " sharedData.bodyInterface.NotifyShapeChanged(", + " body.GetID(),", + " sharedData.getVec3(0, 0, 0),", + " false,", + " Jolt.EActivation_DontActivate", + " );", + " this._wakeUpBodiesOnChangedSamples();", + " this.changedSamples.clear();", + " }", + "", + " /** Bodies sleeping on the changed area would otherwise float or sink. */", + " _wakeUpBodiesOnChangedSamples() {", + " const { object, data } = this.terrain;", + " const worldInvScale = this.physics._sharedData.worldInvScale;", + " const cellWidth = object.getWidth() / data.resolution;", + " const cellHeight = object.getHeight() / data.resolution;", + " const { minI, minJ, maxI, maxJ } = this.changedSamples;", + " const min = new Jolt.Vec3(", + " (object.getX() + (minI - 1) * cellWidth) * worldInvScale,", + " (object.getY() + (minJ - 1) * cellHeight) * worldInvScale,", + " (object.getZ() - 1) * worldInvScale", + " );", + " const max = new Jolt.Vec3(", + " (object.getX() + (maxI + 1) * cellWidth) * worldInvScale,", + " (object.getY() + (maxJ + 1) * cellHeight) * worldInvScale,", + " (object.getZ() + object.getDepth() + 1) * worldInvScale", + " );", + " const box = new Jolt.AABox(min, max);", + " const broadPhaseLayerFilter = new Jolt.BroadPhaseLayerFilter();", + " const objectLayerFilter = new Jolt.ObjectLayerFilter();", + " this.physics._sharedData.bodyInterface.ActivateBodiesInAABox(box, broadPhaseLayerFilter, objectLayerFilter);", + " Jolt.destroy(min);", + " Jolt.destroy(max);", + " Jolt.destroy(box);", + " Jolt.destroy(broadPhaseLayerFilter);", + " Jolt.destroy(objectLayerFilter);", + " }", + "", + " recreateShape() {", + " const body = this.physics._body;", + " if (!body) return;", + " this.physics._sharedData.bodyInterface.SetShape(", + " body.GetID(),", + " this.createShape(),", + " false,", + " Jolt.EActivation_DontActivate", + " );", + " const { resolution } = this.terrain.data;", + " this.changedSamples.set(0, 0, resolution, resolution);", + " this._wakeUpBodiesOnChangedSamples();", + " this.changedSamples.clear();", + " }", + "", + " destroyBody() {", + " this.defaultBodyUpdater.destroyBody();", + " this.heightField = null;", + " }", + "", + " dispose() {", + " if (this.heightsBufferPointer) Jolt._webidl_free(this.heightsBufferPointer);", + " this.heightsBufferPointer = 0;", + " this.heightsBufferLength = 0;", + " }", + "}", + "", + "/** @param {TerrainData} data */", + "const copyTerrainData = (data) => {", + " const copy = new TerrainData(data.resolution);", + " copy.copyFrom(data);", + " return copy;", + "};", + "", + "// Generating a relief or reading an image is long for big terrains, and done", + "// for every instance and every hot-reload: the results are kept.", + "const baseDataCache = new Map();", + "const MAX_CACHED_BASE_DATA = 8;", + "// Changes of the shape of a terrain kept for consumers of its surface that", + "// missed some versions: older changes update the whole surface.", + "const MAX_SURFACE_CHANGES = 64;", + "// Unique among all terrains: a terrain replaced by another one never has its version.", + "let lastGroundVersion = 0;", + "", + "/**", + " * The terrain of a Terrain3D object: its data, how it's drawn and its", + " * surface (for collisions and navigation meshes). Positions are in the scene.", + " */", + "class Terrain {", + " /** @param {gdjs.CustomRuntimeObject3D} object */", + " constructor(object) {", + " this.object = object;", + " /** @type {TerrainData} */", + " this.data = new TerrainData(DEFAULT_RESOLUTION);", + " /** The data before any sculpting or painting, from the relief or heightmap. */", + " this.baseData = this.data;", + " this.renderer = new TerrainRenderer(this);", + " /** @type {TerrainBodyUpdater[]} */", + " this.bodyUpdaters = [];", + " this.changedSamples = new SampleRectangle();", + " this.hasPaintChanged = false;", + " /** Increased each time the ground changes, for objects following it (like grass). */", + " this.version = 0;", + " /** Increased each time the shape of the ground changes (not its paint or position). */", + " this.surfaceVersion = 0;", + " /**", + " * The last changes of the shape of the ground, for `getChangedArea`.", + " * @type {Array<{version: number, samples: SampleRectangle}>}", + " */", + " this.surfaceChanges = [];", + " /** The changes since this version are all in `surfaceChanges`. */", + " this.surfaceChangesStartVersion = 0;", + " this._stroke = { cellWidth: 1, cellHeight: 1, ax: 0, ay: 0, bx: 0, by: 0, radius: 0 };", + " this._strokeChangedSamples = new SampleRectangle();", + " /** The properties the data was loaded from, to reload it only if they change. */", + " this.loadedFrom = '';", + " /** The heightmap image being loaded: the relief is used until it is. */", + " this.loadingHeightmapImage = '';", + " this.isDisposed = false;", + " /** The last value of the sculpt data property read or saved. */", + " this.sculptData = '';", + " /**", + " * Values saved by the editor tools: the editor sends them back with a", + " * hot-reload, possibly after newer ones were saved.", + " * @type {string[]}", + " */", + " this.savedSculptData = [];", + " /** The heights of the surface: the heights of the data, and NaN in holes. */", + " this.surfaceHeights = new Float32Array(0);", + " this.loadFromProperties();", + " this._transform = this._getTransform();", + " getGrounds(object.getRuntimeScene()).add(this);", + " /**", + " * The shape of the ground, for the physics engine and navigation meshes.", + " * @type {gdjs.Surface}", + " */", + " this.surface = {", + " getVersion: () => this.surfaceVersion,", + " getChangedArea: (sinceVersion) => this.getChangedArea(sinceVersion),", + " getTriangles: () => null,", + " getHeightField: () => ({ columns: this.data.size, rows: this.data.size, heights: this.surfaceHeights }),", + " };", + " if (typeof object.setSurface === 'function') {", + " object.setSurface(this.surface);", + " } else if (gdjs.Physics3DRuntimeBehavior) {", + " // Older GDevelop versions don't use surfaces.", + " for (const behavior of object._behaviors) {", + " if (behavior instanceof gdjs.Physics3DRuntimeBehavior) {", + " this.bodyUpdaters.push(new TerrainBodyUpdater(this, behavior));", + " }", + " }", + " }", + " }", + "", + " _getSourceProperties() {", + " const { object } = this;", + " return [", + " object._getResolution(),", + " object._getRelief(),", + " object._getSeed(),", + " object._getHeightmapImage(),", + " object._getEdits(),", + " ].join('|');", + " }", + "", + " loadFromProperties() {", + " const { object } = this;", + " const resolution = parseInt(object._getResolution(), 10);", + " const validResolution = RESOLUTIONS.indexOf(resolution) !== -1 ? resolution : DEFAULT_RESOLUTION;", + " this.sculptData = object._getSculptData();", + " this.loadedFrom = this._getSourceProperties();", + "", + " this.baseData = this._getBaseData(validResolution);", + " let data = codec.decode(this.sculptData, (resolution) => copyTerrainData(this._getBaseData(resolution)));", + " if (data && data.resolution !== validResolution) {", + " data = data.resampled(validResolution);", + " }", + " this.setData(data || copyTerrainData(this.baseData));", + " }", + "", + " /**", + " * @param {number} resolution", + " * @returns {TerrainData} The data from the relief or the heightmap, shared: it must not be changed.", + " */", + " _getBaseData(resolution) {", + " const { object } = this;", + " const key = [resolution, object._getRelief(), object._getSeed(), object._getHeightmapImage(), object._getEdits()].join('|');", + " const cachedData = baseDataCache.get(key);", + " if (cachedData) return cachedData;", + "", + " const data = new TerrainData(resolution);", + " const heightmapImage = object._getHeightmapImage();", + " const isHeightmapLoaded =", + " !!heightmapImage && loadHeightsFromImage(data, object.getRuntimeScene().getGame(), heightmapImage);", + " if (!isHeightmapLoaded) {", + " relief.generate(data, object._getRelief(), object._getSeed());", + " if (heightmapImage) this._reloadOnceLoaded(heightmapImage);", + " }", + " edits.apply(data, object._getEdits());", + " // Not kept if the image is missing: it may be loaded later.", + " if (isHeightmapLoaded || !heightmapImage) baseDataCache.set(key, data);", + " if (baseDataCache.size > MAX_CACHED_BASE_DATA) {", + " baseDataCache.delete(baseDataCache.keys().next().value);", + " }", + " return data;", + " }", + "", + " /** @param {TerrainData} data */", + " setData(data) {", + " this.version = ++lastGroundVersion;", + " this.surfaceVersion = this.version;", + " this.surfaceChanges.length = 0;", + " this.surfaceChangesStartVersion = this.version;", + " const hasSameResolution = data.resolution === this.data.resolution && this.renderer.chunks.length > 0;", + " this.data = data;", + " const allSamples = new SampleRectangle().set(0, 0, data.resolution, data.resolution);", + " this.surfaceHeights = new Float32Array(data.heights.length);", + " this.renderer.setHasHoles(false);", + " this._updateSurfaceHeights(allSamples);", + " if (hasSameResolution) {", + " this.renderer.updateSamples(allSamples, true);", + " } else {", + " this.renderer.rebuild();", + " }", + " for (const bodyUpdater of this.bodyUpdaters) {", + " bodyUpdater.recreateShape();", + " }", + " }", + "", + " onHotReloading() {", + " const sculptData = this.object._getSculptData();", + " const savedIndex = this.savedSculptData.indexOf(sculptData);", + " if (savedIndex !== -1 && this._getSourceProperties() === this.loadedFrom) {", + " // The terrain already has this data, or newer changes.", + " this.savedSculptData.splice(0, savedIndex + 1);", + " } else if (sculptData !== this.sculptData || this._getSourceProperties() !== this.loadedFrom) {", + " this.savedSculptData.length = 0;", + " this.loadFromProperties();", + " }", + " this.renderer.updateMaterial();", + " }", + "", + " /** @returns {string} The data to save in the sculpt data property. */", + " save() {", + " this.sculptData = codec.encode(this.data, this.baseData);", + " this.savedSculptData.push(this.sculptData);", + " return this.sculptData;", + " }", + "", + " getCellWidth() {", + " return this.object.getWidth() / this.data.resolution;", + " }", + "", + " getCellHeight() {", + " return this.object.getHeight() / this.data.resolution;", + " }", + "", + " _toGridX(x) {", + " return (x - this.object.getX()) / this.getCellWidth();", + " }", + "", + " _toGridY(y) {", + " return (y - this.object.getY()) / this.getCellHeight();", + " }", + "", + " /** @returns {number} The Z position of the ground. */", + " getHeightAt(x, y) {", + " return this.object.getZ() + this.data.sampleHeight(this._toGridX(x), this._toGridY(y)) * this.object.getDepth();", + " }", + "", + " /** @returns {number} The angle of the ground with the horizontal, in degrees. */", + " getSlopeAt(x, y) {", + " const gridX = this._toGridX(x);", + " const gridY = this._toGridY(y);", + " const { data } = this;", + " const depth = this.object.getDepth();", + " const slopeX = ((data.sampleHeight(gridX + 0.5, gridY) - data.sampleHeight(gridX - 0.5, gridY)) * depth) / this.getCellWidth();", + " const slopeY = ((data.sampleHeight(gridX, gridY + 0.5) - data.sampleHeight(gridX, gridY - 0.5)) * depth) / this.getCellHeight();", + " return gdjs.toDegrees(Math.atan(Math.hypot(slopeX, slopeY)));", + " }", + "", + " /** @returns {number} The layer painted the most, from 1 to 4. */", + " getLayerAt(x, y) {", + " return this.data.getDominantLayer(this._toGridX(x), this._toGridY(y)) + 1;", + " }", + "", + " /**", + " * @param {number} layer From 1 to 4.", + " * @returns {number} How much the layer is painted, from 0 to 1.", + " */", + " getLayerWeightAt(x, y, layer) {", + " const layerIndex = clampInteger(layer - 1, 0, LAYER_COUNT - 1);", + " return this.data.sampleLayerWeight(this._toGridX(x), this._toGridY(y), layerIndex);", + " }", + "", + " /** @returns {number[]} The painted tint: red, green and blue, from 0 to 255 (white when there is none). */", + " getTintAt(x, y) {", + " const gridX = this._toGridX(x);", + " const gridY = this._toGridY(y);", + " return [0, 1, 2].map((channel) => Math.round(this.data.sampleColor(gridX, gridY, channel) * 255));", + " }", + "", + " /** @returns {number} How wet the ground is, from 0 (dry) to 1, painted or near the water (see the wet shore). */", + " getWetnessAt(x, y) {", + " const paintedWetness = this.data.sampleColor(this._toGridX(x), this._toGridY(y), 3);", + " const { object } = this;", + " if (!object._getWetShore()) return paintedWetness;", + " const fade = Math.max(object._getWetShoreFade(), 0.001);", + " const shoreWetness = clamp01((object._getWetShoreZ() + fade - this.getHeightAt(x, y)) / fade);", + " // Like the smoothstep of the shader.", + " return Math.max(paintedWetness, shoreWetness * shoreWetness * (3 - 2 * shoreWetness));", + " }", + "", + " _isInArea(x, y) {", + " const { object } = this;", + " return (", + " x >= object.getX() &&", + " x <= object.getX() + object.getWidth() &&", + " y >= object.getY() &&", + " y <= object.getY() + object.getHeight()", + " );", + " }", + "", + " /** @returns {boolean} true if the position is above or under the terrain, and not in a hole. */", + " containsPoint(x, y) {", + " return this._isInArea(x, y) && this.data.sampleSolidity(this._toGridX(x), this._toGridY(y)) >= 0.5;", + " }", + "", + " /** @returns {boolean} true if there is a hole in the terrain at the position, like what is drawn. */", + " isHoleAt(x, y) {", + " return this._isInArea(x, y) && this.data.sampleSolidity(this._toGridX(x), this._toGridY(y)) < 0.5;", + " }", + "", + " _getStroke(x1, y1, x2, y2, radius) {", + " const stroke = this._stroke;", + " stroke.cellWidth = this.getCellWidth();", + " stroke.cellHeight = this.getCellHeight();", + " stroke.ax = x1 - this.object.getX();", + " stroke.ay = y1 - this.object.getY();", + " stroke.bx = x2 - this.object.getX();", + " stroke.by = y2 - this.object.getY();", + " stroke.radius = Math.max(radius, 0);", + " return stroke;", + " }", + "", + " /**", + " * @param {SampleRectangle} samples", + " * @param {boolean} haveHeightsChanged false when only the paint changed.", + " */", + " _onSamplesChanged(samples, haveHeightsChanged) {", + " if (samples.isEmpty()) return;", + " this.version = ++lastGroundVersion;", + " this.changedSamples.add(samples);", + " // Painting changes the surface when it makes holes or fills them.", + " const hasSurfaceChanged = this._updateSurfaceHeights(samples);", + " if (!haveHeightsChanged && !hasSurfaceChanged) return;", + " this.surfaceVersion = this.version;", + " this.surfaceChanges.push({ version: this.version, samples: new SampleRectangle().set(samples.minI, samples.minJ, samples.maxI, samples.maxJ) });", + " if (this.surfaceChanges.length > MAX_SURFACE_CHANGES) {", + " this.surfaceChangesStartVersion = this.surfaceChanges.shift().version;", + " }", + " for (const bodyUpdater of this.bodyUpdaters) {", + " bodyUpdater.changedSamples.add(samples);", + " }", + " }", + "", + " /**", + " * @param {SampleRectangle} samples", + " * @returns {boolean} true if heights of the surface changed, or holes were made or filled.", + " */", + " _updateSurfaceHeights(samples) {", + " const { data, surfaceHeights } = this;", + " let hasChanged = false;", + " let hasHoles = false;", + " for (let j = samples.minJ; j <= samples.maxJ; j++) {", + " for (let i = samples.minI; i <= samples.maxI; i++) {", + " const index = j * data.size + i;", + " const isHole = data.isHole(index);", + " const height = isHole ? NaN : data.heights[index];", + " const previousHeight = surfaceHeights[index];", + " if (isHole) {", + " hasHoles = true;", + " if (Number.isNaN(previousHeight)) continue;", + " } else if (height === previousHeight) {", + " continue;", + " }", + " surfaceHeights[index] = height;", + " hasChanged = true;", + " }", + " }", + " // Filled holes keep the shaders drawing holes until the data is loaded again.", + " if (hasHoles) this.renderer.setHasHoles(true);", + " return hasChanged;", + " }", + "", + " /** @param {number} height In scene units, negative to lower the ground. */", + " raise(x1, y1, x2, y2, radius, height) {", + " const samples = this._strokeChangedSamples;", + " brushes.raise(this.data, this._getStroke(x1, y1, x2, y2, radius), height / this.object.getDepth(), samples);", + " this._onSamplesChanged(samples, true);", + " }", + "", + " /** @param {number} z The Z position to flatten the ground to. */", + " /** @param {number} [toZ] The Z position at the end of the segment, for a slope. The same as z by default. */", + " flatten(x1, y1, x2, y2, radius, z, strength, toZ = z) {", + " const samples = this._strokeChangedSamples;", + " const toHeight = (value) => (value - this.object.getZ()) / this.object.getDepth();", + " brushes.flatten(this.data, this._getStroke(x1, y1, x2, y2, radius), toHeight(z), strength, samples, toHeight(toZ));", + " this._onSamplesChanged(samples, true);", + " }", + "", + " smooth(x1, y1, x2, y2, radius, strength) {", + " const samples = this._strokeChangedSamples;", + " brushes.smooth(this.data, this._getStroke(x1, y1, x2, y2, radius), strength, samples);", + " this._onSamplesChanged(samples, true);", + " }", + "", + " /** @param {number} layer From 1 to 4. */", + " paint(x1, y1, x2, y2, radius, layer, strength) {", + " const layerIndex = clampInteger(layer - 1, 0, LAYER_COUNT - 1);", + " const samples = this._strokeChangedSamples;", + " brushes.paint(this.data, this._getStroke(x1, y1, x2, y2, radius), layerIndex, strength, samples);", + " this.hasPaintChanged = true;", + " this._onSamplesChanged(samples, false);", + " }", + "", + " /** @param {number[]} color The red, green and blue (0-255) of the tint: white removes it. */", + " tint(x1, y1, x2, y2, radius, color, strength) {", + " const samples = this._strokeChangedSamples;", + " brushes.tint(this.data, this._getStroke(x1, y1, x2, y2, radius), color, strength, samples);", + " this.hasPaintChanged = true;", + " this._onSamplesChanged(samples, false);", + " }", + "", + " /** @param {number} wetness From 0 (dry) to 1. */", + " wet(x1, y1, x2, y2, radius, wetness, strength) {", + " const samples = this._strokeChangedSamples;", + " brushes.wet(this.data, this._getStroke(x1, y1, x2, y2, radius), wetness, strength, samples);", + " this.hasPaintChanged = true;", + " this._onSamplesChanged(samples, false);", + " }", + "", + " /** Make a hole around a segment (`fillHoles` fills it again). */", + " makeHole(x1, y1, x2, y2, radius) {", + " const samples = this._strokeChangedSamples;", + " brushes.hole(this.data, this._getStroke(x1, y1, x2, y2, radius), samples);", + " this.hasPaintChanged = true;", + " this._onSamplesChanged(samples, false);", + " }", + "", + " /** Fill the holes around a segment with the layers of the ground around them. */", + " fillHoles(x1, y1, x2, y2, radius) {", + " const samples = this._strokeChangedSamples;", + " brushes.fill(this.data, this._getStroke(x1, y1, x2, y2, radius), samples);", + " this.hasPaintChanged = true;", + " this._onSamplesChanged(samples, false);", + " }", + "", + " /**", + " * Make the entrance of a tunnel or a cave: a hole, with the ground in", + " * front of it flattened to the floor of the tunnel.", + " * @param {number} angle The direction going into the ground, in degrees.", + " * @param {number} floorZ", + " * @param {number} length The length of the flat ground in front of the entrance.", + " */", + " carveEntrance(x, y, radius, angle, floorZ, length) {", + " const directionX = Math.cos(gdjs.toRad(angle));", + " const directionY = Math.sin(gdjs.toRad(angle));", + " this.flatten(x - directionX * length, y - directionY * length, x, y, radius * 1.5, floorZ, 1);", + " this.makeHole(x, y, x + directionX * radius, y + directionY * radius, radius);", + " }", + "", + " generate(reliefName, seed) {", + " this.baseData = new TerrainData(this.data.resolution);", + " relief.generate(this.baseData, reliefName, seed);", + " this.setData(copyTerrainData(this.baseData));", + " }", + "", + " loadHeightmap(imageResourceName) {", + " const baseData = new TerrainData(this.data.resolution);", + " if (loadHeightsFromImage(baseData, this.object.getRuntimeScene().getGame(), imageResourceName)) {", + " baseData.splat.set(this.data.splat);", + " baseData.colors.set(this.data.colors);", + " this.baseData = baseData;", + " this.setData(copyTerrainData(baseData));", + " }", + " }", + "", + " /** @param {gdjs.RuntimeObject[]} objects */", + " placeOnGround(objects) {", + " for (const object of objects) {", + " if (object === this.object || !gdjs.Base3DHandler.is3D(object)) continue;", + " const groundZ = this.getHeightAt(object.getCenterXInScene(), object.getCenterYInScene());", + " // The origin of the object is not always at its bottom.", + " object.setZ(groundZ + object.getZ() - object.getUnrotatedAABBMinZ());", + " }", + " }", + "", + " /**", + " * @returns {THREE.Vector3 | null} The point of the ground under the cursor,", + " * in the terrain group coordinates. Holes are hit too, so that brushes can fill them.", + " */", + " raycastFromCursor(game, raycaster, ndc) {", + " const layer = this.object.getInstanceContainer().getLayer(this.object.getLayer());", + " const camera = layer.getRenderer().getThreeCamera();", + " if (!camera) return null;", + " const inputManager = game.getInputManager();", + " ndc.set(", + " (inputManager.getCursorX() / game.getGameResolutionWidth()) * 2 - 1,", + " -(inputManager.getCursorY() / game.getGameResolutionHeight()) * 2 + 1", + " );", + " raycaster.setFromCamera(ndc, camera);", + " this.renderer.group.updateWorldMatrix(true, false);", + " const ray = raycaster.ray.clone().applyMatrix4(this.renderer.group.matrixWorld.clone().invert());", + " ray.direction.normalize();", + "", + " const box = new THREE.Box3(new THREE.Vector3(0, 0, 0), new THREE.Vector3(AREA_SIZE, AREA_SIZE, AREA_DEPTH));", + " const entry = box.containsPoint(ray.origin) ? ray.origin.clone() : ray.intersectBox(box, new THREE.Vector3());", + " if (!entry) return null;", + " const cellSize = AREA_SIZE / this.data.resolution;", + " const groundZAt = (point) => this.data.sampleHeight(point.x / cellSize, point.y / cellSize) * AREA_DEPTH;", + " const step = cellSize / 2;", + " const point = entry.clone();", + " const previousPoint = entry.clone();", + " for (let distance = 0; distance < AREA_SIZE * 2; distance += step) {", + " if (point.z <= groundZAt(point)) {", + " // Refine between the last point above the ground and this one.", + " for (let iteration = 0; iteration < 8; iteration++) {", + " const middle = previousPoint.clone().lerp(point, 0.5);", + " if (middle.z <= groundZAt(middle)) point.copy(middle);", + " else previousPoint.copy(middle);", + " }", + " return point;", + " }", + " if (!box.containsPoint(point)) return null;", + " previousPoint.copy(point);", + " point.addScaledVector(ray.direction, step);", + " }", + " return null;", + " }", + "", + " /**", + " * @param {THREE.Vector3} localPosition In the terrain group coordinates (like from `raycastFromCursor`).", + " * @returns {{x: number, y: number, z: number}} The position in the scene.", + " */", + " toScenePosition(localPosition) {", + " const { object } = this;", + " return {", + " x: object.getX() + (localPosition.x / AREA_SIZE) * object.getWidth(),", + " y: object.getY() + (localPosition.y / AREA_SIZE) * object.getHeight(),", + " z: object.getZ() + (localPosition.z / AREA_DEPTH) * object.getDepth(),", + " };", + " }", + "", + " /** Draw the changes of the ground (collisions are updated by the physics engine). */", + " applyChanges() {", + " this.renderer.updateSamples(this.changedSamples, this.hasPaintChanged);", + " this.changedSamples.clear();", + " this.hasPaintChanged = false;", + " }", + "", + " /** @returns {number} A number changing each time the ground changes (see `getGrounds`). */", + " getVersion() {", + " return this.version;", + " }", + "", + " /**", + " * @param {number} sinceVersion", + " * @returns {gdjs.SurfaceArea | null} The part of the ground whose shape changed since the version (see `gdjs.Surface`).", + " */", + " getChangedArea(sinceVersion) {", + " if (sinceVersion < this.surfaceChangesStartVersion) return null;", + " const samples = new SampleRectangle();", + " for (const change of this.surfaceChanges) {", + " if (change.version > sinceVersion) samples.add(change.samples);", + " }", + " if (samples.isEmpty()) return { minX: 0, minY: 0, maxX: 0, maxY: 0 };", + " const { resolution } = this.data;", + " return {", + " minX: samples.minI / resolution,", + " minY: samples.minJ / resolution,", + " maxX: samples.maxI / resolution,", + " maxY: samples.maxJ / resolution,", + " };", + " }", + "", + " _getTransform() {", + " const { object } = this;", + " return [object.getX(), object.getY(), object.getZ(), object.getWidth(), object.getHeight(), object.getDepth()].join(',');", + " }", + "", + " /** @param {string} imageResourceName A heightmap image that is not loaded yet. */", + " _reloadOnceLoaded(imageResourceName) {", + " if (this.loadingHeightmapImage === imageResourceName) return;", + " this.loadingHeightmapImage = imageResourceName;", + " const game = this.object.getRuntimeScene().getGame();", + " game.getImageManager()", + " .loadResource(imageResourceName)", + " .then(() => {", + " if (this.isDisposed || this.loadingHeightmapImage !== imageResourceName) return;", + " this.loadingHeightmapImage = '';", + " if (getLoadedImage(game, imageResourceName)) this.loadFromProperties();", + " })", + " .catch(() => {});", + " }", + "", + " update() {", + " const transform = this._getTransform();", + " if (transform !== this._transform) {", + " this._transform = transform;", + " this.version = ++lastGroundVersion;", + " }", + " this.applyChanges();", + " this.renderer.uniforms.terrainWorldScale.value = getWorldScale(this.object);", + " this._updateWetShore();", + " const layer = this.object.getInstanceContainer().getLayer(this.object.getLayer());", + " this.renderer.updateLevelsOfDetail(layer.getRenderer().getThreeCamera());", + " }", + "", + " _updateWetShore() {", + " const { object } = this;", + " // In the coordinates of the terrain group, like the vertices.", + " const depthScale = AREA_DEPTH / Math.max(object.getDepth(), 0.0001);", + " this.renderer.uniforms.terrainWetShore.value.set(", + " (object._getWetShoreZ() - object.getZ()) * depthScale,", + " Math.max(object._getWetShoreFade(), 0.001) * depthScale,", + " object._getWetShore() ? 1 : 0", + " );", + " }", + "", + " dispose() {", + " this.isDisposed = true;", + " getGrounds(this.object.getRuntimeScene()).delete(this);", + " if (typeof this.object.setSurface === 'function' && this.object.getSurface() === this.surface) {", + " this.object.setSurface(null);", + " }", + " this.renderer.dispose();", + " for (const bodyUpdater of this.bodyUpdaters) {", + " bodyUpdater.dispose();", + " }", + " }", + "}", + "", + "/**", + " * @param {gdjs.RuntimeObject} object", + " * @returns {number} The number of scene units by 3D world unit (1 in GDevelop versions before the world scale).", + " */", + "const getWorldScale = (object) => {", + " const scene = object.getRuntimeScene();", + " return typeof scene.getRenderer3DWorldScale === 'function' ? scene.getRenderer3DWorldScale() : 1;", + "};", + "", + "const averageColors = new WeakMap();", + "let white = null;", + "/**", + " * @param {THREE.Texture} texture", + " * @returns {THREE.Color} The average color of the texture (white when it's not loaded).", + " */", + "const getAverageColor = (texture) => {", + " const cachedColor = averageColors.get(texture);", + " if (cachedColor) return cachedColor;", + " const image = texture.image;", + " const isDrawable =", + " typeof document !== 'undefined' &&", + " ((image instanceof HTMLImageElement && image.complete && image.naturalWidth > 0) ||", + " image instanceof HTMLCanvasElement ||", + " (typeof ImageBitmap !== 'undefined' && image instanceof ImageBitmap));", + " if (!isDrawable) return white || (white = new THREE.Color(1, 1, 1));", + " const size = 8;", + " const canvas = document.createElement('canvas');", + " canvas.width = size;", + " canvas.height = size;", + " const context = canvas.getContext('2d');", + " context.drawImage(image, 0, 0, size, size);", + " const pixels = context.getImageData(0, 0, size, size).data;", + " let red = 0;", + " let green = 0;", + " let blue = 0;", + " for (let index = 0; index < pixels.length; index += 4) {", + " red += pixels[index];", + " green += pixels[index + 1];", + " blue += pixels[index + 2];", + " }", + " const count = size * size * 255;", + " const color = new THREE.Color().setRGB(red / count, green / count, blue / count, THREE.SRGBColorSpace);", + " averageColors.set(texture, color);", + " return color;", + "};", + "", + "const svgIcon = (content) =>", + " 'data:image/svg+xml,' +", + " encodeURIComponent('' + content + '');", + "const strokes = 'fill=\"none\" stroke=\"#000\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"';", + "const toolIcons = {", + " Raise: svgIcon(''),", + " Lower: svgIcon(''),", + " Smooth: svgIcon(''),", + " Flatten: svgIcon(''),", + " Size: svgIcon(''),", + " Strength: svgIcon(''),", + " Undo: svgIcon(''),", + " Hole: svgIcon(''),", + " FillHoles: svgIcon(''),", + " Wetness: svgIcon(''),", + "};", + "// Tints for the color brush when the editor has no color picker (older GDevelop versions).", + "const tintPresets = ['#e2c879', '#a5c45a', '#8a6a48', '#c98a62', '#9a9a92', '#ffffff'];", + "", + "/**", + " * The sculpt and paint tools shown in the scene editor when a terrain is", + " * selected. Without the needed editor functions (older GDevelop versions),", + " * nothing is shown: the terrain can still be changed with events.", + " */", + "class TerrainEditorTools {", + " constructor() {", + " /** '' when no brush is used. */", + " this.toolName = '';", + " this.sizePercent = 30;", + " this.strengthPercent = 50;", + " /** @type {Terrain | null} */", + " this.terrain = null;", + " /** @type {gdjs.InGameEditor | null} */", + " this.editor = null;", + " this.isStroking = false;", + " this.flattenHeight = 0;", + " /** The color of the \"Tint\" brush, as \"#rrggbb\". */", + " this.tintColor = tintPresets[0];", + " this.wetnessPercent = 100;", + " this.lastStrokeTime = 0;", + " /** @type {{data: TerrainData, baseData: TerrainData}[]} */", + " this.undoSteps = [];", + " this.raycaster = new THREE.Raycaster();", + " this.ndc = new THREE.Vector2();", + " this._swatchColor = new THREE.Color();", + " }", + "", + " /** @param {gdjs.InGameEditor} editor */", + " update(editor) {", + " if (typeof editor.showToolbar !== 'function') {", + " // Older GDevelop versions: the terrain can only be changed with events.", + " return;", + " }", + " this.editor = editor;", + " const selectedObjects = editor.getSelectedObjects();", + " const terrain =", + " selectedObjects.length === 1 && selectedObjects[0].__terrain3D ? selectedObjects[0].__terrain3D : null;", + " if (terrain !== this.terrain) {", + " this._endStroke();", + " if (this.terrain) this.terrain.renderer.setBrush(null, 0);", + " this.terrain = terrain;", + " this.undoSteps.length = 0;", + " }", + " if (terrain) editor.showToolbar('Terrain3D', this._getToolbarItems(terrain));", + " const game = editor.getRuntimeGame();", + " if (!terrain || !this.toolName) {", + " if (terrain) terrain.renderer.setBrush(null, 0);", + " return;", + " }", + "", + " const inputManager = game.getInputManager();", + " if (inputManager.wasKeyJustPressed(gdjs.evtTools.input.keysNameToCode.Escape)) {", + " this._endStroke();", + " this.toolName = '';", + " terrain.renderer.setBrush(null, 0);", + " return;", + " }", + " editor.captureLeftMouseButton();", + " const hit = terrain.raycastFromCursor(game, this.raycaster, this.ndc);", + " const radius = this._getRadius();", + " terrain.renderer.setBrush(hit, radius);", + "", + " // Older GDevelop versions don't have it.", + " const isPressedForTool =", + " typeof editor.isCapturedLeftMouseButtonPressed === 'function'", + " ? editor.isCapturedLeftMouseButtonPressed()", + " : inputManager.isMouseButtonPressed(0);", + " // The button is released outside the canvas when the mouse is on a toolbar.", + " const isPressed = isPressedForTool && inputManager.isMouseInsideCanvas();", + " if (isPressed && hit && !this.isStroking) {", + " this._startStroke(terrain, hit);", + " }", + " if (this.isStroking && isPressed && hit) {", + " this._applyBrush(terrain, hit, radius);", + " }", + " if (!isPressed) {", + " this._endStroke();", + " }", + " }", + "", + " _getRadius() {", + " const { object } = this.terrain;", + " const maxRadius = Math.max(object.getWidth(), object.getHeight()) / 4;", + " return Math.max(maxRadius * Math.pow(this.sizePercent / 100, 2), this.terrain.getCellWidth());", + " }", + "", + " _startStroke(terrain, hit) {", + " this.isStroking = true;", + " this.lastStrokeTime = performance.now();", + " this.flattenHeight = terrain.toScenePosition(hit).z;", + " this.undoSteps.push({ data: copyTerrainData(terrain.data), baseData: terrain.baseData });", + " const maxUndoSteps = Math.max(Math.floor(UNDO_STEPS_SAMPLES / terrain.data.heights.length), 2);", + " while (this.undoSteps.length > maxUndoSteps) this.undoSteps.shift();", + " }", + "", + " _endStroke() {", + " if (!this.isStroking) return;", + " this.isStroking = false;", + " this._save();", + " }", + "", + " _applyBrush(terrain, hit, radius) {", + " const now = performance.now();", + " const elapsedSeconds = Math.min((now - this.lastStrokeTime) / 1000, 0.1);", + " this.lastStrokeTime = now;", + " const { x, y } = terrain.toScenePosition(hit);", + " const amount = (this.strengthPercent / 100) * elapsedSeconds;", + " const depth = terrain.object.getDepth();", + " if (this.toolName === 'Raise') terrain.raise(x, y, x, y, radius, amount * depth * 0.5);", + " else if (this.toolName === 'Lower') terrain.raise(x, y, x, y, radius, -amount * depth * 0.5);", + " else if (this.toolName === 'Smooth') terrain.smooth(x, y, x, y, radius, amount * 10);", + " else if (this.toolName === 'Flatten') terrain.flatten(x, y, x, y, radius, this.flattenHeight, amount * 10);", + " else if (this.toolName.startsWith('Paint')) {", + " terrain.paint(x, y, x, y, radius, parseInt(this.toolName.slice(5), 10), amount * 8);", + " } else if (this.toolName === 'Hole') {", + " terrain.makeHole(x, y, x, y, radius);", + " } else if (this.toolName === 'FillHoles') {", + " terrain.fillHoles(x, y, x, y, radius);", + " } else if (this.toolName === 'Tint') {", + " terrain.tint(x, y, x, y, radius, gdjs.rgbOrHexToRGBColor(this.tintColor), amount * 8);", + " } else if (this.toolName === 'Wetness') {", + " terrain.wet(x, y, x, y, radius, this.wetnessPercent / 100, amount * 8);", + " }", + " // Drawn now, as the terrain was updated earlier in this frame.", + " terrain.applyChanges();", + " }", + "", + " _save() {", + " const { terrain, editor } = this;", + " if (!terrain || !editor) return;", + " editor.updateObjectProperties(terrain.object.getName(), { SculptData: terrain.save() });", + " }", + "", + " _undo() {", + " const undoStep = this.undoSteps.pop();", + " // The relief, the edits or the resolution may have changed since the stroke.", + " if (!this.terrain || !undoStep || undoStep.baseData !== this.terrain.baseData) {", + " this.undoSteps.length = 0;", + " return;", + " }", + " this.terrain.setData(undoStep.data);", + " this._save();", + " }", + "", + " /** @returns {Array} The settings of the tint or wetness brush, when it's used. */", + " _getColorToolItems() {", + " const { editor } = this;", + " if (this.toolName === 'Tint') {", + " if (editor && typeof editor.isToolbarItemTypeSupported === 'function' && editor.isToolbarItemTypeSupported('color')) {", + " return [", + " {", + " type: 'color',", + " id: 'TintColor',", + " tooltip: 'Color of the tint',", + " value: this.tintColor,", + " onChange: (color) => (this.tintColor = color),", + " },", + " ];", + " }", + " return tintPresets.map((color) => ({", + " type: 'button',", + " id: 'TintPreset' + color,", + " tooltip: color === '#ffffff' ? 'No tint' : 'Tint with this color',", + " color,", + " isActive: this.tintColor === color,", + " onClick: () => (this.tintColor = color),", + " }));", + " }", + " if (this.toolName === 'Wetness') {", + " return [", + " {", + " type: 'slider',", + " id: 'WetnessAmount',", + " tooltip: 'Wetness',", + " iconUrl: toolIcons.Wetness,", + " min: 0,", + " max: 100,", + " value: this.wetnessPercent,", + " onChange: (value) => (this.wetnessPercent = value),", + " },", + " ];", + " }", + " return [];", + " }", + "", + " /**", + " * @param {Terrain} terrain", + " * @returns {Array} The toolbar for the current state (the editor only", + " * updates what changed).", + " */", + " _getToolbarItems(terrain) {", + " const { object } = terrain;", + " const toolButton = (toolName, tooltip, color) => ({", + " type: 'button',", + " id: toolName,", + " tooltip,", + " iconUrl: toolIcons[toolName],", + " color,", + " isActive: this.toolName === toolName,", + " onClick: () => (this.toolName = this.toolName === toolName ? '' : toolName),", + " });", + " const layerColors = [object._getLayer1Color(), object._getLayer2Color(), object._getLayer3Color(), object._getLayer4Color()];", + " // Layers are shown with their color, tinting their texture.", + " const swatchColors = layerColors.map((color, index) =>", + " this._swatchColor", + " .set(gdjs.rgbOrHexStringToNumber(color))", + " .multiply(getAverageColor(terrain.renderer.uniforms['terrainLayerMap' + index].value))", + " .getHexString()", + " );", + " return [", + " toolButton('Raise', 'Raise the ground (drag on the terrain)'),", + " toolButton('Lower', 'Lower the ground'),", + " toolButton('Smooth', 'Smooth the ground'),", + " toolButton('Flatten', 'Flatten the ground to the height where the drag starts'),", + " { type: 'divider', id: 'PaintDivider' },", + " ...swatchColors.map((swatchColor, index) =>", + " toolButton('Paint' + (index + 1), 'Paint layer ' + (index + 1), '#' + swatchColor)", + " ),", + " toolButton('Hole', 'Make a hole, for the entrance of a tunnel or a cave'),", + " toolButton('FillHoles', 'Fill holes, with the layers of the ground around them'),", + " { type: 'divider', id: 'ColorDivider' },", + " toolButton('Tint', 'Tint the ground (white removes the tint)', this.tintColor),", + " toolButton('Wetness', 'Make the ground wet, or dry with a wetness of 0'),", + " ...this._getColorToolItems(),", + " { type: 'divider', id: 'BrushDivider' },", + " {", + " type: 'slider',", + " id: 'Size',", + " tooltip: 'Brush size',", + " iconUrl: toolIcons.Size,", + " min: 1,", + " max: 100,", + " value: this.sizePercent,", + " onChange: (value) => (this.sizePercent = value),", + " },", + " {", + " type: 'slider',", + " id: 'Strength',", + " tooltip: 'Brush strength',", + " iconUrl: toolIcons.Strength,", + " min: 5,", + " max: 100,", + " value: this.strengthPercent,", + " onChange: (value) => (this.strengthPercent = value),", + " },", + " { type: 'button', id: 'Undo', tooltip: 'Undo the last stroke on this terrain', iconUrl: toolIcons.Undo, onClick: () => this._undo() },", + " ];", + " }", + "}", + "", + "const editorTools = new TerrainEditorTools();", + "if (gdjs.registerInGameEditorPostStepCallback) {", + " gdjs.registerInGameEditorPostStepCallback((editor) => editorTools.update(editor));", + "}", + "", + "gdjs.__terrain3DExtension = {", + " Terrain,", + " // Used by the terrain grass and the scattered models.", + " getWorldScale,", + " getGrounds,", + " patchShaderCode,", + " createRandom,", + " findHighestGround,", + " GroundPlacement,", + " svgIcon,", + " toolIcons,", + " // Exposed for tests.", + " TerrainBodyUpdater,", + " TerrainData,", + " SampleRectangle,", + " brushes,", + " edits,", + " codec,", + " relief,", + "};" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [], + "objectGroups": [] + }, + { + "description": "Define grass helper classes JavaScript code.", + "fullName": "Define grass helper classes", + "functionType": "Action", + "name": "DefineGrassHelperClasses", + "private": true, + "sentence": "Define grass helper classes JavaScript code", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "if (gdjs.__terrainGrassExtension) {", + " return;", + "}", + "// Blades grow on the grounds of the scene: the 3D terrains (see `getGrounds`", + "// in the terrain helper).", + "", + "// Size of the object inner area: instances are scaled from it.", + "const AREA_SIZE = 1000;", + "const AREA_DEPTH = 40;", + "const CHUNK_SIZE = 500;", + "const MAX_BENDING_OBJECTS = 8;", + "", + "/**", + " * A blade: a thin, tapered strip. Its width and height are 1: blades are", + " * scaled by the shader.", + " */", + "const createBladeGeometry = () => {", + " const halfWidths = [0.5, 0.42, 0.28, 0];", + " const heights = [0, 0.35, 0.7, 1];", + " const positions = [];", + " halfWidths.forEach((halfWidth, level) => {", + " positions.push(-halfWidth, 0, heights[level]);", + " if (halfWidth > 0) positions.push(halfWidth, 0, heights[level]);", + " });", + " const indices = [0, 1, 2, 1, 3, 2, 2, 3, 4, 3, 5, 4, 4, 5, 6];", + " const geometry = new THREE.InstancedBufferGeometry();", + " geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));", + " geometry.setAttribute('normal', new THREE.Float32BufferAttribute(new Array(positions.length).fill(0), 3));", + " geometry.setIndex(indices);", + " return geometry;", + "};", + "", + "const vertexShaderDeclarations = `", + "attribute vec4 grassRoot;", + "attribute vec2 grassShape;", + "// The tint of the ground at the root (linear, from 0 to 1).", + "attribute vec3 grassGroundTint;", + "uniform mat3 grassWorldToLocal;", + "uniform float grassWorldScale;", + "uniform float grassBladeWidth;", + "uniform float grassTime;", + "uniform float grassWindStrength;", + "uniform float grassWindSpeed;", + "uniform vec4 grassBenders[${MAX_BENDING_OBJECTS}];", + "uniform vec3 grassBaseColor;", + "uniform vec3 grassTipColor;", + "varying vec3 vGrassColor;", + "`;", + "const vertexShaderPosition = `", + "// Blades are shaped in scene units (with Y flipped like the 3D world), then", + "// moved back in the (scaled) object space.", + "float bladeHeight = position.z;", + "float grassAngle = grassRoot.w;", + "float bladeWidth = position.x * grassBladeWidth;", + "vec3 blade = vec3(", + " bladeWidth * cos(grassAngle),", + " bladeWidth * sin(grassAngle),", + " bladeHeight * grassShape.x", + ");", + "vec3 rootWorld = (modelMatrix * vec4(grassRoot.xyz, 1.0)).xyz * grassWorldScale;", + "// Tips bend more than the base.", + "float bendFactor = bladeHeight * bladeHeight * grassShape.x;", + "float gust = sin(grassTime * grassWindSpeed + rootWorld.x * 0.013 + rootWorld.y * 0.011);", + "float flutter = sin(grassTime * grassWindSpeed * 2.7 + grassShape.y * 40.0) * 0.3;", + "blade.xy += vec2(0.8, 0.5) * (gust + flutter + 0.6) * grassWindStrength * 0.35 * bendFactor;", + "for (int i = 0; i < ${MAX_BENDING_OBJECTS}; i++) {", + " vec4 bender = grassBenders[i];", + " if (bender.z <= 0.0) continue;", + " vec2 away = rootWorld.xy - bender.xy;", + " float distanceToBender = length(away);", + " float push = 1.0 - smoothstep(bender.z * 0.3, bender.z, distanceToBender);", + " if (push > 0.0) {", + " blade.xy += normalize(away + vec2(0.001)) * push * bendFactor * 0.9;", + " blade.z *= 1.0 - push * 0.5;", + " }", + "}", + "vec3 transformed = grassRoot.xyz + grassWorldToLocal * (blade / grassWorldScale);", + "vGrassColor = mix(grassBaseColor, grassTipColor, bladeHeight) * (0.85 + 0.3 * grassShape.y) * grassGroundTint;", + "`;", + "const vertexShaderNormal = `", + "// Blades are lit like the ground below them.", + "transformedNormal = normalize((viewMatrix * vec4(0.0, 0.0, 1.0, 0.0)).xyz);", + "`;", + "const fragmentShaderDeclarations = `", + "varying vec3 vGrassColor;", + "`;", + "const fragmentShaderColor = `", + "diffuseColor.rgb *= vGrassColor;", + "`;", + "// Both faces of blades are lit like the ground (not flipped for back faces).", + "const fragmentShaderNormal = `", + "#include ", + "normal = normalize(vNormal);", + "nonPerturbedNormal = normal;", + "`;", + "", + "const { getWorldScale, patchShaderCode, createRandom, findHighestGround, GroundPlacement } = gdjs.__terrain3DExtension;", + "", + "/**", + " * The grass of a terrain grass object: blades drawn with GPU instancing, in chunks", + " * that are hidden when off-screen and thinned out with the distance. They grow", + " * on the grounds below, like 3D terrains (only where a given layer is painted).", + " */", + "class Grass {", + " /** @param {gdjs.CustomRuntimeObject3D} object */", + " constructor(object) {", + " this.object = object;", + " this.group = new THREE.Group();", + " object.get3DRendererObject().add(this.group);", + " this.bladeGeometry = createBladeGeometry();", + " /** @type {Array<{mesh: THREE.Mesh, blades: number, center: THREE.Vector3}>} */", + " this.chunks = [];", + " this.placement = new GroundPlacement(object.getRuntimeScene());", + " this._groundAtBlade = { ground: null, z: 0 };", + " this.uniforms = {", + " grassWorldToLocal: { value: new THREE.Matrix3() },", + " // Scene units by 3D world unit.", + " grassWorldScale: { value: 1 },", + " grassBladeWidth: { value: 1 },", + " grassTime: { value: 0 },", + " grassWindStrength: { value: 1 },", + " grassWindSpeed: { value: 1 },", + " grassBenders: { value: Array.from({ length: MAX_BENDING_OBJECTS }, () => new THREE.Vector4()) },", + " grassBaseColor: { value: new THREE.Color() },", + " grassTipColor: { value: new THREE.Color() },", + " };", + " this.material = new THREE.MeshStandardMaterial({ roughness: 1, metalness: 0, side: THREE.DoubleSide });", + " this.material.onBeforeCompile = (shader) => {", + " Object.assign(shader.uniforms, this.uniforms);", + " shader.vertexShader = patchShaderCode(shader.vertexShader, [", + " ['#include ', '#include \\n' + vertexShaderDeclarations],", + " ['#include ', vertexShaderPosition],", + " ['#include ', '#include \\n' + vertexShaderNormal],", + " ], 'TerrainGrass');", + " shader.fragmentShader = patchShaderCode(shader.fragmentShader, [", + " ['#include ', '#include \\n' + fragmentShaderDeclarations],", + " ['#include ', '#include \\n' + fragmentShaderColor],", + " ['#include ', fragmentShaderNormal],", + " ], 'TerrainGrass');", + " };", + " this.material.customProgramCacheKey = () => 'TerrainGrass';", + "", + " this._matrix = new THREE.Matrix4();", + " this._cameraPosition = new THREE.Vector3();", + " this._chunkPosition = new THREE.Vector3();", + " this._scale = new THREE.Vector3();", + " this.updateFromProperties();", + " }", + "", + " updateFromProperties() {", + " const { object } = this;", + " this.uniforms.grassBaseColor.value.set(gdjs.rgbOrHexStringToNumber(object._getBaseColor()));", + " this.uniforms.grassTipColor.value.set(gdjs.rgbOrHexStringToNumber(object._getTipColor()));", + " this.uniforms.grassWindStrength.value = object._getWindStrength();", + " this.uniforms.grassWindSpeed.value = object._getWindSpeed();", + " this.uniforms.grassBladeWidth.value = Math.max(object._getBladeWidth(), 0);", + " this.material.needsUpdate = true;", + " this.placement.placeAgain();", + " }", + "", + " _getPlacementKey() {", + " const { object } = this;", + " return [", + " object.getX(), object.getY(), object.getZ(), object.getWidth(), object.getHeight(), object.getDepth(),", + " object._getDensity(), object._getBladeHeight(), object._getGroundLayer(), object._getMaxSlope(),", + " object._getSeed(),", + " ].join('|');", + " }", + "", + " /** Place the blades again, for example after the ground or the object changed. */", + " rebuild() {", + " const grounds = Array.from(this.placement.grounds);", + " for (const chunk of this.chunks) {", + " chunk.mesh.removeFromParent();", + " chunk.mesh.geometry.dispose();", + " }", + " this.chunks.length = 0;", + "", + " const { object } = this;", + " const width = object.getWidth();", + " const height = object.getHeight();", + " const scaleX = width / AREA_SIZE;", + " const scaleY = height / AREA_SIZE;", + " const scaleZ = Math.max(object.getDepth() / AREA_DEPTH, 0.0001);", + " const bladesPerChunk = Math.round((Math.max(object._getDensity(), 0) * CHUNK_SIZE * CHUNK_SIZE) / 10000);", + " const groundLayer = parseInt(object._getGroundLayer(), 10) || 0;", + " const maxSlope = object._getMaxSlope();", + " const chunksX = Math.max(Math.ceil(width / CHUNK_SIZE), 1);", + " const chunksY = Math.max(Math.ceil(height / CHUNK_SIZE), 1);", + " const nextRandom = createRandom(object._getSeed() * 7919 + 1);", + " const bladeHeight = Math.max(object._getBladeHeight(), 0);", + "", + " for (let chunkY = 0; chunkY < chunksY; chunkY++) {", + " for (let chunkX = 0; chunkX < chunksX; chunkX++) {", + " const roots = new Float32Array(bladesPerChunk * 4);", + " const shapes = new Float32Array(bladesPerChunk * 2);", + " const groundTints = new Uint8Array(bladesPerChunk * 3).fill(255);", + " let blades = 0;", + " let minZ = Infinity;", + " let maxZ = -Infinity;", + " for (let attempt = 0; attempt < bladesPerChunk; attempt++) {", + " // In scene units, relative to the object.", + " const x = (chunkX + nextRandom()) * CHUNK_SIZE;", + " const y = (chunkY + nextRandom()) * CHUNK_SIZE;", + " const angle = nextRandom() * Math.PI * 2;", + " const heightFactor = 0.6 + 0.8 * nextRandom();", + " const tint = nextRandom();", + " const growth = nextRandom();", + " if (x > width || y > height) continue;", + " const sceneX = object.getX() + x;", + " const sceneY = object.getY() + y;", + " const { ground, z } = findHighestGround(grounds, sceneX, sceneY, this._groundAtBlade);", + " const groundZ = ground ? z : object.getZ();", + " if (ground) {", + " if (groundLayer > 0 && growth > ground.getLayerWeightAt(sceneX, sceneY, groundLayer)) continue;", + " if (ground.getSlopeAt(sceneX, sceneY) > maxSlope) continue;", + " if (typeof ground.getTintAt === 'function') {", + " const tint = ground.getTintAt(sceneX, sceneY);", + " for (let channel = 0; channel < 3; channel++) {", + " groundTints[blades * 3 + channel] = Math.round(255 * Math.pow(tint[channel] / 255, 2.2));", + " }", + " }", + " }", + " const localZ = (groundZ - object.getZ()) / scaleZ;", + " roots[blades * 4] = x / scaleX;", + " roots[blades * 4 + 1] = y / scaleY;", + " roots[blades * 4 + 2] = localZ;", + " roots[blades * 4 + 3] = angle;", + " shapes[blades * 2] = heightFactor * bladeHeight;", + " shapes[blades * 2 + 1] = tint;", + " minZ = Math.min(minZ, localZ);", + " maxZ = Math.max(maxZ, localZ);", + " blades++;", + " }", + " if (blades === 0) continue;", + "", + " const geometry = new THREE.InstancedBufferGeometry();", + " geometry.index = this.bladeGeometry.index;", + " geometry.setAttribute('position', this.bladeGeometry.attributes.position);", + " geometry.setAttribute('normal', this.bladeGeometry.attributes.normal);", + " geometry.setAttribute('grassRoot', new THREE.InstancedBufferAttribute(roots.subarray(0, blades * 4), 4));", + " geometry.setAttribute('grassShape', new THREE.InstancedBufferAttribute(shapes.subarray(0, blades * 2), 2));", + " geometry.setAttribute('grassGroundTint', new THREE.InstancedBufferAttribute(groundTints.subarray(0, blades * 3), 3, true));", + " geometry.instanceCount = blades;", + " // The furthest tips can go with the wind and the bending objects (see the shader).", + " const maxBladeHeight = bladeHeight * 1.4;", + " const lean =", + " (0.35 * 1.9 * 0.943 * Math.max(object._getWindStrength(), 0) + 0.9) * maxBladeHeight +", + " Math.max(object._getBladeWidth(), 0) / 2;", + " const leanX = lean / scaleX;", + " const leanY = lean / scaleY;", + " geometry.boundingBox = new THREE.Box3(", + " new THREE.Vector3((chunkX * CHUNK_SIZE) / scaleX - leanX, (chunkY * CHUNK_SIZE) / scaleY - leanY, minZ),", + " new THREE.Vector3(", + " Math.min((chunkX + 1) * CHUNK_SIZE, width) / scaleX + leanX,", + " Math.min((chunkY + 1) * CHUNK_SIZE, height) / scaleY + leanY,", + " maxZ + (bladeHeight * 1.4) / scaleZ", + " )", + " );", + " geometry.boundingSphere = geometry.boundingBox.getBoundingSphere(new THREE.Sphere());", + " const mesh = new THREE.Mesh(geometry, this.material);", + " mesh.receiveShadow = true;", + " // Raycasts would only test one blade: the selection plane is used instead.", + " mesh.raycast = () => {};", + " this.group.add(mesh);", + " this.chunks.push({ mesh, blades, center: geometry.boundingSphere.center.clone() });", + " }", + " }", + " }", + "", + " _updateBendingObjects() {", + " const benders = this.uniforms.grassBenders.value;", + " const objectName = this.object._getBendingObject();", + " const bendingObjects = objectName ? this.object.getInstanceContainer().getObjects(objectName) || [] : [];", + " for (let index = 0; index < MAX_BENDING_OBJECTS; index++) {", + " const bendingObject = bendingObjects[index];", + " if (!bendingObject) {", + " benders[index].set(0, 0, 0, 0);", + " continue;", + " }", + " // Blades are pushed in the 3D scene, which is flipped on Y.", + " const radius = Math.max(bendingObject.getWidth(), bendingObject.getHeight()) * 1.2;", + " benders[index].set(bendingObject.getCenterXInScene(), -bendingObject.getCenterYInScene(), radius, 0);", + " }", + " }", + "", + " /**", + " * @param {number} elapsedSeconds", + " * @param {number} time In seconds, the same for all grass objects.", + " */", + " update(elapsedSeconds, time, camera) {", + " if (this.placement.shouldPlaceAgain(elapsedSeconds, this._getPlacementKey())) {", + " this.rebuild();", + " }", + "", + " this.uniforms.grassTime.value = time;", + " this.uniforms.grassWorldScale.value = getWorldScale(this.object);", + " // The object transformation is applied just before rendering, it's needed now.", + " this.object.getRenderer().ensureUpToDate();", + " this.group.updateWorldMatrix(true, false);", + " this.uniforms.grassWorldToLocal.value.setFromMatrix4(this._matrix.copy(this.group.matrixWorld).invert());", + " this._updateBendingObjects();", + " if (!camera) return;", + "", + " // Far chunks are hidden, and thinned out before (in 3D world units).", + " const fadeDistance = Math.max(this.object._getFadeDistance(), 1) / getWorldScale(this.object);", + " this._cameraPosition.setFromMatrixPosition(camera.matrixWorld);", + " for (const chunk of this.chunks) {", + " const distance = this._chunkPosition", + " .copy(chunk.center)", + " .applyMatrix4(this.group.matrixWorld)", + " .distanceTo(this._cameraPosition);", + " const density = gdjs.evtTools.common.clamp((fadeDistance - distance) / (fadeDistance * 0.5), 0, 1);", + " chunk.mesh.visible = density > 0;", + " chunk.mesh.geometry.instanceCount = Math.ceil(chunk.blades * density);", + " }", + " }", + "", + " dispose() {", + " for (const chunk of this.chunks) {", + " chunk.mesh.geometry.dispose();", + " }", + " this.bladeGeometry.dispose();", + " this.material.dispose();", + " this.group.removeFromParent();", + " }", + "}", + "", + "gdjs.__terrainGrassExtension = { Grass };" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [], + "objectGroups": [] + }, + { + "description": "Define scattered models helper classes JavaScript code.", + "fullName": "Define scattered models helper classes", + "functionType": "Action", + "name": "DefineScatterHelperClasses", + "private": true, + "sentence": "Define scattered models helper classes JavaScript code", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "if (gdjs.__terrainScatterExtension) {", + " return;", + "}", + "// Copies grow on the grounds of the scene, like the terrain grass (see", + "// `getGrounds` in the terrain helper).", + "", + "const {", + " getWorldScale,", + " getGrounds,", + " patchShaderCode,", + " createRandom,", + " findHighestGround,", + " GroundPlacement,", + " SampleRectangle,", + " brushes,", + " codec,", + " svgIcon,", + " toolIcons,", + "} = gdjs.__terrain3DExtension;", + "", + "// Size of the object inner area: instances are scaled from it.", + "const AREA_SIZE = 2000;", + "const AREA_DEPTH = 200;", + "// The painted density is stored in a grid of this many cells on each side,", + "// whatever the size of the object.", + "const MASK_RESOLUTION = 256;", + "const MASK_FORMAT_VERSION = '1';", + "// Copies are hidden in chunks: big chunks mean fewer draw calls, small ones", + "// hide more copies out of the screen.", + "const MIN_CHUNK_SIZE = 500;", + "const MAX_CHUNK_SIZE = 2000;", + "// Copies shrink to nothing over this part of the draw distance, instead of popping out.", + "const FADE_PART = 0.15;", + "const MAX_COPIES = 200000;", + "// Prisms of the trunk collisions.", + "const TRUNK_SIDES = 6;", + "", + "const clamp01 = (value) => (value < 0 ? 0 : value > 1 ? 1 : value);", + "", + "/**", + " * @returns {number} A number for a cell of the copies grid, changing with the seed.", + " */", + "const hashCell = (seed, i, j) => {", + " let h = Math.imul(i, 374761393) + Math.imul(j, 668265263) + Math.imul(seed, 982451653);", + " h = Math.imul(h ^ (h >>> 13), 1274126177);", + " return (h ^ (h >>> 16)) >>> 0;", + "};", + "", + "/**", + " * How dense copies are, from 0 to 1, in a grid covering the object: painted", + " * with the editor brushes, the edits or events.", + " */", + "class DensityMask {", + " /** @param {number} fillValue From 0 to 255. */", + " constructor(fillValue) {", + " // `resolution` and `size` let terrain brushes paint it.", + " this.resolution = MASK_RESOLUTION;", + " this.size = MASK_RESOLUTION + 1;", + " this.values = new Uint8Array(this.size * this.size).fill(fillValue);", + " }", + "", + " /**", + " * @param {number} u From 0 to 1, along the object width.", + " * @param {number} v From 0 to 1, along the object height.", + " * @returns {number} The density, from 0 to 1, interpolated between samples.", + " */", + " sample(u, v) {", + " const x = clamp01(u) * this.resolution;", + " const y = clamp01(v) * this.resolution;", + " const i = Math.min(Math.floor(x), this.resolution - 1);", + " const j = Math.min(Math.floor(y), this.resolution - 1);", + " const fx = x - i;", + " const fy = y - j;", + " const value = (i, j) => this.values[j * this.size + i];", + " const top = value(i, j) + fx * (value(i + 1, j) - value(i, j));", + " const bottom = value(i, j + 1) + fx * (value(i + 1, j + 1) - value(i, j + 1));", + " return (top + fy * (bottom - top)) / 255;", + " }", + "", + " /**", + " * Bring the density towards a target around a segment: fully up to 70%", + " * of the radius, then fading out (forests have dense centers and soft edges).", + " * @param {{cellWidth: number, cellHeight: number, ax: number, ay: number, bx: number, by: number, radius: number}} stroke", + " * @param {number} density The target, from 0 to 1.", + " * @param {number} strength From 0 to 1.", + " * @param {SampleRectangle} changedSamples", + " */", + " paint(stroke, density, strength, changedSamples) {", + " const { values } = this;", + " const target = Math.round(clamp01(density) * 255);", + " brushes.forEachSample(", + " this,", + " stroke,", + " (index, falloff) => {", + " // The falloff of terrain brushes is (1 - distance² / radius²)².", + " const weight = clamp01(2 * Math.sqrt(falloff));", + " // Rounded towards the target, so that light strokes still paint.", + " const change = (target - values[index]) * clamp01(weight * strength);", + " values[index] += change > 0 ? Math.ceil(change) : Math.floor(change);", + " },", + " changedSamples", + " );", + " }", + "", + " /** @returns {DensityMask} */", + " clone() {", + " const mask = new DensityMask(0);", + " mask.values.set(this.values);", + " return mask;", + " }", + "}", + "", + "/**", + " * Edits written in the Edits property: a JSON list of brush strokes, like", + " * `[{\"tool\": \"add\", \"x\": 0.5, \"y\": 0.5, \"radius\": 0.2}]`. Positions and", + " * radius are fractions of the object size, like the terrain edits.", + " */", + "const applyEdits = (mask, text) => {", + " if (!text.trim()) return;", + " let editList;", + " try {", + " editList = JSON.parse(text);", + " } catch (error) {", + " console.warn('The edits of scattered models are not valid JSON: ' + error.message);", + " return;", + " }", + " if (!Array.isArray(editList)) {", + " console.warn('The edits of scattered models must be a JSON list (like [{\"tool\": \"add\", ...}]).');", + " return;", + " }", + " const numberOr = (value, defaultValue) => (typeof value === 'number' && isFinite(value) ? value : defaultValue);", + " const changedSamples = new SampleRectangle();", + " for (const edit of editList) {", + " if (!edit || typeof edit !== 'object') continue;", + " const x = numberOr(edit.x, 0.5);", + " const y = numberOr(edit.y, 0.5);", + " const stroke = {", + " cellWidth: 1 / mask.resolution,", + " cellHeight: 1 / mask.resolution,", + " ax: x,", + " ay: y,", + " bx: numberOr(edit.toX, x),", + " by: numberOr(edit.toY, y),", + " radius: numberOr(edit.radius, 0.1),", + " };", + " const strength = clamp01(numberOr(edit.strength, 1));", + " if (edit.tool === 'add') {", + " mask.paint(stroke, numberOr(edit.density, 1), strength, changedSamples);", + " } else if (edit.tool === 'erase') {", + " mask.paint(stroke, 0, strength, changedSamples);", + " } else {", + " console.warn('Unknown tool in the edits of scattered models: \"' + edit.tool + '\" (use add or erase).');", + " }", + " }", + "};", + "", + "/**", + " * The density painted in the editor, stored as its difference with the", + " * density from the properties (the coverage and the edits), like the terrain", + " * sculpt data.", + " */", + "const maskCodec = {", + " /**", + " * @param {DensityMask} mask", + " * @param {DensityMask} baseMask", + " */", + " encode(mask, baseMask) {", + " const changes = new Int32Array(mask.values.length);", + " let hasChanges = false;", + " for (let index = 0; index < changes.length; index++) {", + " changes[index] = mask.values[index] - baseMask.values[index];", + " if (changes[index] !== 0) hasChanges = true;", + " }", + " if (!hasChanges) return '';", + " return MASK_FORMAT_VERSION + ';' + MASK_RESOLUTION + ';' + codec.encodeGrids([changes], mask.size);", + " },", + "", + " /**", + " * @param {string} text", + " * @param {DensityMask} baseMask", + " * @returns {DensityMask | null} null if the text is empty or not valid.", + " */", + " decode(text, baseMask) {", + " if (!text) return null;", + " const parts = text.split(';');", + " if (parts[0] !== MASK_FORMAT_VERSION || parseInt(parts[1], 10) !== MASK_RESOLUTION) return null;", + " try {", + " const mask = baseMask.clone();", + " const [changes] = codec.decodeGrids(parts[2], mask.size, 1);", + " for (let index = 0; index < changes.length; index++) {", + " // The base may have changed since the data was saved.", + " mask.values[index] = Math.min(Math.max(mask.values[index] + changes[index], 0), 255);", + " }", + " return mask;", + " } catch (error) {", + " console.error('The painted data of scattered models could not be read:', error);", + " return null;", + " }", + " },", + "};", + "", + "const fadeShaderDeclarations = `", + "uniform float scatterDrawDistance;", + "`;", + "// Instance matrices are applied after this: copies shrink towards their base.", + "const fadeShaderCode = `", + "#ifdef USE_INSTANCING", + "vec3 scatterCopyPosition = (modelMatrix * instanceMatrix * vec4(0.0, 0.0, 0.0, 1.0)).xyz;", + "transformed *= clamp(", + " (scatterDrawDistance - distance(scatterCopyPosition, cameraPosition)) / (scatterDrawDistance * ${FADE_PART}),", + " 0.0,", + " 1.0", + ");", + "#endif", + "`;", + "", + "const flipTriangles = (geometry) => {", + " const index = geometry.index;", + " for (let offset = 0; offset + 2 < index.count; offset += 3) {", + " const second = index.getX(offset + 1);", + " index.setX(offset + 1, index.getX(offset + 2));", + " index.setX(offset + 2, second);", + " }", + "};", + "", + "/**", + " * The parts of a model, ready to be drawn with instancing: each glTF", + " * primitive with its material. Geometries are moved so that the model base", + " * is at the origin, its height is 1 and it stands along Z (like 3D model", + " * objects with a 90° rotation around X).", + " * @param {THREE.Object3D} modelScene", + " * @param {object} uniforms Uniforms added to the materials.", + " * @returns {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}}", + " */", + "const createModelParts = (modelScene, uniforms) => {", + " modelScene.updateMatrixWorld(true);", + " const rootInverse = modelScene.matrixWorld.clone().invert();", + " const standUp = new THREE.Matrix4().makeRotationX(Math.PI / 2);", + " /** @type {Array<{mesh: THREE.Mesh, matrix: THREE.Matrix4}>} */", + " const meshes = [];", + " const box = new THREE.Box3();", + " modelScene.traverse((node) => {", + " if (!node.isMesh || !node.geometry || !node.geometry.attributes.position) return;", + " const matrix = standUp.clone().multiply(rootInverse).multiply(node.matrixWorld);", + " if (!node.geometry.boundingBox) node.geometry.computeBoundingBox();", + " box.union(node.geometry.boundingBox.clone().applyMatrix4(matrix));", + " meshes.push({ mesh: node, matrix });", + " });", + " if (box.isEmpty()) return { parts: [], width: 0, depth: 0 };", + "", + " const height = Math.max(box.max.z - box.min.z, 0.000001);", + " // The Y axis of the scene goes down: the model is flipped, like 3D model objects.", + " const normalization = new THREE.Matrix4()", + " .makeScale(1 / height, -1 / height, 1 / height)", + " .multiply(", + " new THREE.Matrix4().makeTranslation(", + " -(box.min.x + box.max.x) / 2,", + " -(box.min.y + box.max.y) / 2,", + " -box.min.z", + " )", + " );", + " const parts = meshes.map(({ mesh, matrix }) => {", + " let geometry = mesh.geometry.clone();", + " if (!geometry.index) {", + " const indices = Array.from({ length: geometry.attributes.position.count }, (_, index) => index);", + " geometry.setIndex(indices);", + " }", + " geometry.applyMatrix4(normalization.clone().multiply(matrix));", + " // The flip turned the faces inside out.", + " flipTriangles(geometry);", + " geometry.computeBoundingSphere();", + " geometry.computeBoundingBox();", + " const patchMaterial = (originalMaterial) => {", + " const material = originalMaterial.clone();", + " // Like 3D model objects (\"Standard without metalness\"), which look better without an environment map.", + " if (material.metalness) material.metalness = 0;", + " material.onBeforeCompile = (shader) => {", + " Object.assign(shader.uniforms, uniforms);", + " shader.vertexShader = patchShaderCode(shader.vertexShader, [", + " ['#include ', '#include \\n' + fadeShaderDeclarations],", + " ['#include ', '#include \\n' + fadeShaderCode],", + " ], 'ScatteredModels');", + " };", + " material.customProgramCacheKey = () => 'ScatteredModels';", + " return material;", + " };", + " const material = Array.isArray(mesh.material) ? mesh.material.map(patchMaterial) : patchMaterial(mesh.material);", + " return { geometry, material };", + " });", + " return {", + " parts,", + " width: (box.max.x - box.min.x) / height,", + " depth: (box.max.y - box.min.y) / height,", + " };", + "};", + "", + "/**", + " * A copy of the model: its base position in the scene, its scale (the", + " * height of the copy) and the key of its cell in the grid of copies.", + " * @typedef {{x: number, y: number, z: number, angle: number, height: number, key: number}} Copy", + " */", + "", + "/**", + " * The scattered models of an object: copies of a 3D model placed on the", + " * grounds of the scene, drawn with GPU instancing in chunks that are hidden", + " * when far or off-screen.", + " */", + "class Scatter {", + " /** @param {gdjs.CustomRuntimeObject3D} object */", + " constructor(object) {", + " this.object = object;", + " this.group = new THREE.Group();", + " object.get3DRendererObject().add(this.group);", + " /** @type {Array<{firstCellI: number, firstCellJ: number, meshes: THREE.InstancedMesh[], copies: Copy[], center: THREE.Vector3, radius: number}>} */", + " this.chunks = [];", + " /**", + " * The grid of copies: one copy at most in each cell, cells grouped in chunks.", + " * @type {{spacing: number, cellsPerChunk: number, cellsX: number, cellsY: number} | null}", + " */", + " this.grid = null;", + " this.placement = new GroundPlacement(object.getRuntimeScene());", + " this.uniforms = {", + " // In 3D world units.", + " scatterDrawDistance: { value: 1 },", + " };", + " /** @type {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}} */", + " this.model = { parts: [], width: 0, depth: 0 };", + " this.modelResourceName = '';", + " this.loadingModelResourceName = '';", + " /**", + " * Copies removed by events (for example, cut trees), by key.", + " * @type {Map}", + " */", + " this.removedCopies = new Map();", + " /** Increased each time copies change, for the collisions. */", + " this.version = 0;", + " this.isDisposed = false;", + " /** The last value of the painted data property read or saved. */", + " this.paintData = '';", + " /**", + " * Values saved by the editor tools: the editor sends them back with a", + " * hot-reload, possibly after newer ones were saved.", + " * @type {string[]}", + " */", + " this.savedPaintData = [];", + " this.maskSource = '';", + " /** @type {DensityMask} */", + " this.baseMask = new DensityMask(255);", + " /** @type {DensityMask} */", + " this.mask = this.baseMask;", + " this.changedSamples = new SampleRectangle();", + " this._stroke = { cellWidth: 1, cellHeight: 1, ax: 0, ay: 0, bx: 0, by: 0, radius: 0 };", + " this._groundAtCopy = { ground: null, z: 0 };", + " this._matrix = new THREE.Matrix4();", + " this._raycastSphere = new THREE.Sphere();", + " this._raycastRay = new THREE.Ray();", + " this._raycastBox = new THREE.Box3();", + " this._raycastInverseMatrix = new THREE.Matrix4();", + " this._raycastScale = new THREE.Vector3();", + " /** Placed on a copy to test the triangles of the model. */", + " this._raycastMesh = new THREE.Mesh();", + " /** @type {THREE.Intersection[]} */", + " this._raycastHits = [];", + " this._color = new THREE.Color();", + " this._tint = new THREE.Color();", + " this._cameraPosition = new THREE.Vector3();", + " this._chunkPosition = new THREE.Vector3();", + " /**", + " * Trunks, for the 3D physics behavior and navigation meshes. Always given,", + " * empty without trunk collisions: otherwise these behaviors would use the", + " * object box, covering the whole area.", + " * @type {gdjs.Surface}", + " */", + " this.surface = {", + " getVersion: () => this.version,", + " getChangedArea: () => null,", + " getTriangles: () => this._getTrunkTriangles(),", + " getHeightField: () => null,", + " };", + " /** The trunk collision properties, changing the surface. */", + " this.trunkKey = '';", + " this.loadMaskFromProperties();", + " this.updateFromProperties();", + " // Before the behaviors create their shapes.", + " this._updateSurface();", + " }", + "", + " _getMaskSource() {", + " return this.object._getCoverage() + '|' + this.object._getEdits();", + " }", + "", + " loadMaskFromProperties() {", + " const { object } = this;", + " this.maskSource = this._getMaskSource();", + " this.paintData = object._getPaintData();", + " this.baseMask = new DensityMask(object._getCoverage() === 'Painted' ? 0 : 255);", + " applyEdits(this.baseMask, object._getEdits());", + " this.mask = maskCodec.decode(this.paintData, this.baseMask) || this.baseMask.clone();", + " this.placement.placeAgain();", + " }", + "", + " onHotReloading() {", + " const paintData = this.object._getPaintData();", + " const savedIndex = this.savedPaintData.indexOf(paintData);", + " if (savedIndex !== -1 && this._getMaskSource() === this.maskSource) {", + " // The object already has this data, or newer changes.", + " this.savedPaintData.splice(0, savedIndex + 1);", + " } else if (paintData !== this.paintData || this._getMaskSource() !== this.maskSource) {", + " this.savedPaintData.length = 0;", + " this.loadMaskFromProperties();", + " }", + " this.updateFromProperties();", + " }", + "", + " /** @returns {string} The data to save in the painted data property. */", + " save() {", + " this.paintData = maskCodec.encode(this.mask, this.baseMask);", + " this.savedPaintData.push(this.paintData);", + " return this.paintData;", + " }", + "", + " updateFromProperties() {", + " const { object } = this;", + " const modelResourceName = object._getModel();", + " if (modelResourceName !== this.modelResourceName) {", + " this.modelResourceName = modelResourceName;", + " this._loadModel();", + " }", + " this.placement.placeAgain();", + " }", + "", + " _disposeModel() {", + " for (const part of this.model.parts) {", + " part.geometry.dispose();", + " for (const material of Array.isArray(part.material) ? part.material : [part.material]) {", + " material.dispose();", + " }", + " }", + " this.model = { parts: [], width: 0, depth: 0 };", + " }", + "", + " _loadModel() {", + " this._disposeChunks(this.chunks);", + " this._disposeModel();", + " const resourceName = this.modelResourceName;", + " if (!resourceName) return;", + " const game = this.object.getRuntimeScene().getGame();", + " const modelManager = game.getModel3DManager();", + " const gltf = modelManager.getModel(resourceName);", + " if (gltf === modelManager._invalidModel) {", + " // Not loaded yet, for example a model just chosen in the editor.", + " if (this.loadingModelResourceName === resourceName) return;", + " this.loadingModelResourceName = resourceName;", + " game.getResourceLoader()", + " .loadResource(resourceName)", + " .then(() => {", + " if (this.isDisposed || this.modelResourceName !== resourceName) return;", + " this.loadingModelResourceName = '';", + " if (modelManager.getModel(resourceName) !== modelManager._invalidModel) {", + " this._loadModel();", + " this.placement.placeAgain();", + " }", + " })", + " .catch(() => {});", + " return;", + " }", + " this.model = createModelParts(gltf.scene, this.uniforms);", + " }", + "", + " _getPlacementKey() {", + " const { object } = this;", + " return [", + " object.getX(), object.getY(), object.getZ(), object.getWidth(), object.getHeight(), object.getDepth(),", + " object._getDensity(), object._getSize(), object._getMinScale(), object._getMaxScale(),", + " object._getRandomRotation(), object._getRandomTint(), object._getGroundLayer(), object._getMaxSlope(),", + " object._getMinGroundZ(), object._getMaxGroundZ(), object._getSeed(), object._getDrawDistance(),", + " object._getCastShadow(), object._getReceiveShadow(), this.model.parts.length,", + " ].join('|');", + " }", + "", + " _getChunkSize() {", + " return Math.min(Math.max(this.object._getDrawDistance() / 4, MIN_CHUNK_SIZE), MAX_CHUNK_SIZE);", + " }", + "", + " /** @returns {number} The distance between copies (one copy per cell of this size at most). */", + " _getSpacing() {", + " const { object } = this;", + " const density = Math.max(object._getDensity(), 0);", + " if (density <= 0) return 0;", + " const spacing = 100 / Math.sqrt(density);", + " // Too many cells would freeze the game: the density is lowered.", + " const cellCount = (object.getWidth() / spacing) * (object.getHeight() / spacing);", + " return cellCount > MAX_COPIES ? spacing * Math.sqrt(cellCount / MAX_COPIES) : spacing;", + " }", + "", + " /**", + " * Find the copy of a cell of the grid, if there is one.", + " * @returns {Copy | null}", + " */", + " _placeCopy(cellI, cellJ, spacing, grounds) {", + " const { object } = this;", + " const width = object.getWidth();", + " const height = object.getHeight();", + " const nextRandom = createRandom(hashCell(Math.floor(object._getSeed()), cellI, cellJ));", + " // Always drawn in the same order, so that a copy stays the same when properties change.", + " const x = (cellI + nextRandom()) * spacing;", + " const y = (cellJ + nextRandom()) * spacing;", + " const presence = nextRandom();", + " const growth = nextRandom();", + " const scaleFactor = nextRandom();", + " const angle = nextRandom() * Math.PI * 2;", + " if (x > width || y > height) return null;", + " if (presence >= this.mask.sample(x / width, y / height)) return null;", + " const key = cellJ * 65536 + cellI;", + " if (this.removedCopies.has(key)) return null;", + "", + " const sceneX = object.getX() + x;", + " const sceneY = object.getY() + y;", + " const { ground, z } = findHighestGround(grounds, sceneX, sceneY, this._groundAtCopy);", + " const groundZ = ground ? z : object.getZ();", + " if (groundZ < object._getMinGroundZ() || groundZ > object._getMaxGroundZ()) return null;", + " const copyHeight =", + " Math.max(object._getSize(), 0) *", + " (object._getMinScale() + (object._getMaxScale() - object._getMinScale()) * scaleFactor);", + " if (ground) {", + " if (ground.getSlopeAt(sceneX, sceneY) > object._getMaxSlope()) return null;", + " const groundLayer = parseInt(object._getGroundLayer(), 10) || 0;", + " if (groundLayer > 0) {", + " if (growth > ground.getLayerWeightAt(sceneX, sceneY, groundLayer)) return null;", + " // Copies don't stand at the border of their layer, so that trees don't cover roads.", + " const margin = (copyHeight * Math.max(this.model.width, this.model.depth)) / 4;", + " for (const [offsetX, offsetY] of [[margin, 0], [-margin, 0], [0, margin], [0, -margin]]) {", + " if (ground.getLayerWeightAt(sceneX + offsetX, sceneY + offsetY, groundLayer) < 0.5) return null;", + " }", + " }", + " }", + " return {", + " x: sceneX,", + " y: sceneY,", + " z: groundZ,", + " angle: object._getRandomRotation() ? angle : 0,", + " height: copyHeight,", + " key,", + " };", + " }", + "", + " _disposeChunks(chunks) {", + " for (const chunk of chunks) {", + " for (const mesh of chunk.meshes) {", + " mesh.removeFromParent();", + " // Geometries and materials are the ones of the model.", + " mesh.dispose();", + " }", + " }", + " if (chunks === this.chunks) this.chunks = [];", + " }", + "", + " /** Place the copies again, for example after the ground or the object changed. */", + " rebuild() {", + " this._disposeChunks(this.chunks);", + " this.version++;", + " const { object } = this;", + " const spacing = this._getSpacing();", + " if (spacing <= 0 || this.model.parts.length === 0) {", + " this.grid = null;", + " return;", + " }", + " const cellsPerChunk = Math.max(Math.round(this._getChunkSize() / spacing), 1);", + " const grid = {", + " spacing,", + " cellsPerChunk,", + " cellsX: Math.ceil(object.getWidth() / spacing),", + " cellsY: Math.ceil(object.getHeight() / spacing),", + " };", + " this.grid = grid;", + " for (let firstCellJ = 0; firstCellJ < grid.cellsY; firstCellJ += cellsPerChunk) {", + " for (let firstCellI = 0; firstCellI < grid.cellsX; firstCellI += cellsPerChunk) {", + " this._createChunk(firstCellI, firstCellJ);", + " }", + " }", + " }", + "", + " /**", + " * Place again the copies of the chunks touching a rectangle of the scene,", + " * for example after painting.", + " */", + " _rebuildArea(minX, minY, maxX, maxY) {", + " const { grid, object } = this;", + " if (!grid) return;", + " const chunkSize = grid.spacing * grid.cellsPerChunk;", + " const firstChunkI = Math.max(Math.floor((minX - object.getX()) / chunkSize), 0);", + " const firstChunkJ = Math.max(Math.floor((minY - object.getY()) / chunkSize), 0);", + " const lastChunkI = Math.min(Math.floor((maxX - object.getX()) / chunkSize), Math.ceil(grid.cellsX / grid.cellsPerChunk) - 1);", + " const lastChunkJ = Math.min(Math.floor((maxY - object.getY()) / chunkSize), Math.ceil(grid.cellsY / grid.cellsPerChunk) - 1);", + " if (firstChunkI > lastChunkI || firstChunkJ > lastChunkJ) return;", + " const isInArea = (chunk) => {", + " const chunkI = chunk.firstCellI / grid.cellsPerChunk;", + " const chunkJ = chunk.firstCellJ / grid.cellsPerChunk;", + " return chunkI >= firstChunkI && chunkI <= lastChunkI && chunkJ >= firstChunkJ && chunkJ <= lastChunkJ;", + " };", + " this._disposeChunks(this.chunks.filter(isInArea));", + " this.chunks = this.chunks.filter((chunk) => !isInArea(chunk));", + " for (let chunkJ = firstChunkJ; chunkJ <= lastChunkJ; chunkJ++) {", + " for (let chunkI = firstChunkI; chunkI <= lastChunkI; chunkI++) {", + " this._createChunk(chunkI * grid.cellsPerChunk, chunkJ * grid.cellsPerChunk);", + " }", + " }", + " this.version++;", + " }", + "", + " _createChunk(firstCellI, firstCellJ) {", + " const { grid } = this;", + " const grounds = Array.from(this.placement.grounds);", + " /** @type {Copy[]} */", + " const copies = [];", + " for (let cellJ = firstCellJ; cellJ < Math.min(firstCellJ + grid.cellsPerChunk, grid.cellsY); cellJ++) {", + " for (let cellI = firstCellI; cellI < Math.min(firstCellI + grid.cellsPerChunk, grid.cellsX); cellI++) {", + " const copy = this._placeCopy(cellI, cellJ, grid.spacing, grounds);", + " if (copy) copies.push(copy);", + " }", + " }", + " if (copies.length === 0) return;", + "", + " const { object } = this;", + " // Copies are placed in scene units, and the object group is scaled", + " // from its inner area: they are scaled back to keep their proportions.", + " const scaleX = Math.max(object.getWidth() / AREA_SIZE, 0.0001);", + " const scaleY = Math.max(object.getHeight() / AREA_SIZE, 0.0001);", + " const scaleZ = Math.max(object.getDepth() / AREA_DEPTH, 0.0001);", + " const matrices = new Float32Array(copies.length * 16);", + " const tintColor = gdjs.rgbOrHexStringToNumber(object._getRandomTint());", + " const hasTint = tintColor !== 0xffffff;", + " const colors = hasTint ? new Float32Array(copies.length * 3) : null;", + " this._tint.setHex(tintColor);", + " const min = new THREE.Vector3(Infinity, Infinity, Infinity);", + " const max = new THREE.Vector3(-Infinity, -Infinity, -Infinity);", + " copies.forEach((copy, index) => {", + " const cos = Math.cos(copy.angle) * copy.height;", + " const sin = Math.sin(copy.angle) * copy.height;", + " const localX = (copy.x - object.getX()) / scaleX;", + " const localY = (copy.y - object.getY()) / scaleY;", + " const localZ = (copy.z - object.getZ()) / scaleZ;", + " this._matrix.set(", + " cos / scaleX, -sin / scaleX, 0, localX,", + " sin / scaleY, cos / scaleY, 0, localY,", + " 0, 0, copy.height / scaleZ, localZ,", + " 0, 0, 0, 1", + " );", + " this._matrix.toArray(matrices, index * 16);", + " if (colors) {", + " // The tint of each copy is between white and the tint color.", + " this._color.setRGB(1, 1, 1).lerp(this._tint, createRandom(copy.key + 1)());", + " this._color.toArray(colors, index * 3);", + " }", + " const reach = (copy.height * Math.max(this.model.width, this.model.depth)) / 2;", + " min.min(this._chunkPosition.set(copy.x - reach, copy.y - reach, copy.z));", + " max.max(this._chunkPosition.set(copy.x + reach, copy.y + reach, copy.z + copy.height));", + " });", + " const instanceMatrix = new THREE.InstancedBufferAttribute(matrices, 16);", + " const instanceColor = colors ? new THREE.InstancedBufferAttribute(colors, 3) : null;", + " const castShadow = object._getCastShadow();", + " const receiveShadow = object._getReceiveShadow();", + " const meshes = this.model.parts.map((part) => {", + " const mesh = new THREE.InstancedMesh(part.geometry, part.material, 0);", + " // All the parts of the model share the positions of the copies.", + " mesh.instanceMatrix = instanceMatrix;", + " mesh.instanceColor = instanceColor;", + " mesh.count = copies.length;", + " mesh.computeBoundingSphere();", + " mesh.castShadow = castShadow;", + " mesh.receiveShadow = receiveShadow;", + " // The first mesh tests all the parts of the copies.", + " mesh.raycast = () => {};", + " this.group.add(mesh);", + " return mesh;", + " });", + " const center = new THREE.Vector3(", + " ((min.x + max.x) / 2 - object.getX()) / scaleX,", + " ((min.y + max.y) / 2 - object.getY()) / scaleY,", + " ((min.z + max.z) / 2 - object.getZ()) / scaleZ", + " );", + " const chunk = { firstCellI, firstCellJ, meshes, copies, center, radius: min.distanceTo(max) / 2 };", + " // Used by the scene editor to select the object when a copy is clicked.", + " meshes[0].raycast = (raycaster, intersects) => this._raycastChunk(chunk, raycaster, intersects);", + " this.chunks.push(chunk);", + " }", + "", + " /**", + " * Add the hits of a ray on the copies of a chunk, like they are drawn. There", + " * can be thousands of copies: only the ones whose box is crossed by the ray", + " * are tested with the triangles of the model.", + " * @param {{meshes: THREE.InstancedMesh[], copies: Copy[], center: THREE.Vector3, radius: number}} chunk", + " * @param {THREE.Raycaster} raycaster", + " * @param {THREE.Intersection[]} intersects", + " */", + " _raycastChunk(chunk, raycaster, intersects) {", + " const { object, group } = this;", + " this._raycastSphere.center.copy(chunk.center).applyMatrix4(group.matrixWorld);", + " this._raycastSphere.radius = chunk.radius / getWorldScale(object);", + " if (!raycaster.ray.intersectsSphere(this._raycastSphere)) return;", + "", + " // The ray in the scene coordinates, like the copies.", + " const scale = this._raycastScale.set(", + " Math.max(object.getWidth() / AREA_SIZE, 0.0001),", + " Math.max(object.getHeight() / AREA_SIZE, 0.0001),", + " Math.max(object.getDepth() / AREA_DEPTH, 0.0001)", + " );", + " const ray = this._raycastRay.copy(raycaster.ray).applyMatrix4(this._raycastInverseMatrix.copy(group.matrixWorld).invert());", + " ray.origin.multiply(scale).add(this._chunkPosition.set(object.getX(), object.getY(), object.getZ()));", + " ray.direction.multiply(scale);", + "", + " const reachPerHeight = Math.max(this.model.width, this.model.depth) / 2;", + " const instanceMatrices = chunk.meshes[0].instanceMatrix.array;", + " chunk.copies.forEach((copy, index) => {", + " const reach = copy.height * reachPerHeight;", + " this._raycastBox.min.set(copy.x - reach, copy.y - reach, copy.z);", + " this._raycastBox.max.set(copy.x + reach, copy.y + reach, copy.z + copy.height);", + " if (!ray.intersectsBox(this._raycastBox)) return;", + " this._raycastMesh.matrixWorld.fromArray(instanceMatrices, index * 16).premultiply(group.matrixWorld);", + " for (const mesh of chunk.meshes) {", + " this._raycastMesh.geometry = mesh.geometry;", + " this._raycastMesh.material = mesh.material;", + " this._raycastHits.length = 0;", + " this._raycastMesh.raycast(raycaster, this._raycastHits);", + " for (const hit of this._raycastHits) {", + " hit.object = mesh;", + " hit.instanceId = index;", + " intersects.push(hit);", + " }", + " }", + " });", + " }", + "", + " /** @param {THREE.Camera | null} camera */", + " _hideFarChunks(camera) {", + " if (!camera) return;", + " const worldScale = getWorldScale(this.object);", + " const drawDistance = Math.max(this.object._getDrawDistance(), 1) / worldScale;", + " this.uniforms.scatterDrawDistance.value = drawDistance;", + " this._cameraPosition.setFromMatrixPosition(camera.matrixWorld);", + " for (const chunk of this.chunks) {", + " const distance = this._chunkPosition", + " .copy(chunk.center)", + " .applyMatrix4(this.group.matrixWorld)", + " .distanceTo(this._cameraPosition);", + " const isVisible = distance - chunk.radius / worldScale < drawDistance;", + " for (const mesh of chunk.meshes) mesh.visible = isVisible;", + " }", + " }", + "", + " /**", + " * @param {number} elapsedSeconds", + " * @param {THREE.Camera | null} camera", + " */", + " update(elapsedSeconds, camera) {", + " if (this.placement.shouldPlaceAgain(elapsedSeconds, this._getPlacementKey())) {", + " this.rebuild();", + " }", + " if (!this.changedSamples.isEmpty()) {", + " const { object } = this;", + " const sampleWidth = object.getWidth() / MASK_RESOLUTION;", + " const sampleHeight = object.getHeight() / MASK_RESOLUTION;", + " const { minI, minJ, maxI, maxJ } = this.changedSamples;", + " // Copies between samples use the density of the samples around them.", + " this._rebuildArea(", + " object.getX() + (minI - 1) * sampleWidth,", + " object.getY() + (minJ - 1) * sampleHeight,", + " object.getX() + (maxI + 1) * sampleWidth,", + " object.getY() + (maxJ + 1) * sampleHeight", + " );", + " this.changedSamples.clear();", + " }", + " // The object transformation is applied just before rendering, it's needed now.", + " this.object.getRenderer().ensureUpToDate();", + " this.group.updateWorldMatrix(true, false);", + " this._hideFarChunks(camera);", + " this._updateSurface();", + " }", + "", + " _updateSurface() {", + " const { object } = this;", + " if (typeof object.setSurface !== 'function') return;", + " const trunkKey = object._getTrunkCollisions() + '|' + object._getTrunkDiameter();", + " if (trunkKey !== this.trunkKey) {", + " this.trunkKey = trunkKey;", + " this.version++;", + " }", + " if (object.getSurface() !== this.surface) object.setSurface(this.surface);", + " }", + "", + " /** @returns {Copy[]} */", + " getCopies() {", + " const copies = [];", + " for (const chunk of this.chunks) copies.push(...chunk.copies);", + " return copies;", + " }", + "", + " /**", + " * @returns {Copy | null} The copy whose base is the nearest to a position (on X and Y), if any.", + " */", + " findNearestCopy(x, y) {", + " let nearestCopy = null;", + " let nearestDistanceSquared = Infinity;", + " for (const chunk of this.chunks) {", + " for (const copy of chunk.copies) {", + " const distanceSquared = (copy.x - x) * (copy.x - x) + (copy.y - y) * (copy.y - y);", + " if (distanceSquared < nearestDistanceSquared) {", + " nearestDistanceSquared = distanceSquared;", + " nearestCopy = copy;", + " }", + " }", + " }", + " return nearestCopy;", + " }", + "", + " getCopyCount() {", + " let count = 0;", + " for (const chunk of this.chunks) count += chunk.copies.length;", + " return count;", + " }", + "", + " /** Remove the copies whose base is in a circle (they come back when the scene is restarted). */", + " removeCopies(x, y, radius) {", + " let hasRemovedCopies = false;", + " for (const chunk of this.chunks) {", + " for (const copy of chunk.copies) {", + " if ((copy.x - x) * (copy.x - x) + (copy.y - y) * (copy.y - y) <= radius * radius) {", + " this.removedCopies.set(copy.key, copy);", + " hasRemovedCopies = true;", + " }", + " }", + " }", + " if (hasRemovedCopies) this._rebuildArea(x - radius, y - radius, x + radius, y + radius);", + " }", + "", + " /** Paint the full density around a position, and bring back the copies removed there. */", + " addCopies(x, y, radius) {", + " for (const [key, copy] of this.removedCopies) {", + " if ((copy.x - x) * (copy.x - x) + (copy.y - y) * (copy.y - y) <= radius * radius) {", + " this.removedCopies.delete(key);", + " }", + " }", + " this.paint(x, y, x, y, radius, 1, 1);", + " }", + "", + " _getStroke(x1, y1, x2, y2, radius) {", + " const { object } = this;", + " const stroke = this._stroke;", + " stroke.cellWidth = object.getWidth() / MASK_RESOLUTION;", + " stroke.cellHeight = object.getHeight() / MASK_RESOLUTION;", + " stroke.ax = x1 - object.getX();", + " stroke.ay = y1 - object.getY();", + " stroke.bx = x2 - object.getX();", + " stroke.by = y2 - object.getY();", + " stroke.radius = Math.max(radius, 0);", + " return stroke;", + " }", + "", + " /**", + " * Paint the density around a segment (copies are placed again at the next frame).", + " * @param {number} density From 0 (erase) to 1 (as dense as the Density property).", + " * @param {number} strength From 0 to 1.", + " */", + " paint(x1, y1, x2, y2, radius, density, strength) {", + " const changedSamples = new SampleRectangle();", + " this.mask.paint(this._getStroke(x1, y1, x2, y2, radius), density, strength, changedSamples);", + " this.changedSamples.add(changedSamples);", + " }", + "", + " /** @param {DensityMask} mask */", + " setMask(mask) {", + " this.mask = mask;", + " this.rebuild();", + " }", + "", + " /**", + " * Prisms around the trunks, from 0 to 1 in the object box (see `gdjs.Surface`).", + " * @returns {gdjs.SurfaceTriangles}", + " */", + " _getTrunkTriangles() {", + " const { object } = this;", + " const copies = object._getTrunkCollisions() ? this.getCopies() : [];", + " const verticesPerCopy = TRUNK_SIDES * 2 + 1;", + " const positions = new Float32Array(copies.length * verticesPerCopy * 3);", + " const indices = new Uint32Array(copies.length * TRUNK_SIDES * 3 * 3);", + " const width = object.getWidth() || 1;", + " const height = object.getHeight() || 1;", + " const depth = object.getDepth() || 1;", + " let indexOffset = 0;", + " copies.forEach((copy, copyIndex) => {", + " const radius = (Math.max(object._getTrunkDiameter(), 0) / 2) * (copy.height / Math.max(object._getSize(), 1));", + " const top = copy.z + copy.height * 0.8;", + " const firstVertex = copyIndex * verticesPerCopy;", + " const setVertex = (vertex, x, y, z) => {", + " positions[vertex * 3] = (x - object.getX()) / width;", + " positions[vertex * 3 + 1] = (y - object.getY()) / height;", + " positions[vertex * 3 + 2] = (z - object.getZ()) / depth;", + " };", + " for (let side = 0; side < TRUNK_SIDES; side++) {", + " const angle = ((side + 0.5) / TRUNK_SIDES) * Math.PI * 2;", + " const x = copy.x + Math.cos(angle) * radius;", + " const y = copy.y + Math.sin(angle) * radius;", + " // Starting a bit under the ground, which can be sloped.", + " setVertex(firstVertex + side * 2, x, y, copy.z - radius);", + " setVertex(firstVertex + side * 2 + 1, x, y, top);", + " }", + " const center = firstVertex + TRUNK_SIDES * 2;", + " setVertex(center, copy.x, copy.y, top);", + " for (let side = 0; side < TRUNK_SIDES; side++) {", + " const bottom = firstVertex + side * 2;", + " const nextBottom = firstVertex + ((side + 1) % TRUNK_SIDES) * 2;", + " // Triangles face out of the prism (vertices at odd indices are at the top).", + " indices.set([bottom, nextBottom, bottom + 1, nextBottom, nextBottom + 1, bottom + 1], indexOffset);", + " indices.set([center, bottom + 1, nextBottom + 1], indexOffset + 6);", + " indexOffset += 9;", + " }", + " });", + " if (copies.length === 0) {", + " // Without triangles, the physics engine and navigation meshes would use", + " // the object box: a tiny triangle under the object is used instead.", + " return { positions: new Float32Array([0, 0, -1, 0.0001, 0, -1, 0, 0.0001, -1]), indices: new Uint32Array([0, 2, 1]) };", + " }", + " return { positions, indices };", + " }", + "", + " dispose() {", + " this.isDisposed = true;", + " if (typeof this.object.setSurface === 'function' && this.object.getSurface() === this.surface) {", + " this.object.setSurface(null);", + " }", + " this._disposeChunks(this.chunks);", + " this._disposeModel();", + " this.group.removeFromParent();", + " }", + "}", + "", + "const scatterToolIcons = {", + " Add: svgIcon(", + " ''", + " ),", + " Erase: svgIcon(", + " ''", + " ),", + "};", + "", + "/**", + " * The brushes shown in the scene editor when scattered models are selected,", + " * to add or erase copies. Without the needed editor functions (older GDevelop", + " * versions), nothing is shown: copies can still be painted with events.", + " */", + "class ScatterEditorTools {", + " constructor() {", + " /** '' when no brush is used. */", + " this.toolName = '';", + " this.sizePercent = 30;", + " this.strengthPercent = 50;", + " /** @type {Scatter | null} */", + " this.scatter = null;", + " /** @type {gdjs.InGameEditor | null} */", + " this.editor = null;", + " this.isStroking = false;", + " this.lastStrokeTime = 0;", + " /** @type {Array<{mask: DensityMask, baseMask: DensityMask}>} */", + " this.undoSteps = [];", + " this.raycaster = new THREE.Raycaster();", + " this.ndc = new THREE.Vector2();", + " this._groundAtCursor = { ground: null, z: 0 };", + " /** A circle following the ground under the cursor. */", + " this.brushRing = new THREE.LineLoop(", + " new THREE.BufferGeometry().setAttribute('position', new THREE.BufferAttribute(new Float32Array(64 * 3), 3)),", + " new THREE.LineBasicMaterial({ color: 0xffffff, depthTest: false, transparent: true })", + " );", + " this.brushRing.renderOrder = 1000;", + " this.brushRing.frustumCulled = false;", + " this.brushRing.raycast = () => {};", + " }", + "", + " /** @param {gdjs.InGameEditor} editor */", + " update(editor) {", + " if (typeof editor.showToolbar !== 'function') return;", + " this.editor = editor;", + " const selectedObjects = editor.getSelectedObjects();", + " const scatter =", + " selectedObjects.length === 1 && selectedObjects[0].__terrainScatter ? selectedObjects[0].__terrainScatter : null;", + " if (scatter !== this.scatter) {", + " this._endStroke();", + " this.scatter = scatter;", + " this.undoSteps.length = 0;", + " }", + " if (scatter) editor.showToolbar('ScatteredModels', this._getToolbarItems());", + " const game = editor.getRuntimeGame();", + " if (!scatter || !this.toolName) {", + " this.brushRing.removeFromParent();", + " return;", + " }", + " const inputManager = game.getInputManager();", + " if (inputManager.wasKeyJustPressed(gdjs.evtTools.input.keysNameToCode.Escape)) {", + " this._endStroke();", + " this.toolName = '';", + " this.brushRing.removeFromParent();", + " return;", + " }", + " editor.captureLeftMouseButton();", + " const hit = this._getPositionUnderCursor(scatter, game);", + " const radius = this._getRadius();", + " this._showBrushRing(scatter, hit, radius);", + "", + " const isPressedForTool =", + " typeof editor.isCapturedLeftMouseButtonPressed === 'function'", + " ? editor.isCapturedLeftMouseButtonPressed()", + " : inputManager.isMouseButtonPressed(0);", + " // The button is released outside the canvas when the mouse is on a toolbar.", + " const isPressed = isPressedForTool && inputManager.isMouseInsideCanvas();", + " if (isPressed && hit && !this.isStroking) {", + " this._startStroke(scatter);", + " }", + " if (this.isStroking && isPressed && hit) {", + " this._applyBrush(scatter, hit, radius);", + " }", + " if (!isPressed) {", + " this._endStroke();", + " }", + " }", + "", + " _getRadius() {", + " const { object } = this.scatter;", + " const maxRadius = Math.max(object.getWidth(), object.getHeight()) / 4;", + " return Math.max(maxRadius * Math.pow(this.sizePercent / 100, 2), 10);", + " }", + "", + " /**", + " * @returns {{x: number, y: number} | null} The position of the scene under the cursor: on the", + " * highest ground hit, or on the plane at the object Z.", + " */", + " _getPositionUnderCursor(scatter, game) {", + " let hit = null;", + " for (const ground of getGrounds(scatter.object.getRuntimeScene())) {", + " if (typeof ground.raycastFromCursor !== 'function') continue;", + " const localHit = ground.raycastFromCursor(game, this.raycaster, this.ndc);", + " if (!localHit) continue;", + " const sceneHit = ground.toScenePosition(localHit);", + " if (!hit || sceneHit.z > hit.z) hit = sceneHit;", + " }", + " if (hit) return hit;", + "", + " const { object } = scatter;", + " const layer = object.getInstanceContainer().getLayer(object.getLayer());", + " const camera = layer.getRenderer().getThreeCamera();", + " if (!camera) return null;", + " const inputManager = game.getInputManager();", + " this.ndc.set(", + " (inputManager.getCursorX() / game.getGameResolutionWidth()) * 2 - 1,", + " -(inputManager.getCursorY() / game.getGameResolutionHeight()) * 2 + 1", + " );", + " this.raycaster.setFromCamera(this.ndc, camera);", + " scatter.group.updateWorldMatrix(true, false);", + " const ray = this.raycaster.ray.clone().applyMatrix4(scatter.group.matrixWorld.clone().invert());", + " const point = ray.intersectPlane(new THREE.Plane(new THREE.Vector3(0, 0, 1), 0), new THREE.Vector3());", + " if (!point) return null;", + " return {", + " x: object.getX() + (point.x / AREA_SIZE) * object.getWidth(),", + " y: object.getY() + (point.y / AREA_SIZE) * object.getHeight(),", + " };", + " }", + "", + " _showBrushRing(scatter, hit, radius) {", + " if (!hit) {", + " this.brushRing.removeFromParent();", + " return;", + " }", + " const { object } = scatter;", + " if (this.brushRing.parent !== scatter.group) scatter.group.add(this.brushRing);", + " const grounds = getGrounds(object.getRuntimeScene());", + " const positions = this.brushRing.geometry.attributes.position;", + " for (let index = 0; index < positions.count; index++) {", + " const angle = (index / positions.count) * Math.PI * 2;", + " const x = hit.x + Math.cos(angle) * radius;", + " const y = hit.y + Math.sin(angle) * radius;", + " const { ground, z } = findHighestGround(grounds, x, y, this._groundAtCursor);", + " positions.setXYZ(", + " index,", + " ((x - object.getX()) / object.getWidth()) * AREA_SIZE,", + " ((y - object.getY()) / object.getHeight()) * AREA_SIZE,", + " (((ground ? z : object.getZ()) + 2 - object.getZ()) / Math.max(object.getDepth(), 0.0001)) * AREA_DEPTH", + " );", + " }", + " positions.needsUpdate = true;", + " }", + "", + " _startStroke(scatter) {", + " this.isStroking = true;", + " this.lastStrokeTime = performance.now();", + " this.undoSteps.push({ mask: scatter.mask.clone(), baseMask: scatter.baseMask });", + " while (this.undoSteps.length > 30) this.undoSteps.shift();", + " }", + "", + " _endStroke() {", + " if (!this.isStroking) return;", + " this.isStroking = false;", + " this._save();", + " }", + "", + " _applyBrush(scatter, hit, radius) {", + " const now = performance.now();", + " const elapsedSeconds = Math.min((now - this.lastStrokeTime) / 1000, 0.1);", + " this.lastStrokeTime = now;", + " const strength = (this.strengthPercent / 100) * elapsedSeconds * 8;", + " scatter.paint(hit.x, hit.y, hit.x, hit.y, radius, this.toolName === 'Add' ? 1 : 0, strength);", + " }", + "", + " _save() {", + " const { scatter, editor } = this;", + " if (!scatter || !editor) return;", + " editor.updateObjectProperties(scatter.object.getName(), { PaintData: scatter.save() });", + " }", + "", + " _undo() {", + " const undoStep = this.undoSteps.pop();", + " // The coverage or the edits may have changed since the stroke.", + " if (!this.scatter || !undoStep || undoStep.baseMask !== this.scatter.baseMask) {", + " this.undoSteps.length = 0;", + " return;", + " }", + " this.scatter.setMask(undoStep.mask);", + " this._save();", + " }", + "", + " /** @returns {Array} */", + " _getToolbarItems() {", + " const toolButton = (toolName, tooltip) => ({", + " type: 'button',", + " id: toolName,", + " tooltip,", + " iconUrl: scatterToolIcons[toolName],", + " isActive: this.toolName === toolName,", + " onClick: () => (this.toolName = this.toolName === toolName ? '' : toolName),", + " });", + " return [", + " toolButton('Add', 'Add copies (drag on the ground)'),", + " toolButton('Erase', 'Erase copies'),", + " { type: 'divider', id: 'BrushDivider' },", + " {", + " type: 'slider',", + " id: 'Size',", + " tooltip: 'Brush size',", + " iconUrl: toolIcons.Size,", + " min: 1,", + " max: 100,", + " value: this.sizePercent,", + " onChange: (value) => (this.sizePercent = value),", + " },", + " {", + " type: 'slider',", + " id: 'Strength',", + " tooltip: 'Brush strength',", + " iconUrl: toolIcons.Strength,", + " min: 5,", + " max: 100,", + " value: this.strengthPercent,", + " onChange: (value) => (this.strengthPercent = value),", + " },", + " {", + " type: 'button',", + " id: 'Undo',", + " tooltip: 'Undo the last stroke on these scattered models',", + " iconUrl: toolIcons.Undo,", + " onClick: () => this._undo(),", + " },", + " ];", + " }", + "}", + "", + "const editorTools = new ScatterEditorTools();", + "if (gdjs.registerInGameEditorPostStepCallback) {", + " gdjs.registerInGameEditorPostStepCallback((editor) => editorTools.update(editor));", + "}", + "", + "gdjs.__terrainScatterExtension = {", + " Scatter,", + " // Exposed for tests.", + " DensityMask,", + " maskCodec,", + " applyEdits,", + "};", + "" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [], + "objectGroups": [] + } + ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "DefineHelperClasses" + }, + { + "functionName": "DefineGrassHelperClasses" + }, + { + "functionName": "DefineScatterHelperClasses" + } + ] + }, + "eventsBasedBehaviors": [], + "eventsBasedObjects": [ + { + "areaMaxX": 4096, + "areaMaxY": 4096, + "areaMaxZ": 600, + "areaMinX": 0, + "areaMinY": 0, + "areaMinZ": 0, + "assetStoreTag": "3d terrains", + "defaultName": "Terrain", + "description": "A ground with hills, mountains or valleys that can be sculpted and painted in the scene editor, or changed with events. Add the 3D physics behavior to it to make objects walk or roll on it.", + "fullName": "3D terrain", + "helpPath": "", + "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLXRlcnJhaW4iIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTQsNkwxMC4yNSwxMUwxMy4xLDE0LjhMMTEuNSwxNkM5LjgxLDEzLjc1IDcsMTAgNywxMEwxLDE4SDIzTDE0LDZaIiAvPjwvc3ZnPg==", + "is3D": true, + "isUsingLegacyInstancesRenderer": true, + "name": "Terrain3D", + "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", + "objects": [ + { + "assetStoreId": "", + "name": "AreaPlaceholder", + "type": "Scene3D::Cube3DObject", + "variables": [], + "effects": [], + "behaviors": [], + "content": { + "width": 4096, + "height": 4096, + "depth": 600, + "enableTextureTransparency": false, + "facesOrientation": "Y", + "backFaceUpThroughWhichAxisRotation": "X", + "materialType": "Basic", + "tint": "255;255;255", + "frontFaceResourceName": "", + "frontFaceVisible": false, + "frontFaceResourceRepeat": false, + "backFaceResourceName": "", + "backFaceVisible": false, + "backFaceResourceRepeat": false, + "leftFaceResourceName": "", + "leftFaceVisible": false, + "leftFaceResourceRepeat": false, + "rightFaceResourceName": "", + "rightFaceVisible": false, + "rightFaceResourceRepeat": false, + "topFaceResourceName": "", + "topFaceVisible": false, + "topFaceResourceRepeat": false, + "bottomFaceResourceName": "", + "bottomFaceVisible": false, + "bottomFaceResourceRepeat": false + } + } + ], + "objectsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "objectName": "AreaPlaceholder" + } + ] + }, + "objectsGroups": [], + "layers": [ + { + "ambientLightColorB": 200, + "ambientLightColorG": 200, + "ambientLightColorR": 200, + "camera2DPlaneMaxDrawingDistance": 5000, + "camera3DFarPlaneDistance": 10000, + "camera3DFieldOfView": 45, + "camera3DNearPlaneDistance": 3, + "cameraType": "", + "followBaseLayerCamera": false, + "isLightingLayer": false, + "isLocked": false, + "name": "", + "renderingType": "", + "visibility": true, + "cameras": [ + { + "defaultSize": true, + "defaultViewport": true, + "height": 0, + "viewportBottom": 1, + "viewportLeft": 0, + "viewportRight": 1, + "viewportTop": 0, + "width": 0 + } + ], + "effects": [] + } + ], + "instances": [ + { + "angle": 0, + "customSize": true, + "depth": 600, + "height": 4096, + "keepRatio": true, + "layer": "", + "name": "AreaPlaceholder", + "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-004096004096", + "width": 4096, + "x": 0, + "y": 0, + "zOrder": 1, + "numberProperties": [], + "stringProperties": [], + "initialVariables": [] + } + ], + "editionSettings": {}, + "eventsFunctions": [ + { + "fullName": "", + "functionType": "Action", + "name": "onCreated", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Terrain3D::DefineHelperClasses" + }, + "parameters": [ + "", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "for (const child of object.getChildrenContainer().getAdhocListOfAllInstances()) {", + " child.deleteFromScene();", + "}", + "if (object.__terrain3D) object.__terrain3D.dispose();", + "object.__terrain3D = new gdjs.__terrain3DExtension.Terrain(object);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "onDestroy", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrain3D.dispose();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "doStepPostEvents", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrain3D.update();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "onHotReloading", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrain3D.onHotReloading();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "description": "Raise the ground around a position (or lower it with a negative height). It's raised the most at the center and less and less up to the radius.", + "fullName": "Raise or lower the ground", + "functionType": "Action", + "group": "Sculpt", + "name": "RaiseGround", + "sentence": "Raise the ground of _PARAM0_ by _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const height = eventsFunctionContext.getArgument(\"Height\");", + "object.__terrain3D.raise(x, y, x, y, radius, height);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Height to add (negative to lower the ground)", + "name": "Height", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Raise the ground along a line, for example for a ridge, or lower it with a negative height to dig a river bed or a valley.", + "fullName": "Raise or lower the ground along a line", + "functionType": "Action", + "group": "Sculpt", + "name": "RaiseGroundAlongLine", + "sentence": "Raise the ground of _PARAM0_ by _PARAM6_ from _PARAM1_; _PARAM2_ to _PARAM3_; _PARAM4_ with a radius of _PARAM5_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x1 = eventsFunctionContext.getArgument(\"X1\");", + "const y1 = eventsFunctionContext.getArgument(\"Y1\");", + "const x2 = eventsFunctionContext.getArgument(\"X2\");", + "const y2 = eventsFunctionContext.getArgument(\"Y2\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const height = eventsFunctionContext.getArgument(\"Height\");", + "object.__terrain3D.raise(x1, y1, x2, y2, radius, height);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the start", + "name": "X1", + "type": "expression" + }, + { + "description": "Y position of the start", + "name": "Y1", + "type": "expression" + }, + { + "description": "X position of the end", + "name": "X2", + "type": "expression" + }, + { + "description": "Y position of the end", + "name": "Y2", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance from the line.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Height to add (negative to lower the ground)", + "name": "Height", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Flatten the ground around a position to a given height, for example to place a building.", + "fullName": "Flatten the ground", + "functionType": "Action", + "group": "Sculpt", + "name": "FlattenGround", + "sentence": "Flatten the ground of _PARAM0_ to a Z of _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const z = eventsFunctionContext.getArgument(\"Z\");", + "object.__terrain3D.flatten(x, y, x, y, radius, z, 1);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Z position of the flat ground", + "name": "Z", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Smooth the ground around a position, softening bumps and cliffs.", + "fullName": "Smooth the ground", + "functionType": "Action", + "group": "Sculpt", + "name": "SmoothGround", + "sentence": "Smooth the ground of _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_ (strength: _PARAM4_)", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const strength = eventsFunctionContext.getArgument(\"Strength\");", + "object.__terrain3D.smooth(x, y, x, y, radius, strength);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Strength (between 0 and 1)", + "name": "Strength", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Paint a layer (like grass, dirt, rock or sand) on the ground around a position.", + "fullName": "Paint a layer", + "functionType": "Action", + "group": "Paint", + "name": "PaintLayer", + "sentence": "Paint layer _PARAM4_ on _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const layer = eventsFunctionContext.getArgument(\"Layer\");", + "object.__terrain3D.paint(x, y, x, y, radius, layer, 1);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Layer (1 to 4)", + "name": "Layer", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Paint a layer on the ground along a line, for example for a path or a road.", + "fullName": "Paint a layer along a line", + "functionType": "Action", + "group": "Paint", + "name": "PaintLayerAlongLine", + "sentence": "Paint layer _PARAM6_ on _PARAM0_ from _PARAM1_; _PARAM2_ to _PARAM3_; _PARAM4_ with a radius of _PARAM5_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x1 = eventsFunctionContext.getArgument(\"X1\");", + "const y1 = eventsFunctionContext.getArgument(\"Y1\");", + "const x2 = eventsFunctionContext.getArgument(\"X2\");", + "const y2 = eventsFunctionContext.getArgument(\"Y2\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const layer = eventsFunctionContext.getArgument(\"Layer\");", + "object.__terrain3D.paint(x1, y1, x2, y2, radius, layer, 1);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the start", + "name": "X1", + "type": "expression" + }, + { + "description": "Y position of the start", + "name": "Y1", + "type": "expression" + }, + { + "description": "X position of the end", + "name": "X2", + "type": "expression" + }, + { + "description": "Y position of the end", + "name": "Y2", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance from the line.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Layer (1 to 4)", + "name": "Layer", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Tint the ground around a position with a color multiplying its layers, like dry grass or darker mud. White removes the tint.", + "fullName": "Tint the ground", + "functionType": "Action", + "group": "Paint", + "name": "TintGround", + "sentence": "Tint the ground of _PARAM0_ with _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const color = gdjs.rgbOrHexToRGBColor(eventsFunctionContext.getArgument(\"Color\"));", + "object.__terrain3D.tint(x, y, x, y, radius, color, 1);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Color", + "name": "Color", + "type": "color" + } + ], + "objectGroups": [] + }, + { + "description": "Make the ground around a position wet (darker and shiny, like after rain or near water) or dry.", + "fullName": "Change the wetness of the ground", + "functionType": "Action", + "group": "Paint", + "name": "WetGround", + "sentence": "Change the wetness of the ground of _PARAM0_ to _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const wetness = eventsFunctionContext.getArgument(\"Wetness\");", + "object.__terrain3D.wet(x, y, x, y, radius, wetness, 1);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Wetness (from 0 for dry to 1)", + "name": "Wetness", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "the wetness of the ground at a position, from 0 (dry) to 1, painted or near the water (wet shore).", + "fullName": "Ground wetness", + "functionType": "ExpressionAndCondition", + "group": "Paint", + "name": "GroundWetness", + "sentence": "the wetness of the ground at _PARAM1_; _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "eventsFunctionContext.returnValue = object.__terrain3D.getWetnessAt(x, y);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "The tint painted on the ground at a position, like \"255;255;255\" (white when there is none).", + "fullName": "Ground tint", + "functionType": "StringExpression", + "group": "Paint", + "name": "GroundTint", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "eventsFunctionContext.returnValue = object.__terrain3D.getTintAt(x, y).join(\";\");" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "string" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Remove the ground in a circle, for the entrance of a tunnel, a cave or a well: there is nothing drawn and no collision there. Fill it again with the \"Fill holes\" action. A height field can't go under itself: put a 3D model (with a collision shape) for the tunnel or the cave under the ground. The physics engine removes the triangles touching a hole, so the hole in the collisions can be up to a cell larger than the one drawn: make the model go a bit further than the hole, or put rocks around the entrance.", + "fullName": "Make a hole", + "functionType": "Action", + "group": "Holes", + "name": "MakeHole", + "sentence": "Make a hole in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "object.__terrain3D.makeHole(x, y, x, y, radius);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "name": "Radius", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Fill the holes in a circle with the layers of the ground around them: the ground is drawn and has collisions again.", + "fullName": "Fill holes", + "functionType": "Action", + "group": "Holes", + "name": "FillHoles", + "sentence": "Fill the holes in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "object.__terrain3D.fillHoles(x, y, x, y, radius);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "name": "Radius", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Make the entrance of a tunnel or a cave on a slope: a hole, with the ground in front of it flattened to the floor of the tunnel.", + "fullName": "Carve an entrance", + "functionType": "Action", + "group": "Holes", + "name": "CarveEntrance", + "sentence": "Carve an entrance in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_, going in at _PARAM4_° with its floor at Z _PARAM5_ and _PARAM6_ pixels of flat ground in front", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const angle = eventsFunctionContext.getArgument(\"Angle\");", + "const floorZ = eventsFunctionContext.getArgument(\"FloorZ\");", + "const length = eventsFunctionContext.getArgument(\"Length\");", + "object.__terrain3D.carveEntrance(x, y, radius, angle, floorZ, length);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "name": "Radius", + "type": "expression" + }, + { + "description": "Direction going into the ground (in degrees)", + "name": "Angle", + "type": "expression" + }, + { + "description": "Z position of the floor", + "name": "FloorZ", + "type": "expression" + }, + { + "description": "Length of the flat ground in front of the entrance (in pixels)", + "name": "Length", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Check if there is a hole in the ground at a position.", + "fullName": "Hole", + "functionType": "Condition", + "group": "Holes", + "name": "IsHole", + "sentence": "There is a hole in _PARAM0_ at _PARAM1_; _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "eventsFunctionContext.returnValue = object.__terrain3D.isHoleAt(x, y);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "The Z position of the ground at a position, or the given value where there is a hole.", + "fullName": "Ground Z position, with holes", + "functionType": "Expression", + "group": "Holes", + "name": "GroundZWithHoles", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const terrain = object.__terrain3D;", + "eventsFunctionContext.returnValue = terrain.isHoleAt(x, y) ? eventsFunctionContext.getArgument(\"ZInHoles\") : terrain.getHeightAt(x, y);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + }, + { + "description": "Value in holes", + "longDescription": "For example, a Z position far under the ground.", + "name": "ZInHoles", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Replace the ground with a generated relief. Painted layers are reset.", + "fullName": "Generate a relief", + "functionType": "Action", + "group": "Generation", + "name": "GenerateRelief", + "sentence": "Generate a relief of _PARAM1_ on _PARAM0_ with seed _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const relief = eventsFunctionContext.getArgument(\"Relief\");", + "const seed = eventsFunctionContext.getArgument(\"Seed\");", + "object.__terrain3D.generate(relief, seed);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "Relief", + "name": "Relief", + "supplementaryInformation": "[\"Flat\", \"Hills\", \"Mountains\", \"Island\"]", + "type": "stringWithSelector" + }, + { + "description": "Seed", + "longDescription": "Each seed gives a different terrain with the same relief.", + "name": "Seed", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Replace the ground with the heights of an image. Painted layers are kept.", + "fullName": "Load a heightmap", + "functionType": "Action", + "group": "Generation", + "name": "LoadHeightmap", + "sentence": "Load the heights of _PARAM0_ from _PARAM1_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const image = eventsFunctionContext.getArgument(\"Image\");", + "object.__terrain3D.loadHeightmap(image);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "Heightmap image", + "longDescription": "Black is the lowest, white the highest. For more precision, store the height on 16 bits in the red (most significant byte) and green channels.", + "name": "Image", + "type": "imageResource" + } + ], + "objectGroups": [] + }, + { + "description": "Move objects up or down so that their bottom is on the ground, under their center. Useful to place trees, rocks or characters on a terrain.", + "fullName": "Put objects on the ground", + "functionType": "Action", + "group": "Position", + "name": "PutOnGround", + "sentence": "Put _PARAM1_ on the ground of _PARAM0_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrain3D.placeOnGround(eventsFunctionContext.getObjects(\"Objects\"));" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "Objects to put on the ground", + "name": "Objects", + "type": "objectList" + } + ], + "objectGroups": [] + }, + { + "description": "the Z position of the ground at a position of the scene.", + "fullName": "Ground Z position", + "functionType": "ExpressionAndCondition", + "group": "Position", + "name": "GroundZ", + "sentence": "the ground Z position at _PARAM1_; _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "eventsFunctionContext.returnValue = object.__terrain3D.getHeightAt(x, y);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "the slope of the ground (angle with the horizontal, in degrees) at a position of the scene.", + "fullName": "Ground slope", + "functionType": "ExpressionAndCondition", + "group": "Position", + "name": "GroundSlope", + "sentence": "the ground slope at _PARAM1_; _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "eventsFunctionContext.returnValue = object.__terrain3D.getSlopeAt(x, y);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "the layer (from 1 to 4) painted the most at a position of the scene. Useful to play footstep sounds or to slow down characters in water or mud.", + "fullName": "Ground layer", + "functionType": "ExpressionAndCondition", + "group": "Paint", + "name": "GroundLayer", + "sentence": "the ground layer at _PARAM1_; _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "eventsFunctionContext.returnValue = object.__terrain3D.getLayerAt(x, y);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + } + ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + }, + { + "folderName": "Sculpt", + "children": [ + { + "functionName": "RaiseGround" + }, + { + "functionName": "RaiseGroundAlongLine" + }, + { + "functionName": "FlattenGround" + }, + { + "functionName": "SmoothGround" + } + ] + }, + { + "folderName": "Paint", + "children": [ + { + "functionName": "PaintLayer" + }, + { + "functionName": "PaintLayerAlongLine" + }, + { + "functionName": "TintGround" + }, + { + "functionName": "WetGround" + }, + { + "functionName": "GroundWetness" + }, + { + "functionName": "GroundTint" + }, + { + "functionName": "GroundLayer" + } + ] + }, + { + "folderName": "Holes", + "children": [ + { + "functionName": "MakeHole" + }, + { + "functionName": "FillHoles" + }, + { + "functionName": "CarveEntrance" + }, + { + "functionName": "IsHole" + }, + { + "functionName": "GroundZWithHoles" + } + ] + }, + { + "folderName": "Generation", + "children": [ + { + "functionName": "GenerateRelief" + }, + { + "functionName": "LoadHeightmap" + } + ] + }, + { + "folderName": "Position", + "children": [ + { + "functionName": "PutOnGround" + }, + { + "functionName": "GroundZ" + }, + { + "functionName": "GroundSlope" + } + ] + } + ] + }, + "propertyDescriptors": [ + { + "value": "Hills", + "type": "Choice", + "label": "Relief", + "description": "The initial shape, before sculpting.", + "group": "Shape", + "choices": [ + { + "label": "Flat", + "value": "Flat" + }, + { + "label": "Hills", + "value": "Hills" + }, + { + "label": "Mountains", + "value": "Mountains" + }, + { + "label": "Island", + "value": "Island" + } + ], + "name": "Relief" + }, + { + "value": "1", + "type": "Number", + "label": "Seed", + "description": "Each seed gives a different terrain with the same relief.", + "group": "Shape", + "name": "Seed" + }, + { + "value": "", + "type": "Resource", + "label": "Heightmap image", + "description": "Optional. Replaces the relief: black is the lowest, white the highest (the object depth).", + "group": "Shape", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "HeightmapImage" + }, + { + "value": "", + "type": "MultilineString", + "label": "Edits", + "description": "Changes of the relief or heightmap, applied in order, as a JSON list, for example [{\"tool\": \"raise\", \"x\": 0.3, \"y\": 0.4, \"radius\": 0.15, \"height\": 0.4}, {\"tool\": \"paint\", \"x\": 0.1, \"y\": 0.9, \"toX\": 0.8, \"toY\": 0.2, \"radius\": 0.02, \"layer\": 2}]. Tools: raise (height added to the ground, negative to lower), flatten (z: the ground is brought to this height, fully at the center, cutting what is above it and filling what is below; with toX and toY, toZ gives the height at the end of the line, for a road going up), smooth (strength), paint (layer, from 1 to 4, and strength), tint (color, like \"120;140;60\", multiplying the layers, white removing the tint, and strength), wetness (amount, from 0 for dry to 1 for wet, and strength), hole (no ground in the radius, for the entrances of tunnels and caves) and fill (the holes in the radius are filled with the layers of the ground around them). x, y and radius are fractions (0 to 1) of the terrain size (x is (scene X - terrain X) / terrain width). height and z are fractions of its depth: the ground at z is at Z = terrain Z + z * terrain depth. Add toX and toY to change the ground along a line (paths, rivers, ridges). Shown in the scene editor.", + "group": "Shape", + "advanced": true, + "name": "Edits" + }, + { + "value": "256", + "type": "Choice", + "label": "Resolution", + "description": "The number of cells on each side of the terrain. Higher is more detailed but slower.", + "group": "Shape", + "choices": [ + { + "label": "64 x 64", + "value": "64" + }, + { + "label": "128 x 128", + "value": "128" + }, + { + "label": "256 x 256", + "value": "256" + }, + { + "label": "512 x 512", + "value": "512" + }, + { + "label": "1024 x 1024", + "value": "1024" + } + ], + "name": "Resolution" + }, + { + "value": "96;160;64", + "type": "Color", + "label": "Layer 1 color (grass)", + "group": "Layers", + "name": "Layer1Color" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 1 texture", + "description": "Optional. Tinted by the layer color.", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer1Texture" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 1 normal map", + "description": "Optional. The bumps of the texture, lit by the lights (an OpenGL normal map, like the textures of ambientCG).", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer1NormalMap" + }, + { + "value": "150;112;72", + "type": "Color", + "label": "Layer 2 color (dirt)", + "group": "Layers", + "name": "Layer2Color" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 2 texture", + "description": "Optional. Tinted by the layer color.", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer2Texture" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 2 normal map", + "description": "Optional. The bumps of the texture, lit by the lights (an OpenGL normal map, like the textures of ambientCG).", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer2NormalMap" + }, + { + "value": "124;118;112", + "type": "Color", + "label": "Layer 3 color (rock)", + "group": "Layers", + "name": "Layer3Color" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 3 texture", + "description": "Optional. Tinted by the layer color.", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer3Texture" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 3 normal map", + "description": "Optional. The bumps of the texture, lit by the lights (an OpenGL normal map, like the textures of ambientCG).", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer3NormalMap" + }, + { + "value": "224;204;148", + "type": "Color", + "label": "Layer 4 color (sand)", + "group": "Layers", + "name": "Layer4Color" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 4 texture", + "description": "Optional. Tinted by the layer color.", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer4Texture" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 4 normal map", + "description": "Optional. The bumps of the texture, lit by the lights (an OpenGL normal map, like the textures of ambientCG).", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer4NormalMap" + }, + { + "value": "1", + "type": "Number", + "label": "Normal map strength", + "description": "How much the normal maps bump the ground: 0 for none, 1 as they are, more to exaggerate them.", + "group": "Layers", + "name": "NormalMapStrength" + }, + { + "value": "256", + "type": "Number", + "unit": "Pixel", + "label": "Texture size", + "description": "The size, in the scene, of one repetition of the layer textures.", + "group": "Layers", + "name": "TextureSize" + }, + { + "value": "0.5", + "type": "Number", + "label": "Height blending", + "description": "Where layers meet, the bright parts of textures (stones, tufts) cover the dark ones (gaps, sand) instead of fading into each other: 0 for none, 1 for the most.", + "group": "Layers", + "name": "HeightBlending" + }, + { + "value": "0.5", + "type": "Number", + "label": "Texture variation", + "description": "Makes the repetition of textures less visible, with patches of bigger and rotated textures and colors slowly varying: 0 for none, 1 for the most.", + "group": "Layers", + "name": "TextureVariation" + }, + { + "value": "false", + "type": "Boolean", + "label": "Wet shore", + "description": "Make the ground wet (darker and shiny) under a Z position, like the sand of a beach near the water.", + "group": "Wetness", + "name": "WetShore" + }, + { + "value": "0", + "type": "Number", + "unit": "Pixel", + "label": "Wet below Z", + "description": "The ground is wet under this Z position (for example, the Z of a 3D water object).", + "group": "Wetness", + "name": "WetShoreZ" + }, + { + "value": "60", + "type": "Number", + "unit": "Pixel", + "label": "Wet shore fade", + "description": "The ground becomes dry over this height above \"Wet below Z\".", + "group": "Wetness", + "name": "WetShoreFade" + }, + { + "value": "3", + "type": "Choice", + "label": "Layer on steep slopes", + "description": "This layer is shown where the ground is steep, whatever is painted.", + "group": "Layers", + "choices": [ + { + "label": "None", + "value": "0" + }, + { + "label": "Layer 1", + "value": "1" + }, + { + "label": "Layer 2", + "value": "2" + }, + { + "label": "Layer 3", + "value": "3" + }, + { + "label": "Layer 4", + "value": "4" + } + ], + "name": "CliffLayer" + }, + { + "value": "45", + "type": "Number", + "unit": "DegreeAngle", + "label": "Steep slope angle", + "group": "Layers", + "name": "CliffAngle" + }, + { + "value": "true", + "type": "Boolean", + "label": "Cast shadows", + "group": "Rendering", + "name": "CastShadow" + }, + { + "value": "true", + "type": "Boolean", + "label": "Receive shadows", + "group": "Rendering", + "name": "ReceiveShadow" + }, + { + "value": "", + "type": "String", + "label": "Sculpted and painted data", + "description": "Written by the sculpt and paint tools of the scene editor, in a compact format not meant to be read or written by hand: use Edits instead. Clear it to go back to the relief, heightmap and edits.", + "advanced": true, + "name": "SculptData" + } + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Shape", + "children": [ + { + "propertyName": "Relief" + }, + { + "propertyName": "Seed" + }, + { + "propertyName": "HeightmapImage" + }, + { + "propertyName": "Edits" + }, + { + "propertyName": "Resolution" + } + ] + }, + { + "folderName": "Layers", + "children": [ + { + "propertyName": "Layer1Color" + }, + { + "propertyName": "Layer1Texture" + }, + { + "propertyName": "Layer1NormalMap" + }, + { + "propertyName": "Layer2Color" + }, + { + "propertyName": "Layer2Texture" + }, + { + "propertyName": "Layer2NormalMap" + }, + { + "propertyName": "Layer3Color" + }, + { + "propertyName": "Layer3Texture" + }, + { + "propertyName": "Layer3NormalMap" + }, + { + "propertyName": "Layer4Color" + }, + { + "propertyName": "Layer4Texture" + }, + { + "propertyName": "Layer4NormalMap" + }, + { + "propertyName": "NormalMapStrength" + }, + { + "propertyName": "TextureSize" + }, + { + "propertyName": "HeightBlending" + }, + { + "propertyName": "TextureVariation" + }, + { + "propertyName": "CliffLayer" + }, + { + "propertyName": "CliffAngle" + } + ] + }, + { + "folderName": "Wetness", + "children": [ + { + "propertyName": "WetShore" + }, + { + "propertyName": "WetShoreZ" + }, + { + "propertyName": "WetShoreFade" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "CastShadow" + }, + { + "propertyName": "ReceiveShadow" + } + ] + }, + { + "propertyName": "SculptData" + } + ] + }, + "variants": [] + }, + { + "areaMaxX": 1000, + "areaMaxY": 1000, + "areaMaxZ": 40, + "areaMinX": 0, + "areaMinY": 0, + "areaMinZ": 0, + "defaultName": "Grass", + "description": "A field of grass blades swaying in the wind and pushed aside by characters. They grow in the area of the object, on the highest 3D terrain at each point (one field can cover several terrains), or at the Z position of the object where there is no terrain.", + "fullName": "Terrain grass", + "helpPath": "", + "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGQ9Ik0zIDIxYzEtNSAxLTkgMC0xMyAyIDMgMyA3IDMgMTN6bTQgMGMwLTYgMS0xMSA0LTE2LTEgNS0xIDEwIDAgMTZ6bTUgMGMwLTUgMi05IDUtMTItMiA0LTIgOC0xIDEyem01IDBjMS00IDItNiA0LTgtMSAzLTEgNS0xIDh6Ii8+PC9zdmc+", + "is3D": true, + "isUsingLegacyInstancesRenderer": true, + "name": "TerrainGrass", + "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/732ef90b7fcf5dd9171fe95d0cf262e09159b487304915a4693baa893d9ce16c_grass.svg", + "objects": [ + { + "assetStoreId": "", + "name": "AreaPlaceholder", + "type": "Scene3D::Cube3DObject", + "variables": [], + "effects": [], + "behaviors": [], + "content": { + "width": 1000, + "height": 1000, + "depth": 40, + "enableTextureTransparency": false, + "facesOrientation": "Y", + "backFaceUpThroughWhichAxisRotation": "X", + "materialType": "Basic", + "tint": "255;255;255", + "frontFaceResourceName": "", + "frontFaceVisible": false, + "frontFaceResourceRepeat": false, + "backFaceResourceName": "", + "backFaceVisible": false, + "backFaceResourceRepeat": false, + "leftFaceResourceName": "", + "leftFaceVisible": false, + "leftFaceResourceRepeat": false, + "rightFaceResourceName": "", + "rightFaceVisible": false, + "rightFaceResourceRepeat": false, + "topFaceResourceName": "", + "topFaceVisible": false, + "topFaceResourceRepeat": false, + "bottomFaceResourceName": "", + "bottomFaceVisible": false, + "bottomFaceResourceRepeat": false + } + } + ], + "objectsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "objectName": "AreaPlaceholder" + } + ] + }, + "objectsGroups": [], + "layers": [ + { + "ambientLightColorB": 200, + "ambientLightColorG": 200, + "ambientLightColorR": 200, + "camera2DPlaneMaxDrawingDistance": 5000, + "camera3DFarPlaneDistance": 10000, + "camera3DFieldOfView": 45, + "camera3DNearPlaneDistance": 3, + "cameraType": "", + "followBaseLayerCamera": false, + "isLightingLayer": false, + "isLocked": false, + "name": "", + "renderingType": "", + "visibility": true, + "cameras": [ + { + "defaultSize": true, + "defaultViewport": true, + "height": 0, + "viewportBottom": 1, + "viewportLeft": 0, + "viewportRight": 1, + "viewportTop": 0, + "width": 0 + } + ], + "effects": [] + } + ], + "instances": [ + { + "angle": 0, + "customSize": true, + "depth": 40, + "height": 1000, + "keepRatio": true, + "layer": "", + "name": "AreaPlaceholder", + "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-001000001000", + "width": 1000, + "x": 0, + "y": 0, + "zOrder": 1, + "numberProperties": [], + "stringProperties": [], + "initialVariables": [] + } + ], + "editionSettings": {}, + "eventsFunctions": [ + { + "fullName": "", + "functionType": "Action", + "name": "onCreated", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Terrain3D::DefineHelperClasses" + }, + "parameters": [ + "", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Terrain3D::DefineGrassHelperClasses" + }, + "parameters": [ + "", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "for (const child of object.getChildrenContainer().getAdhocListOfAllInstances()) {", + " child.deleteFromScene();", + "}", + "if (object.__terrainGrass) object.__terrainGrass.dispose();", + "object.__terrainGrass = new gdjs.__terrainGrassExtension.Grass(object);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "onDestroy", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrainGrass.dispose();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "doStepPostEvents", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const game = runtimeScene.getGame();", + "// Time doesn't pass in the scene editor: real time is used to show the wind.", + "const isInGameEdition = game.isInGameEdition && game.isInGameEdition();", + "const elapsedTime = isInGameEdition ? 1 / 60 : object.getElapsedTime() / 1000;", + "const time = isInGameEdition", + " ? performance.now() / 1000", + " : object.getRuntimeScene().getTimeManager().getTimeFromStart() / 1000;", + "const layer = object.getInstanceContainer().getLayer(object.getLayer());", + "object.__terrainGrass.update(elapsedTime, time, layer.getRenderer().getThreeCamera());" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "onHotReloading", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrainGrass.updateFromProperties();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" + } + ], + "objectGroups": [] + } + ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + } + ] + }, + "propertyDescriptors": [ + { + "value": "60", + "type": "Number", + "label": "Density", + "description": "The number of blades in a square of 100 x 100 pixels.", + "group": "Blades", + "name": "Density" + }, + { + "value": "36", + "type": "Number", + "unit": "Pixel", + "label": "Blade height", + "description": "The average height of blades (each blade is between 60% and 140% of it).", + "group": "Blades", + "name": "BladeHeight" + }, + { + "value": "5", + "type": "Number", + "unit": "Pixel", + "label": "Blade width", + "group": "Blades", + "name": "BladeWidth" + }, + { + "value": "52;104;36", + "type": "Color", + "label": "Base color", + "group": "Blades", + "name": "BaseColor" + }, + { + "value": "150;196;82", + "type": "Color", + "label": "Tip color", + "group": "Blades", + "name": "TipColor" + }, + { + "value": "1", + "type": "Number", + "label": "Seed", + "description": "Each seed places blades differently.", + "group": "Blades", + "name": "Seed" + }, + { + "value": "1", + "type": "Choice", + "label": "Grows on terrain layer", + "description": "On a 3D terrain, blades only grow where this layer is painted. A new terrain has layer 1 painted everywhere.", + "group": "Ground", + "choices": [ + { + "label": "Any", + "value": "0" + }, + { + "label": "Layer 1", + "value": "1" + }, + { + "label": "Layer 2", + "value": "2" + }, + { + "label": "Layer 3", + "value": "3" + }, + { + "label": "Layer 4", + "value": "4" + } + ], + "name": "GroundLayer" + }, + { + "value": "40", + "type": "Number", + "unit": "DegreeAngle", + "label": "Maximum slope", + "description": "No blades grow on steeper ground.", + "group": "Ground", + "name": "MaxSlope" + }, + { + "value": "1", + "type": "Number", + "label": "Wind strength", + "description": "0 for no wind.", + "group": "Wind", + "name": "WindStrength" + }, + { + "value": "2", + "type": "Number", + "label": "Wind speed", + "group": "Wind", + "name": "WindSpeed" + }, + { + "value": "", + "type": "String", + "label": "Object bending the grass", + "description": "The name of an object pushing blades aside when walking through the grass, like the player. The 8 first instances are used.", + "group": "Wind", + "name": "BendingObject" + }, + { + "value": "2500", + "type": "Number", + "unit": "Pixel", + "label": "Fade distance", + "description": "Blades are less and less dense up to this distance from the camera, and hidden after.", + "group": "Rendering", + "name": "FadeDistance" + } + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Blades", + "children": [ + { + "propertyName": "Density" + }, + { + "propertyName": "BladeHeight" + }, + { + "propertyName": "BladeWidth" + }, + { + "propertyName": "BaseColor" + }, + { + "propertyName": "TipColor" + }, + { + "propertyName": "Seed" + } + ] + }, + { + "folderName": "Ground", + "children": [ + { + "propertyName": "GroundLayer" + }, + { + "propertyName": "MaxSlope" + } + ] + }, + { + "folderName": "Wind", + "children": [ + { + "propertyName": "WindStrength" + }, + { + "propertyName": "WindSpeed" + }, + { + "propertyName": "BendingObject" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "FadeDistance" + } + ] + } + ] + }, + "variants": [] + }, + { + "areaMaxX": 2000, + "areaMaxY": 2000, + "areaMaxZ": 200, + "areaMinX": 0, + "areaMinY": 0, + "areaMinZ": 0, + "assetStoreTag": "3d scattered models", + "defaultName": "ScatteredModels", + "description": "Many copies of a 3D model (trees, rocks, bushes, flowers...) placed in the area of the object and drawn all at once, which is much faster than an object for each one. They stand on the highest 3D terrain at each point, or at the Z position of the object where there is no terrain. Paint where they grow with the brushes of the scene editor.", + "fullName": "Scattered models (forests, rocks...)", + "helpPath": "", + "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGQ9Ik0xMCAyMXYtM0gzbDUtNUg1bDUtNUg3bDUtNSA1IDVoLTNsNSA1aC0zbDUgNWgtN3YzeiIvPjwvc3ZnPg==", + "is3D": true, + "isUsingLegacyInstancesRenderer": true, + "name": "ScatteredModels", + "previewIconUrl": "", + "objects": [ + { + "assetStoreId": "", + "name": "AreaPlaceholder", + "type": "Scene3D::Cube3DObject", + "variables": [], + "effects": [], + "behaviors": [], + "content": { + "width": 2000, + "height": 2000, + "depth": 200, + "enableTextureTransparency": false, + "facesOrientation": "Y", + "backFaceUpThroughWhichAxisRotation": "X", + "materialType": "Basic", + "tint": "255;255;255", + "frontFaceResourceName": "", + "frontFaceVisible": false, + "frontFaceResourceRepeat": false, + "backFaceResourceName": "", + "backFaceVisible": false, + "backFaceResourceRepeat": false, + "leftFaceResourceName": "", + "leftFaceVisible": false, + "leftFaceResourceRepeat": false, + "rightFaceResourceName": "", + "rightFaceVisible": false, + "rightFaceResourceRepeat": false, + "topFaceResourceName": "", + "topFaceVisible": false, + "topFaceResourceRepeat": false, + "bottomFaceResourceName": "", + "bottomFaceVisible": false, + "bottomFaceResourceRepeat": false + } + } + ], + "objectsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "objectName": "AreaPlaceholder" + } + ] + }, + "objectsGroups": [], + "layers": [ + { + "ambientLightColorB": 200, + "ambientLightColorG": 200, + "ambientLightColorR": 200, + "camera2DPlaneMaxDrawingDistance": 5000, + "camera3DFarPlaneDistance": 10000, + "camera3DFieldOfView": 45, + "camera3DNearPlaneDistance": 3, + "cameraType": "", + "followBaseLayerCamera": false, + "isLightingLayer": false, + "isLocked": false, + "name": "", + "renderingType": "", + "visibility": true, + "cameras": [ + { + "defaultSize": true, + "defaultViewport": true, + "height": 0, + "viewportBottom": 1, + "viewportLeft": 0, + "viewportRight": 1, + "viewportTop": 0, + "width": 0 + } + ], + "effects": [] + } + ], + "instances": [ + { + "angle": 0, + "customSize": true, + "depth": 200, + "height": 2000, + "keepRatio": true, + "layer": "", + "name": "AreaPlaceholder", + "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-002000002000", + "width": 2000, + "x": 0, + "y": 0, + "zOrder": 1, + "numberProperties": [], + "stringProperties": [], + "initialVariables": [] + } + ], + "editionSettings": {}, + "eventsFunctions": [ + { + "fullName": "", + "functionType": "Action", + "name": "onCreated", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Terrain3D::DefineHelperClasses" + }, + "parameters": [ + "", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Terrain3D::DefineScatterHelperClasses" + }, + "parameters": [ + "", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "for (const child of object.getChildrenContainer().getAdhocListOfAllInstances()) {", + " child.deleteFromScene();", + "}", + "if (object.__terrainScatter) object.__terrainScatter.dispose();", + "object.__terrainScatter = new gdjs.__terrainScatterExtension.Scatter(object);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "onDestroy", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrainScatter.dispose();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "doStepPostEvents", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const game = runtimeScene.getGame();", + "const isInGameEdition = game.isInGameEdition && game.isInGameEdition();", + "const elapsedTime = isInGameEdition ? 1 / 60 : object.getElapsedTime() / 1000;", + "const layer = object.getInstanceContainer().getLayer(object.getLayer());", + "object.__terrainScatter.update(elapsedTime, layer.getRenderer().getThreeCamera());" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "onHotReloading", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrainScatter.onHotReloading();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "description": "Paint the density of copies around a position, up to the density of the object (copies removed with \"Erase copies\" come back). The effect fades out up to the radius.", + "fullName": "Add copies", + "functionType": "Action", + "group": "Paint", + "name": "AddCopies", + "sentence": "Add copies of _PARAM0_ around _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "object.__terrainScatter.addCopies(x, y, radius);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "name": "Radius", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Remove the copies standing in a circle, for example a tree cut by the player (use the position of the nearest copy and a small radius) or a forest cleared for a building. They come back when the scene is restarted.", + "fullName": "Erase copies", + "functionType": "Action", + "group": "Paint", + "name": "EraseCopies", + "sentence": "Erase the copies of _PARAM0_ in a circle at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "object.__terrainScatter.removeCopies(x, y, radius);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "name": "Radius", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "the number of copies placed in the area.", + "fullName": "Number of copies", + "functionType": "ExpressionAndCondition", + "group": "Copies", + "name": "CopyCount", + "sentence": "the number of copies", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "eventsFunctionContext.returnValue = object.__terrainScatter.getCopyCount();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "description": "The X position of the base of the copy nearest to a position (the position itself if there is no copy).", + "fullName": "X position of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyX", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? copy.x : x;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "The Y position of the base of the copy nearest to a position (the position itself if there is no copy).", + "fullName": "Y position of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyY", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? copy.y : y;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "The Z position of the base of the copy nearest to a position (on the ground).", + "fullName": "Z position of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyZ", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? copy.z : object.getZ();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "the distance (on X and Y) from a position to the base of the nearest copy. It's a very big number when there is no copy.", + "fullName": "Distance to the nearest copy", + "functionType": "ExpressionAndCondition", + "group": "Copies", + "name": "NearestCopyDistance", + "sentence": "the distance from _PARAM1_; _PARAM2_ to the nearest copy", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? Math.hypot(copy.x - x, copy.y - y) : Number.MAX_VALUE;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + } + ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + }, + { + "folderName": "Paint", + "children": [ + { + "functionName": "AddCopies" + }, + { + "functionName": "EraseCopies" + } + ] + }, + { + "folderName": "Copies", + "children": [ + { + "functionName": "CopyCount" + }, + { + "functionName": "NearestCopyX" + }, + { + "functionName": "NearestCopyY" + }, + { + "functionName": "NearestCopyZ" + }, + { + "functionName": "NearestCopyDistance" + } + ] + } + ] + }, + "propertyDescriptors": [ + { + "value": "", + "type": "Resource", + "label": "3D model", + "description": "The model copied everywhere (a .glb file). Models standing on the Y axis, like most models of the asset store, are stood up along Z.", + "group": "Model", + "extraInformation": [ + "model3D" + ], + "choices": [], + "name": "Model" + }, + { + "value": "300", + "type": "Number", + "unit": "Pixel", + "label": "Height of a copy", + "description": "Copies keep the proportions of the model.", + "group": "Model", + "name": "Size" + }, + { + "value": "0.8", + "type": "Number", + "label": "Smallest random scale", + "description": "Each copy is scaled at random between the smallest and the largest scale.", + "group": "Model", + "name": "MinScale" + }, + { + "value": "1.2", + "type": "Number", + "label": "Largest random scale", + "group": "Model", + "name": "MaxScale" + }, + { + "value": "true", + "type": "Boolean", + "label": "Random rotation", + "description": "Turn each copy around the Z axis at random.", + "group": "Model", + "name": "RandomRotation" + }, + { + "value": "255;255;255", + "type": "Color", + "label": "Random tint", + "description": "Each copy is tinted between white and this color, at random. White for no tint.", + "group": "Model", + "name": "RandomTint" + }, + { + "value": "0.3", + "type": "Number", + "label": "Density", + "description": "The number of copies in a square of 100 x 100 pixels where the density is fully painted (0.3 is about one copy every 180 pixels). Copies never overlap their neighbors much: they are placed at random in a grid.", + "group": "Placement", + "name": "Density" + }, + { + "value": "1", + "type": "Number", + "label": "Seed", + "description": "Each seed places copies differently.", + "group": "Placement", + "name": "Seed" + }, + { + "value": "Everywhere", + "type": "Choice", + "label": "Copies grow", + "description": "Everywhere in the area, or only where they are painted with the \"Add copies\" brush (or the edits).", + "group": "Placement", + "choices": [ + { + "label": "Everywhere (erase them with the brush)", + "value": "Everywhere" + }, + { + "label": "Only where painted", + "value": "Painted" + } + ], + "name": "Coverage" + }, + { + "value": "1", + "type": "Choice", + "label": "Grows on terrain layer", + "description": "On a 3D terrain, copies only grow where this layer is painted, away from its borders (so that trees don't stand on roads painted with another layer). A new terrain has layer 1 painted everywhere.", + "group": "Ground", + "choices": [ + { + "label": "Any", + "value": "0" + }, + { + "label": "Layer 1", + "value": "1" + }, + { + "label": "Layer 2", + "value": "2" + }, + { + "label": "Layer 3", + "value": "3" + }, + { + "label": "Layer 4", + "value": "4" + } + ], + "name": "GroundLayer" + }, + { + "value": "30", + "type": "Number", + "unit": "DegreeAngle", + "label": "Maximum slope", + "description": "No copies grow on steeper ground.", + "group": "Ground", + "name": "MaxSlope" + }, + { + "value": "-100000", + "type": "Number", + "unit": "Pixel", + "label": "Minimum ground Z", + "description": "No copies grow where the ground is lower (for example, under the water level).", + "group": "Ground", + "name": "MinGroundZ" + }, + { + "value": "100000", + "type": "Number", + "unit": "Pixel", + "label": "Maximum ground Z", + "description": "No copies grow where the ground is higher (for example, above the tree line).", + "group": "Ground", + "name": "MaxGroundZ" + }, + { + "value": "false", + "type": "Boolean", + "label": "Trunk collisions", + "description": "Give each copy a static collision shape around its trunk, for the 3D physics behavior (add it to this object, with the \"Static\" type) and navigation meshes. Without them, these behaviors have no effect on this object.", + "group": "Collisions", + "name": "TrunkCollisions" + }, + { + "value": "30", + "type": "Number", + "unit": "Pixel", + "label": "Trunk diameter", + "description": "The diameter of the collision shape of a copy at the height given in \"Height of a copy\" (it is scaled with the copy).", + "group": "Collisions", + "name": "TrunkDiameter" + }, + { + "value": "8000", + "type": "Number", + "unit": "Pixel", + "label": "Draw distance", + "description": "Copies shrink and are hidden at this distance from the camera.", + "group": "Rendering", + "name": "DrawDistance" + }, + { + "value": "true", + "type": "Boolean", + "label": "Cast shadows", + "group": "Rendering", + "name": "CastShadow" + }, + { + "value": "true", + "type": "Boolean", + "label": "Receive shadows", + "group": "Rendering", + "name": "ReceiveShadow" + }, + { + "value": "", + "type": "MultilineString", + "label": "Edits", + "description": "Where copies are added or erased, applied in order, as a JSON list, for example [{\"tool\": \"add\", \"x\": 0.3, \"y\": 0.4, \"radius\": 0.15}, {\"tool\": \"erase\", \"x\": 0.1, \"y\": 0.9, \"toX\": 0.8, \"toY\": 0.2, \"radius\": 0.02}]. Tools: add (density: from 0 to 1, 1 by default, how dense copies are compared to the Density property) and erase. strength (from 0 to 1) makes a stroke lighter. x, y and radius are fractions (0 to 1) of the object size (x is (scene X - object X) / object width). Add toX and toY to paint along a line (a row of trees, a path through a forest). With \"Copies grow: only where painted\", only the added areas have copies. Shown in the scene editor.", + "group": "Placement", + "advanced": true, + "name": "Edits" + }, + { + "value": "", + "type": "String", + "label": "Painted data", + "description": "Written by the brushes of the scene editor, in a compact format not meant to be read or written by hand: use Edits instead. Clear it to go back to the coverage and the edits.", + "advanced": true, + "name": "PaintData" + } + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Model", + "children": [ + { + "propertyName": "Model" + }, + { + "propertyName": "Size" + }, + { + "propertyName": "MinScale" + }, + { + "propertyName": "MaxScale" + }, + { + "propertyName": "RandomRotation" + }, + { + "propertyName": "RandomTint" + } + ] + }, + { + "folderName": "Placement", + "children": [ + { + "propertyName": "Density" + }, + { + "propertyName": "Seed" + }, + { + "propertyName": "Coverage" + }, + { + "propertyName": "Edits" + } + ] + }, + { + "folderName": "Ground", + "children": [ + { + "propertyName": "GroundLayer" + }, + { + "propertyName": "MaxSlope" + }, + { + "propertyName": "MinGroundZ" + }, + { + "propertyName": "MaxGroundZ" + } + ] + }, + { + "folderName": "Collisions", + "children": [ + { + "propertyName": "TrunkCollisions" + }, + { + "propertyName": "TrunkDiameter" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "DrawDistance" + }, + { + "propertyName": "CastShadow" + }, + { + "propertyName": "ReceiveShadow" + } + ] + }, + { + "propertyName": "PaintData" + } + ] + }, + "variants": [] + } + ] } ], "externalLayouts": [] diff --git a/examples/starting-3d-rts-unit-selection/assets/StartingWall.png b/examples/starting-3d-rts-unit-selection/assets/StartingWall.png index b3a097887..06278d6e5 100644 Binary files a/examples/starting-3d-rts-unit-selection/assets/StartingWall.png and b/examples/starting-3d-rts-unit-selection/assets/StartingWall.png differ diff --git a/examples/starting-3d-tank/assets/StartingWall.png b/examples/starting-3d-tank/assets/StartingWall.png index b3a097887..06278d6e5 100644 Binary files a/examples/starting-3d-tank/assets/StartingWall.png and b/examples/starting-3d-tank/assets/StartingWall.png differ diff --git a/examples/starting-3d-tank/starting-3d-tank.json b/examples/starting-3d-tank/starting-3d-tank.json index 1fb051431..819928141 100644 --- a/examples/starting-3d-tank/starting-3d-tank.json +++ b/examples/starting-3d-tank/starting-3d-tank.json @@ -1,7 +1,7 @@ { "firstLayout": "", "gdVersion": { - "build": 283, + "build": 284, "major": 5, "minor": 6, "revision": 0 @@ -255,6 +255,22 @@ "name": "assets/StartingWall.png", "smoothed": true, "userAdded": false + }, + { + "file": "assets/StartingGround.png", + "kind": "image", + "metadata": "", + "name": "assets/StartingGround.png", + "smoothed": true, + "userAdded": false + }, + { + "file": "assets/StartingCheckeredPattern.png", + "kind": "image", + "metadata": "", + "name": "assets/StartingCheckeredPattern.png", + "smoothed": true, + "userAdded": false } ] }, @@ -271,12 +287,13 @@ "mangledName": "Game_32Scene", "name": "Game Scene", "r": 255, + "renderer3DWorldScale": 100, "standardSortMethod": true, "stopSoundsOnStartup": true, "title": "", "v": 255, "uiSettings": { - "grid": false, + "grid": true, "gridType": "rectangular", "gridWidth": 64, "gridHeight": 64, @@ -286,8 +303,8 @@ "gridOffsetZ": 0, "gridColor": 10401023, "gridAlpha": 0.8, - "snap": false, - "zoomFactor": 0.5290357402157136, + "snap": true, + "zoomFactor": 0.2233600589381265, "windowMask": false, "selectedLayer": "", "gameEditorMode": "embedded-game" @@ -296,9 +313,6 @@ { "name": "BulletObstacles", "objects": [ - { - "name": "Grass_3D" - }, { "name": "Road_3D" }, @@ -307,66 +321,15 @@ }, { "name": "Walls_3D" + }, + { + "name": "Terrain" } ] } ], "variables": [], "instances": [ - { - "angle": 0, - "customSize": true, - "depth": 10, - "height": 704, - "layer": "", - "locked": true, - "name": "Road_3D", - "persistentUuid": "46a0d799-55c4-4f44-b430-120b5b0bc11e", - "width": 4736, - "x": -256, - "y": 0, - "z": -10, - "zOrder": 1, - "numberProperties": [], - "stringProperties": [], - "initialVariables": [] - }, - { - "angle": 0, - "customSize": true, - "depth": 10, - "height": 512, - "layer": "", - "locked": true, - "name": "Grass_3D", - "persistentUuid": "0ded717a-a950-4abf-8393-0a554a43b2f2", - "width": 4736, - "x": -256, - "y": -512, - "z": -10, - "zOrder": 7, - "numberProperties": [], - "stringProperties": [], - "initialVariables": [] - }, - { - "angle": 0, - "customSize": true, - "depth": 10, - "height": 512, - "layer": "", - "locked": true, - "name": "Grass_3D", - "persistentUuid": "f9ea97fc-7ed3-421a-8559-e7f829e15f39", - "width": 4736, - "x": -256, - "y": 704, - "z": -10, - "zOrder": 7, - "numberProperties": [], - "stringProperties": [], - "initialVariables": [] - }, { "angle": 0, "customSize": false, @@ -421,55 +384,16 @@ { "angle": 0, "customSize": true, - "depth": 32, - "height": 320, - "layer": "", - "locked": true, - "name": "Grass_3D", - "persistentUuid": "2093989a-3c96-4db2-97fa-5d1ae3d50bb1", - "rotationY": -10, - "width": 128, - "x": 2050, - "y": 704, - "z": -21, - "zOrder": 13, - "numberProperties": [], - "stringProperties": [], - "initialVariables": [] - }, - { - "angle": 0, - "customSize": true, - "depth": 32, - "height": 320, - "layer": "", - "locked": true, - "name": "Grass_3D", - "persistentUuid": "082a703d-777a-4997-8c8a-1f33dfbb4317", - "rotationY": -10, - "width": 128, - "x": 2050, - "y": -320, - "z": -21, - "zOrder": 13, - "numberProperties": [], - "stringProperties": [], - "initialVariables": [] - }, - { - "angle": 0, - "customSize": true, - "depth": 125, + "depth": 192, "height": 704, "layer": "", - "locked": true, "name": "Road_3D", "persistentUuid": "34198b97-6c2f-49b6-bcb9-e3528843a310", - "rotationY": -8, - "width": 1040, - "x": 1359, + "rotationY": 352, + "width": 1315, + "x": 1280, "y": 0, - "z": -57, + "z": -101, "zOrder": 1, "numberProperties": [], "stringProperties": [], @@ -478,60 +402,21 @@ { "angle": 0, "customSize": true, - "depth": 125, + "depth": 192, "height": 702, "layer": "", - "locked": true, "name": "Road_3D", "persistentUuid": "4554397a-68bb-496a-ae71-eeeed93ded8b", "rotationY": 8, - "width": 1040, - "x": 2373, + "width": 1315, + "x": 2556, "y": 1, - "z": -57, + "z": -101, "zOrder": 1, "numberProperties": [], "stringProperties": [], "initialVariables": [] }, - { - "angle": 0, - "customSize": true, - "depth": 32, - "height": 320, - "layer": "", - "locked": true, - "name": "Grass_3D", - "persistentUuid": "9c1661e3-b9ba-4f6d-a633-a9363880a881", - "rotationY": 10, - "width": 128, - "x": 2562, - "y": -320, - "z": -21, - "zOrder": 13, - "numberProperties": [], - "stringProperties": [], - "initialVariables": [] - }, - { - "angle": 0, - "customSize": true, - "depth": 32, - "height": 320, - "layer": "", - "locked": true, - "name": "Grass_3D", - "persistentUuid": "ad409562-7f5a-4f4a-95f5-e90c6e7932ee", - "rotationY": 10, - "width": 128, - "x": 2562, - "y": 704, - "z": -21, - "zOrder": 13, - "numberProperties": [], - "stringProperties": [], - "initialVariables": [] - }, { "angle": 0, "customSize": false, @@ -679,6 +564,22 @@ "numberProperties": [], "stringProperties": [], "initialVariables": [] + }, + { + "angle": 0, + "customSize": true, + "height": 1728, + "layer": "", + "locked": true, + "name": "Terrain", + "persistentUuid": "0a5f3e72-1ea0-4cda-a110-890f866877ec", + "width": 4736, + "x": -256, + "y": -512, + "zOrder": 0, + "numberProperties": [], + "stringProperties": [], + "initialVariables": [] } ], "objects": [ @@ -704,7 +605,7 @@ "shapeDimensionB": 65, "shapeDimensionC": 25, "density": 1, - "friction": 10, + "friction": 5, "restitution": 0.2, "linearDamping": 0.1, "angularDamping": 0.1, @@ -870,11 +771,97 @@ "tileScale": 0.2 } }, + { + "assetStoreId": "e71bd69f896d6c7531b48c65ceb5da25071d4fbdeb518aeceecba8d21f34ed8d", + "name": "Steer_Joystick", + "persistentUuid": "bf4f4377-e66f-47ac-a554-7c6b3210d5f5", + "type": "SpriteMultitouchJoystick::SpriteMultitouchJoystick", + "variant": "", + "variables": [], + "effects": [], + "behaviors": [], + "content": { + "JoystickIdentifier": "Secondary", + "DeadZoneRadius": 0.3 + }, + "childrenContent": { + "Border": { + "adaptCollisionMaskAutomatically": false, + "updateIfNotVisible": false, + "animations": [ + { + "name": "Idle", + "useMultipleDirections": false, + "directions": [ + { + "looping": false, + "timeBetweenFrames": 0.08, + "sprites": [ + { + "hasCustomCollisionMask": false, + "image": "assets\\Flat dark joystick border.png", + "points": [], + "originPoint": { + "name": "origine", + "x": 0, + "y": 0 + }, + "centerPoint": { + "automatic": true, + "name": "centre", + "x": 0, + "y": 0 + }, + "customCollisionMask": [] + } + ] + } + ] + } + ] + }, + "Thumb": { + "adaptCollisionMaskAutomatically": false, + "updateIfNotVisible": false, + "animations": [ + { + "name": "Idle", + "useMultipleDirections": false, + "directions": [ + { + "looping": false, + "timeBetweenFrames": 0.08, + "sprites": [ + { + "hasCustomCollisionMask": false, + "image": "assets\\Flat dark joystick thumb.png", + "points": [], + "originPoint": { + "name": "origine", + "x": 0, + "y": 0 + }, + "centerPoint": { + "automatic": true, + "name": "centre", + "x": 0, + "y": 0 + }, + "customCollisionMask": [] + } + ] + } + ] + } + ] + } + } + }, { "assetStoreId": "", - "name": "Grass_3D", - "persistentUuid": "2c14579d-7de1-45fb-8143-1c3d91b82221", - "type": "Scene3D::Cube3DObject", + "name": "Target", + "persistentUuid": "23bcbae1-680c-4be5-bb17-8f4432e0947e", + "type": "Scene3D::Model3DObject", "variables": [], "effects": [], "behaviors": [ @@ -882,67 +869,53 @@ "name": "Physics3D", "type": "Physics3D::Physics3DBehavior", "object3D": "Object3D", - "bodyType": "Static", + "bodyType": "Dynamic", "bullet": false, "fixedRotation": false, - "shape": "Box", + "shape": "Capsule", "shapeOrientation": "Z", "shapeDimensionA": 0, "shapeDimensionB": 0, "shapeDimensionC": 0, - "density": 1, - "friction": 5, - "restitution": 0.1, - "linearDamping": 0.1, - "angularDamping": 0.1, - "gravityScale": 1, - "layers": 17, - "masks": 17, "shapeOffsetX": 0, "shapeOffsetY": 0, "shapeOffsetZ": 0, "massCenterOffsetX": 0, "massCenterOffsetY": 0, - "massCenterOffsetZ": 0, - "massOverride": 0 + "massCenterOffsetZ": -35, + "massOverride": 0, + "density": 1, + "friction": 0.75, + "restitution": 0.5, + "linearDamping": 0.5, + "angularDamping": 0.1, + "gravityScale": 2, + "layers": 17, + "masks": 17 } ], "content": { - "width": 128, - "height": 128, - "depth": 128, - "enableTextureTransparency": false, - "facesOrientation": "Y", - "frontFaceResourceName": "assets\\StartingGround.png", - "backFaceResourceName": "assets\\StartingGround.png", - "backFaceUpThroughWhichAxisRotation": "X", - "leftFaceResourceName": "assets\\StartingGround.png", - "rightFaceResourceName": "assets\\StartingGround.png", - "topFaceResourceName": "assets\\StartingGround.png", - "bottomFaceResourceName": "assets\\StartingGround.png", - "frontFaceVisible": true, - "backFaceVisible": true, - "leftFaceVisible": true, - "rightFaceVisible": true, - "topFaceVisible": true, - "bottomFaceVisible": true, - "frontFaceResourceRepeat": true, - "backFaceResourceRepeat": true, - "leftFaceResourceRepeat": true, - "rightFaceResourceRepeat": true, - "topFaceResourceRepeat": true, - "bottomFaceResourceRepeat": true, - "materialType": "StandardWithoutMetalness", - "tint": "255;255;255", + "centerLocation": "ObjectCenter", + "crossfadeDuration": 0.10000000149011612, + "depth": 117.33, + "height": 54.15, "isCastingShadow": true, "isReceivingShadow": true, - "tileScale": 0.2 + "keepAspectRatio": true, + "materialType": "StandardWithoutMetalness", + "modelResourceName": "assets\\starting_unit_red.glb", + "originLocation": "BottomCenterZ", + "rotationX": 0, + "rotationY": 0, + "rotationZ": 0, + "width": 54.15, + "animations": [] } }, { "assetStoreId": "e71bd69f896d6c7531b48c65ceb5da25071d4fbdeb518aeceecba8d21f34ed8d", - "name": "Steer_Joystick", - "persistentUuid": "bf4f4377-e66f-47ac-a554-7c6b3210d5f5", + "name": "Cannon_Joystick", + "persistentUuid": "afc29e44-b269-4af9-86d2-80190493dedb", "type": "SpriteMultitouchJoystick::SpriteMultitouchJoystick", "variant": "", "variables": [], @@ -950,148 +923,8 @@ "behaviors": [], "content": { "JoystickIdentifier": "Secondary", - "DeadZoneRadius": 0.3 - }, - "childrenContent": { - "Border": { - "adaptCollisionMaskAutomatically": false, - "updateIfNotVisible": false, - "animations": [ - { - "name": "Idle", - "useMultipleDirections": false, - "directions": [ - { - "looping": false, - "timeBetweenFrames": 0.08, - "sprites": [ - { - "hasCustomCollisionMask": false, - "image": "assets\\Flat dark joystick border.png", - "points": [], - "originPoint": { - "name": "origine", - "x": 0, - "y": 0 - }, - "centerPoint": { - "automatic": true, - "name": "centre", - "x": 0, - "y": 0 - }, - "customCollisionMask": [] - } - ] - } - ] - } - ] - }, - "Thumb": { - "adaptCollisionMaskAutomatically": false, - "updateIfNotVisible": false, - "animations": [ - { - "name": "Idle", - "useMultipleDirections": false, - "directions": [ - { - "looping": false, - "timeBetweenFrames": 0.08, - "sprites": [ - { - "hasCustomCollisionMask": false, - "image": "assets\\Flat dark joystick thumb.png", - "points": [], - "originPoint": { - "name": "origine", - "x": 0, - "y": 0 - }, - "centerPoint": { - "automatic": true, - "name": "centre", - "x": 0, - "y": 0 - }, - "customCollisionMask": [] - } - ] - } - ] - } - ] - } - } - }, - { - "assetStoreId": "", - "name": "Target", - "type": "Scene3D::Model3DObject", - "variables": [], - "effects": [], - "behaviors": [ - { - "name": "Physics3D", - "type": "Physics3D::Physics3DBehavior", - "object3D": "Object3D", - "bodyType": "Dynamic", - "bullet": false, - "fixedRotation": false, - "shape": "Capsule", - "shapeOrientation": "Z", - "shapeDimensionA": 0, - "shapeDimensionB": 0, - "shapeDimensionC": 0, - "shapeOffsetX": 0, - "shapeOffsetY": 0, - "shapeOffsetZ": 0, - "massCenterOffsetX": 0, - "massCenterOffsetY": 0, - "massCenterOffsetZ": -35, - "massOverride": 0, - "density": 1, - "friction": 0.75, - "restitution": 0.5, - "linearDamping": 0.5, - "angularDamping": 0.1, - "gravityScale": 2, - "layers": 17, - "masks": 17 - } - ], - "content": { - "centerLocation": "ObjectCenter", - "crossfadeDuration": 0.10000000149011612, - "depth": 117.33, - "height": 54.15, - "isCastingShadow": true, - "isReceivingShadow": true, - "keepAspectRatio": true, - "materialType": "StandardWithoutMetalness", - "modelResourceName": "assets\\starting_unit_red.glb", - "originLocation": "BottomCenterZ", - "rotationX": 0, - "rotationY": 0, - "rotationZ": 0, - "width": 54.15, - "animations": [] - } - }, - { - "assetStoreId": "e71bd69f896d6c7531b48c65ceb5da25071d4fbdeb518aeceecba8d21f34ed8d", - "name": "Cannon_Joystick", - "persistentUuid": "afc29e44-b269-4af9-86d2-80190493dedb", - "type": "SpriteMultitouchJoystick::SpriteMultitouchJoystick", - "variant": "", - "variables": [], - "effects": [], - "behaviors": [], - "content": { - "JoystickIdentifier": "Secondary", - "DeadZoneRadius": 0.3, - "ControllerIdentifier": 3 + "DeadZoneRadius": 0.3, + "ControllerIdentifier": 3 }, "childrenContent": { "Border": { @@ -1234,6 +1067,7 @@ { "assetStoreId": "", "name": "Bullet", + "persistentUuid": "2e74a857-b37f-4c49-8061-0f5626379a58", "type": "Scene3D::Model3DObject", "variables": [], "effects": [], @@ -1288,6 +1122,7 @@ { "assetStoreId": "8bfb3b8d354233214690205fb41cba03284ca34141297a0d98e09a2f080c029c", "name": "BulletParticle", + "persistentUuid": "55803206-e659-42c8-ab23-fc4711592b33", "type": "ParticleEmitter3D::ParticleEmitter3D", "variant": "", "variables": [], @@ -1506,6 +1341,60 @@ "isReceivingShadow": true, "tileScale": 0.2 } + }, + { + "assetStoreId": "", + "name": "Terrain", + "persistentUuid": "52ba0676-9bd0-4f26-b6ae-6b4379f5b3ec", + "type": "Terrain3D::Terrain3D", + "variant": "", + "variables": [], + "effects": [], + "behaviors": [ + { + "name": "Physics3D", + "type": "Physics3D::Physics3DBehavior", + "object3D": "Object3D", + "bodyType": "Static", + "bullet": false, + "fixedRotation": false, + "shape": "Box", + "meshShapeResourceName": "", + "shapeOrientation": "Z", + "shapeDimensionA": 0, + "shapeDimensionB": 0, + "shapeDimensionC": 0, + "shapeOffsetX": 0, + "shapeOffsetY": 0, + "shapeOffsetZ": 0, + "massCenterOffsetX": 0, + "massCenterOffsetY": 0, + "massCenterOffsetZ": 0, + "massOverride": 0, + "density": 1, + "friction": 7, + "restitution": 0.1, + "linearDamping": 0.1, + "angularDamping": 0.1, + "gravityScale": 1, + "layers": 17, + "masks": 17 + } + ], + "content": { + "SculptData": "2;256;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", + "Layer1Texture": "assets/StartingGround.png", + "Layer1Color": "255;255;255", + "HeightBlending": 0.5, + "TextureVariation": 0, + "TextureSize": 128, + "CliffLayer": "3", + "Layer2Texture": "assets/StartingCheckeredPattern.png", + "Layer3Texture": "", + "CliffAngle": 45, + "CastShadow": false, + "Layer2Color": "255;255;255" + } } ], "objectsFolderStructure": { @@ -1518,7 +1407,7 @@ "objectName": "Walls_3D" }, { - "objectName": "Grass_3D" + "objectName": "Terrain" }, { "objectName": "PlayerTank" @@ -3762,7 +3651,6 @@ "name": "ThirdPersonCamera", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -5706,7 +5594,6 @@ "unit": "Second", "label": "Halfway time for rotation", "group": "Catch-up speed", - "quickCustomizationVisibility": "hidden", "name": "RotationHalfwayDuration" }, { @@ -5715,7 +5602,6 @@ "unit": "Second", "label": "Halfway time for elevation rotation", "group": "Catch-up speed", - "quickCustomizationVisibility": "hidden", "name": "ElevationHalfwayDuration" }, { @@ -5724,7 +5610,6 @@ "unit": "Second", "label": "Halfway time on Z axis", "group": "Catch-up speed", - "quickCustomizationVisibility": "hidden", "name": "TranslationZHalfwayDuration" }, { @@ -5782,7 +5667,6 @@ "label": "Follow free area top border on Z axis", "group": "Position", "advanced": true, - "quickCustomizationVisibility": "hidden", "name": "FollowFreeAreaZMax" }, { @@ -5792,7 +5676,6 @@ "label": "Follow free area bottom border on Z axis", "group": "Position", "advanced": true, - "quickCustomizationVisibility": "hidden", "name": "FollowFreeAreaZMin" }, { @@ -5842,14 +5725,12 @@ "value": "true", "type": "Boolean", "label": "Automatically rotate the camera with the object", - "quickCustomizationVisibility": "hidden", "name": "IsRotatingWithObject" }, { "value": "false", "type": "Boolean", "label": "Automatically rotate the camera with the object (elevation)", - "quickCustomizationVisibility": "hidden", "name": "IsElevatingWithObject" }, { @@ -9930,7 +9811,6 @@ "name": "PlatformerMultitouchMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -10184,7 +10064,6 @@ "name": "Platformer3DMultitouchMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -10359,7 +10238,6 @@ "name": "Shooter3DMultitouchMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -10523,7 +10401,6 @@ "name": "FirstPersonMultitouchMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -11728,7 +11605,6 @@ "name": "PhysicsCar3DMultitouchMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -11941,7 +11817,6 @@ "name": "TopDownMultitouchMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -14020,7 +13895,6 @@ "name": "PhysicsCar3DKeyboardMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -36381,6 +36255,7802 @@ } ], "eventsBasedObjects": [] + }, + { + "author": "", + "category": "General", + "dimension": "3D", + "extensionNamespace": "", + "fullName": "3D terrain", + "gdevelopVersion": ">=5.5.222", + "helpPath": "", + "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLXRlcnJhaW4iIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTQsNkwxMC4yNSwxMUwxMy4xLDE0LjhMMTEuNSwxNkM5LjgxLDEzLjc1IDcsMTAgNywxMEwxLDE4SDIzTDE0LDZaIiAvPjwvc3ZnPg==", + "name": "Terrain3D", + "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", + "shortDescription": "A ground with hills and valleys that can be sculpted and painted in the editor, with collisions.", + "version": "0.1.0", + "description": [ + "A 3D terrain object for open worlds, adventure or racing games: hills, mountains, islands or valleys, with painted layers of grass, dirt, rock or sand.", + "", + "- **Sculpt and paint in the scene editor**: select a terrain to show the tools (raise, lower, smooth, flatten and paint 4 layers).", + "- **Start from a relief** (hills, mountains, island) or **from a heightmap image**.", + "- **Steep slopes automatically show rock** (or any layer).", + "- **Natural borders between layers**: stones and tufts of a texture cover the gaps of the other where layers meet (height blending), and the repetition of textures is made less visible (texture variation).", + "- **Physics**: add the **3D physics** behavior to the terrain. Characters and objects collide with the ground exactly. The terrain is always static.", + "- **Pathfinding**: add the **floor/obstacle for pathfinding (navmesh based)** behavior to the terrain, so that characters with the navmesh pathfinding behavior walk on its hills and around its cliffs (in recent GDevelop versions).", + "- **Shape it with the Edits property**: a list of raise, flatten, smooth and paint strokes written in JSON, shown in the scene editor. It's readable and can be changed by hand or by an AI agent, like the relief, the heightmap and the other properties. Actions only change the terrain during the game.", + "- **Change it during the game** with actions (raise, lower, flatten, smooth, paint, along a line for paths and rivers) and read the ground height, slope or layer with expressions.", + "- **Grass**: add a **terrain grass** object over the terrain. Thousands of blades sway in the wind, grow where a layer (like grass) is painted and where the ground is not too steep, and are pushed aside by characters. Sculpt or paint the terrain and the grass follows.", + "- **Forests, rocks and bushes**: add a **scattered models** object over the terrain. It places hundreds or thousands of copies of a 3D model (trees, rocks, bushes, flowers...) on the ground and draws them all at once, which is much faster than an object for each. Copies grow where a layer is painted, away from steep slopes, and follow the ground when it's sculpted. Add or erase them with the brushes of the scene editor or with the Edits property, give trees trunk collisions, and cut them down with actions. Ready-made forests, rocks and bushes are in the asset store.", + "- **Tunnels and caves**: make holes in the ground (and fill them again) with the brushes, the Edits property or actions, and put a 3D model of the tunnel or the cave (with a mesh collision shape) under the ground. Nothing is drawn in holes, and characters, cars and rays go through them.", + "- **Made for large worlds**: the terrain is split in chunks that are hidden when off-screen and simplified when far away, and collisions are updated only where the ground changes." + ], + "origin": { + "identifier": "Terrain3D", + "name": "gdevelop-extension-store" + }, + "tags": [ + "3d", + "terrain", + "ground", + "heightmap", + "landscape", + "open world", + "grass", + "foliage", + "forest", + "trees", + "tunnel", + "cave" + ], + "authorIds": [], + "dependencies": [], + "globalVariables": [], + "sceneVariables": [], + "eventsFunctions": [ + { + "description": "Define helper classes JavaScript code.", + "fullName": "Define helper classes", + "functionType": "Action", + "name": "DefineHelperClasses", + "private": true, + "sentence": "Define helper classes JavaScript code", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "if (gdjs.__terrain3DExtension) {", + " return;", + "}", + "", + "// Tested by the gameplay tests of the \"3D island terrain\" example (GDevelop-examples repository).", + "", + "const groundsByScene = new WeakMap();", + "/**", + " * The grounds of a scene (the terrains), followed by the terrain grass.", + " *", + " * A ground has: `getVersion()` (changing when the ground changes),", + " * `containsPoint(x, y)`, `getHeightAt(x, y)` (Z position of the ground),", + " * `getSlopeAt(x, y)` (in degrees), `getLayerWeightAt(x, y, layer)` (how", + " * much a painted layer, from 1 to 4, is there, from 0 to 1) and optionally", + " * `getTintAt(x, y)` (the painted tint, as red, green and blue from 0 to 255).", + " * @param {gdjs.RuntimeScene} runtimeScene", + " * @returns {Set}", + " */", + "const getGrounds = (runtimeScene) => {", + " let grounds = groundsByScene.get(runtimeScene);", + " if (!grounds) {", + " grounds = new Set();", + " groundsByScene.set(runtimeScene, grounds);", + " }", + " return grounds;", + "};", + "", + "/**", + " * @param {number} seed", + " * @returns {() => number} Random numbers between 0 (included) and 1 (excluded), the same for the same seed.", + " */", + "const createRandom = (seed) => {", + " let state = seed >>> 0 || 1;", + " return () => {", + " state ^= state << 13;", + " state ^= state >>> 17;", + " state ^= state << 5;", + " return (state >>> 0) / 4294967296;", + " };", + "};", + "", + "/**", + " * Find the highest ground at a position: grounds can overlap (like an island over a sea floor).", + " * @param {Iterable} grounds", + " * @param {{ground: object | null, z: number}} result Set to the ground (null if there is none) and its Z position.", + " */", + "const findHighestGround = (grounds, x, y, result) => {", + " result.ground = null;", + " for (const ground of grounds) {", + " if (!ground.containsPoint(x, y)) continue;", + " const z = ground.getHeightAt(x, y);", + " if (!result.ground || z > result.z) {", + " result.ground = ground;", + " result.z = z;", + " }", + " }", + " return result;", + "};", + "", + "// The grounds or the objects placed on them can change every frame (while", + "// sculpting or moving them in the editor): things are placed again at most this often.", + "const PLACEMENT_DELAY = 0.3;", + "", + "/**", + " * Tells when things placed on the grounds of a scene (grass blades, scattered", + " * models) must be placed again: after the grounds or the given properties", + " * changed, at most every `PLACEMENT_DELAY` seconds.", + " */", + "class GroundPlacement {", + " /** @param {gdjs.RuntimeScene} runtimeScene */", + " constructor(runtimeScene) {", + " this.grounds = getGrounds(runtimeScene);", + " this.placedFrom = '';", + " this.timeSincePlacement = 0;", + " }", + "", + " /**", + " * @param {number} elapsedSeconds", + " * @param {string} propertiesKey Changes when the things must be placed again.", + " */", + " shouldPlaceAgain(elapsedSeconds, propertiesKey) {", + " this.timeSincePlacement += elapsedSeconds;", + " if (this.placedFrom && this.timeSincePlacement < PLACEMENT_DELAY) return false;", + " let placementKey = propertiesKey;", + " for (const ground of this.grounds) placementKey += '|' + ground.getVersion();", + " if (placementKey === this.placedFrom) return false;", + " this.placedFrom = placementKey;", + " this.timeSincePlacement = 0;", + " return true;", + " }", + "", + " placeAgain() {", + " this.placedFrom = '';", + " }", + "}", + "", + "/**", + " * Replace parts of the shader code of a Three.js material, with an error if", + " * a part is missing (for example after an update of Three.js).", + " * @param {string} shaderCode", + " * @param {Array<[string, string]>} replacements The searched code and its replacement.", + " * @param {string} materialName", + " */", + "const patchShaderCode = (shaderCode, replacements, materialName) => {", + " for (const [searchedCode, newCode] of replacements) {", + " if (!shaderCode.includes(searchedCode)) {", + " console.error(`${materialName}: \"${searchedCode}\" was not found in the shader, which won't look as expected.`);", + " }", + " shaderCode = shaderCode.replace(searchedCode, newCode);", + " }", + " return shaderCode;", + "};", + "", + "// Size of the object inner area. Instances are scaled from it, so these only", + "// define the default size of a new terrain.", + "const AREA_SIZE = 4096;", + "const AREA_DEPTH = 600;", + "", + "// Terrains have at most 8 x 8 chunks (so at most 64 draw calls), of at least", + "// 32 x 32 cells.", + "const MIN_CHUNK_CELLS = 32;", + "const MAX_CHUNKS_PER_SIDE = 8;", + "/** @param {number} resolution */", + "const getChunkCells = (resolution) => Math.max(MIN_CHUNK_CELLS, resolution / MAX_CHUNKS_PER_SIDE);", + "// Grid steps of each level of detail, and the distance (in chunk sizes)", + "// from which each one is used.", + "const LOD_STEPS = [1, 2, 4];", + "const LOD_DISTANCES = [0, 3, 6];", + "", + "const LAYER_COUNT = 4;", + "const RESOLUTIONS = [64, 128, 256, 512, 1024];", + "const DEFAULT_RESOLUTION = 256;", + "// Each undo step is a copy of the data: fewer are kept for big terrains.", + "const UNDO_STEPS_SAMPLES = 20 * 257 * 257;", + "", + "const clamp01 = (value) => (value < 0 ? 0 : value > 1 ? 1 : value);", + "const clampInteger = (value, min, max) =>", + " value < min ? min : value > max ? max : Math.floor(value);", + "", + "/**", + " * A rectangle of grid samples, inclusive. Empty when `minI > maxI`.", + " */", + "class SampleRectangle {", + " constructor() {", + " this.clear();", + " }", + "", + " clear() {", + " this.minI = Number.MAX_SAFE_INTEGER;", + " this.minJ = Number.MAX_SAFE_INTEGER;", + " this.maxI = -1;", + " this.maxJ = -1;", + " }", + "", + " isEmpty() {", + " return this.minI > this.maxI;", + " }", + "", + " /** @param {SampleRectangle} other */", + " add(other) {", + " if (other.isEmpty()) return;", + " this.minI = Math.min(this.minI, other.minI);", + " this.minJ = Math.min(this.minJ, other.minJ);", + " this.maxI = Math.max(this.maxI, other.maxI);", + " this.maxJ = Math.max(this.maxJ, other.maxJ);", + " }", + "", + " set(minI, minJ, maxI, maxJ) {", + " this.minI = minI;", + " this.minJ = minJ;", + " this.maxI = maxI;", + " this.maxJ = maxJ;", + " return this;", + " }", + "}", + "", + "/**", + " * The heights (between 0 and 1) and the painted layers weights of a square", + " * grid of `size * size` samples.", + " */", + "class TerrainData {", + " /** @param {number} resolution The number of cells on each side. */", + " constructor(resolution) {", + " this.resolution = resolution;", + " this.size = resolution + 1;", + " this.heights = new Float32Array(this.size * this.size);", + " /** 4 weights (0-255) per sample, one for each layer. */", + " this.splat = new Uint8Array(this.size * this.size * LAYER_COUNT);", + " this.fillLayer(0);", + " /**", + " * 4 bytes per sample: a tint multiplying the layers (red, green and", + " * blue, white for none) and the wetness (0 for dry).", + " */", + " this.colors = new Uint8Array(this.size * this.size * 4);", + " this.clearColors();", + " }", + "", + " clearColors() {", + " this.colors.fill(255);", + " for (let index = 3; index < this.colors.length; index += 4) {", + " this.colors[index] = 0;", + " }", + " }", + "", + " /** @param {number} layer */", + " fillLayer(layer) {", + " this.splat.fill(0);", + " for (let index = layer; index < this.splat.length; index += LAYER_COUNT) {", + " this.splat[index] = 255;", + " }", + " }", + "", + " getHeight(i, j) {", + " const last = this.resolution;", + " const clampedI = i < 0 ? 0 : i > last ? last : i;", + " const clampedJ = j < 0 ? 0 : j > last ? last : j;", + " return this.heights[clampedJ * this.size + clampedI];", + " }", + "", + " /**", + " * @param {number} gridX", + " * @param {number} gridY", + " * @returns {number} The height interpolated on the triangles that are drawn.", + " */", + " sampleHeight(gridX, gridY) {", + " const x = Math.min(Math.max(gridX, 0), this.resolution);", + " const y = Math.min(Math.max(gridY, 0), this.resolution);", + " const i = Math.min(Math.floor(x), this.resolution - 1);", + " const j = Math.min(Math.floor(y), this.resolution - 1);", + " const fx = x - i;", + " const fy = y - j;", + " const topRight = this.getHeight(i + 1, j);", + " const bottomLeft = this.getHeight(i, j + 1);", + " if (fx + fy <= 1) {", + " const topLeft = this.getHeight(i, j);", + " return topLeft + fx * (topRight - topLeft) + fy * (bottomLeft - topLeft);", + " }", + " const bottomRight = this.getHeight(i + 1, j + 1);", + " return (", + " bottomRight +", + " (1 - fx) * (bottomLeft - bottomRight) +", + " (1 - fy) * (topRight - bottomRight)", + " );", + " }", + "", + " /** @returns {number} The weight (0-1) of a layer, interpolated between samples. */", + " sampleLayerWeight(gridX, gridY, layer) {", + " const x = Math.min(Math.max(gridX, 0), this.resolution);", + " const y = Math.min(Math.max(gridY, 0), this.resolution);", + " const i = Math.min(Math.floor(x), this.resolution - 1);", + " const j = Math.min(Math.floor(y), this.resolution - 1);", + " const fx = x - i;", + " const fy = y - j;", + " const weight = (i, j) => this.splat[(j * this.size + i) * LAYER_COUNT + layer];", + " const top = weight(i, j) + fx * (weight(i + 1, j) - weight(i, j));", + " const bottom = weight(i, j + 1) + fx * (weight(i + 1, j + 1) - weight(i, j + 1));", + " return (top + fy * (bottom - top)) / 255;", + " }", + "", + " /**", + " * @param {number} channel 0, 1 and 2 for the tint, 3 for the wetness.", + " * @returns {number} The channel (0-1), interpolated between samples.", + " */", + " sampleColor(gridX, gridY, channel) {", + " const x = Math.min(Math.max(gridX, 0), this.resolution);", + " const y = Math.min(Math.max(gridY, 0), this.resolution);", + " const i = Math.min(Math.floor(x), this.resolution - 1);", + " const j = Math.min(Math.floor(y), this.resolution - 1);", + " const fx = x - i;", + " const fy = y - j;", + " const value = (i, j) => this.colors[(j * this.size + i) * 4 + channel];", + " const top = value(i, j) + fx * (value(i + 1, j) - value(i, j));", + " const bottom = value(i, j + 1) + fx * (value(i + 1, j + 1) - value(i, j + 1));", + " return (top + fy * (bottom - top)) / 255;", + " }", + "", + " /**", + " * @param {number} index The index of a sample.", + " * @returns {boolean} true if no layer is painted on the sample: it's a hole.", + " */", + " isHole(index) {", + " const offset = index * LAYER_COUNT;", + " return (this.splat[offset] | this.splat[offset + 1] | this.splat[offset + 2] | this.splat[offset + 3]) === 0;", + " }", + "", + " /**", + " * @returns {number} The sum of the layer weights (0-1), interpolated between samples: there is a", + " * hole where it's under 0.5, like what is drawn.", + " */", + " sampleSolidity(gridX, gridY) {", + " let solidity = 0;", + " for (let layer = 0; layer < LAYER_COUNT; layer++) solidity += this.sampleLayerWeight(gridX, gridY, layer);", + " return solidity;", + " }", + "", + " /** @returns {number} The index (from 0) of the layer painted the most. */", + " getDominantLayer(gridX, gridY) {", + " const i = clampInteger(Math.round(gridX), 0, this.resolution);", + " const j = clampInteger(Math.round(gridY), 0, this.resolution);", + " const offset = (j * this.size + i) * LAYER_COUNT;", + " let dominantLayer = 0;", + " for (let layer = 1; layer < LAYER_COUNT; layer++) {", + " if (this.splat[offset + layer] > this.splat[offset + dominantLayer]) {", + " dominantLayer = layer;", + " }", + " }", + " return dominantLayer;", + " }", + "", + " /** @param {TerrainData} other */", + " copyFrom(other) {", + " this.heights.set(other.heights);", + " this.splat.set(other.splat);", + " this.colors.set(other.colors);", + " }", + "", + " /**", + " * @param {number} resolution", + " * @returns {TerrainData} A copy of this data with another resolution.", + " */", + " resampled(resolution) {", + " const data = new TerrainData(resolution);", + " const ratio = this.resolution / resolution;", + " for (let j = 0; j < data.size; j++) {", + " for (let i = 0; i < data.size; i++) {", + " const index = j * data.size + i;", + " data.heights[index] = this.sampleHeight(i * ratio, j * ratio);", + " const sourceOffset =", + " (Math.round(j * ratio) * this.size + Math.round(i * ratio)) * LAYER_COUNT;", + " for (let layer = 0; layer < LAYER_COUNT; layer++) {", + " data.splat[index * LAYER_COUNT + layer] = this.splat[sourceOffset + layer];", + " }", + " for (let channel = 0; channel < 4; channel++) {", + " data.colors[index * 4 + channel] = this.colors[sourceOffset + channel];", + " }", + " }", + " }", + " return data;", + " }", + "}", + "", + "/**", + " * Brushes change the samples around a segment, with an effect going smoothly", + " * from full on the segment to none at `radius`. A point is a segment of", + " * length 0. Positions are in scene units relative to the terrain origin.", + " */", + "const brushes = {", + " /**", + " * @param {TerrainData} data", + " * @param {{cellWidth: number, cellHeight: number, ax: number, ay: number, bx: number, by: number, radius: number}} stroke", + " * @param {(sampleIndex: number, weight: number, t: number) => void} apply Called with the weight of", + " * the brush and the position (from 0 to 1) of the nearest point of the segment.", + " * @param {SampleRectangle} changedSamples", + " */", + " forEachSample(data, stroke, apply, changedSamples) {", + " const { cellWidth, cellHeight, ax, ay, bx, by, radius } = stroke;", + " const last = data.resolution;", + " const minI = clampInteger(Math.floor((Math.min(ax, bx) - radius) / cellWidth), 0, last);", + " const maxI = clampInteger(Math.ceil((Math.max(ax, bx) + radius) / cellWidth), 0, last);", + " const minJ = clampInteger(Math.floor((Math.min(ay, by) - radius) / cellHeight), 0, last);", + " const maxJ = clampInteger(Math.ceil((Math.max(ay, by) + radius) / cellHeight), 0, last);", + " changedSamples.set(minI, minJ, maxI, maxJ);", + " if (radius <= 0) return;", + "", + " const abX = bx - ax;", + " const abY = by - ay;", + " const abLengthSquared = abX * abX + abY * abY;", + " const radiusSquared = radius * radius;", + " for (let j = minJ; j <= maxJ; j++) {", + " const y = j * cellHeight;", + " for (let i = minI; i <= maxI; i++) {", + " const x = i * cellWidth;", + " const t =", + " abLengthSquared > 0", + " ? clamp01(((x - ax) * abX + (y - ay) * abY) / abLengthSquared)", + " : 0;", + " const dx = x - (ax + t * abX);", + " const dy = y - (ay + t * abY);", + " const distanceSquared = dx * dx + dy * dy;", + " if (distanceSquared >= radiusSquared) continue;", + " const falloff = 1 - distanceSquared / radiusSquared;", + " apply(j * data.size + i, falloff * falloff, t);", + " }", + " }", + " },", + "", + " raise(data, stroke, amount, changedSamples) {", + " const { heights } = data;", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index, weight) => {", + " heights[index] = clamp01(heights[index] + amount * weight);", + " },", + " changedSamples", + " );", + " },", + "", + " /**", + " * @param {number} targetHeight", + " * @param {number} [endTargetHeight] The target at the end of the segment, for a slope (like a road", + " * going up). The same target as at its start by default.", + " */", + " flatten(data, stroke, targetHeight, strength, changedSamples, endTargetHeight = targetHeight) {", + " const { heights } = data;", + " const startTarget = clamp01(targetHeight);", + " const endTarget = clamp01(endTargetHeight);", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index, weight, t) => {", + " const target = startTarget + (endTarget - startTarget) * t;", + " heights[index] += (target - heights[index]) * clamp01(weight * strength);", + " },", + " changedSamples", + " );", + " },", + "", + " smooth(data, stroke, strength, changedSamples) {", + " const { heights, size } = data;", + " const changes = [];", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index, weight) => {", + " const i = index % size;", + " const j = (index - i) / size;", + " let sum = 0;", + " for (let dj = -1; dj <= 1; dj++) {", + " for (let di = -1; di <= 1; di++) {", + " sum += data.getHeight(i + di, j + dj);", + " }", + " }", + " changes.push(index, heights[index] + (sum / 9 - heights[index]) * clamp01(weight * strength));", + " },", + " changedSamples", + " );", + " // Written after all averages are computed, so that the result", + " // doesn't depend on the order in which samples are visited.", + " for (let index = 0; index < changes.length; index += 2) {", + " heights[changes[index]] = changes[index + 1];", + " }", + " },", + "", + " paint(data, stroke, layer, strength, changedSamples) {", + " const { splat } = data;", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index, weight) => {", + " const amount = clamp01(weight * strength);", + " const offset = index * LAYER_COUNT;", + " for (let otherLayer = 0; otherLayer < LAYER_COUNT; otherLayer++) {", + " const target = otherLayer === layer ? 255 : 0;", + " const value = splat[offset + otherLayer];", + " // Rounded towards the target, so that light strokes still paint.", + " const change = (target - value) * amount;", + " splat[offset + otherLayer] = value + (change > 0 ? Math.ceil(change) : Math.floor(change));", + " }", + " },", + " changedSamples", + " );", + " },", + "", + " /** Remove all the layers, making a hole (`fill` or painting a layer fills it again). */", + " hole(data, stroke, changedSamples) {", + " const { splat } = data;", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index) => {", + " splat.fill(0, index * LAYER_COUNT, (index + 1) * LAYER_COUNT);", + " },", + " changedSamples", + " );", + " },", + "", + " /**", + " * Fill the holes in the radius with the layers of the ground around them:", + " * each pass fills the holes next to the ground with the average of their", + " * neighbors, from the edges of the holes inward.", + " */", + " fill(data, stroke, changedSamples) {", + " const { splat, size, resolution } = data;", + " /** @type {number[]} */", + " let holes = [];", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index) => {", + " if (data.isHole(index)) holes.push(index);", + " },", + " changedSamples", + " );", + " const filledWeights = new Float32Array(holes.length * LAYER_COUNT);", + " while (holes.length > 0) {", + " /** @type {number[]} */", + " const remainingHoles = [];", + " /** @type {number[]} */", + " const filledHoles = [];", + " for (const index of holes) {", + " const i = index % size;", + " const j = (index - i) / size;", + " const weights = filledWeights.subarray(filledHoles.length * LAYER_COUNT, (filledHoles.length + 1) * LAYER_COUNT);", + " weights.fill(0);", + " for (let neighborJ = Math.max(j - 1, 0); neighborJ <= Math.min(j + 1, resolution); neighborJ++) {", + " for (let neighborI = Math.max(i - 1, 0); neighborI <= Math.min(i + 1, resolution); neighborI++) {", + " const neighbor = neighborJ * size + neighborI;", + " if (data.isHole(neighbor)) continue;", + " for (let layer = 0; layer < LAYER_COUNT; layer++) weights[layer] += splat[neighbor * LAYER_COUNT + layer];", + " }", + " }", + " if (weights[0] + weights[1] + weights[2] + weights[3] > 0) filledHoles.push(index);", + " else remainingHoles.push(index);", + " }", + " if (filledHoles.length === 0) {", + " // No ground around (a hole bigger than the brush): the first layer is used.", + " for (const index of remainingHoles) splat[index * LAYER_COUNT] = 255;", + " break;", + " }", + " // Filled after the pass, so that the holes are filled from the edges inward.", + " filledHoles.forEach((index, filledIndex) => {", + " const weights = filledWeights.subarray(filledIndex * LAYER_COUNT, (filledIndex + 1) * LAYER_COUNT);", + " const total = weights[0] + weights[1] + weights[2] + weights[3];", + " for (let layer = 0; layer < LAYER_COUNT; layer++) {", + " splat[index * LAYER_COUNT + layer] = Math.round((weights[layer] / total) * 255);", + " }", + " });", + " holes = remainingHoles;", + " }", + " },", + "", + " /** @param {number[]} color The red, green and blue (0-255) of the tint: white removes it. */", + " tint(data, stroke, color, strength, changedSamples) {", + " brushes.paintColors(data, stroke, 0, color, strength, changedSamples);", + " },", + "", + " /** @param {number} wetness From 0 (dry) to 1. */", + " wet(data, stroke, wetness, strength, changedSamples) {", + " brushes.paintColors(data, stroke, 3, [Math.round(clamp01(wetness) * 255)], strength, changedSamples);", + " },", + "", + " /**", + " * Bring channels of the colors (see `TerrainData.colors`) towards targets.", + " * @param {number} firstChannel", + " * @param {number[]} targets From 0 to 255, for the channels from the first one.", + " */", + " paintColors(data, stroke, firstChannel, targets, strength, changedSamples) {", + " const { colors } = data;", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index, weight) => {", + " const amount = clamp01(weight * strength);", + " for (let channel = 0; channel < targets.length; channel++) {", + " const offset = index * 4 + firstChannel + channel;", + " // Rounded towards the target, so that light strokes still paint.", + " const change = (targets[channel] - colors[offset]) * amount;", + " colors[offset] += change > 0 ? Math.ceil(change) : Math.floor(change);", + " }", + " },", + " changedSamples", + " );", + " },", + "};", + "", + "const relief = (() => {", + " const hash = (x, y, seed) => {", + " let h = Math.imul(x, 374761393) + Math.imul(y, 668265263) + Math.imul(seed, 982451653);", + " h = Math.imul(h ^ (h >>> 13), 1274126177);", + " return ((h ^ (h >>> 16)) >>> 0) / 4294967296;", + " };", + " const valueNoise = (x, y, seed) => {", + " const x0 = Math.floor(x);", + " const y0 = Math.floor(y);", + " const fx = x - x0;", + " const fy = y - y0;", + " const sx = fx * fx * (3 - 2 * fx);", + " const sy = fy * fy * (3 - 2 * fy);", + " const top = hash(x0, y0, seed) + sx * (hash(x0 + 1, y0, seed) - hash(x0, y0, seed));", + " const bottom =", + " hash(x0, y0 + 1, seed) + sx * (hash(x0 + 1, y0 + 1, seed) - hash(x0, y0 + 1, seed));", + " return top + sy * (bottom - top);", + " };", + " /** Fractal noise between 0 and 1. */", + " const fractalNoise = (x, y, seed) => {", + " let value = 0;", + " let amplitude = 0.5;", + " let frequency = 1;", + " for (let octave = 0; octave < 5; octave++) {", + " value += amplitude * valueNoise(x * frequency, y * frequency, seed + octave);", + " amplitude *= 0.5;", + " frequency *= 2;", + " }", + " return value / 0.96875;", + " };", + " const smoothstep = (edge0, edge1, x) => {", + " const t = clamp01((x - edge0) / (edge1 - edge0));", + " return t * t * (3 - 2 * t);", + " };", + " const reliefHeights = {", + " Flat: () => 0,", + " Hills: (u, v, seed) => 0.05 + 0.5 * Math.pow(fractalNoise(u * 3, v * 3, seed), 2),", + " Mountains: (u, v, seed) => {", + " const ridge = 1 - Math.abs(2 * fractalNoise(u * 2.5, v * 2.5, seed) - 1);", + " return 0.9 * Math.pow(ridge, 2.5);", + " },", + " Island: (u, v, seed) => {", + " const distanceToCenter = 2 * Math.hypot(u - 0.5, v - 0.5);", + " const coast = 0.2 * (fractalNoise(u * 4, v * 4, seed + 7) - 0.5);", + " const land = smoothstep(0.95, 0.55, distanceToCenter + coast);", + " return land * (0.12 + 0.6 * Math.pow(fractalNoise(u * 3, v * 3, seed), 1.5));", + " },", + " };", + " const beachLayer = 3;", + "", + " return {", + " /**", + " * @param {TerrainData} data", + " * @param {string} reliefName Flat, Hills, Mountains or Island.", + " * @param {number} seed", + " */", + " generate(data, reliefName, seed) {", + " const getHeight = reliefHeights[reliefName] || reliefHeights.Flat;", + " const integerSeed = Math.floor(seed) | 0;", + " data.fillLayer(0);", + " for (let j = 0; j < data.size; j++) {", + " for (let i = 0; i < data.size; i++) {", + " const index = j * data.size + i;", + " const height = clamp01(", + " getHeight(i / data.resolution, j / data.resolution, integerSeed)", + " );", + " data.heights[index] = height;", + " if (reliefName === 'Island') {", + " const sand = Math.round(255 * smoothstep(0.16, 0.1, height));", + " const offset = index * LAYER_COUNT;", + " data.splat[offset] = 255 - sand;", + " data.splat[offset + beachLayer] = sand;", + " }", + " }", + " }", + " },", + " };", + "})();", + "", + "/**", + " * Edits written in the Edits property: a JSON list of brush strokes, like", + " * `[{\"tool\": \"raise\", \"x\": 0.5, \"y\": 0.5, \"radius\": 0.2, \"height\": 0.3}]`,", + " * applied on the relief or the heightmap. Positions and radius are fractions", + " * of the terrain size, heights and Z positions fractions of its depth: they", + " * are the same for all instances, whatever their size. They are made to be", + " * read and written by people and by AI agents.", + " */", + "const edits = {", + " /**", + " * @param {TerrainData} data", + " * @param {string} text", + " */", + " apply(data, text) {", + " if (!text.trim()) return;", + " let editList;", + " try {", + " editList = JSON.parse(text);", + " } catch (error) {", + " console.warn('The edits of a terrain are not valid JSON: ' + error.message);", + " return;", + " }", + " if (!Array.isArray(editList)) {", + " console.warn('The edits of a terrain must be a JSON list (like [{\"tool\": \"raise\", ...}]).');", + " return;", + " }", + " const numberOr = (value, defaultValue) => (typeof value === 'number' && isFinite(value) ? value : defaultValue);", + " const changedSamples = new SampleRectangle();", + " for (const edit of editList) {", + " if (!edit || typeof edit !== 'object') continue;", + " const x = numberOr(edit.x, 0.5);", + " const y = numberOr(edit.y, 0.5);", + " const stroke = {", + " cellWidth: 1 / data.resolution,", + " cellHeight: 1 / data.resolution,", + " ax: x,", + " ay: y,", + " bx: numberOr(edit.toX, x),", + " by: numberOr(edit.toY, y),", + " radius: numberOr(edit.radius, 0.1),", + " };", + " const strength = clamp01(numberOr(edit.strength, 1));", + " if (edit.tool === 'raise') {", + " brushes.raise(data, stroke, numberOr(edit.height, 0.1), changedSamples);", + " } else if (edit.tool === 'flatten') {", + " const z = numberOr(edit.z, 0);", + " brushes.flatten(data, stroke, z, strength, changedSamples, numberOr(edit.toZ, z));", + " } else if (edit.tool === 'smooth') {", + " // Smoothing the same fraction of the terrain at any resolution.", + " const passes = Math.max(Math.round(((1 + 9 * strength) * data.resolution) / 256), 1);", + " for (let pass = 0; pass < passes; pass++) brushes.smooth(data, stroke, 1, changedSamples);", + " } else if (edit.tool === 'paint') {", + " const layer = clampInteger(numberOr(edit.layer, 1) - 1, 0, LAYER_COUNT - 1);", + " brushes.paint(data, stroke, layer, strength, changedSamples);", + " } else if (edit.tool === 'tint') {", + " const color = gdjs.rgbOrHexToRGBColor(typeof edit.color === 'string' ? edit.color : '255;255;255');", + " brushes.tint(data, stroke, color, strength, changedSamples);", + " } else if (edit.tool === 'wetness') {", + " brushes.wet(data, stroke, numberOr(edit.amount, 1), strength, changedSamples);", + " } else if (edit.tool === 'hole') {", + " brushes.hole(data, stroke, changedSamples);", + " } else if (edit.tool === 'fill') {", + " brushes.fill(data, stroke, changedSamples);", + " } else {", + " console.warn('Unknown tool in the edits of a terrain: \"' + edit.tool + '\" (use raise, flatten, smooth, paint, tint, wetness, hole or fill).');", + " }", + " }", + " },", + "};", + "", + "/**", + " * @param {gdjs.RuntimeGame} game", + " * @param {string} imageResourceName", + " * @returns {CanvasImageSource | null} The image, or null if it's not loaded (yet).", + " */", + "const getLoadedImage = (game, imageResourceName) => {", + " const imageManager = game.getImageManager();", + " if (typeof imageManager.getImageSource === 'function') return imageManager.getImageSource(imageResourceName);", + " // Older GDevelop versions: a 192 x 192 placeholder is given for images that are not loaded.", + " const texture = imageManager.getPIXITexture(imageResourceName);", + " return texture && texture !== imageManager.getInvalidPIXITexture() ? texture.baseTexture.getDrawableSource() : null;", + "};", + "", + "/**", + " * Loads heights from an image: black is the lowest, white the highest.", + " * Heights have 16 bits of precision when the red channel holds the most", + " * significant byte and the green one the least significant (a gray image", + " * works as expected too).", + " * @param {TerrainData} data", + " * @param {gdjs.RuntimeGame} game", + " * @param {string} imageResourceName", + " * @returns {boolean} true if the image could be read.", + " */", + "const loadHeightsFromImage = (data, game, imageResourceName) => {", + " const source = getLoadedImage(game, imageResourceName);", + " if (!source) return false;", + "", + " const canvas = document.createElement('canvas');", + " canvas.width = source.width;", + " canvas.height = source.height;", + " const context = canvas.getContext('2d', { willReadFrequently: true });", + " if (!context) return false;", + " context.drawImage(source, 0, 0);", + " const pixels = context.getImageData(0, 0, source.width, source.height).data;", + " // Pixels are decoded before being interpolated, to keep the 16 bits.", + " const pixelHeight = (x, y) => {", + " const offset = (y * source.width + x) * 4;", + " return (256 * pixels[offset] + pixels[offset + 1]) / 65535;", + " };", + " for (let j = 0; j < data.size; j++) {", + " const y = (j / data.resolution) * (source.height - 1);", + " const y0 = Math.floor(y);", + " const y1 = Math.min(y0 + 1, source.height - 1);", + " for (let i = 0; i < data.size; i++) {", + " const x = (i / data.resolution) * (source.width - 1);", + " const x0 = Math.floor(x);", + " const x1 = Math.min(x0 + 1, source.width - 1);", + " const top = pixelHeight(x0, y0) + (x - x0) * (pixelHeight(x1, y0) - pixelHeight(x0, y0));", + " const bottom = pixelHeight(x0, y1) + (x - x0) * (pixelHeight(x1, y1) - pixelHeight(x0, y1));", + " data.heights[j * data.size + i] = top + (y - y0) * (bottom - top);", + " }", + " }", + " return true;", + "};", + "", + "/**", + " * Stores the data in a string. Only the differences with the base data (from", + " * the relief or the heightmap) are stored: heights on 16 bits, layer weights", + " * on 8 bits, then (since format 2) the tint and wetness on 8 bits. Each value is predicted from its neighbors and the", + " * prediction errors are written as variable length integers, with runs of", + " * zeros collapsed. Untouched or smoothly changed areas take almost no space.", + " */", + "const codec = (() => {", + " const formatVersion = '2';", + " // Format 1 has no tint and wetness.", + " const readableFormatVersions = ['1', '2'];", + "", + " class ByteWriter {", + " constructor() {", + " this.bytes = new Uint8Array(1024);", + " this.length = 0;", + " }", + " writeVarUint(value) {", + " if (this.length + 5 > this.bytes.length) {", + " const bytes = new Uint8Array(this.bytes.length * 2);", + " bytes.set(this.bytes);", + " this.bytes = bytes;", + " }", + " while (value >= 0x80) {", + " this.bytes[this.length++] = (value & 0x7f) | 0x80;", + " value >>>= 7;", + " }", + " this.bytes[this.length++] = value;", + " }", + " }", + "", + " class ByteReader {", + " /** @param {Uint8Array} bytes */", + " constructor(bytes) {", + " this.bytes = bytes;", + " this.position = 0;", + " }", + " readVarUint() {", + " let value = 0;", + " let shift = 0;", + " let byte;", + " do {", + " if (this.position >= this.bytes.length) throw new Error('Truncated terrain data.');", + " byte = this.bytes[this.position++];", + " value += (byte & 0x7f) * Math.pow(2, shift);", + " shift += 7;", + " } while (byte & 0x80);", + " return value;", + " }", + " }", + "", + " /** Predicts a value from the left, top and top-left ones. */", + " const predict = (values, size, index) => {", + " const i = index % size;", + " if (index < size) return i === 0 ? 0 : values[index - 1];", + " if (i === 0) return values[index - size];", + " return values[index - 1] + values[index - size] - values[index - size - 1];", + " };", + "", + " /**", + " * @param {ByteWriter} writer", + " * @param {Int32Array} values A grid of `size * size` values.", + " */", + " const writeGrid = (writer, values, size) => {", + " let zeroRun = 0;", + " for (let index = 0; index < values.length; index++) {", + " const error = values[index] - predict(values, size, index);", + " if (error === 0) {", + " zeroRun++;", + " continue;", + " }", + " if (zeroRun > 0) {", + " writer.writeVarUint(0);", + " writer.writeVarUint(zeroRun);", + " zeroRun = 0;", + " }", + " // Zigzag: small negative and positive numbers get small codes.", + " writer.writeVarUint(error > 0 ? error * 2 : -error * 2 - 1);", + " }", + " if (zeroRun > 0) {", + " writer.writeVarUint(0);", + " writer.writeVarUint(zeroRun);", + " }", + " };", + "", + " /** @param {ByteReader} reader */", + " const readGrid = (reader, size) => {", + " const values = new Int32Array(size * size);", + " let zeroRun = 0;", + " for (let index = 0; index < values.length; index++) {", + " let error = 0;", + " if (zeroRun > 0) {", + " zeroRun--;", + " } else {", + " const code = reader.readVarUint();", + " if (code === 0) {", + " zeroRun = reader.readVarUint() - 1;", + " } else {", + " error = code % 2 === 0 ? code / 2 : -(code + 1) / 2;", + " }", + " }", + " values[index] = predict(values, size, index) + error;", + " }", + " return values;", + " };", + "", + " const toBase64 = (bytes, length) => {", + " let binary = '';", + " for (let offset = 0; offset < length; offset += 0x8000) {", + " binary += String.fromCharCode.apply(null, bytes.subarray(offset, Math.min(offset + 0x8000, length)));", + " }", + " return btoa(binary);", + " };", + "", + " const fromBase64 = (base64) => {", + " const binary = atob(base64);", + " const bytes = new Uint8Array(binary.length);", + " for (let index = 0; index < binary.length; index++) {", + " bytes[index] = binary.charCodeAt(index);", + " }", + " return bytes;", + " };", + "", + " return {", + " /**", + " * @param {TerrainData} data", + " * @param {TerrainData} baseData The data without changes, with the same resolution.", + " */", + " encode(data, baseData) {", + " const writer = new ByteWriter();", + " const values = new Int32Array(data.heights.length);", + " for (let index = 0; index < values.length; index++) {", + " values[index] = Math.round((data.heights[index] - baseData.heights[index]) * 65535);", + " }", + " writeGrid(writer, values, data.size);", + " for (let layer = 0; layer < LAYER_COUNT; layer++) {", + " for (let index = 0; index < values.length; index++) {", + " const offset = index * LAYER_COUNT + layer;", + " values[index] = data.splat[offset] - baseData.splat[offset];", + " }", + " writeGrid(writer, values, data.size);", + " }", + " // Not painted, these grids are zeros: a few bytes each.", + " for (let channel = 0; channel < 4; channel++) {", + " for (let index = 0; index < values.length; index++) {", + " const offset = index * 4 + channel;", + " values[index] = data.colors[offset] - baseData.colors[offset];", + " }", + " writeGrid(writer, values, data.size);", + " }", + " return formatVersion + ';' + data.resolution + ';' + toBase64(writer.bytes, writer.length);", + " },", + "", + " /**", + " * Store grids of integers in a string, like the terrain data. Used by other objects (the scattered models).", + " * @param {Int32Array[]} grids Grids of `size * size` values.", + " * @param {number} size", + " */", + " encodeGrids(grids, size) {", + " const writer = new ByteWriter();", + " for (const grid of grids) writeGrid(writer, grid, size);", + " return toBase64(writer.bytes, writer.length);", + " },", + "", + " /**", + " * @param {string} base64 A string from `encodeGrids`.", + " * @param {number} size", + " * @param {number} count The number of grids.", + " * @returns {Int32Array[]} The grids (an error is thrown if the string is not valid).", + " */", + " decodeGrids(base64, size, count) {", + " const reader = new ByteReader(fromBase64(base64));", + " return Array.from({ length: count }, () => readGrid(reader, size));", + " },", + "", + " /**", + " * @param {string} text", + " * @param {(resolution: number) => TerrainData} createBaseData", + " * @returns {TerrainData | null} null if the text is empty or not valid.", + " */", + " decode(text, createBaseData) {", + " if (!text) return null;", + " const parts = text.split(';');", + " const resolution = parseInt(parts[1], 10);", + " if (readableFormatVersions.indexOf(parts[0]) === -1 || RESOLUTIONS.indexOf(resolution) === -1) {", + " return null;", + " }", + " try {", + " const reader = new ByteReader(fromBase64(parts[2]));", + " const data = createBaseData(resolution);", + " const heightChanges = readGrid(reader, data.size);", + " for (let index = 0; index < heightChanges.length; index++) {", + " data.heights[index] = clamp01(data.heights[index] + heightChanges[index] / 65535);", + " }", + " for (let layer = 0; layer < LAYER_COUNT; layer++) {", + " const weightChanges = readGrid(reader, data.size);", + " for (let index = 0; index < weightChanges.length; index++) {", + " const offset = index * LAYER_COUNT + layer;", + " // The base may have changed since the data was saved.", + " data.splat[offset] = Math.min(Math.max(data.splat[offset] + weightChanges[index], 0), 255);", + " }", + " }", + " if (parts[0] !== '1') {", + " for (let channel = 0; channel < 4; channel++) {", + " const colorChanges = readGrid(reader, data.size);", + " for (let index = 0; index < colorChanges.length; index++) {", + " const offset = index * 4 + channel;", + " data.colors[offset] = Math.min(Math.max(data.colors[offset] + colorChanges[index], 0), 255);", + " }", + " }", + " }", + " return data;", + " } catch (error) {", + " console.error('Terrain data could not be read:', error);", + " return null;", + " }", + " },", + " };", + "})();", + "", + "const lodIndices = new Map();", + "/**", + " * The triangles of a chunk, shared by all chunks of the same size. Chunk", + " * borders have a skirt going down, hiding the gaps between chunks drawn", + " * with different levels of detail.", + " * @param {number} chunkCells", + " * @param {number} step", + " */", + "const getLodIndex = (chunkCells, step) => {", + " const key = chunkCells + ';' + step;", + " const existingIndex = lodIndices.get(key);", + " if (existingIndex) return existingIndex;", + "", + " const sideVertices = chunkCells + 1;", + " const gridVertices = sideVertices * sideVertices;", + " const indices = [];", + " const grid = (i, j) => j * sideVertices + i;", + " for (let j = 0; j < chunkCells; j += step) {", + " for (let i = 0; i < chunkCells; i += step) {", + " const topLeft = grid(i, j);", + " const topRight = grid(i + step, j);", + " const bottomLeft = grid(i, j + step);", + " const bottomRight = grid(i + step, j + step);", + " indices.push(topLeft, topRight, bottomLeft, topRight, bottomRight, bottomLeft);", + " }", + " }", + " const borders = [", + " (k) => grid(k, 0),", + " (k) => grid(k, chunkCells),", + " (k) => grid(0, k),", + " (k) => grid(chunkCells, k),", + " ];", + " borders.forEach((borderVertex, border) => {", + " const skirtVertex = (k) => gridVertices + border * sideVertices + k;", + " for (let k = 0; k < chunkCells; k += step) {", + " const a = borderVertex(k);", + " const b = borderVertex(k + step);", + " const skirtA = skirtVertex(k);", + " const skirtB = skirtVertex(k + step);", + " // Both faces, as a skirt can be seen from either side.", + " indices.push(a, b, skirtB, a, skirtB, skirtA, a, skirtB, b, a, skirtA, skirtB);", + " }", + " });", + " const index = new THREE.BufferAttribute(new Uint16Array(indices), 1);", + " lodIndices.set(key, index);", + " return index;", + "};", + "", + "const vertexShaderDeclarations = `", + "varying vec2 vTerrainLocalXY;", + "varying float vTerrainLocalZ;", + "uniform float terrainWorldScale;", + "varying vec2 vTerrainWorldXY;", + "varying float vTerrainUp;", + "`;", + "const vertexShaderCode = `", + "vTerrainLocalXY = position.xy;", + "vTerrainLocalZ = position.z;", + "vTerrainWorldXY = (modelMatrix * vec4(transformed, 1.0)).xy * terrainWorldScale;", + "vTerrainUp = normalize(transpose(inverse(mat3(modelMatrix))) * objectNormal).z;", + "`;", + "const fragmentShaderDeclarations = `", + "uniform sampler2D terrainSplatMap;", + "uniform vec2 terrainSplatUvScaleAndOffset;", + "uniform vec3 terrainLayerColors[4];", + "uniform sampler2D terrainLayerMap0;", + "uniform sampler2D terrainLayerMap1;", + "uniform sampler2D terrainLayerMap2;", + "uniform sampler2D terrainLayerMap3;", + "uniform sampler2D terrainLayerNormalMap0;", + "uniform sampler2D terrainLayerNormalMap1;", + "uniform sampler2D terrainLayerNormalMap2;", + "uniform sampler2D terrainLayerNormalMap3;", + "uniform float terrainHasNormalMaps;", + "uniform float terrainNormalMapStrength;", + "uniform float terrainTextureSize;", + "uniform float terrainHeightBlending;", + "uniform float terrainTextureVariation;", + "uniform vec4 terrainCliffLayer;", + "uniform vec2 terrainCliffCosines;", + "uniform vec4 terrainBrush;", + "uniform sampler2D terrainColorMap;", + "uniform vec3 terrainWetShore;", + "varying vec2 vTerrainLocalXY;", + "varying float vTerrainLocalZ;", + "varying vec2 vTerrainWorldXY;", + "varying float vTerrainUp;", + "", + "float terrainHash(vec2 p) {", + " return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);", + "}", + "// The rotation of the copies of textures used to make repetition less visible.", + "const mat2 terrainCopyRotation = mat2(0.8, -0.6, 0.6, 0.8);", + "/** A bump of a normal map (from -1 to 1), mixed with its rotated copy in patches. */", + "vec3 terrainSampleNormal(sampler2D normalMap, vec2 uv, vec2 uv2, float patches) {", + " vec3 bump = texture2D(normalMap, uv).xyz * 2.0 - 1.0;", + " if (patches <= 0.0) return bump;", + " vec3 copyBump = texture2D(normalMap, uv2).xyz * 2.0 - 1.0;", + " // The bumps of the copy are rotated back with the inverse rotation.", + " copyBump.xy = transpose(terrainCopyRotation) * copyBump.xy;", + " return mix(bump, copyBump, patches);", + "}", + "float terrainNoise(vec2 p) {", + " vec2 i = floor(p);", + " vec2 f = fract(p);", + " vec2 u = f * f * (3.0 - 2.0 * f);", + " return mix(", + " mix(terrainHash(i), terrainHash(i + vec2(1.0, 0.0)), u.x),", + " mix(terrainHash(i + vec2(0.0, 1.0)), terrainHash(i + vec2(1.0, 1.0)), u.x),", + " u.y", + " );", + "}", + "`;", + "// Holes are samples without any painted layer: the interpolated sum of the", + "// weights gives smooth edges. Only compiled for terrains having holes, as", + "// discard makes drawing slower on some GPUs.", + "const holeShaderCode = `", + "#ifdef TERRAIN_HOLES", + "if (dot(texture2D(terrainSplatMap, vTerrainLocalXY * terrainSplatUvScaleAndOffset.x + terrainSplatUvScaleAndOffset.y), vec4(1.0)) < 0.5) discard;", + "#endif", + "`;", + "// For the shadows: the depth of holes is not drawn.", + "const holeDepthShaderDeclarations = `", + "uniform sampler2D terrainSplatMap;", + "uniform vec2 terrainSplatUvScaleAndOffset;", + "varying vec2 vTerrainLocalXY;", + "`;", + "", + "const fragmentShaderCode = holeShaderCode + `", + "// Borders between layers are made irregular and sharper, to look painted.", + "vec2 terrainNoisePosition = vTerrainWorldXY / 24.0;", + "vec4 terrainJitter = vec4(", + " terrainNoise(terrainNoisePosition),", + " terrainNoise(terrainNoisePosition + 19.1),", + " terrainNoise(terrainNoisePosition + 37.7),", + " terrainNoise(terrainNoisePosition + 53.3)", + ");", + "// Layer weights are in a texture, so that painted details stay the same", + "// whatever the level of detail of the ground.", + "vec2 terrainSplatUv = vTerrainLocalXY * terrainSplatUvScaleAndOffset.x + terrainSplatUvScaleAndOffset.y;", + "vec4 terrainWeights = texture2D(terrainSplatMap, terrainSplatUv) * (0.7 + 0.6 * terrainJitter);", + "terrainWeights = terrainWeights * terrainWeights;", + "terrainWeights /= max(dot(terrainWeights, vec4(1.0)), 0.00001);", + "float terrainCliff = 1.0 - smoothstep(terrainCliffCosines.x, terrainCliffCosines.y, vTerrainUp);", + "terrainWeights = mix(terrainWeights, terrainCliffLayer, terrainCliff * dot(terrainCliffLayer, vec4(1.0)));", + "", + "vec2 terrainUv = vTerrainWorldXY / terrainTextureSize;", + "vec3 terrainTexture0 = texture2D(terrainLayerMap0, terrainUv).rgb;", + "vec3 terrainTexture1 = texture2D(terrainLayerMap1, terrainUv).rgb;", + "vec3 terrainTexture2 = texture2D(terrainLayerMap2, terrainUv).rgb;", + "vec3 terrainTexture3 = texture2D(terrainLayerMap3, terrainUv).rgb;", + "", + "// Less visible repetition: in irregular patches, textures are replaced by a", + "// rotated and shifted copy of themselves, and colors slowly vary.", + "float terrainPatches = 0.0;", + "vec2 terrainUv2 = terrainUv;", + "float terrainColorVariation = 1.0;", + "if (terrainTextureVariation > 0.0) {", + " // Up to half of the ground is covered by the copies, each patch showing", + " // only one of them (mixing them would make them blurry).", + " float terrainPatchThreshold = 1.0 - 0.5 * terrainTextureVariation;", + " terrainPatches = smoothstep(terrainPatchThreshold - 0.05, terrainPatchThreshold + 0.05, terrainNoise(terrainUv * 0.23 + 7.3));", + " terrainUv2 = terrainCopyRotation * terrainUv + vec2(0.31, 0.73);", + " terrainTexture0 = mix(terrainTexture0, texture2D(terrainLayerMap0, terrainUv2).rgb, terrainPatches);", + " terrainTexture1 = mix(terrainTexture1, texture2D(terrainLayerMap1, terrainUv2).rgb, terrainPatches);", + " terrainTexture2 = mix(terrainTexture2, texture2D(terrainLayerMap2, terrainUv2).rgb, terrainPatches);", + " terrainTexture3 = mix(terrainTexture3, texture2D(terrainLayerMap3, terrainUv2).rgb, terrainPatches);", + " terrainColorVariation += terrainTextureVariation * 0.3 *", + " (terrainNoise(terrainUv * 0.11) + 0.5 * terrainNoise(terrainUv * 0.37 + 3.1) - 0.75);", + "}", + "", + "// Height-based blending: the brightness of a texture, compared to its", + "// average, is used as its height. Where layers meet, the high parts (stones,", + "// tufts) cover the low parts (sand, gaps) instead of fading into each other.", + "if (terrainHeightBlending > 0.0) {", + " const vec3 terrainLuminance = vec3(0.299, 0.587, 0.114);", + " vec4 terrainHeights = vec4(", + " dot(terrainTexture0 - textureLod(terrainLayerMap0, vec2(0.5), 16.0).rgb, terrainLuminance),", + " dot(terrainTexture1 - textureLod(terrainLayerMap1, vec2(0.5), 16.0).rgb, terrainLuminance),", + " dot(terrainTexture2 - textureLod(terrainLayerMap2, vec2(0.5), 16.0).rgb, terrainLuminance),", + " dot(terrainTexture3 - textureLod(terrainLayerMap3, vec2(0.5), 16.0).rgb, terrainLuminance)", + " );", + " vec4 terrainHeightWeights = terrainWeights + terrainHeights;", + " float terrainTop = max(max(terrainHeightWeights.x, terrainHeightWeights.y), max(terrainHeightWeights.z, terrainHeightWeights.w));", + " // Only painted layers (and the cliff layer) can show.", + " vec4 terrainHeightBlend = max(terrainHeightWeights - terrainTop + 0.1, 0.0) * step(0.004, terrainWeights);", + " terrainHeightBlend /= max(dot(terrainHeightBlend, vec4(1.0)), 0.00001);", + " terrainWeights = mix(terrainWeights, terrainHeightBlend, terrainHeightBlending);", + "}", + "", + "vec3 terrainColor = terrainColorVariation * (", + " terrainWeights.x * terrainLayerColors[0] * terrainTexture0 +", + " terrainWeights.y * terrainLayerColors[1] * terrainTexture1 +", + " terrainWeights.z * terrainLayerColors[2] * terrainTexture2 +", + " terrainWeights.w * terrainLayerColors[3] * terrainTexture3);", + "", + "// The painted tint multiplies all layers. Wet ground is darker (and", + "// shinier, see the roughness).", + "vec4 terrainTintAndWetness = texture2D(terrainColorMap, terrainSplatUv);", + "float terrainWetness = max(", + " terrainTintAndWetness.a,", + " terrainWetShore.z * (1.0 - smoothstep(terrainWetShore.x, terrainWetShore.x + terrainWetShore.y, vTerrainLocalZ))", + ");", + "terrainColor *= terrainTintAndWetness.rgb * (1.0 - 0.4 * terrainWetness);", + "diffuseColor.rgb *= terrainColor;", + "", + "if (terrainBrush.w > 0.5) {", + " float brushDistance = distance(vTerrainWorldXY, terrainBrush.xy);", + " float ringWidth = max(terrainBrush.z * 0.04, 1.5);", + " float ring = 1.0 - smoothstep(0.0, ringWidth, abs(brushDistance - terrainBrush.z));", + " float disc = brushDistance < terrainBrush.z ? 0.12 : 0.0;", + " diffuseColor.rgb = mix(diffuseColor.rgb, vec3(1.0), max(ring * 0.85, disc));", + "}", + "`;", + "", + "const fragmentShaderRoughnessCode = `", + "roughnessFactor = mix(roughnessFactor, 0.3, terrainWetness);", + "`;", + "", + "const fragmentShaderNormalCode = `", + "if (terrainHasNormalMaps > 0.5) {", + " vec3 terrainMapNormal =", + " terrainWeights.x * terrainSampleNormal(terrainLayerNormalMap0, terrainUv, terrainUv2, terrainPatches) +", + " terrainWeights.y * terrainSampleNormal(terrainLayerNormalMap1, terrainUv, terrainUv2, terrainPatches) +", + " terrainWeights.z * terrainSampleNormal(terrainLayerNormalMap2, terrainUv, terrainUv2, terrainPatches) +", + " terrainWeights.w * terrainSampleNormal(terrainLayerNormalMap3, terrainUv, terrainUv2, terrainPatches);", + " terrainMapNormal.xy *= terrainNormalMapStrength;", + " // The X and Y of textures follow the X and Y of the 3D world, projected on", + " // the ground (the camera space of the normal is used).", + " vec3 terrainTangent = (viewMatrix * vec4(1.0, 0.0, 0.0, 0.0)).xyz;", + " terrainTangent -= normal * dot(normal, terrainTangent);", + " // A ground facing the X axis (a rotated terrain) uses the Y axis instead.", + " if (dot(terrainTangent, terrainTangent) < 0.000001) {", + " terrainTangent = (viewMatrix * vec4(0.0, 1.0, 0.0, 0.0)).xyz;", + " terrainTangent -= normal * dot(normal, terrainTangent);", + " }", + " terrainTangent = normalize(terrainTangent);", + " vec3 terrainBitangent = cross(normal, terrainTangent);", + " normal = normalize(mat3(terrainTangent, terrainBitangent, normal) * terrainMapNormal);", + "}", + "`;", + "", + "let whiteTexture = null;", + "const getWhiteTexture = () => {", + " if (!whiteTexture) {", + " whiteTexture = new THREE.DataTexture(new Uint8Array([255, 255, 255, 255]), 1, 1);", + " whiteTexture.needsUpdate = true;", + " }", + " return whiteTexture;", + "};", + "", + "let flatNormalTexture = null;", + "const getFlatNormalTexture = () => {", + " if (!flatNormalTexture) {", + " flatNormalTexture = new THREE.DataTexture(new Uint8Array([128, 128, 255, 255]), 1, 1);", + " flatNormalTexture.needsUpdate = true;", + " }", + " return flatNormalTexture;", + "};", + "", + "const layerTextures = new WeakMap();", + "const layerNormalMaps = new WeakMap();", + "/**", + " * Textures of the image manager have no mipmaps: they would shimmer when", + " * repeated in the distance.", + " * @param {gdjs.RuntimeGame} game", + " * @param {string} resourceName", + " * @param {boolean} isNormalMap Normal maps are directions, not colors.", + " */", + "const getLayerTexture = (game, resourceName, isNormalMap) => {", + " if (!resourceName) return isNormalMap ? getFlatNormalTexture() : getWhiteTexture();", + " const imageTexture = game.getImageManager().getThreeTexture(resourceName);", + " const textures = isNormalMap ? layerNormalMaps : layerTextures;", + " let texture = textures.get(imageTexture);", + " if (!texture) {", + " texture = imageTexture.clone();", + " if (isNormalMap) texture.colorSpace = THREE.NoColorSpace;", + " texture.wrapS = THREE.RepeatWrapping;", + " texture.wrapT = THREE.RepeatWrapping;", + " texture.minFilter = THREE.LinearMipmapLinearFilter;", + " texture.generateMipmaps = true;", + " texture.anisotropy = 4;", + " texture.needsUpdate = true;", + " textures.set(imageTexture, texture);", + " }", + " return texture;", + "};", + "", + "/**", + " * A texture of 4 bytes per sample of a grid of `size * size` samples (layer", + " * weights, tint and wetness...), interpolated between samples.", + " * @param {Uint8Array} bytes", + " * @param {number} size", + " */", + "const createGridTexture = (bytes, size) => {", + " const texture = new THREE.DataTexture(bytes, size, size);", + " texture.magFilter = THREE.LinearFilter;", + " texture.minFilter = THREE.LinearFilter;", + " texture.userData.isUploaded = false;", + " texture.onUpdate = () => {", + " texture.userData.isUploaded = true;", + " };", + " return texture;", + "};", + "", + "/**", + " * Send the rows of the changed samples to the GPU, or the whole texture", + " * before its first upload (and in older GDevelop versions).", + " * @param {THREE.DataTexture} texture", + " * @param {SampleRectangle} samples", + " */", + "const uploadGridTextureRows = (texture, samples) => {", + " if (texture.userData.isUploaded && typeof texture.addUpdateRange === 'function') {", + " const size = texture.image.width;", + " for (let j = samples.minJ; j <= samples.maxJ; j++) {", + " texture.addUpdateRange((j * size + samples.minI) * 4, (samples.maxI - samples.minI + 1) * 4);", + " }", + " }", + " texture.needsUpdate = true;", + "};", + "", + "class TerrainRenderer {", + " /** @param {Terrain} terrain */", + " constructor(terrain) {", + " this.terrain = terrain;", + " this.group = new THREE.Group();", + " terrain.object.get3DRendererObject().add(this.group);", + " /** @type {Array<{mesh: THREE.Mesh, geometry: THREE.BufferGeometry, firstI: number, firstJ: number, center: THREE.Vector3, lod: number}>} */", + " this.chunks = [];", + " this.chunksPerSide = 0;", + " this.chunkCells = 0;", + " this.chunkSideVertices = 0;", + " this.chunkGridVertices = 0;", + " this.chunkVertices = 0;", + " /** @type {THREE.DataTexture | null} */", + " this.splatTexture = null;", + " /** @type {THREE.DataTexture | null} */", + " this.colorTexture = null;", + " this.uniforms = {", + " terrainSplatMap: { value: getWhiteTexture() },", + " terrainSplatUvScaleAndOffset: { value: new THREE.Vector2() },", + " terrainLayerColors: { value: [new THREE.Color(), new THREE.Color(), new THREE.Color(), new THREE.Color()] },", + " terrainLayerMap0: { value: getWhiteTexture() },", + " terrainLayerMap1: { value: getWhiteTexture() },", + " terrainLayerMap2: { value: getWhiteTexture() },", + " terrainLayerMap3: { value: getWhiteTexture() },", + " terrainLayerNormalMap0: { value: getFlatNormalTexture() },", + " terrainLayerNormalMap1: { value: getFlatNormalTexture() },", + " terrainLayerNormalMap2: { value: getFlatNormalTexture() },", + " terrainLayerNormalMap3: { value: getFlatNormalTexture() },", + " terrainHasNormalMaps: { value: 0 },", + " terrainNormalMapStrength: { value: 1 },", + " terrainTextureSize: { value: 256 },", + " terrainHeightBlending: { value: 0 },", + " terrainTextureVariation: { value: 0 },", + " terrainCliffLayer: { value: new THREE.Vector4() },", + " terrainCliffCosines: { value: new THREE.Vector2() },", + " terrainBrush: { value: new THREE.Vector4() },", + " terrainColorMap: { value: getWhiteTexture() },", + " // Z (in the terrain group) under which the ground is wet, fade height, and 1 if it's used.", + " terrainWetShore: { value: new THREE.Vector3() },", + " // Scene units by 3D world unit.", + " terrainWorldScale: { value: 1 },", + " };", + " this.material = new THREE.MeshStandardMaterial({ roughness: 1, metalness: 0 });", + " this.material.onBeforeCompile = (shader) => {", + " Object.assign(shader.uniforms, this.uniforms);", + " shader.vertexShader = patchShaderCode(shader.vertexShader, [", + " ['#include ', '#include \\n' + vertexShaderDeclarations],", + " ['#include ', '#include \\n' + vertexShaderCode],", + " ], 'Terrain3D');", + " shader.fragmentShader = patchShaderCode(shader.fragmentShader, [", + " ['#include ', '#include \\n' + fragmentShaderDeclarations],", + " ['#include ', fragmentShaderCode],", + " ['#include ', '#include \\n' + fragmentShaderRoughnessCode],", + " ['#include ', '#include \\n' + fragmentShaderNormalCode],", + " ], 'Terrain3D');", + " };", + " this.material.customProgramCacheKey = () => (this.hasHoles ? 'Terrain3DWithHoles' : 'Terrain3D');", + " this.hasHoles = false;", + " // Without them, holes would cast shadows.", + " this.depthMaterial = this._createHoleDepthMaterial(new THREE.MeshDepthMaterial());", + " this.distanceMaterial = this._createHoleDepthMaterial(new THREE.MeshDistanceMaterial());", + " this._cameraPosition = new THREE.Vector3();", + " this._chunkPosition = new THREE.Vector3();", + " this._scale = new THREE.Vector3();", + " }", + "", + " /**", + " * @param {THREE.MeshDepthMaterial | THREE.MeshDistanceMaterial} material", + " * @returns The material, drawing nothing in holes.", + " */", + " _createHoleDepthMaterial(material) {", + " material.defines = { TERRAIN_HOLES: '' };", + " material.onBeforeCompile = (shader) => {", + " Object.assign(shader.uniforms, this.uniforms);", + " shader.vertexShader = patchShaderCode(shader.vertexShader, [", + " ['#include ', '#include \\nvarying vec2 vTerrainLocalXY;'],", + " ['#include ', '#include \\nvTerrainLocalXY = position.xy;'],", + " ], 'Terrain3D shadows');", + " shader.fragmentShader = patchShaderCode(shader.fragmentShader, [", + " ['#include ', '#include \\n' + holeDepthShaderDeclarations],", + " ['#include ', holeShaderCode + '#include '],", + " ], 'Terrain3D shadows');", + " };", + " material.customProgramCacheKey = () => 'Terrain3DHoles';", + " return material;", + " }", + "", + " /**", + " * Use the shaders drawing holes, or the faster ones without them.", + " * @param {boolean} hasHoles", + " */", + " setHasHoles(hasHoles) {", + " if (hasHoles === this.hasHoles) return;", + " this.hasHoles = hasHoles;", + " // The material has its own defines (STANDARD).", + " if (hasHoles) this.material.defines.TERRAIN_HOLES = '';", + " else delete this.material.defines.TERRAIN_HOLES;", + " this.material.needsUpdate = true;", + " for (const chunk of this.chunks) this._setChunkShadowMaterials(chunk.mesh);", + " }", + "", + " /** @param {THREE.Mesh} mesh */", + " _setChunkShadowMaterials(mesh) {", + " mesh.customDepthMaterial = this.hasHoles ? this.depthMaterial : undefined;", + " mesh.customDistanceMaterial = this.hasHoles ? this.distanceMaterial : undefined;", + " }", + "", + " /** Recreate all the chunks, for example after the resolution changed. */", + " rebuild() {", + " this.disposeChunks();", + " const { data } = this.terrain;", + " this.chunkCells = getChunkCells(data.resolution);", + " this.chunkSideVertices = this.chunkCells + 1;", + " this.chunkGridVertices = this.chunkSideVertices * this.chunkSideVertices;", + " this.chunkVertices = this.chunkGridVertices + 4 * this.chunkSideVertices;", + " this.chunksPerSide = data.resolution / this.chunkCells;", + " const cellSize = AREA_SIZE / data.resolution;", + " // Sample centers are at the texel centers.", + " this.splatTexture = createGridTexture(data.splat, data.size);", + " this.uniforms.terrainSplatMap.value = this.splatTexture;", + " this.colorTexture = createGridTexture(data.colors, data.size);", + " // Tints are colors chosen like the layer colors.", + " this.colorTexture.colorSpace = THREE.SRGBColorSpace;", + " this.uniforms.terrainColorMap.value = this.colorTexture;", + " this.uniforms.terrainSplatUvScaleAndOffset.value.set(1 / (cellSize * data.size), 0.5 / data.size);", + " for (let chunkJ = 0; chunkJ < this.chunksPerSide; chunkJ++) {", + " for (let chunkI = 0; chunkI < this.chunksPerSide; chunkI++) {", + " const firstI = chunkI * this.chunkCells;", + " const firstJ = chunkJ * this.chunkCells;", + " const positions = new Float32Array(this.chunkVertices * 3);", + " for (let j = 0; j < this.chunkSideVertices; j++) {", + " for (let i = 0; i < this.chunkSideVertices; i++) {", + " const vertex = j * this.chunkSideVertices + i;", + " positions[vertex * 3] = (firstI + i) * cellSize;", + " positions[vertex * 3 + 1] = (firstJ + j) * cellSize;", + " }", + " }", + " // Skirt vertices are below border vertices.", + " this._forEachSkirtVertex((skirtVertex, borderVertex) => {", + " positions[skirtVertex * 3] = positions[borderVertex * 3];", + " positions[skirtVertex * 3 + 1] = positions[borderVertex * 3 + 1];", + " });", + " const geometry = new THREE.BufferGeometry();", + " geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));", + " geometry.setAttribute('normal', new THREE.BufferAttribute(new Float32Array(this.chunkVertices * 3), 3));", + " geometry.setIndex(getLodIndex(this.chunkCells, LOD_STEPS[0]));", + " geometry.boundingBox = new THREE.Box3();", + " geometry.boundingSphere = new THREE.Sphere();", + " const mesh = new THREE.Mesh(geometry, this.material);", + " mesh.matrixAutoUpdate = false;", + " // Rays hit the ground at its full resolution, whatever the level of", + " // detail drawn, and go through holes.", + " const chunkCells = this.chunkCells;", + " const terrainData = () => this.terrain.data;", + " const localPoint = new THREE.Vector3();", + " mesh.raycast = function (raycaster, intersects) {", + " const drawnIndex = geometry.index;", + " const firstIntersection = intersects.length;", + " geometry.setIndex(getLodIndex(chunkCells, LOD_STEPS[0]));", + " THREE.Mesh.prototype.raycast.call(this, raycaster, intersects);", + " geometry.setIndex(drawnIndex);", + " const data = terrainData();", + " const cellSize = AREA_SIZE / data.resolution;", + " for (let index = intersects.length - 1; index >= firstIntersection; index--) {", + " this.worldToLocal(localPoint.copy(intersects[index].point));", + " if (data.sampleSolidity(localPoint.x / cellSize, localPoint.y / cellSize) < 0.5) {", + " intersects.splice(index, 1);", + " }", + " }", + " };", + " this._setChunkShadowMaterials(mesh);", + " this.group.add(mesh);", + " this.chunks.push({", + " mesh,", + " geometry,", + " firstI,", + " firstJ,", + " center: new THREE.Vector3(", + " (firstI + this.chunkCells / 2) * cellSize,", + " (firstJ + this.chunkCells / 2) * cellSize,", + " 0", + " ),", + " lod: 0,", + " });", + " }", + " }", + " this.updateSamples(new SampleRectangle().set(0, 0, data.resolution, data.resolution), true);", + " this.updateMaterial();", + " }", + "", + " _forEachSkirtVertex(callback) {", + " for (let k = 0; k < this.chunkSideVertices; k++) {", + " callback(this.chunkGridVertices + k, k);", + " callback(this.chunkGridVertices + this.chunkSideVertices + k, this.chunkCells * this.chunkSideVertices + k);", + " callback(this.chunkGridVertices + 2 * this.chunkSideVertices + k, k * this.chunkSideVertices);", + " callback(", + " this.chunkGridVertices + 3 * this.chunkSideVertices + k,", + " k * this.chunkSideVertices + this.chunkCells", + " );", + " }", + " }", + "", + " /**", + " * Skirts go down only to the lowest point a coarser neighbor chunk can", + " * draw along the border, so that they can't be seen through holes.", + " * @param {Float32Array} positions", + " */", + " _updateSkirtHeights(positions) {", + " const coarsestStep = LOD_STEPS[LOD_STEPS.length - 1];", + " const borderVertices = [", + " (k) => k,", + " (k) => this.chunkCells * this.chunkSideVertices + k,", + " (k) => k * this.chunkSideVertices,", + " (k) => k * this.chunkSideVertices + this.chunkCells,", + " ];", + " borderVertices.forEach((borderVertex, border) => {", + " for (let k = 0; k < this.chunkSideVertices; k++) {", + " let minZ = Infinity;", + " const from = Math.max(k - coarsestStep, 0);", + " const to = Math.min(k + coarsestStep, this.chunkCells);", + " for (let other = from; other <= to; other++) {", + " minZ = Math.min(minZ, positions[borderVertex(other) * 3 + 2]);", + " }", + " const skirtVertex = this.chunkGridVertices + border * this.chunkSideVertices + k;", + " positions[skirtVertex * 3 + 2] = minZ;", + " }", + " });", + " }", + "", + " /**", + " * Update the vertices of the chunks sharing the given samples.", + " * @param {SampleRectangle} samples", + " * @param {boolean} hasPaintChanged", + " */", + " updateSamples(samples, hasPaintChanged) {", + " if (samples.isEmpty()) return;", + " const { data } = this.terrain;", + " const cellSize = AREA_SIZE / data.resolution;", + " const slopeFactor = AREA_DEPTH / cellSize;", + " // Normals of samples next to changed ones change too.", + " const minI = Math.max(samples.minI - 1, 0);", + " const minJ = Math.max(samples.minJ - 1, 0);", + " const maxI = Math.min(samples.maxI + 1, data.resolution);", + " const maxJ = Math.min(samples.maxJ + 1, data.resolution);", + " const firstChunkI = Math.max(Math.ceil(minI / this.chunkCells) - 1, 0);", + " const firstChunkJ = Math.max(Math.ceil(minJ / this.chunkCells) - 1, 0);", + " const lastChunkI = Math.min(Math.floor(maxI / this.chunkCells), this.chunksPerSide - 1);", + " const lastChunkJ = Math.min(Math.floor(maxJ / this.chunkCells), this.chunksPerSide - 1);", + "", + " for (let chunkJ = firstChunkJ; chunkJ <= lastChunkJ; chunkJ++) {", + " for (let chunkI = firstChunkI; chunkI <= lastChunkI; chunkI++) {", + " const chunk = this.chunks[chunkJ * this.chunksPerSide + chunkI];", + " const positions = chunk.geometry.attributes.position.array;", + " const normals = chunk.geometry.attributes.normal.array;", + " const fromI = Math.max(minI - chunk.firstI, 0);", + " const toI = Math.min(maxI - chunk.firstI, this.chunkCells);", + " const fromJ = Math.max(minJ - chunk.firstJ, 0);", + " const toJ = Math.min(maxJ - chunk.firstJ, this.chunkCells);", + " for (let j = fromJ; j <= toJ; j++) {", + " const gridJ = chunk.firstJ + j;", + " for (let i = fromI; i <= toI; i++) {", + " const gridI = chunk.firstI + i;", + " const vertex = j * this.chunkSideVertices + i;", + " const sample = gridJ * data.size + gridI;", + " positions[vertex * 3 + 2] = data.heights[sample] * AREA_DEPTH;", + "", + " const leftI = Math.max(gridI - 1, 0);", + " const rightI = Math.min(gridI + 1, data.resolution);", + " const upJ = Math.max(gridJ - 1, 0);", + " const downJ = Math.min(gridJ + 1, data.resolution);", + " const nx =", + " ((data.getHeight(leftI, gridJ) - data.getHeight(rightI, gridJ)) * slopeFactor) /", + " (rightI - leftI);", + " const ny =", + " ((data.getHeight(gridI, upJ) - data.getHeight(gridI, downJ)) * slopeFactor) /", + " (downJ - upJ);", + " const inverseLength = 1 / Math.sqrt(nx * nx + ny * ny + 1);", + " normals[vertex * 3] = nx * inverseLength;", + " normals[vertex * 3 + 1] = ny * inverseLength;", + " normals[vertex * 3 + 2] = inverseLength;", + " }", + " }", + " this._forEachSkirtVertex((skirtVertex, borderVertex) => {", + " for (let axis = 0; axis < 3; axis++) {", + " normals[skirtVertex * 3 + axis] = normals[borderVertex * 3 + axis];", + " }", + " });", + " this._updateSkirtHeights(positions);", + " let maxZ = 0;", + " for (let vertex = 0; vertex < this.chunkGridVertices; vertex++) {", + " maxZ = Math.max(maxZ, positions[vertex * 3 + 2]);", + " }", + " const chunkSize = this.chunkCells * cellSize;", + " chunk.geometry.boundingBox.min.set(chunk.firstI * cellSize, chunk.firstJ * cellSize, 0);", + " chunk.geometry.boundingBox.max.set(", + " chunk.firstI * cellSize + chunkSize,", + " chunk.firstJ * cellSize + chunkSize,", + " maxZ", + " );", + " chunk.geometry.boundingBox.getBoundingSphere(chunk.geometry.boundingSphere);", + " chunk.center.z = maxZ / 2;", + " chunk.geometry.attributes.position.needsUpdate = true;", + " chunk.geometry.attributes.normal.needsUpdate = true;", + " }", + " }", + " if (hasPaintChanged) {", + " // The data may have been replaced by one with the same resolution.", + " this.splatTexture.image.data = data.splat;", + " uploadGridTextureRows(this.splatTexture, samples);", + " this.colorTexture.image.data = data.colors;", + " uploadGridTextureRows(this.colorTexture, samples);", + " }", + " }", + "", + " updateMaterial() {", + " const { object } = this.terrain;", + " const game = object.getRuntimeScene().getGame();", + " const colors = [object._getLayer1Color(), object._getLayer2Color(), object._getLayer3Color(), object._getLayer4Color()];", + " const textures = [", + " object._getLayer1Texture(),", + " object._getLayer2Texture(),", + " object._getLayer3Texture(),", + " object._getLayer4Texture(),", + " ];", + " const normalMaps = [", + " object._getLayer1NormalMap(),", + " object._getLayer2NormalMap(),", + " object._getLayer3NormalMap(),", + " object._getLayer4NormalMap(),", + " ];", + " for (let layer = 0; layer < LAYER_COUNT; layer++) {", + " this.uniforms.terrainLayerColors.value[layer].set(gdjs.rgbOrHexStringToNumber(colors[layer]));", + " this.uniforms['terrainLayerMap' + layer].value = getLayerTexture(game, textures[layer], false);", + " this.uniforms['terrainLayerNormalMap' + layer].value = getLayerTexture(game, normalMaps[layer], true);", + " }", + " this.uniforms.terrainHasNormalMaps.value = normalMaps.some((normalMap) => !!normalMap) ? 1 : 0;", + " this.uniforms.terrainNormalMapStrength.value = Math.max(object._getNormalMapStrength(), 0);", + " this.uniforms.terrainTextureSize.value = Math.max(object._getTextureSize(), 1);", + " this.uniforms.terrainHeightBlending.value = clamp01(object._getHeightBlending());", + " this.uniforms.terrainTextureVariation.value = clamp01(object._getTextureVariation());", + "", + " const cliffLayer = parseInt(object._getCliffLayer(), 10);", + " const cliffAngle = gdjs.toRad(object._getCliffAngle());", + " const cliffBlendAngle = gdjs.toRad(4);", + " this.uniforms.terrainCliffLayer.value.set(", + " cliffLayer === 1 ? 1 : 0,", + " cliffLayer === 2 ? 1 : 0,", + " cliffLayer === 3 ? 1 : 0,", + " cliffLayer === 4 ? 1 : 0", + " );", + " this.uniforms.terrainCliffCosines.value.set(", + " Math.cos(cliffAngle + cliffBlendAngle),", + " Math.cos(cliffAngle - cliffBlendAngle)", + " );", + "", + " const castShadow = object._getCastShadow();", + " const receiveShadow = object._getReceiveShadow();", + " for (const chunk of this.chunks) {", + " chunk.mesh.castShadow = castShadow;", + " chunk.mesh.receiveShadow = receiveShadow;", + " }", + " }", + "", + " /**", + " * Show the brush circle of the editor.", + " * @param {THREE.Vector3 | null} localPosition", + " * @param {number} radius In scene units.", + " */", + " setBrush(localPosition, radius) {", + " const brush = this.uniforms.terrainBrush.value;", + " if (!localPosition) {", + " brush.w = 0;", + " return;", + " }", + " this.group.updateWorldMatrix(true, false);", + " const worldPosition = this._chunkPosition.copy(localPosition).applyMatrix4(this.group.matrixWorld);", + " const worldScale = this.uniforms.terrainWorldScale.value;", + " brush.set(worldPosition.x * worldScale, worldPosition.y * worldScale, radius, 1);", + " }", + "", + " /** @param {THREE.Camera | null} camera */", + " updateLevelsOfDetail(camera) {", + " if (!camera || !this.group.visible) return;", + " const matrixWorld = this.group.matrixWorld;", + " this._cameraPosition.setFromMatrixPosition(camera.matrixWorld);", + " this._scale.setFromMatrixScale(matrixWorld);", + " const cellSize = AREA_SIZE / this.terrain.data.resolution;", + " const chunkWorldSize = this.chunkCells * cellSize * Math.max(this._scale.x, this._scale.y);", + " for (const chunk of this.chunks) {", + " const distance =", + " this._chunkPosition.copy(chunk.center).applyMatrix4(matrixWorld).distanceTo(this._cameraPosition) /", + " chunkWorldSize;", + " let lod = LOD_DISTANCES.length - 1;", + " while (lod > 0 && distance < LOD_DISTANCES[lod]) lod--;", + " if (lod !== chunk.lod) {", + " chunk.lod = lod;", + " chunk.geometry.setIndex(getLodIndex(this.chunkCells, LOD_STEPS[lod]));", + " }", + " }", + " }", + "", + " disposeChunks() {", + " for (const chunk of this.chunks) {", + " chunk.mesh.removeFromParent();", + " // The index is shared by all chunks: it must stay on the GPU.", + " chunk.geometry.setIndex(null);", + " chunk.geometry.dispose();", + " }", + " this.chunks.length = 0;", + " if (this.splatTexture) this.splatTexture.dispose();", + " this.splatTexture = null;", + " if (this.colorTexture) this.colorTexture.dispose();", + " this.colorTexture = null;", + " }", + "", + " dispose() {", + " this.disposeChunks();", + " this.material.dispose();", + " this.depthMaterial.dispose();", + " this.distanceMaterial.dispose();", + " this.group.removeFromParent();", + " }", + "}", + "", + "/**", + " * Makes a Physics3D behavior use a height field shape following the terrain.", + " * Changes of heights are applied to the shape without recreating it.", + " * @implements {gdjs.Physics3DRuntimeBehavior.BodyUpdater}", + " */", + "class TerrainBodyUpdater {", + " /**", + " * @param {Terrain} terrain", + " * @param {gdjs.Physics3DRuntimeBehavior} physics", + " */", + " constructor(terrain, physics) {", + " this.terrain = terrain;", + " this.physics = physics;", + " this.defaultBodyUpdater = physics.bodyUpdater;", + " this.changedSamples = new SampleRectangle();", + " /** @type {Jolt.HeightFieldShape | null} */", + " this.heightField = null;", + " this.heightScale = 1;", + " this.heightsBufferPointer = 0;", + " this.heightsBufferLength = 0;", + " this.bodyX = 0;", + " this.bodyY = 0;", + " this.bodyZ = 0;", + " this.bodyHeight = 0;", + " physics.bodyType = 'Static';", + " physics.bodyUpdater = this;", + " }", + "", + " createShape() {", + " const { data, object } = this.terrain;", + " const worldInvScale = this.physics._sharedData.worldInvScale;", + " const { size } = data;", + " this.heightScale = object.getDepth() * worldInvScale;", + "", + " // Changes of heights (SetHeights) break collisions unless the sample", + " // count is a power of 2: extra samples have no collision.", + " const sampleCount = Math.pow(2, Math.ceil(Math.log2(size)));", + " const settings = new Jolt.HeightFieldShapeSettings();", + " settings.mScale.Set(", + " (object.getWidth() / data.resolution) * worldInvScale,", + " 1,", + " (object.getHeight() / data.resolution) * worldInvScale", + " );", + " settings.mSampleCount = sampleCount;", + " settings.mBlockSize = 4;", + " // Allows later changes of heights on the whole depth of the object.", + " settings.mMinHeightValue = 0;", + " settings.mMaxHeightValue = this.heightScale;", + " settings.mHeightSamples.resize(sampleCount * sampleCount);", + " const samples = new Float32Array(", + " Jolt.HEAPF32.buffer,", + " Jolt.getPointer(settings.mHeightSamples.data()),", + " sampleCount * sampleCount", + " );", + " const noCollision = Jolt.HeightFieldShapeConstantValues.prototype.cNoCollisionValue;", + " samples.fill(noCollision);", + " // Height fields go along X and Z: rows are flipped so that, once", + " // rotated to have heights along Z, rows go along Y.", + " for (let j = 0; j < size; j++) {", + " const row = (size - 1 - j) * sampleCount;", + " for (let i = 0; i < size; i++) {", + " samples[row + i] = this._getSampleHeight(j * size + i, noCollision);", + " }", + " }", + " const shape = settings.Create().Get();", + " Jolt.destroy(settings);", + " this.heightField = Jolt.castObject(shape, Jolt.HeightFieldShape);", + " this.changedSamples.clear();", + " return shape;", + " }", + "", + " /** @returns {number} The height of a sample in the shape, or no collision in a hole. */", + " _getSampleHeight(index, noCollision) {", + " const height = this.terrain.surfaceHeights[index];", + " return Number.isNaN(height) ? noCollision : height * this.heightScale;", + " }", + "", + " _getBodyPosition() {", + " const { object } = this.terrain;", + " const worldInvScale = this.physics._sharedData.worldInvScale;", + " this.bodyX = object.getX();", + " this.bodyY = object.getY();", + " this.bodyZ = object.getZ();", + " // The origin of the height field is at the bottom of the terrain (see createShape).", + " this.bodyHeight = object.getHeight();", + " return this.physics._sharedData.getRVec3(", + " object.getX() * worldInvScale,", + " (object.getY() + object.getHeight()) * worldInvScale,", + " object.getZ() * worldInvScale", + " );", + " }", + "", + " _getBodyRotation() {", + " // A quarter turn around X: heights go along Z.", + " return this.physics._sharedData.getQuat(Math.SQRT1_2, 0, 0, Math.SQRT1_2);", + " }", + "", + " createAndAddBody() {", + " const { physics } = this;", + " const { bodyInterface } = physics._sharedData;", + " const shape = this.createShape();", + " const settings = new Jolt.BodyCreationSettings(", + " shape,", + " this._getBodyPosition(),", + " this._getBodyRotation(),", + " Jolt.EMotionType_Static,", + " physics.getBodyLayer()", + " );", + " settings.mFriction = physics.friction;", + " settings.mRestitution = physics.restitution;", + " const body = bodyInterface.CreateBody(settings);", + " Jolt.destroy(settings);", + " bodyInterface.AddBody(body.GetID(), Jolt.EActivation_DontActivate);", + " return body;", + " }", + "", + " updateObjectFromBody() {", + " // The terrain is static: the body never moves it.", + " }", + "", + " updateBodyFromObject() {", + " const { physics } = this;", + " const body = physics._body;", + " if (!body) return;", + " const { object } = this.terrain;", + " const { bodyInterface } = physics._sharedData;", + " if (", + " object.getX() !== this.bodyX ||", + " object.getY() !== this.bodyY ||", + " object.getZ() !== this.bodyZ ||", + " object.getHeight() !== this.bodyHeight", + " ) {", + " bodyInterface.SetPositionAndRotation(", + " body.GetID(),", + " this._getBodyPosition(),", + " this._getBodyRotation(),", + " Jolt.EActivation_DontActivate", + " );", + " }", + " if (!this.changedSamples.isEmpty()) {", + " this._applyChangedHeights(body);", + " }", + " }", + "", + " _applyChangedHeights(body) {", + " const { data } = this.terrain;", + " const { size } = data;", + " const { minI, minJ, maxI, maxJ } = this.changedSamples;", + " // Jolt only changes blocks of samples. Rows are flipped and extra", + " // samples have no collision (see createShape).", + " const blockSize = this.heightField.GetBlockSize();", + " const sampleCount = this.heightField.GetSampleCount();", + " const alignDown = (value) => Math.floor(value / blockSize) * blockSize;", + " const alignUp = (value) => Math.min(Math.ceil(value / blockSize) * blockSize, sampleCount);", + " const firstX = alignDown(minI);", + " const firstRow = alignDown(size - 1 - maxJ);", + " const width = alignUp(maxI + 1) - firstX;", + " const height = alignUp(size - minJ) - firstRow;", + " if (width * height > this.heightsBufferLength) {", + " if (this.heightsBufferPointer) Jolt._webidl_free(this.heightsBufferPointer);", + " this.heightsBufferLength = width * height;", + " this.heightsBufferPointer = Jolt._webidl_malloc(this.heightsBufferLength * 4);", + " }", + " const heights = new Float32Array(Jolt.HEAPF32.buffer, this.heightsBufferPointer, width * height);", + " const noCollision = Jolt.HeightFieldShapeConstantValues.prototype.cNoCollisionValue;", + " for (let row = 0; row < height; row++) {", + " const j = size - 1 - (firstRow + row);", + " for (let column = 0; column < width; column++) {", + " const i = firstX + column;", + " heights[row * width + column] =", + " i < size && j >= 0 ? this._getSampleHeight(j * size + i, noCollision) : noCollision;", + " }", + " }", + " const sharedData = this.physics._sharedData;", + " this.heightField.SetHeights(", + " firstX,", + " firstRow,", + " width,", + " height,", + " this.heightsBufferPointer,", + " width,", + " sharedData.jolt.GetTempAllocator()", + " );", + " sharedData.bodyInterface.NotifyShapeChanged(", + " body.GetID(),", + " sharedData.getVec3(0, 0, 0),", + " false,", + " Jolt.EActivation_DontActivate", + " );", + " this._wakeUpBodiesOnChangedSamples();", + " this.changedSamples.clear();", + " }", + "", + " /** Bodies sleeping on the changed area would otherwise float or sink. */", + " _wakeUpBodiesOnChangedSamples() {", + " const { object, data } = this.terrain;", + " const worldInvScale = this.physics._sharedData.worldInvScale;", + " const cellWidth = object.getWidth() / data.resolution;", + " const cellHeight = object.getHeight() / data.resolution;", + " const { minI, minJ, maxI, maxJ } = this.changedSamples;", + " const min = new Jolt.Vec3(", + " (object.getX() + (minI - 1) * cellWidth) * worldInvScale,", + " (object.getY() + (minJ - 1) * cellHeight) * worldInvScale,", + " (object.getZ() - 1) * worldInvScale", + " );", + " const max = new Jolt.Vec3(", + " (object.getX() + (maxI + 1) * cellWidth) * worldInvScale,", + " (object.getY() + (maxJ + 1) * cellHeight) * worldInvScale,", + " (object.getZ() + object.getDepth() + 1) * worldInvScale", + " );", + " const box = new Jolt.AABox(min, max);", + " const broadPhaseLayerFilter = new Jolt.BroadPhaseLayerFilter();", + " const objectLayerFilter = new Jolt.ObjectLayerFilter();", + " this.physics._sharedData.bodyInterface.ActivateBodiesInAABox(box, broadPhaseLayerFilter, objectLayerFilter);", + " Jolt.destroy(min);", + " Jolt.destroy(max);", + " Jolt.destroy(box);", + " Jolt.destroy(broadPhaseLayerFilter);", + " Jolt.destroy(objectLayerFilter);", + " }", + "", + " recreateShape() {", + " const body = this.physics._body;", + " if (!body) return;", + " this.physics._sharedData.bodyInterface.SetShape(", + " body.GetID(),", + " this.createShape(),", + " false,", + " Jolt.EActivation_DontActivate", + " );", + " const { resolution } = this.terrain.data;", + " this.changedSamples.set(0, 0, resolution, resolution);", + " this._wakeUpBodiesOnChangedSamples();", + " this.changedSamples.clear();", + " }", + "", + " destroyBody() {", + " this.defaultBodyUpdater.destroyBody();", + " this.heightField = null;", + " }", + "", + " dispose() {", + " if (this.heightsBufferPointer) Jolt._webidl_free(this.heightsBufferPointer);", + " this.heightsBufferPointer = 0;", + " this.heightsBufferLength = 0;", + " }", + "}", + "", + "/** @param {TerrainData} data */", + "const copyTerrainData = (data) => {", + " const copy = new TerrainData(data.resolution);", + " copy.copyFrom(data);", + " return copy;", + "};", + "", + "// Generating a relief or reading an image is long for big terrains, and done", + "// for every instance and every hot-reload: the results are kept.", + "const baseDataCache = new Map();", + "const MAX_CACHED_BASE_DATA = 8;", + "// Changes of the shape of a terrain kept for consumers of its surface that", + "// missed some versions: older changes update the whole surface.", + "const MAX_SURFACE_CHANGES = 64;", + "// Unique among all terrains: a terrain replaced by another one never has its version.", + "let lastGroundVersion = 0;", + "", + "/**", + " * The terrain of a Terrain3D object: its data, how it's drawn and its", + " * surface (for collisions and navigation meshes). Positions are in the scene.", + " */", + "class Terrain {", + " /** @param {gdjs.CustomRuntimeObject3D} object */", + " constructor(object) {", + " this.object = object;", + " /** @type {TerrainData} */", + " this.data = new TerrainData(DEFAULT_RESOLUTION);", + " /** The data before any sculpting or painting, from the relief or heightmap. */", + " this.baseData = this.data;", + " this.renderer = new TerrainRenderer(this);", + " /** @type {TerrainBodyUpdater[]} */", + " this.bodyUpdaters = [];", + " this.changedSamples = new SampleRectangle();", + " this.hasPaintChanged = false;", + " /** Increased each time the ground changes, for objects following it (like grass). */", + " this.version = 0;", + " /** Increased each time the shape of the ground changes (not its paint or position). */", + " this.surfaceVersion = 0;", + " /**", + " * The last changes of the shape of the ground, for `getChangedArea`.", + " * @type {Array<{version: number, samples: SampleRectangle}>}", + " */", + " this.surfaceChanges = [];", + " /** The changes since this version are all in `surfaceChanges`. */", + " this.surfaceChangesStartVersion = 0;", + " this._stroke = { cellWidth: 1, cellHeight: 1, ax: 0, ay: 0, bx: 0, by: 0, radius: 0 };", + " this._strokeChangedSamples = new SampleRectangle();", + " /** The properties the data was loaded from, to reload it only if they change. */", + " this.loadedFrom = '';", + " /** The heightmap image being loaded: the relief is used until it is. */", + " this.loadingHeightmapImage = '';", + " this.isDisposed = false;", + " /** The last value of the sculpt data property read or saved. */", + " this.sculptData = '';", + " /**", + " * Values saved by the editor tools: the editor sends them back with a", + " * hot-reload, possibly after newer ones were saved.", + " * @type {string[]}", + " */", + " this.savedSculptData = [];", + " /** The heights of the surface: the heights of the data, and NaN in holes. */", + " this.surfaceHeights = new Float32Array(0);", + " this.loadFromProperties();", + " this._transform = this._getTransform();", + " getGrounds(object.getRuntimeScene()).add(this);", + " /**", + " * The shape of the ground, for the physics engine and navigation meshes.", + " * @type {gdjs.Surface}", + " */", + " this.surface = {", + " getVersion: () => this.surfaceVersion,", + " getChangedArea: (sinceVersion) => this.getChangedArea(sinceVersion),", + " getTriangles: () => null,", + " getHeightField: () => ({ columns: this.data.size, rows: this.data.size, heights: this.surfaceHeights }),", + " };", + " if (typeof object.setSurface === 'function') {", + " object.setSurface(this.surface);", + " } else if (gdjs.Physics3DRuntimeBehavior) {", + " // Older GDevelop versions don't use surfaces.", + " for (const behavior of object._behaviors) {", + " if (behavior instanceof gdjs.Physics3DRuntimeBehavior) {", + " this.bodyUpdaters.push(new TerrainBodyUpdater(this, behavior));", + " }", + " }", + " }", + " }", + "", + " _getSourceProperties() {", + " const { object } = this;", + " return [", + " object._getResolution(),", + " object._getRelief(),", + " object._getSeed(),", + " object._getHeightmapImage(),", + " object._getEdits(),", + " ].join('|');", + " }", + "", + " loadFromProperties() {", + " const { object } = this;", + " const resolution = parseInt(object._getResolution(), 10);", + " const validResolution = RESOLUTIONS.indexOf(resolution) !== -1 ? resolution : DEFAULT_RESOLUTION;", + " this.sculptData = object._getSculptData();", + " this.loadedFrom = this._getSourceProperties();", + "", + " this.baseData = this._getBaseData(validResolution);", + " let data = codec.decode(this.sculptData, (resolution) => copyTerrainData(this._getBaseData(resolution)));", + " if (data && data.resolution !== validResolution) {", + " data = data.resampled(validResolution);", + " }", + " this.setData(data || copyTerrainData(this.baseData));", + " }", + "", + " /**", + " * @param {number} resolution", + " * @returns {TerrainData} The data from the relief or the heightmap, shared: it must not be changed.", + " */", + " _getBaseData(resolution) {", + " const { object } = this;", + " const key = [resolution, object._getRelief(), object._getSeed(), object._getHeightmapImage(), object._getEdits()].join('|');", + " const cachedData = baseDataCache.get(key);", + " if (cachedData) return cachedData;", + "", + " const data = new TerrainData(resolution);", + " const heightmapImage = object._getHeightmapImage();", + " const isHeightmapLoaded =", + " !!heightmapImage && loadHeightsFromImage(data, object.getRuntimeScene().getGame(), heightmapImage);", + " if (!isHeightmapLoaded) {", + " relief.generate(data, object._getRelief(), object._getSeed());", + " if (heightmapImage) this._reloadOnceLoaded(heightmapImage);", + " }", + " edits.apply(data, object._getEdits());", + " // Not kept if the image is missing: it may be loaded later.", + " if (isHeightmapLoaded || !heightmapImage) baseDataCache.set(key, data);", + " if (baseDataCache.size > MAX_CACHED_BASE_DATA) {", + " baseDataCache.delete(baseDataCache.keys().next().value);", + " }", + " return data;", + " }", + "", + " /** @param {TerrainData} data */", + " setData(data) {", + " this.version = ++lastGroundVersion;", + " this.surfaceVersion = this.version;", + " this.surfaceChanges.length = 0;", + " this.surfaceChangesStartVersion = this.version;", + " const hasSameResolution = data.resolution === this.data.resolution && this.renderer.chunks.length > 0;", + " this.data = data;", + " const allSamples = new SampleRectangle().set(0, 0, data.resolution, data.resolution);", + " this.surfaceHeights = new Float32Array(data.heights.length);", + " this.renderer.setHasHoles(false);", + " this._updateSurfaceHeights(allSamples);", + " if (hasSameResolution) {", + " this.renderer.updateSamples(allSamples, true);", + " } else {", + " this.renderer.rebuild();", + " }", + " for (const bodyUpdater of this.bodyUpdaters) {", + " bodyUpdater.recreateShape();", + " }", + " }", + "", + " onHotReloading() {", + " const sculptData = this.object._getSculptData();", + " const savedIndex = this.savedSculptData.indexOf(sculptData);", + " if (savedIndex !== -1 && this._getSourceProperties() === this.loadedFrom) {", + " // The terrain already has this data, or newer changes.", + " this.savedSculptData.splice(0, savedIndex + 1);", + " } else if (sculptData !== this.sculptData || this._getSourceProperties() !== this.loadedFrom) {", + " this.savedSculptData.length = 0;", + " this.loadFromProperties();", + " }", + " this.renderer.updateMaterial();", + " }", + "", + " /** @returns {string} The data to save in the sculpt data property. */", + " save() {", + " this.sculptData = codec.encode(this.data, this.baseData);", + " this.savedSculptData.push(this.sculptData);", + " return this.sculptData;", + " }", + "", + " getCellWidth() {", + " return this.object.getWidth() / this.data.resolution;", + " }", + "", + " getCellHeight() {", + " return this.object.getHeight() / this.data.resolution;", + " }", + "", + " _toGridX(x) {", + " return (x - this.object.getX()) / this.getCellWidth();", + " }", + "", + " _toGridY(y) {", + " return (y - this.object.getY()) / this.getCellHeight();", + " }", + "", + " /** @returns {number} The Z position of the ground. */", + " getHeightAt(x, y) {", + " return this.object.getZ() + this.data.sampleHeight(this._toGridX(x), this._toGridY(y)) * this.object.getDepth();", + " }", + "", + " /** @returns {number} The angle of the ground with the horizontal, in degrees. */", + " getSlopeAt(x, y) {", + " const gridX = this._toGridX(x);", + " const gridY = this._toGridY(y);", + " const { data } = this;", + " const depth = this.object.getDepth();", + " const slopeX = ((data.sampleHeight(gridX + 0.5, gridY) - data.sampleHeight(gridX - 0.5, gridY)) * depth) / this.getCellWidth();", + " const slopeY = ((data.sampleHeight(gridX, gridY + 0.5) - data.sampleHeight(gridX, gridY - 0.5)) * depth) / this.getCellHeight();", + " return gdjs.toDegrees(Math.atan(Math.hypot(slopeX, slopeY)));", + " }", + "", + " /** @returns {number} The layer painted the most, from 1 to 4. */", + " getLayerAt(x, y) {", + " return this.data.getDominantLayer(this._toGridX(x), this._toGridY(y)) + 1;", + " }", + "", + " /**", + " * @param {number} layer From 1 to 4.", + " * @returns {number} How much the layer is painted, from 0 to 1.", + " */", + " getLayerWeightAt(x, y, layer) {", + " const layerIndex = clampInteger(layer - 1, 0, LAYER_COUNT - 1);", + " return this.data.sampleLayerWeight(this._toGridX(x), this._toGridY(y), layerIndex);", + " }", + "", + " /** @returns {number[]} The painted tint: red, green and blue, from 0 to 255 (white when there is none). */", + " getTintAt(x, y) {", + " const gridX = this._toGridX(x);", + " const gridY = this._toGridY(y);", + " return [0, 1, 2].map((channel) => Math.round(this.data.sampleColor(gridX, gridY, channel) * 255));", + " }", + "", + " /** @returns {number} How wet the ground is, from 0 (dry) to 1, painted or near the water (see the wet shore). */", + " getWetnessAt(x, y) {", + " const paintedWetness = this.data.sampleColor(this._toGridX(x), this._toGridY(y), 3);", + " const { object } = this;", + " if (!object._getWetShore()) return paintedWetness;", + " const fade = Math.max(object._getWetShoreFade(), 0.001);", + " const shoreWetness = clamp01((object._getWetShoreZ() + fade - this.getHeightAt(x, y)) / fade);", + " // Like the smoothstep of the shader.", + " return Math.max(paintedWetness, shoreWetness * shoreWetness * (3 - 2 * shoreWetness));", + " }", + "", + " _isInArea(x, y) {", + " const { object } = this;", + " return (", + " x >= object.getX() &&", + " x <= object.getX() + object.getWidth() &&", + " y >= object.getY() &&", + " y <= object.getY() + object.getHeight()", + " );", + " }", + "", + " /** @returns {boolean} true if the position is above or under the terrain, and not in a hole. */", + " containsPoint(x, y) {", + " return this._isInArea(x, y) && this.data.sampleSolidity(this._toGridX(x), this._toGridY(y)) >= 0.5;", + " }", + "", + " /** @returns {boolean} true if there is a hole in the terrain at the position, like what is drawn. */", + " isHoleAt(x, y) {", + " return this._isInArea(x, y) && this.data.sampleSolidity(this._toGridX(x), this._toGridY(y)) < 0.5;", + " }", + "", + " _getStroke(x1, y1, x2, y2, radius) {", + " const stroke = this._stroke;", + " stroke.cellWidth = this.getCellWidth();", + " stroke.cellHeight = this.getCellHeight();", + " stroke.ax = x1 - this.object.getX();", + " stroke.ay = y1 - this.object.getY();", + " stroke.bx = x2 - this.object.getX();", + " stroke.by = y2 - this.object.getY();", + " stroke.radius = Math.max(radius, 0);", + " return stroke;", + " }", + "", + " /**", + " * @param {SampleRectangle} samples", + " * @param {boolean} haveHeightsChanged false when only the paint changed.", + " */", + " _onSamplesChanged(samples, haveHeightsChanged) {", + " if (samples.isEmpty()) return;", + " this.version = ++lastGroundVersion;", + " this.changedSamples.add(samples);", + " // Painting changes the surface when it makes holes or fills them.", + " const hasSurfaceChanged = this._updateSurfaceHeights(samples);", + " if (!haveHeightsChanged && !hasSurfaceChanged) return;", + " this.surfaceVersion = this.version;", + " this.surfaceChanges.push({ version: this.version, samples: new SampleRectangle().set(samples.minI, samples.minJ, samples.maxI, samples.maxJ) });", + " if (this.surfaceChanges.length > MAX_SURFACE_CHANGES) {", + " this.surfaceChangesStartVersion = this.surfaceChanges.shift().version;", + " }", + " for (const bodyUpdater of this.bodyUpdaters) {", + " bodyUpdater.changedSamples.add(samples);", + " }", + " }", + "", + " /**", + " * @param {SampleRectangle} samples", + " * @returns {boolean} true if heights of the surface changed, or holes were made or filled.", + " */", + " _updateSurfaceHeights(samples) {", + " const { data, surfaceHeights } = this;", + " let hasChanged = false;", + " let hasHoles = false;", + " for (let j = samples.minJ; j <= samples.maxJ; j++) {", + " for (let i = samples.minI; i <= samples.maxI; i++) {", + " const index = j * data.size + i;", + " const isHole = data.isHole(index);", + " const height = isHole ? NaN : data.heights[index];", + " const previousHeight = surfaceHeights[index];", + " if (isHole) {", + " hasHoles = true;", + " if (Number.isNaN(previousHeight)) continue;", + " } else if (height === previousHeight) {", + " continue;", + " }", + " surfaceHeights[index] = height;", + " hasChanged = true;", + " }", + " }", + " // Filled holes keep the shaders drawing holes until the data is loaded again.", + " if (hasHoles) this.renderer.setHasHoles(true);", + " return hasChanged;", + " }", + "", + " /** @param {number} height In scene units, negative to lower the ground. */", + " raise(x1, y1, x2, y2, radius, height) {", + " const samples = this._strokeChangedSamples;", + " brushes.raise(this.data, this._getStroke(x1, y1, x2, y2, radius), height / this.object.getDepth(), samples);", + " this._onSamplesChanged(samples, true);", + " }", + "", + " /** @param {number} z The Z position to flatten the ground to. */", + " /** @param {number} [toZ] The Z position at the end of the segment, for a slope. The same as z by default. */", + " flatten(x1, y1, x2, y2, radius, z, strength, toZ = z) {", + " const samples = this._strokeChangedSamples;", + " const toHeight = (value) => (value - this.object.getZ()) / this.object.getDepth();", + " brushes.flatten(this.data, this._getStroke(x1, y1, x2, y2, radius), toHeight(z), strength, samples, toHeight(toZ));", + " this._onSamplesChanged(samples, true);", + " }", + "", + " smooth(x1, y1, x2, y2, radius, strength) {", + " const samples = this._strokeChangedSamples;", + " brushes.smooth(this.data, this._getStroke(x1, y1, x2, y2, radius), strength, samples);", + " this._onSamplesChanged(samples, true);", + " }", + "", + " /** @param {number} layer From 1 to 4. */", + " paint(x1, y1, x2, y2, radius, layer, strength) {", + " const layerIndex = clampInteger(layer - 1, 0, LAYER_COUNT - 1);", + " const samples = this._strokeChangedSamples;", + " brushes.paint(this.data, this._getStroke(x1, y1, x2, y2, radius), layerIndex, strength, samples);", + " this.hasPaintChanged = true;", + " this._onSamplesChanged(samples, false);", + " }", + "", + " /** @param {number[]} color The red, green and blue (0-255) of the tint: white removes it. */", + " tint(x1, y1, x2, y2, radius, color, strength) {", + " const samples = this._strokeChangedSamples;", + " brushes.tint(this.data, this._getStroke(x1, y1, x2, y2, radius), color, strength, samples);", + " this.hasPaintChanged = true;", + " this._onSamplesChanged(samples, false);", + " }", + "", + " /** @param {number} wetness From 0 (dry) to 1. */", + " wet(x1, y1, x2, y2, radius, wetness, strength) {", + " const samples = this._strokeChangedSamples;", + " brushes.wet(this.data, this._getStroke(x1, y1, x2, y2, radius), wetness, strength, samples);", + " this.hasPaintChanged = true;", + " this._onSamplesChanged(samples, false);", + " }", + "", + " /** Make a hole around a segment (`fillHoles` fills it again). */", + " makeHole(x1, y1, x2, y2, radius) {", + " const samples = this._strokeChangedSamples;", + " brushes.hole(this.data, this._getStroke(x1, y1, x2, y2, radius), samples);", + " this.hasPaintChanged = true;", + " this._onSamplesChanged(samples, false);", + " }", + "", + " /** Fill the holes around a segment with the layers of the ground around them. */", + " fillHoles(x1, y1, x2, y2, radius) {", + " const samples = this._strokeChangedSamples;", + " brushes.fill(this.data, this._getStroke(x1, y1, x2, y2, radius), samples);", + " this.hasPaintChanged = true;", + " this._onSamplesChanged(samples, false);", + " }", + "", + " /**", + " * Make the entrance of a tunnel or a cave: a hole, with the ground in", + " * front of it flattened to the floor of the tunnel.", + " * @param {number} angle The direction going into the ground, in degrees.", + " * @param {number} floorZ", + " * @param {number} length The length of the flat ground in front of the entrance.", + " */", + " carveEntrance(x, y, radius, angle, floorZ, length) {", + " const directionX = Math.cos(gdjs.toRad(angle));", + " const directionY = Math.sin(gdjs.toRad(angle));", + " this.flatten(x - directionX * length, y - directionY * length, x, y, radius * 1.5, floorZ, 1);", + " this.makeHole(x, y, x + directionX * radius, y + directionY * radius, radius);", + " }", + "", + " generate(reliefName, seed) {", + " this.baseData = new TerrainData(this.data.resolution);", + " relief.generate(this.baseData, reliefName, seed);", + " this.setData(copyTerrainData(this.baseData));", + " }", + "", + " loadHeightmap(imageResourceName) {", + " const baseData = new TerrainData(this.data.resolution);", + " if (loadHeightsFromImage(baseData, this.object.getRuntimeScene().getGame(), imageResourceName)) {", + " baseData.splat.set(this.data.splat);", + " baseData.colors.set(this.data.colors);", + " this.baseData = baseData;", + " this.setData(copyTerrainData(baseData));", + " }", + " }", + "", + " /** @param {gdjs.RuntimeObject[]} objects */", + " placeOnGround(objects) {", + " for (const object of objects) {", + " if (object === this.object || !gdjs.Base3DHandler.is3D(object)) continue;", + " const groundZ = this.getHeightAt(object.getCenterXInScene(), object.getCenterYInScene());", + " // The origin of the object is not always at its bottom.", + " object.setZ(groundZ + object.getZ() - object.getUnrotatedAABBMinZ());", + " }", + " }", + "", + " /**", + " * @returns {THREE.Vector3 | null} The point of the ground under the cursor,", + " * in the terrain group coordinates. Holes are hit too, so that brushes can fill them.", + " */", + " raycastFromCursor(game, raycaster, ndc) {", + " const layer = this.object.getInstanceContainer().getLayer(this.object.getLayer());", + " const camera = layer.getRenderer().getThreeCamera();", + " if (!camera) return null;", + " const inputManager = game.getInputManager();", + " ndc.set(", + " (inputManager.getCursorX() / game.getGameResolutionWidth()) * 2 - 1,", + " -(inputManager.getCursorY() / game.getGameResolutionHeight()) * 2 + 1", + " );", + " raycaster.setFromCamera(ndc, camera);", + " this.renderer.group.updateWorldMatrix(true, false);", + " const ray = raycaster.ray.clone().applyMatrix4(this.renderer.group.matrixWorld.clone().invert());", + " ray.direction.normalize();", + "", + " const box = new THREE.Box3(new THREE.Vector3(0, 0, 0), new THREE.Vector3(AREA_SIZE, AREA_SIZE, AREA_DEPTH));", + " const entry = box.containsPoint(ray.origin) ? ray.origin.clone() : ray.intersectBox(box, new THREE.Vector3());", + " if (!entry) return null;", + " const cellSize = AREA_SIZE / this.data.resolution;", + " const groundZAt = (point) => this.data.sampleHeight(point.x / cellSize, point.y / cellSize) * AREA_DEPTH;", + " const step = cellSize / 2;", + " const point = entry.clone();", + " const previousPoint = entry.clone();", + " for (let distance = 0; distance < AREA_SIZE * 2; distance += step) {", + " if (point.z <= groundZAt(point)) {", + " // Refine between the last point above the ground and this one.", + " for (let iteration = 0; iteration < 8; iteration++) {", + " const middle = previousPoint.clone().lerp(point, 0.5);", + " if (middle.z <= groundZAt(middle)) point.copy(middle);", + " else previousPoint.copy(middle);", + " }", + " return point;", + " }", + " if (!box.containsPoint(point)) return null;", + " previousPoint.copy(point);", + " point.addScaledVector(ray.direction, step);", + " }", + " return null;", + " }", + "", + " /**", + " * @param {THREE.Vector3} localPosition In the terrain group coordinates (like from `raycastFromCursor`).", + " * @returns {{x: number, y: number, z: number}} The position in the scene.", + " */", + " toScenePosition(localPosition) {", + " const { object } = this;", + " return {", + " x: object.getX() + (localPosition.x / AREA_SIZE) * object.getWidth(),", + " y: object.getY() + (localPosition.y / AREA_SIZE) * object.getHeight(),", + " z: object.getZ() + (localPosition.z / AREA_DEPTH) * object.getDepth(),", + " };", + " }", + "", + " /** Draw the changes of the ground (collisions are updated by the physics engine). */", + " applyChanges() {", + " this.renderer.updateSamples(this.changedSamples, this.hasPaintChanged);", + " this.changedSamples.clear();", + " this.hasPaintChanged = false;", + " }", + "", + " /** @returns {number} A number changing each time the ground changes (see `getGrounds`). */", + " getVersion() {", + " return this.version;", + " }", + "", + " /**", + " * @param {number} sinceVersion", + " * @returns {gdjs.SurfaceArea | null} The part of the ground whose shape changed since the version (see `gdjs.Surface`).", + " */", + " getChangedArea(sinceVersion) {", + " if (sinceVersion < this.surfaceChangesStartVersion) return null;", + " const samples = new SampleRectangle();", + " for (const change of this.surfaceChanges) {", + " if (change.version > sinceVersion) samples.add(change.samples);", + " }", + " if (samples.isEmpty()) return { minX: 0, minY: 0, maxX: 0, maxY: 0 };", + " const { resolution } = this.data;", + " return {", + " minX: samples.minI / resolution,", + " minY: samples.minJ / resolution,", + " maxX: samples.maxI / resolution,", + " maxY: samples.maxJ / resolution,", + " };", + " }", + "", + " _getTransform() {", + " const { object } = this;", + " return [object.getX(), object.getY(), object.getZ(), object.getWidth(), object.getHeight(), object.getDepth()].join(',');", + " }", + "", + " /** @param {string} imageResourceName A heightmap image that is not loaded yet. */", + " _reloadOnceLoaded(imageResourceName) {", + " if (this.loadingHeightmapImage === imageResourceName) return;", + " this.loadingHeightmapImage = imageResourceName;", + " const game = this.object.getRuntimeScene().getGame();", + " game.getImageManager()", + " .loadResource(imageResourceName)", + " .then(() => {", + " if (this.isDisposed || this.loadingHeightmapImage !== imageResourceName) return;", + " this.loadingHeightmapImage = '';", + " if (getLoadedImage(game, imageResourceName)) this.loadFromProperties();", + " })", + " .catch(() => {});", + " }", + "", + " update() {", + " const transform = this._getTransform();", + " if (transform !== this._transform) {", + " this._transform = transform;", + " this.version = ++lastGroundVersion;", + " }", + " this.applyChanges();", + " this.renderer.uniforms.terrainWorldScale.value = getWorldScale(this.object);", + " this._updateWetShore();", + " const layer = this.object.getInstanceContainer().getLayer(this.object.getLayer());", + " this.renderer.updateLevelsOfDetail(layer.getRenderer().getThreeCamera());", + " }", + "", + " _updateWetShore() {", + " const { object } = this;", + " // In the coordinates of the terrain group, like the vertices.", + " const depthScale = AREA_DEPTH / Math.max(object.getDepth(), 0.0001);", + " this.renderer.uniforms.terrainWetShore.value.set(", + " (object._getWetShoreZ() - object.getZ()) * depthScale,", + " Math.max(object._getWetShoreFade(), 0.001) * depthScale,", + " object._getWetShore() ? 1 : 0", + " );", + " }", + "", + " dispose() {", + " this.isDisposed = true;", + " getGrounds(this.object.getRuntimeScene()).delete(this);", + " if (typeof this.object.setSurface === 'function' && this.object.getSurface() === this.surface) {", + " this.object.setSurface(null);", + " }", + " this.renderer.dispose();", + " for (const bodyUpdater of this.bodyUpdaters) {", + " bodyUpdater.dispose();", + " }", + " }", + "}", + "", + "/**", + " * @param {gdjs.RuntimeObject} object", + " * @returns {number} The number of scene units by 3D world unit (1 in GDevelop versions before the world scale).", + " */", + "const getWorldScale = (object) => {", + " const scene = object.getRuntimeScene();", + " return typeof scene.getRenderer3DWorldScale === 'function' ? scene.getRenderer3DWorldScale() : 1;", + "};", + "", + "const averageColors = new WeakMap();", + "let white = null;", + "/**", + " * @param {THREE.Texture} texture", + " * @returns {THREE.Color} The average color of the texture (white when it's not loaded).", + " */", + "const getAverageColor = (texture) => {", + " const cachedColor = averageColors.get(texture);", + " if (cachedColor) return cachedColor;", + " const image = texture.image;", + " const isDrawable =", + " typeof document !== 'undefined' &&", + " ((image instanceof HTMLImageElement && image.complete && image.naturalWidth > 0) ||", + " image instanceof HTMLCanvasElement ||", + " (typeof ImageBitmap !== 'undefined' && image instanceof ImageBitmap));", + " if (!isDrawable) return white || (white = new THREE.Color(1, 1, 1));", + " const size = 8;", + " const canvas = document.createElement('canvas');", + " canvas.width = size;", + " canvas.height = size;", + " const context = canvas.getContext('2d');", + " context.drawImage(image, 0, 0, size, size);", + " const pixels = context.getImageData(0, 0, size, size).data;", + " let red = 0;", + " let green = 0;", + " let blue = 0;", + " for (let index = 0; index < pixels.length; index += 4) {", + " red += pixels[index];", + " green += pixels[index + 1];", + " blue += pixels[index + 2];", + " }", + " const count = size * size * 255;", + " const color = new THREE.Color().setRGB(red / count, green / count, blue / count, THREE.SRGBColorSpace);", + " averageColors.set(texture, color);", + " return color;", + "};", + "", + "const svgIcon = (content) =>", + " 'data:image/svg+xml,' +", + " encodeURIComponent('' + content + '');", + "const strokes = 'fill=\"none\" stroke=\"#000\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"';", + "const toolIcons = {", + " Raise: svgIcon(''),", + " Lower: svgIcon(''),", + " Smooth: svgIcon(''),", + " Flatten: svgIcon(''),", + " Size: svgIcon(''),", + " Strength: svgIcon(''),", + " Undo: svgIcon(''),", + " Hole: svgIcon(''),", + " FillHoles: svgIcon(''),", + " Wetness: svgIcon(''),", + "};", + "// Tints for the color brush when the editor has no color picker (older GDevelop versions).", + "const tintPresets = ['#e2c879', '#a5c45a', '#8a6a48', '#c98a62', '#9a9a92', '#ffffff'];", + "", + "/**", + " * The sculpt and paint tools shown in the scene editor when a terrain is", + " * selected. Without the needed editor functions (older GDevelop versions),", + " * nothing is shown: the terrain can still be changed with events.", + " */", + "class TerrainEditorTools {", + " constructor() {", + " /** '' when no brush is used. */", + " this.toolName = '';", + " this.sizePercent = 30;", + " this.strengthPercent = 50;", + " /** @type {Terrain | null} */", + " this.terrain = null;", + " /** @type {gdjs.InGameEditor | null} */", + " this.editor = null;", + " this.isStroking = false;", + " this.flattenHeight = 0;", + " /** The color of the \"Tint\" brush, as \"#rrggbb\". */", + " this.tintColor = tintPresets[0];", + " this.wetnessPercent = 100;", + " this.lastStrokeTime = 0;", + " /** @type {{data: TerrainData, baseData: TerrainData}[]} */", + " this.undoSteps = [];", + " this.raycaster = new THREE.Raycaster();", + " this.ndc = new THREE.Vector2();", + " this._swatchColor = new THREE.Color();", + " }", + "", + " /** @param {gdjs.InGameEditor} editor */", + " update(editor) {", + " if (typeof editor.showToolbar !== 'function') {", + " // Older GDevelop versions: the terrain can only be changed with events.", + " return;", + " }", + " this.editor = editor;", + " const selectedObjects = editor.getSelectedObjects();", + " const terrain =", + " selectedObjects.length === 1 && selectedObjects[0].__terrain3D ? selectedObjects[0].__terrain3D : null;", + " if (terrain !== this.terrain) {", + " this._endStroke();", + " if (this.terrain) this.terrain.renderer.setBrush(null, 0);", + " this.terrain = terrain;", + " this.undoSteps.length = 0;", + " }", + " if (terrain) editor.showToolbar('Terrain3D', this._getToolbarItems(terrain));", + " const game = editor.getRuntimeGame();", + " if (!terrain || !this.toolName) {", + " if (terrain) terrain.renderer.setBrush(null, 0);", + " return;", + " }", + "", + " const inputManager = game.getInputManager();", + " if (inputManager.wasKeyJustPressed(gdjs.evtTools.input.keysNameToCode.Escape)) {", + " this._endStroke();", + " this.toolName = '';", + " terrain.renderer.setBrush(null, 0);", + " return;", + " }", + " editor.captureLeftMouseButton();", + " const hit = terrain.raycastFromCursor(game, this.raycaster, this.ndc);", + " const radius = this._getRadius();", + " terrain.renderer.setBrush(hit, radius);", + "", + " // Older GDevelop versions don't have it.", + " const isPressedForTool =", + " typeof editor.isCapturedLeftMouseButtonPressed === 'function'", + " ? editor.isCapturedLeftMouseButtonPressed()", + " : inputManager.isMouseButtonPressed(0);", + " // The button is released outside the canvas when the mouse is on a toolbar.", + " const isPressed = isPressedForTool && inputManager.isMouseInsideCanvas();", + " if (isPressed && hit && !this.isStroking) {", + " this._startStroke(terrain, hit);", + " }", + " if (this.isStroking && isPressed && hit) {", + " this._applyBrush(terrain, hit, radius);", + " }", + " if (!isPressed) {", + " this._endStroke();", + " }", + " }", + "", + " _getRadius() {", + " const { object } = this.terrain;", + " const maxRadius = Math.max(object.getWidth(), object.getHeight()) / 4;", + " return Math.max(maxRadius * Math.pow(this.sizePercent / 100, 2), this.terrain.getCellWidth());", + " }", + "", + " _startStroke(terrain, hit) {", + " this.isStroking = true;", + " this.lastStrokeTime = performance.now();", + " this.flattenHeight = terrain.toScenePosition(hit).z;", + " this.undoSteps.push({ data: copyTerrainData(terrain.data), baseData: terrain.baseData });", + " const maxUndoSteps = Math.max(Math.floor(UNDO_STEPS_SAMPLES / terrain.data.heights.length), 2);", + " while (this.undoSteps.length > maxUndoSteps) this.undoSteps.shift();", + " }", + "", + " _endStroke() {", + " if (!this.isStroking) return;", + " this.isStroking = false;", + " this._save();", + " }", + "", + " _applyBrush(terrain, hit, radius) {", + " const now = performance.now();", + " const elapsedSeconds = Math.min((now - this.lastStrokeTime) / 1000, 0.1);", + " this.lastStrokeTime = now;", + " const { x, y } = terrain.toScenePosition(hit);", + " const amount = (this.strengthPercent / 100) * elapsedSeconds;", + " const depth = terrain.object.getDepth();", + " if (this.toolName === 'Raise') terrain.raise(x, y, x, y, radius, amount * depth * 0.5);", + " else if (this.toolName === 'Lower') terrain.raise(x, y, x, y, radius, -amount * depth * 0.5);", + " else if (this.toolName === 'Smooth') terrain.smooth(x, y, x, y, radius, amount * 10);", + " else if (this.toolName === 'Flatten') terrain.flatten(x, y, x, y, radius, this.flattenHeight, amount * 10);", + " else if (this.toolName.startsWith('Paint')) {", + " terrain.paint(x, y, x, y, radius, parseInt(this.toolName.slice(5), 10), amount * 8);", + " } else if (this.toolName === 'Hole') {", + " terrain.makeHole(x, y, x, y, radius);", + " } else if (this.toolName === 'FillHoles') {", + " terrain.fillHoles(x, y, x, y, radius);", + " } else if (this.toolName === 'Tint') {", + " terrain.tint(x, y, x, y, radius, gdjs.rgbOrHexToRGBColor(this.tintColor), amount * 8);", + " } else if (this.toolName === 'Wetness') {", + " terrain.wet(x, y, x, y, radius, this.wetnessPercent / 100, amount * 8);", + " }", + " // Drawn now, as the terrain was updated earlier in this frame.", + " terrain.applyChanges();", + " }", + "", + " _save() {", + " const { terrain, editor } = this;", + " if (!terrain || !editor) return;", + " editor.updateObjectProperties(terrain.object.getName(), { SculptData: terrain.save() });", + " }", + "", + " _undo() {", + " const undoStep = this.undoSteps.pop();", + " // The relief, the edits or the resolution may have changed since the stroke.", + " if (!this.terrain || !undoStep || undoStep.baseData !== this.terrain.baseData) {", + " this.undoSteps.length = 0;", + " return;", + " }", + " this.terrain.setData(undoStep.data);", + " this._save();", + " }", + "", + " /** @returns {Array} The settings of the tint or wetness brush, when it's used. */", + " _getColorToolItems() {", + " const { editor } = this;", + " if (this.toolName === 'Tint') {", + " if (editor && typeof editor.isToolbarItemTypeSupported === 'function' && editor.isToolbarItemTypeSupported('color')) {", + " return [", + " {", + " type: 'color',", + " id: 'TintColor',", + " tooltip: 'Color of the tint',", + " value: this.tintColor,", + " onChange: (color) => (this.tintColor = color),", + " },", + " ];", + " }", + " return tintPresets.map((color) => ({", + " type: 'button',", + " id: 'TintPreset' + color,", + " tooltip: color === '#ffffff' ? 'No tint' : 'Tint with this color',", + " color,", + " isActive: this.tintColor === color,", + " onClick: () => (this.tintColor = color),", + " }));", + " }", + " if (this.toolName === 'Wetness') {", + " return [", + " {", + " type: 'slider',", + " id: 'WetnessAmount',", + " tooltip: 'Wetness',", + " iconUrl: toolIcons.Wetness,", + " min: 0,", + " max: 100,", + " value: this.wetnessPercent,", + " onChange: (value) => (this.wetnessPercent = value),", + " },", + " ];", + " }", + " return [];", + " }", + "", + " /**", + " * @param {Terrain} terrain", + " * @returns {Array} The toolbar for the current state (the editor only", + " * updates what changed).", + " */", + " _getToolbarItems(terrain) {", + " const { object } = terrain;", + " const toolButton = (toolName, tooltip, color) => ({", + " type: 'button',", + " id: toolName,", + " tooltip,", + " iconUrl: toolIcons[toolName],", + " color,", + " isActive: this.toolName === toolName,", + " onClick: () => (this.toolName = this.toolName === toolName ? '' : toolName),", + " });", + " const layerColors = [object._getLayer1Color(), object._getLayer2Color(), object._getLayer3Color(), object._getLayer4Color()];", + " // Layers are shown with their color, tinting their texture.", + " const swatchColors = layerColors.map((color, index) =>", + " this._swatchColor", + " .set(gdjs.rgbOrHexStringToNumber(color))", + " .multiply(getAverageColor(terrain.renderer.uniforms['terrainLayerMap' + index].value))", + " .getHexString()", + " );", + " return [", + " toolButton('Raise', 'Raise the ground (drag on the terrain)'),", + " toolButton('Lower', 'Lower the ground'),", + " toolButton('Smooth', 'Smooth the ground'),", + " toolButton('Flatten', 'Flatten the ground to the height where the drag starts'),", + " { type: 'divider', id: 'PaintDivider' },", + " ...swatchColors.map((swatchColor, index) =>", + " toolButton('Paint' + (index + 1), 'Paint layer ' + (index + 1), '#' + swatchColor)", + " ),", + " toolButton('Hole', 'Make a hole, for the entrance of a tunnel or a cave'),", + " toolButton('FillHoles', 'Fill holes, with the layers of the ground around them'),", + " { type: 'divider', id: 'ColorDivider' },", + " toolButton('Tint', 'Tint the ground (white removes the tint)', this.tintColor),", + " toolButton('Wetness', 'Make the ground wet, or dry with a wetness of 0'),", + " ...this._getColorToolItems(),", + " { type: 'divider', id: 'BrushDivider' },", + " {", + " type: 'slider',", + " id: 'Size',", + " tooltip: 'Brush size',", + " iconUrl: toolIcons.Size,", + " min: 1,", + " max: 100,", + " value: this.sizePercent,", + " onChange: (value) => (this.sizePercent = value),", + " },", + " {", + " type: 'slider',", + " id: 'Strength',", + " tooltip: 'Brush strength',", + " iconUrl: toolIcons.Strength,", + " min: 5,", + " max: 100,", + " value: this.strengthPercent,", + " onChange: (value) => (this.strengthPercent = value),", + " },", + " { type: 'button', id: 'Undo', tooltip: 'Undo the last stroke on this terrain', iconUrl: toolIcons.Undo, onClick: () => this._undo() },", + " ];", + " }", + "}", + "", + "const editorTools = new TerrainEditorTools();", + "if (gdjs.registerInGameEditorPostStepCallback) {", + " gdjs.registerInGameEditorPostStepCallback((editor) => editorTools.update(editor));", + "}", + "", + "gdjs.__terrain3DExtension = {", + " Terrain,", + " // Used by the terrain grass and the scattered models.", + " getWorldScale,", + " getGrounds,", + " patchShaderCode,", + " createRandom,", + " findHighestGround,", + " GroundPlacement,", + " svgIcon,", + " toolIcons,", + " // Exposed for tests.", + " TerrainBodyUpdater,", + " TerrainData,", + " SampleRectangle,", + " brushes,", + " edits,", + " codec,", + " relief,", + "};" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [], + "objectGroups": [] + }, + { + "description": "Define grass helper classes JavaScript code.", + "fullName": "Define grass helper classes", + "functionType": "Action", + "name": "DefineGrassHelperClasses", + "private": true, + "sentence": "Define grass helper classes JavaScript code", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "if (gdjs.__terrainGrassExtension) {", + " return;", + "}", + "// Blades grow on the grounds of the scene: the 3D terrains (see `getGrounds`", + "// in the terrain helper).", + "", + "// Size of the object inner area: instances are scaled from it.", + "const AREA_SIZE = 1000;", + "const AREA_DEPTH = 40;", + "const CHUNK_SIZE = 500;", + "const MAX_BENDING_OBJECTS = 8;", + "", + "/**", + " * A blade: a thin, tapered strip. Its width and height are 1: blades are", + " * scaled by the shader.", + " */", + "const createBladeGeometry = () => {", + " const halfWidths = [0.5, 0.42, 0.28, 0];", + " const heights = [0, 0.35, 0.7, 1];", + " const positions = [];", + " halfWidths.forEach((halfWidth, level) => {", + " positions.push(-halfWidth, 0, heights[level]);", + " if (halfWidth > 0) positions.push(halfWidth, 0, heights[level]);", + " });", + " const indices = [0, 1, 2, 1, 3, 2, 2, 3, 4, 3, 5, 4, 4, 5, 6];", + " const geometry = new THREE.InstancedBufferGeometry();", + " geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));", + " geometry.setAttribute('normal', new THREE.Float32BufferAttribute(new Array(positions.length).fill(0), 3));", + " geometry.setIndex(indices);", + " return geometry;", + "};", + "", + "const vertexShaderDeclarations = `", + "attribute vec4 grassRoot;", + "attribute vec2 grassShape;", + "// The tint of the ground at the root (linear, from 0 to 1).", + "attribute vec3 grassGroundTint;", + "uniform mat3 grassWorldToLocal;", + "uniform float grassWorldScale;", + "uniform float grassBladeWidth;", + "uniform float grassTime;", + "uniform float grassWindStrength;", + "uniform float grassWindSpeed;", + "uniform vec4 grassBenders[${MAX_BENDING_OBJECTS}];", + "uniform vec3 grassBaseColor;", + "uniform vec3 grassTipColor;", + "varying vec3 vGrassColor;", + "`;", + "const vertexShaderPosition = `", + "// Blades are shaped in scene units (with Y flipped like the 3D world), then", + "// moved back in the (scaled) object space.", + "float bladeHeight = position.z;", + "float grassAngle = grassRoot.w;", + "float bladeWidth = position.x * grassBladeWidth;", + "vec3 blade = vec3(", + " bladeWidth * cos(grassAngle),", + " bladeWidth * sin(grassAngle),", + " bladeHeight * grassShape.x", + ");", + "vec3 rootWorld = (modelMatrix * vec4(grassRoot.xyz, 1.0)).xyz * grassWorldScale;", + "// Tips bend more than the base.", + "float bendFactor = bladeHeight * bladeHeight * grassShape.x;", + "float gust = sin(grassTime * grassWindSpeed + rootWorld.x * 0.013 + rootWorld.y * 0.011);", + "float flutter = sin(grassTime * grassWindSpeed * 2.7 + grassShape.y * 40.0) * 0.3;", + "blade.xy += vec2(0.8, 0.5) * (gust + flutter + 0.6) * grassWindStrength * 0.35 * bendFactor;", + "for (int i = 0; i < ${MAX_BENDING_OBJECTS}; i++) {", + " vec4 bender = grassBenders[i];", + " if (bender.z <= 0.0) continue;", + " vec2 away = rootWorld.xy - bender.xy;", + " float distanceToBender = length(away);", + " float push = 1.0 - smoothstep(bender.z * 0.3, bender.z, distanceToBender);", + " if (push > 0.0) {", + " blade.xy += normalize(away + vec2(0.001)) * push * bendFactor * 0.9;", + " blade.z *= 1.0 - push * 0.5;", + " }", + "}", + "vec3 transformed = grassRoot.xyz + grassWorldToLocal * (blade / grassWorldScale);", + "vGrassColor = mix(grassBaseColor, grassTipColor, bladeHeight) * (0.85 + 0.3 * grassShape.y) * grassGroundTint;", + "`;", + "const vertexShaderNormal = `", + "// Blades are lit like the ground below them.", + "transformedNormal = normalize((viewMatrix * vec4(0.0, 0.0, 1.0, 0.0)).xyz);", + "`;", + "const fragmentShaderDeclarations = `", + "varying vec3 vGrassColor;", + "`;", + "const fragmentShaderColor = `", + "diffuseColor.rgb *= vGrassColor;", + "`;", + "// Both faces of blades are lit like the ground (not flipped for back faces).", + "const fragmentShaderNormal = `", + "#include ", + "normal = normalize(vNormal);", + "nonPerturbedNormal = normal;", + "`;", + "", + "const { getWorldScale, patchShaderCode, createRandom, findHighestGround, GroundPlacement } = gdjs.__terrain3DExtension;", + "", + "/**", + " * The grass of a terrain grass object: blades drawn with GPU instancing, in chunks", + " * that are hidden when off-screen and thinned out with the distance. They grow", + " * on the grounds below, like 3D terrains (only where a given layer is painted).", + " */", + "class Grass {", + " /** @param {gdjs.CustomRuntimeObject3D} object */", + " constructor(object) {", + " this.object = object;", + " this.group = new THREE.Group();", + " object.get3DRendererObject().add(this.group);", + " this.bladeGeometry = createBladeGeometry();", + " /** @type {Array<{mesh: THREE.Mesh, blades: number, center: THREE.Vector3}>} */", + " this.chunks = [];", + " this.placement = new GroundPlacement(object.getRuntimeScene());", + " this._groundAtBlade = { ground: null, z: 0 };", + " this.uniforms = {", + " grassWorldToLocal: { value: new THREE.Matrix3() },", + " // Scene units by 3D world unit.", + " grassWorldScale: { value: 1 },", + " grassBladeWidth: { value: 1 },", + " grassTime: { value: 0 },", + " grassWindStrength: { value: 1 },", + " grassWindSpeed: { value: 1 },", + " grassBenders: { value: Array.from({ length: MAX_BENDING_OBJECTS }, () => new THREE.Vector4()) },", + " grassBaseColor: { value: new THREE.Color() },", + " grassTipColor: { value: new THREE.Color() },", + " };", + " this.material = new THREE.MeshStandardMaterial({ roughness: 1, metalness: 0, side: THREE.DoubleSide });", + " this.material.onBeforeCompile = (shader) => {", + " Object.assign(shader.uniforms, this.uniforms);", + " shader.vertexShader = patchShaderCode(shader.vertexShader, [", + " ['#include ', '#include \\n' + vertexShaderDeclarations],", + " ['#include ', vertexShaderPosition],", + " ['#include ', '#include \\n' + vertexShaderNormal],", + " ], 'TerrainGrass');", + " shader.fragmentShader = patchShaderCode(shader.fragmentShader, [", + " ['#include ', '#include \\n' + fragmentShaderDeclarations],", + " ['#include ', '#include \\n' + fragmentShaderColor],", + " ['#include ', fragmentShaderNormal],", + " ], 'TerrainGrass');", + " };", + " this.material.customProgramCacheKey = () => 'TerrainGrass';", + "", + " this._matrix = new THREE.Matrix4();", + " this._cameraPosition = new THREE.Vector3();", + " this._chunkPosition = new THREE.Vector3();", + " this._scale = new THREE.Vector3();", + " this.updateFromProperties();", + " }", + "", + " updateFromProperties() {", + " const { object } = this;", + " this.uniforms.grassBaseColor.value.set(gdjs.rgbOrHexStringToNumber(object._getBaseColor()));", + " this.uniforms.grassTipColor.value.set(gdjs.rgbOrHexStringToNumber(object._getTipColor()));", + " this.uniforms.grassWindStrength.value = object._getWindStrength();", + " this.uniforms.grassWindSpeed.value = object._getWindSpeed();", + " this.uniforms.grassBladeWidth.value = Math.max(object._getBladeWidth(), 0);", + " this.material.needsUpdate = true;", + " this.placement.placeAgain();", + " }", + "", + " _getPlacementKey() {", + " const { object } = this;", + " return [", + " object.getX(), object.getY(), object.getZ(), object.getWidth(), object.getHeight(), object.getDepth(),", + " object._getDensity(), object._getBladeHeight(), object._getGroundLayer(), object._getMaxSlope(),", + " object._getSeed(),", + " ].join('|');", + " }", + "", + " /** Place the blades again, for example after the ground or the object changed. */", + " rebuild() {", + " const grounds = Array.from(this.placement.grounds);", + " for (const chunk of this.chunks) {", + " chunk.mesh.removeFromParent();", + " chunk.mesh.geometry.dispose();", + " }", + " this.chunks.length = 0;", + "", + " const { object } = this;", + " const width = object.getWidth();", + " const height = object.getHeight();", + " const scaleX = width / AREA_SIZE;", + " const scaleY = height / AREA_SIZE;", + " const scaleZ = Math.max(object.getDepth() / AREA_DEPTH, 0.0001);", + " const bladesPerChunk = Math.round((Math.max(object._getDensity(), 0) * CHUNK_SIZE * CHUNK_SIZE) / 10000);", + " const groundLayer = parseInt(object._getGroundLayer(), 10) || 0;", + " const maxSlope = object._getMaxSlope();", + " const chunksX = Math.max(Math.ceil(width / CHUNK_SIZE), 1);", + " const chunksY = Math.max(Math.ceil(height / CHUNK_SIZE), 1);", + " const nextRandom = createRandom(object._getSeed() * 7919 + 1);", + " const bladeHeight = Math.max(object._getBladeHeight(), 0);", + "", + " for (let chunkY = 0; chunkY < chunksY; chunkY++) {", + " for (let chunkX = 0; chunkX < chunksX; chunkX++) {", + " const roots = new Float32Array(bladesPerChunk * 4);", + " const shapes = new Float32Array(bladesPerChunk * 2);", + " const groundTints = new Uint8Array(bladesPerChunk * 3).fill(255);", + " let blades = 0;", + " let minZ = Infinity;", + " let maxZ = -Infinity;", + " for (let attempt = 0; attempt < bladesPerChunk; attempt++) {", + " // In scene units, relative to the object.", + " const x = (chunkX + nextRandom()) * CHUNK_SIZE;", + " const y = (chunkY + nextRandom()) * CHUNK_SIZE;", + " const angle = nextRandom() * Math.PI * 2;", + " const heightFactor = 0.6 + 0.8 * nextRandom();", + " const tint = nextRandom();", + " const growth = nextRandom();", + " if (x > width || y > height) continue;", + " const sceneX = object.getX() + x;", + " const sceneY = object.getY() + y;", + " const { ground, z } = findHighestGround(grounds, sceneX, sceneY, this._groundAtBlade);", + " const groundZ = ground ? z : object.getZ();", + " if (ground) {", + " if (groundLayer > 0 && growth > ground.getLayerWeightAt(sceneX, sceneY, groundLayer)) continue;", + " if (ground.getSlopeAt(sceneX, sceneY) > maxSlope) continue;", + " if (typeof ground.getTintAt === 'function') {", + " const tint = ground.getTintAt(sceneX, sceneY);", + " for (let channel = 0; channel < 3; channel++) {", + " groundTints[blades * 3 + channel] = Math.round(255 * Math.pow(tint[channel] / 255, 2.2));", + " }", + " }", + " }", + " const localZ = (groundZ - object.getZ()) / scaleZ;", + " roots[blades * 4] = x / scaleX;", + " roots[blades * 4 + 1] = y / scaleY;", + " roots[blades * 4 + 2] = localZ;", + " roots[blades * 4 + 3] = angle;", + " shapes[blades * 2] = heightFactor * bladeHeight;", + " shapes[blades * 2 + 1] = tint;", + " minZ = Math.min(minZ, localZ);", + " maxZ = Math.max(maxZ, localZ);", + " blades++;", + " }", + " if (blades === 0) continue;", + "", + " const geometry = new THREE.InstancedBufferGeometry();", + " geometry.index = this.bladeGeometry.index;", + " geometry.setAttribute('position', this.bladeGeometry.attributes.position);", + " geometry.setAttribute('normal', this.bladeGeometry.attributes.normal);", + " geometry.setAttribute('grassRoot', new THREE.InstancedBufferAttribute(roots.subarray(0, blades * 4), 4));", + " geometry.setAttribute('grassShape', new THREE.InstancedBufferAttribute(shapes.subarray(0, blades * 2), 2));", + " geometry.setAttribute('grassGroundTint', new THREE.InstancedBufferAttribute(groundTints.subarray(0, blades * 3), 3, true));", + " geometry.instanceCount = blades;", + " // The furthest tips can go with the wind and the bending objects (see the shader).", + " const maxBladeHeight = bladeHeight * 1.4;", + " const lean =", + " (0.35 * 1.9 * 0.943 * Math.max(object._getWindStrength(), 0) + 0.9) * maxBladeHeight +", + " Math.max(object._getBladeWidth(), 0) / 2;", + " const leanX = lean / scaleX;", + " const leanY = lean / scaleY;", + " geometry.boundingBox = new THREE.Box3(", + " new THREE.Vector3((chunkX * CHUNK_SIZE) / scaleX - leanX, (chunkY * CHUNK_SIZE) / scaleY - leanY, minZ),", + " new THREE.Vector3(", + " Math.min((chunkX + 1) * CHUNK_SIZE, width) / scaleX + leanX,", + " Math.min((chunkY + 1) * CHUNK_SIZE, height) / scaleY + leanY,", + " maxZ + (bladeHeight * 1.4) / scaleZ", + " )", + " );", + " geometry.boundingSphere = geometry.boundingBox.getBoundingSphere(new THREE.Sphere());", + " const mesh = new THREE.Mesh(geometry, this.material);", + " mesh.receiveShadow = true;", + " // Raycasts would only test one blade: the selection plane is used instead.", + " mesh.raycast = () => {};", + " this.group.add(mesh);", + " this.chunks.push({ mesh, blades, center: geometry.boundingSphere.center.clone() });", + " }", + " }", + " }", + "", + " _updateBendingObjects() {", + " const benders = this.uniforms.grassBenders.value;", + " const objectName = this.object._getBendingObject();", + " const bendingObjects = objectName ? this.object.getInstanceContainer().getObjects(objectName) || [] : [];", + " for (let index = 0; index < MAX_BENDING_OBJECTS; index++) {", + " const bendingObject = bendingObjects[index];", + " if (!bendingObject) {", + " benders[index].set(0, 0, 0, 0);", + " continue;", + " }", + " // Blades are pushed in the 3D scene, which is flipped on Y.", + " const radius = Math.max(bendingObject.getWidth(), bendingObject.getHeight()) * 1.2;", + " benders[index].set(bendingObject.getCenterXInScene(), -bendingObject.getCenterYInScene(), radius, 0);", + " }", + " }", + "", + " /**", + " * @param {number} elapsedSeconds", + " * @param {number} time In seconds, the same for all grass objects.", + " */", + " update(elapsedSeconds, time, camera) {", + " if (this.placement.shouldPlaceAgain(elapsedSeconds, this._getPlacementKey())) {", + " this.rebuild();", + " }", + "", + " this.uniforms.grassTime.value = time;", + " this.uniforms.grassWorldScale.value = getWorldScale(this.object);", + " // The object transformation is applied just before rendering, it's needed now.", + " this.object.getRenderer().ensureUpToDate();", + " this.group.updateWorldMatrix(true, false);", + " this.uniforms.grassWorldToLocal.value.setFromMatrix4(this._matrix.copy(this.group.matrixWorld).invert());", + " this._updateBendingObjects();", + " if (!camera) return;", + "", + " // Far chunks are hidden, and thinned out before (in 3D world units).", + " const fadeDistance = Math.max(this.object._getFadeDistance(), 1) / getWorldScale(this.object);", + " this._cameraPosition.setFromMatrixPosition(camera.matrixWorld);", + " for (const chunk of this.chunks) {", + " const distance = this._chunkPosition", + " .copy(chunk.center)", + " .applyMatrix4(this.group.matrixWorld)", + " .distanceTo(this._cameraPosition);", + " const density = gdjs.evtTools.common.clamp((fadeDistance - distance) / (fadeDistance * 0.5), 0, 1);", + " chunk.mesh.visible = density > 0;", + " chunk.mesh.geometry.instanceCount = Math.ceil(chunk.blades * density);", + " }", + " }", + "", + " dispose() {", + " for (const chunk of this.chunks) {", + " chunk.mesh.geometry.dispose();", + " }", + " this.bladeGeometry.dispose();", + " this.material.dispose();", + " this.group.removeFromParent();", + " }", + "}", + "", + "gdjs.__terrainGrassExtension = { Grass };" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [], + "objectGroups": [] + }, + { + "description": "Define scattered models helper classes JavaScript code.", + "fullName": "Define scattered models helper classes", + "functionType": "Action", + "name": "DefineScatterHelperClasses", + "private": true, + "sentence": "Define scattered models helper classes JavaScript code", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "if (gdjs.__terrainScatterExtension) {", + " return;", + "}", + "// Copies grow on the grounds of the scene, like the terrain grass (see", + "// `getGrounds` in the terrain helper).", + "", + "const {", + " getWorldScale,", + " getGrounds,", + " patchShaderCode,", + " createRandom,", + " findHighestGround,", + " GroundPlacement,", + " SampleRectangle,", + " brushes,", + " codec,", + " svgIcon,", + " toolIcons,", + "} = gdjs.__terrain3DExtension;", + "", + "// Size of the object inner area: instances are scaled from it.", + "const AREA_SIZE = 2000;", + "const AREA_DEPTH = 200;", + "// The painted density is stored in a grid of this many cells on each side,", + "// whatever the size of the object.", + "const MASK_RESOLUTION = 256;", + "const MASK_FORMAT_VERSION = '1';", + "// Copies are hidden in chunks: big chunks mean fewer draw calls, small ones", + "// hide more copies out of the screen.", + "const MIN_CHUNK_SIZE = 500;", + "const MAX_CHUNK_SIZE = 2000;", + "// Copies shrink to nothing over this part of the draw distance, instead of popping out.", + "const FADE_PART = 0.15;", + "const MAX_COPIES = 200000;", + "// Prisms of the trunk collisions.", + "const TRUNK_SIDES = 6;", + "", + "const clamp01 = (value) => (value < 0 ? 0 : value > 1 ? 1 : value);", + "", + "/**", + " * @returns {number} A number for a cell of the copies grid, changing with the seed.", + " */", + "const hashCell = (seed, i, j) => {", + " let h = Math.imul(i, 374761393) + Math.imul(j, 668265263) + Math.imul(seed, 982451653);", + " h = Math.imul(h ^ (h >>> 13), 1274126177);", + " return (h ^ (h >>> 16)) >>> 0;", + "};", + "", + "/**", + " * How dense copies are, from 0 to 1, in a grid covering the object: painted", + " * with the editor brushes, the edits or events.", + " */", + "class DensityMask {", + " /** @param {number} fillValue From 0 to 255. */", + " constructor(fillValue) {", + " // `resolution` and `size` let terrain brushes paint it.", + " this.resolution = MASK_RESOLUTION;", + " this.size = MASK_RESOLUTION + 1;", + " this.values = new Uint8Array(this.size * this.size).fill(fillValue);", + " }", + "", + " /**", + " * @param {number} u From 0 to 1, along the object width.", + " * @param {number} v From 0 to 1, along the object height.", + " * @returns {number} The density, from 0 to 1, interpolated between samples.", + " */", + " sample(u, v) {", + " const x = clamp01(u) * this.resolution;", + " const y = clamp01(v) * this.resolution;", + " const i = Math.min(Math.floor(x), this.resolution - 1);", + " const j = Math.min(Math.floor(y), this.resolution - 1);", + " const fx = x - i;", + " const fy = y - j;", + " const value = (i, j) => this.values[j * this.size + i];", + " const top = value(i, j) + fx * (value(i + 1, j) - value(i, j));", + " const bottom = value(i, j + 1) + fx * (value(i + 1, j + 1) - value(i, j + 1));", + " return (top + fy * (bottom - top)) / 255;", + " }", + "", + " /**", + " * Bring the density towards a target around a segment: fully up to 70%", + " * of the radius, then fading out (forests have dense centers and soft edges).", + " * @param {{cellWidth: number, cellHeight: number, ax: number, ay: number, bx: number, by: number, radius: number}} stroke", + " * @param {number} density The target, from 0 to 1.", + " * @param {number} strength From 0 to 1.", + " * @param {SampleRectangle} changedSamples", + " */", + " paint(stroke, density, strength, changedSamples) {", + " const { values } = this;", + " const target = Math.round(clamp01(density) * 255);", + " brushes.forEachSample(", + " this,", + " stroke,", + " (index, falloff) => {", + " // The falloff of terrain brushes is (1 - distance² / radius²)².", + " const weight = clamp01(2 * Math.sqrt(falloff));", + " // Rounded towards the target, so that light strokes still paint.", + " const change = (target - values[index]) * clamp01(weight * strength);", + " values[index] += change > 0 ? Math.ceil(change) : Math.floor(change);", + " },", + " changedSamples", + " );", + " }", + "", + " /** @returns {DensityMask} */", + " clone() {", + " const mask = new DensityMask(0);", + " mask.values.set(this.values);", + " return mask;", + " }", + "}", + "", + "/**", + " * Edits written in the Edits property: a JSON list of brush strokes, like", + " * `[{\"tool\": \"add\", \"x\": 0.5, \"y\": 0.5, \"radius\": 0.2}]`. Positions and", + " * radius are fractions of the object size, like the terrain edits.", + " */", + "const applyEdits = (mask, text) => {", + " if (!text.trim()) return;", + " let editList;", + " try {", + " editList = JSON.parse(text);", + " } catch (error) {", + " console.warn('The edits of scattered models are not valid JSON: ' + error.message);", + " return;", + " }", + " if (!Array.isArray(editList)) {", + " console.warn('The edits of scattered models must be a JSON list (like [{\"tool\": \"add\", ...}]).');", + " return;", + " }", + " const numberOr = (value, defaultValue) => (typeof value === 'number' && isFinite(value) ? value : defaultValue);", + " const changedSamples = new SampleRectangle();", + " for (const edit of editList) {", + " if (!edit || typeof edit !== 'object') continue;", + " const x = numberOr(edit.x, 0.5);", + " const y = numberOr(edit.y, 0.5);", + " const stroke = {", + " cellWidth: 1 / mask.resolution,", + " cellHeight: 1 / mask.resolution,", + " ax: x,", + " ay: y,", + " bx: numberOr(edit.toX, x),", + " by: numberOr(edit.toY, y),", + " radius: numberOr(edit.radius, 0.1),", + " };", + " const strength = clamp01(numberOr(edit.strength, 1));", + " if (edit.tool === 'add') {", + " mask.paint(stroke, numberOr(edit.density, 1), strength, changedSamples);", + " } else if (edit.tool === 'erase') {", + " mask.paint(stroke, 0, strength, changedSamples);", + " } else {", + " console.warn('Unknown tool in the edits of scattered models: \"' + edit.tool + '\" (use add or erase).');", + " }", + " }", + "};", + "", + "/**", + " * The density painted in the editor, stored as its difference with the", + " * density from the properties (the coverage and the edits), like the terrain", + " * sculpt data.", + " */", + "const maskCodec = {", + " /**", + " * @param {DensityMask} mask", + " * @param {DensityMask} baseMask", + " */", + " encode(mask, baseMask) {", + " const changes = new Int32Array(mask.values.length);", + " let hasChanges = false;", + " for (let index = 0; index < changes.length; index++) {", + " changes[index] = mask.values[index] - baseMask.values[index];", + " if (changes[index] !== 0) hasChanges = true;", + " }", + " if (!hasChanges) return '';", + " return MASK_FORMAT_VERSION + ';' + MASK_RESOLUTION + ';' + codec.encodeGrids([changes], mask.size);", + " },", + "", + " /**", + " * @param {string} text", + " * @param {DensityMask} baseMask", + " * @returns {DensityMask | null} null if the text is empty or not valid.", + " */", + " decode(text, baseMask) {", + " if (!text) return null;", + " const parts = text.split(';');", + " if (parts[0] !== MASK_FORMAT_VERSION || parseInt(parts[1], 10) !== MASK_RESOLUTION) return null;", + " try {", + " const mask = baseMask.clone();", + " const [changes] = codec.decodeGrids(parts[2], mask.size, 1);", + " for (let index = 0; index < changes.length; index++) {", + " // The base may have changed since the data was saved.", + " mask.values[index] = Math.min(Math.max(mask.values[index] + changes[index], 0), 255);", + " }", + " return mask;", + " } catch (error) {", + " console.error('The painted data of scattered models could not be read:', error);", + " return null;", + " }", + " },", + "};", + "", + "const fadeShaderDeclarations = `", + "uniform float scatterDrawDistance;", + "`;", + "// Instance matrices are applied after this: copies shrink towards their base.", + "const fadeShaderCode = `", + "#ifdef USE_INSTANCING", + "vec3 scatterCopyPosition = (modelMatrix * instanceMatrix * vec4(0.0, 0.0, 0.0, 1.0)).xyz;", + "transformed *= clamp(", + " (scatterDrawDistance - distance(scatterCopyPosition, cameraPosition)) / (scatterDrawDistance * ${FADE_PART}),", + " 0.0,", + " 1.0", + ");", + "#endif", + "`;", + "", + "const flipTriangles = (geometry) => {", + " const index = geometry.index;", + " for (let offset = 0; offset + 2 < index.count; offset += 3) {", + " const second = index.getX(offset + 1);", + " index.setX(offset + 1, index.getX(offset + 2));", + " index.setX(offset + 2, second);", + " }", + "};", + "", + "/**", + " * The parts of a model, ready to be drawn with instancing: each glTF", + " * primitive with its material. Geometries are moved so that the model base", + " * is at the origin, its height is 1 and it stands along Z (like 3D model", + " * objects with a 90° rotation around X).", + " * @param {THREE.Object3D} modelScene", + " * @param {object} uniforms Uniforms added to the materials.", + " * @returns {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}}", + " */", + "const createModelParts = (modelScene, uniforms) => {", + " modelScene.updateMatrixWorld(true);", + " const rootInverse = modelScene.matrixWorld.clone().invert();", + " const standUp = new THREE.Matrix4().makeRotationX(Math.PI / 2);", + " /** @type {Array<{mesh: THREE.Mesh, matrix: THREE.Matrix4}>} */", + " const meshes = [];", + " const box = new THREE.Box3();", + " modelScene.traverse((node) => {", + " if (!node.isMesh || !node.geometry || !node.geometry.attributes.position) return;", + " const matrix = standUp.clone().multiply(rootInverse).multiply(node.matrixWorld);", + " if (!node.geometry.boundingBox) node.geometry.computeBoundingBox();", + " box.union(node.geometry.boundingBox.clone().applyMatrix4(matrix));", + " meshes.push({ mesh: node, matrix });", + " });", + " if (box.isEmpty()) return { parts: [], width: 0, depth: 0 };", + "", + " const height = Math.max(box.max.z - box.min.z, 0.000001);", + " // The Y axis of the scene goes down: the model is flipped, like 3D model objects.", + " const normalization = new THREE.Matrix4()", + " .makeScale(1 / height, -1 / height, 1 / height)", + " .multiply(", + " new THREE.Matrix4().makeTranslation(", + " -(box.min.x + box.max.x) / 2,", + " -(box.min.y + box.max.y) / 2,", + " -box.min.z", + " )", + " );", + " const parts = meshes.map(({ mesh, matrix }) => {", + " let geometry = mesh.geometry.clone();", + " if (!geometry.index) {", + " const indices = Array.from({ length: geometry.attributes.position.count }, (_, index) => index);", + " geometry.setIndex(indices);", + " }", + " geometry.applyMatrix4(normalization.clone().multiply(matrix));", + " // The flip turned the faces inside out.", + " flipTriangles(geometry);", + " geometry.computeBoundingSphere();", + " geometry.computeBoundingBox();", + " const patchMaterial = (originalMaterial) => {", + " const material = originalMaterial.clone();", + " // Like 3D model objects (\"Standard without metalness\"), which look better without an environment map.", + " if (material.metalness) material.metalness = 0;", + " material.onBeforeCompile = (shader) => {", + " Object.assign(shader.uniforms, uniforms);", + " shader.vertexShader = patchShaderCode(shader.vertexShader, [", + " ['#include ', '#include \\n' + fadeShaderDeclarations],", + " ['#include ', '#include \\n' + fadeShaderCode],", + " ], 'ScatteredModels');", + " };", + " material.customProgramCacheKey = () => 'ScatteredModels';", + " return material;", + " };", + " const material = Array.isArray(mesh.material) ? mesh.material.map(patchMaterial) : patchMaterial(mesh.material);", + " return { geometry, material };", + " });", + " return {", + " parts,", + " width: (box.max.x - box.min.x) / height,", + " depth: (box.max.y - box.min.y) / height,", + " };", + "};", + "", + "/**", + " * A copy of the model: its base position in the scene, its scale (the", + " * height of the copy) and the key of its cell in the grid of copies.", + " * @typedef {{x: number, y: number, z: number, angle: number, height: number, key: number}} Copy", + " */", + "", + "/**", + " * The scattered models of an object: copies of a 3D model placed on the", + " * grounds of the scene, drawn with GPU instancing in chunks that are hidden", + " * when far or off-screen.", + " */", + "class Scatter {", + " /** @param {gdjs.CustomRuntimeObject3D} object */", + " constructor(object) {", + " this.object = object;", + " this.group = new THREE.Group();", + " object.get3DRendererObject().add(this.group);", + " /** @type {Array<{firstCellI: number, firstCellJ: number, meshes: THREE.InstancedMesh[], copies: Copy[], center: THREE.Vector3, radius: number}>} */", + " this.chunks = [];", + " /**", + " * The grid of copies: one copy at most in each cell, cells grouped in chunks.", + " * @type {{spacing: number, cellsPerChunk: number, cellsX: number, cellsY: number} | null}", + " */", + " this.grid = null;", + " this.placement = new GroundPlacement(object.getRuntimeScene());", + " this.uniforms = {", + " // In 3D world units.", + " scatterDrawDistance: { value: 1 },", + " };", + " /** @type {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}} */", + " this.model = { parts: [], width: 0, depth: 0 };", + " this.modelResourceName = '';", + " this.loadingModelResourceName = '';", + " /**", + " * Copies removed by events (for example, cut trees), by key.", + " * @type {Map}", + " */", + " this.removedCopies = new Map();", + " /** Increased each time copies change, for the collisions. */", + " this.version = 0;", + " this.isDisposed = false;", + " /** The last value of the painted data property read or saved. */", + " this.paintData = '';", + " /**", + " * Values saved by the editor tools: the editor sends them back with a", + " * hot-reload, possibly after newer ones were saved.", + " * @type {string[]}", + " */", + " this.savedPaintData = [];", + " this.maskSource = '';", + " /** @type {DensityMask} */", + " this.baseMask = new DensityMask(255);", + " /** @type {DensityMask} */", + " this.mask = this.baseMask;", + " this.changedSamples = new SampleRectangle();", + " this._stroke = { cellWidth: 1, cellHeight: 1, ax: 0, ay: 0, bx: 0, by: 0, radius: 0 };", + " this._groundAtCopy = { ground: null, z: 0 };", + " this._matrix = new THREE.Matrix4();", + " this._raycastSphere = new THREE.Sphere();", + " this._raycastRay = new THREE.Ray();", + " this._raycastBox = new THREE.Box3();", + " this._raycastInverseMatrix = new THREE.Matrix4();", + " this._raycastScale = new THREE.Vector3();", + " /** Placed on a copy to test the triangles of the model. */", + " this._raycastMesh = new THREE.Mesh();", + " /** @type {THREE.Intersection[]} */", + " this._raycastHits = [];", + " this._color = new THREE.Color();", + " this._tint = new THREE.Color();", + " this._cameraPosition = new THREE.Vector3();", + " this._chunkPosition = new THREE.Vector3();", + " /**", + " * Trunks, for the 3D physics behavior and navigation meshes. Always given,", + " * empty without trunk collisions: otherwise these behaviors would use the", + " * object box, covering the whole area.", + " * @type {gdjs.Surface}", + " */", + " this.surface = {", + " getVersion: () => this.version,", + " getChangedArea: () => null,", + " getTriangles: () => this._getTrunkTriangles(),", + " getHeightField: () => null,", + " };", + " /** The trunk collision properties, changing the surface. */", + " this.trunkKey = '';", + " this.loadMaskFromProperties();", + " this.updateFromProperties();", + " // Before the behaviors create their shapes.", + " this._updateSurface();", + " }", + "", + " _getMaskSource() {", + " return this.object._getCoverage() + '|' + this.object._getEdits();", + " }", + "", + " loadMaskFromProperties() {", + " const { object } = this;", + " this.maskSource = this._getMaskSource();", + " this.paintData = object._getPaintData();", + " this.baseMask = new DensityMask(object._getCoverage() === 'Painted' ? 0 : 255);", + " applyEdits(this.baseMask, object._getEdits());", + " this.mask = maskCodec.decode(this.paintData, this.baseMask) || this.baseMask.clone();", + " this.placement.placeAgain();", + " }", + "", + " onHotReloading() {", + " const paintData = this.object._getPaintData();", + " const savedIndex = this.savedPaintData.indexOf(paintData);", + " if (savedIndex !== -1 && this._getMaskSource() === this.maskSource) {", + " // The object already has this data, or newer changes.", + " this.savedPaintData.splice(0, savedIndex + 1);", + " } else if (paintData !== this.paintData || this._getMaskSource() !== this.maskSource) {", + " this.savedPaintData.length = 0;", + " this.loadMaskFromProperties();", + " }", + " this.updateFromProperties();", + " }", + "", + " /** @returns {string} The data to save in the painted data property. */", + " save() {", + " this.paintData = maskCodec.encode(this.mask, this.baseMask);", + " this.savedPaintData.push(this.paintData);", + " return this.paintData;", + " }", + "", + " updateFromProperties() {", + " const { object } = this;", + " const modelResourceName = object._getModel();", + " if (modelResourceName !== this.modelResourceName) {", + " this.modelResourceName = modelResourceName;", + " this._loadModel();", + " }", + " this.placement.placeAgain();", + " }", + "", + " _disposeModel() {", + " for (const part of this.model.parts) {", + " part.geometry.dispose();", + " for (const material of Array.isArray(part.material) ? part.material : [part.material]) {", + " material.dispose();", + " }", + " }", + " this.model = { parts: [], width: 0, depth: 0 };", + " }", + "", + " _loadModel() {", + " this._disposeChunks(this.chunks);", + " this._disposeModel();", + " const resourceName = this.modelResourceName;", + " if (!resourceName) return;", + " const game = this.object.getRuntimeScene().getGame();", + " const modelManager = game.getModel3DManager();", + " const gltf = modelManager.getModel(resourceName);", + " if (gltf === modelManager._invalidModel) {", + " // Not loaded yet, for example a model just chosen in the editor.", + " if (this.loadingModelResourceName === resourceName) return;", + " this.loadingModelResourceName = resourceName;", + " game.getResourceLoader()", + " .loadResource(resourceName)", + " .then(() => {", + " if (this.isDisposed || this.modelResourceName !== resourceName) return;", + " this.loadingModelResourceName = '';", + " if (modelManager.getModel(resourceName) !== modelManager._invalidModel) {", + " this._loadModel();", + " this.placement.placeAgain();", + " }", + " })", + " .catch(() => {});", + " return;", + " }", + " this.model = createModelParts(gltf.scene, this.uniforms);", + " }", + "", + " _getPlacementKey() {", + " const { object } = this;", + " return [", + " object.getX(), object.getY(), object.getZ(), object.getWidth(), object.getHeight(), object.getDepth(),", + " object._getDensity(), object._getSize(), object._getMinScale(), object._getMaxScale(),", + " object._getRandomRotation(), object._getRandomTint(), object._getGroundLayer(), object._getMaxSlope(),", + " object._getMinGroundZ(), object._getMaxGroundZ(), object._getSeed(), object._getDrawDistance(),", + " object._getCastShadow(), object._getReceiveShadow(), this.model.parts.length,", + " ].join('|');", + " }", + "", + " _getChunkSize() {", + " return Math.min(Math.max(this.object._getDrawDistance() / 4, MIN_CHUNK_SIZE), MAX_CHUNK_SIZE);", + " }", + "", + " /** @returns {number} The distance between copies (one copy per cell of this size at most). */", + " _getSpacing() {", + " const { object } = this;", + " const density = Math.max(object._getDensity(), 0);", + " if (density <= 0) return 0;", + " const spacing = 100 / Math.sqrt(density);", + " // Too many cells would freeze the game: the density is lowered.", + " const cellCount = (object.getWidth() / spacing) * (object.getHeight() / spacing);", + " return cellCount > MAX_COPIES ? spacing * Math.sqrt(cellCount / MAX_COPIES) : spacing;", + " }", + "", + " /**", + " * Find the copy of a cell of the grid, if there is one.", + " * @returns {Copy | null}", + " */", + " _placeCopy(cellI, cellJ, spacing, grounds) {", + " const { object } = this;", + " const width = object.getWidth();", + " const height = object.getHeight();", + " const nextRandom = createRandom(hashCell(Math.floor(object._getSeed()), cellI, cellJ));", + " // Always drawn in the same order, so that a copy stays the same when properties change.", + " const x = (cellI + nextRandom()) * spacing;", + " const y = (cellJ + nextRandom()) * spacing;", + " const presence = nextRandom();", + " const growth = nextRandom();", + " const scaleFactor = nextRandom();", + " const angle = nextRandom() * Math.PI * 2;", + " if (x > width || y > height) return null;", + " if (presence >= this.mask.sample(x / width, y / height)) return null;", + " const key = cellJ * 65536 + cellI;", + " if (this.removedCopies.has(key)) return null;", + "", + " const sceneX = object.getX() + x;", + " const sceneY = object.getY() + y;", + " const { ground, z } = findHighestGround(grounds, sceneX, sceneY, this._groundAtCopy);", + " const groundZ = ground ? z : object.getZ();", + " if (groundZ < object._getMinGroundZ() || groundZ > object._getMaxGroundZ()) return null;", + " const copyHeight =", + " Math.max(object._getSize(), 0) *", + " (object._getMinScale() + (object._getMaxScale() - object._getMinScale()) * scaleFactor);", + " if (ground) {", + " if (ground.getSlopeAt(sceneX, sceneY) > object._getMaxSlope()) return null;", + " const groundLayer = parseInt(object._getGroundLayer(), 10) || 0;", + " if (groundLayer > 0) {", + " if (growth > ground.getLayerWeightAt(sceneX, sceneY, groundLayer)) return null;", + " // Copies don't stand at the border of their layer, so that trees don't cover roads.", + " const margin = (copyHeight * Math.max(this.model.width, this.model.depth)) / 4;", + " for (const [offsetX, offsetY] of [[margin, 0], [-margin, 0], [0, margin], [0, -margin]]) {", + " if (ground.getLayerWeightAt(sceneX + offsetX, sceneY + offsetY, groundLayer) < 0.5) return null;", + " }", + " }", + " }", + " return {", + " x: sceneX,", + " y: sceneY,", + " z: groundZ,", + " angle: object._getRandomRotation() ? angle : 0,", + " height: copyHeight,", + " key,", + " };", + " }", + "", + " _disposeChunks(chunks) {", + " for (const chunk of chunks) {", + " for (const mesh of chunk.meshes) {", + " mesh.removeFromParent();", + " // Geometries and materials are the ones of the model.", + " mesh.dispose();", + " }", + " }", + " if (chunks === this.chunks) this.chunks = [];", + " }", + "", + " /** Place the copies again, for example after the ground or the object changed. */", + " rebuild() {", + " this._disposeChunks(this.chunks);", + " this.version++;", + " const { object } = this;", + " const spacing = this._getSpacing();", + " if (spacing <= 0 || this.model.parts.length === 0) {", + " this.grid = null;", + " return;", + " }", + " const cellsPerChunk = Math.max(Math.round(this._getChunkSize() / spacing), 1);", + " const grid = {", + " spacing,", + " cellsPerChunk,", + " cellsX: Math.ceil(object.getWidth() / spacing),", + " cellsY: Math.ceil(object.getHeight() / spacing),", + " };", + " this.grid = grid;", + " for (let firstCellJ = 0; firstCellJ < grid.cellsY; firstCellJ += cellsPerChunk) {", + " for (let firstCellI = 0; firstCellI < grid.cellsX; firstCellI += cellsPerChunk) {", + " this._createChunk(firstCellI, firstCellJ);", + " }", + " }", + " }", + "", + " /**", + " * Place again the copies of the chunks touching a rectangle of the scene,", + " * for example after painting.", + " */", + " _rebuildArea(minX, minY, maxX, maxY) {", + " const { grid, object } = this;", + " if (!grid) return;", + " const chunkSize = grid.spacing * grid.cellsPerChunk;", + " const firstChunkI = Math.max(Math.floor((minX - object.getX()) / chunkSize), 0);", + " const firstChunkJ = Math.max(Math.floor((minY - object.getY()) / chunkSize), 0);", + " const lastChunkI = Math.min(Math.floor((maxX - object.getX()) / chunkSize), Math.ceil(grid.cellsX / grid.cellsPerChunk) - 1);", + " const lastChunkJ = Math.min(Math.floor((maxY - object.getY()) / chunkSize), Math.ceil(grid.cellsY / grid.cellsPerChunk) - 1);", + " if (firstChunkI > lastChunkI || firstChunkJ > lastChunkJ) return;", + " const isInArea = (chunk) => {", + " const chunkI = chunk.firstCellI / grid.cellsPerChunk;", + " const chunkJ = chunk.firstCellJ / grid.cellsPerChunk;", + " return chunkI >= firstChunkI && chunkI <= lastChunkI && chunkJ >= firstChunkJ && chunkJ <= lastChunkJ;", + " };", + " this._disposeChunks(this.chunks.filter(isInArea));", + " this.chunks = this.chunks.filter((chunk) => !isInArea(chunk));", + " for (let chunkJ = firstChunkJ; chunkJ <= lastChunkJ; chunkJ++) {", + " for (let chunkI = firstChunkI; chunkI <= lastChunkI; chunkI++) {", + " this._createChunk(chunkI * grid.cellsPerChunk, chunkJ * grid.cellsPerChunk);", + " }", + " }", + " this.version++;", + " }", + "", + " _createChunk(firstCellI, firstCellJ) {", + " const { grid } = this;", + " const grounds = Array.from(this.placement.grounds);", + " /** @type {Copy[]} */", + " const copies = [];", + " for (let cellJ = firstCellJ; cellJ < Math.min(firstCellJ + grid.cellsPerChunk, grid.cellsY); cellJ++) {", + " for (let cellI = firstCellI; cellI < Math.min(firstCellI + grid.cellsPerChunk, grid.cellsX); cellI++) {", + " const copy = this._placeCopy(cellI, cellJ, grid.spacing, grounds);", + " if (copy) copies.push(copy);", + " }", + " }", + " if (copies.length === 0) return;", + "", + " const { object } = this;", + " // Copies are placed in scene units, and the object group is scaled", + " // from its inner area: they are scaled back to keep their proportions.", + " const scaleX = Math.max(object.getWidth() / AREA_SIZE, 0.0001);", + " const scaleY = Math.max(object.getHeight() / AREA_SIZE, 0.0001);", + " const scaleZ = Math.max(object.getDepth() / AREA_DEPTH, 0.0001);", + " const matrices = new Float32Array(copies.length * 16);", + " const tintColor = gdjs.rgbOrHexStringToNumber(object._getRandomTint());", + " const hasTint = tintColor !== 0xffffff;", + " const colors = hasTint ? new Float32Array(copies.length * 3) : null;", + " this._tint.setHex(tintColor);", + " const min = new THREE.Vector3(Infinity, Infinity, Infinity);", + " const max = new THREE.Vector3(-Infinity, -Infinity, -Infinity);", + " copies.forEach((copy, index) => {", + " const cos = Math.cos(copy.angle) * copy.height;", + " const sin = Math.sin(copy.angle) * copy.height;", + " const localX = (copy.x - object.getX()) / scaleX;", + " const localY = (copy.y - object.getY()) / scaleY;", + " const localZ = (copy.z - object.getZ()) / scaleZ;", + " this._matrix.set(", + " cos / scaleX, -sin / scaleX, 0, localX,", + " sin / scaleY, cos / scaleY, 0, localY,", + " 0, 0, copy.height / scaleZ, localZ,", + " 0, 0, 0, 1", + " );", + " this._matrix.toArray(matrices, index * 16);", + " if (colors) {", + " // The tint of each copy is between white and the tint color.", + " this._color.setRGB(1, 1, 1).lerp(this._tint, createRandom(copy.key + 1)());", + " this._color.toArray(colors, index * 3);", + " }", + " const reach = (copy.height * Math.max(this.model.width, this.model.depth)) / 2;", + " min.min(this._chunkPosition.set(copy.x - reach, copy.y - reach, copy.z));", + " max.max(this._chunkPosition.set(copy.x + reach, copy.y + reach, copy.z + copy.height));", + " });", + " const instanceMatrix = new THREE.InstancedBufferAttribute(matrices, 16);", + " const instanceColor = colors ? new THREE.InstancedBufferAttribute(colors, 3) : null;", + " const castShadow = object._getCastShadow();", + " const receiveShadow = object._getReceiveShadow();", + " const meshes = this.model.parts.map((part) => {", + " const mesh = new THREE.InstancedMesh(part.geometry, part.material, 0);", + " // All the parts of the model share the positions of the copies.", + " mesh.instanceMatrix = instanceMatrix;", + " mesh.instanceColor = instanceColor;", + " mesh.count = copies.length;", + " mesh.computeBoundingSphere();", + " mesh.castShadow = castShadow;", + " mesh.receiveShadow = receiveShadow;", + " // The first mesh tests all the parts of the copies.", + " mesh.raycast = () => {};", + " this.group.add(mesh);", + " return mesh;", + " });", + " const center = new THREE.Vector3(", + " ((min.x + max.x) / 2 - object.getX()) / scaleX,", + " ((min.y + max.y) / 2 - object.getY()) / scaleY,", + " ((min.z + max.z) / 2 - object.getZ()) / scaleZ", + " );", + " const chunk = { firstCellI, firstCellJ, meshes, copies, center, radius: min.distanceTo(max) / 2 };", + " // Used by the scene editor to select the object when a copy is clicked.", + " meshes[0].raycast = (raycaster, intersects) => this._raycastChunk(chunk, raycaster, intersects);", + " this.chunks.push(chunk);", + " }", + "", + " /**", + " * Add the hits of a ray on the copies of a chunk, like they are drawn. There", + " * can be thousands of copies: only the ones whose box is crossed by the ray", + " * are tested with the triangles of the model.", + " * @param {{meshes: THREE.InstancedMesh[], copies: Copy[], center: THREE.Vector3, radius: number}} chunk", + " * @param {THREE.Raycaster} raycaster", + " * @param {THREE.Intersection[]} intersects", + " */", + " _raycastChunk(chunk, raycaster, intersects) {", + " const { object, group } = this;", + " this._raycastSphere.center.copy(chunk.center).applyMatrix4(group.matrixWorld);", + " this._raycastSphere.radius = chunk.radius / getWorldScale(object);", + " if (!raycaster.ray.intersectsSphere(this._raycastSphere)) return;", + "", + " // The ray in the scene coordinates, like the copies.", + " const scale = this._raycastScale.set(", + " Math.max(object.getWidth() / AREA_SIZE, 0.0001),", + " Math.max(object.getHeight() / AREA_SIZE, 0.0001),", + " Math.max(object.getDepth() / AREA_DEPTH, 0.0001)", + " );", + " const ray = this._raycastRay.copy(raycaster.ray).applyMatrix4(this._raycastInverseMatrix.copy(group.matrixWorld).invert());", + " ray.origin.multiply(scale).add(this._chunkPosition.set(object.getX(), object.getY(), object.getZ()));", + " ray.direction.multiply(scale);", + "", + " const reachPerHeight = Math.max(this.model.width, this.model.depth) / 2;", + " const instanceMatrices = chunk.meshes[0].instanceMatrix.array;", + " chunk.copies.forEach((copy, index) => {", + " const reach = copy.height * reachPerHeight;", + " this._raycastBox.min.set(copy.x - reach, copy.y - reach, copy.z);", + " this._raycastBox.max.set(copy.x + reach, copy.y + reach, copy.z + copy.height);", + " if (!ray.intersectsBox(this._raycastBox)) return;", + " this._raycastMesh.matrixWorld.fromArray(instanceMatrices, index * 16).premultiply(group.matrixWorld);", + " for (const mesh of chunk.meshes) {", + " this._raycastMesh.geometry = mesh.geometry;", + " this._raycastMesh.material = mesh.material;", + " this._raycastHits.length = 0;", + " this._raycastMesh.raycast(raycaster, this._raycastHits);", + " for (const hit of this._raycastHits) {", + " hit.object = mesh;", + " hit.instanceId = index;", + " intersects.push(hit);", + " }", + " }", + " });", + " }", + "", + " /** @param {THREE.Camera | null} camera */", + " _hideFarChunks(camera) {", + " if (!camera) return;", + " const worldScale = getWorldScale(this.object);", + " const drawDistance = Math.max(this.object._getDrawDistance(), 1) / worldScale;", + " this.uniforms.scatterDrawDistance.value = drawDistance;", + " this._cameraPosition.setFromMatrixPosition(camera.matrixWorld);", + " for (const chunk of this.chunks) {", + " const distance = this._chunkPosition", + " .copy(chunk.center)", + " .applyMatrix4(this.group.matrixWorld)", + " .distanceTo(this._cameraPosition);", + " const isVisible = distance - chunk.radius / worldScale < drawDistance;", + " for (const mesh of chunk.meshes) mesh.visible = isVisible;", + " }", + " }", + "", + " /**", + " * @param {number} elapsedSeconds", + " * @param {THREE.Camera | null} camera", + " */", + " update(elapsedSeconds, camera) {", + " if (this.placement.shouldPlaceAgain(elapsedSeconds, this._getPlacementKey())) {", + " this.rebuild();", + " }", + " if (!this.changedSamples.isEmpty()) {", + " const { object } = this;", + " const sampleWidth = object.getWidth() / MASK_RESOLUTION;", + " const sampleHeight = object.getHeight() / MASK_RESOLUTION;", + " const { minI, minJ, maxI, maxJ } = this.changedSamples;", + " // Copies between samples use the density of the samples around them.", + " this._rebuildArea(", + " object.getX() + (minI - 1) * sampleWidth,", + " object.getY() + (minJ - 1) * sampleHeight,", + " object.getX() + (maxI + 1) * sampleWidth,", + " object.getY() + (maxJ + 1) * sampleHeight", + " );", + " this.changedSamples.clear();", + " }", + " // The object transformation is applied just before rendering, it's needed now.", + " this.object.getRenderer().ensureUpToDate();", + " this.group.updateWorldMatrix(true, false);", + " this._hideFarChunks(camera);", + " this._updateSurface();", + " }", + "", + " _updateSurface() {", + " const { object } = this;", + " if (typeof object.setSurface !== 'function') return;", + " const trunkKey = object._getTrunkCollisions() + '|' + object._getTrunkDiameter();", + " if (trunkKey !== this.trunkKey) {", + " this.trunkKey = trunkKey;", + " this.version++;", + " }", + " if (object.getSurface() !== this.surface) object.setSurface(this.surface);", + " }", + "", + " /** @returns {Copy[]} */", + " getCopies() {", + " const copies = [];", + " for (const chunk of this.chunks) copies.push(...chunk.copies);", + " return copies;", + " }", + "", + " /**", + " * @returns {Copy | null} The copy whose base is the nearest to a position (on X and Y), if any.", + " */", + " findNearestCopy(x, y) {", + " let nearestCopy = null;", + " let nearestDistanceSquared = Infinity;", + " for (const chunk of this.chunks) {", + " for (const copy of chunk.copies) {", + " const distanceSquared = (copy.x - x) * (copy.x - x) + (copy.y - y) * (copy.y - y);", + " if (distanceSquared < nearestDistanceSquared) {", + " nearestDistanceSquared = distanceSquared;", + " nearestCopy = copy;", + " }", + " }", + " }", + " return nearestCopy;", + " }", + "", + " getCopyCount() {", + " let count = 0;", + " for (const chunk of this.chunks) count += chunk.copies.length;", + " return count;", + " }", + "", + " /** Remove the copies whose base is in a circle (they come back when the scene is restarted). */", + " removeCopies(x, y, radius) {", + " let hasRemovedCopies = false;", + " for (const chunk of this.chunks) {", + " for (const copy of chunk.copies) {", + " if ((copy.x - x) * (copy.x - x) + (copy.y - y) * (copy.y - y) <= radius * radius) {", + " this.removedCopies.set(copy.key, copy);", + " hasRemovedCopies = true;", + " }", + " }", + " }", + " if (hasRemovedCopies) this._rebuildArea(x - radius, y - radius, x + radius, y + radius);", + " }", + "", + " /** Paint the full density around a position, and bring back the copies removed there. */", + " addCopies(x, y, radius) {", + " for (const [key, copy] of this.removedCopies) {", + " if ((copy.x - x) * (copy.x - x) + (copy.y - y) * (copy.y - y) <= radius * radius) {", + " this.removedCopies.delete(key);", + " }", + " }", + " this.paint(x, y, x, y, radius, 1, 1);", + " }", + "", + " _getStroke(x1, y1, x2, y2, radius) {", + " const { object } = this;", + " const stroke = this._stroke;", + " stroke.cellWidth = object.getWidth() / MASK_RESOLUTION;", + " stroke.cellHeight = object.getHeight() / MASK_RESOLUTION;", + " stroke.ax = x1 - object.getX();", + " stroke.ay = y1 - object.getY();", + " stroke.bx = x2 - object.getX();", + " stroke.by = y2 - object.getY();", + " stroke.radius = Math.max(radius, 0);", + " return stroke;", + " }", + "", + " /**", + " * Paint the density around a segment (copies are placed again at the next frame).", + " * @param {number} density From 0 (erase) to 1 (as dense as the Density property).", + " * @param {number} strength From 0 to 1.", + " */", + " paint(x1, y1, x2, y2, radius, density, strength) {", + " const changedSamples = new SampleRectangle();", + " this.mask.paint(this._getStroke(x1, y1, x2, y2, radius), density, strength, changedSamples);", + " this.changedSamples.add(changedSamples);", + " }", + "", + " /** @param {DensityMask} mask */", + " setMask(mask) {", + " this.mask = mask;", + " this.rebuild();", + " }", + "", + " /**", + " * Prisms around the trunks, from 0 to 1 in the object box (see `gdjs.Surface`).", + " * @returns {gdjs.SurfaceTriangles}", + " */", + " _getTrunkTriangles() {", + " const { object } = this;", + " const copies = object._getTrunkCollisions() ? this.getCopies() : [];", + " const verticesPerCopy = TRUNK_SIDES * 2 + 1;", + " const positions = new Float32Array(copies.length * verticesPerCopy * 3);", + " const indices = new Uint32Array(copies.length * TRUNK_SIDES * 3 * 3);", + " const width = object.getWidth() || 1;", + " const height = object.getHeight() || 1;", + " const depth = object.getDepth() || 1;", + " let indexOffset = 0;", + " copies.forEach((copy, copyIndex) => {", + " const radius = (Math.max(object._getTrunkDiameter(), 0) / 2) * (copy.height / Math.max(object._getSize(), 1));", + " const top = copy.z + copy.height * 0.8;", + " const firstVertex = copyIndex * verticesPerCopy;", + " const setVertex = (vertex, x, y, z) => {", + " positions[vertex * 3] = (x - object.getX()) / width;", + " positions[vertex * 3 + 1] = (y - object.getY()) / height;", + " positions[vertex * 3 + 2] = (z - object.getZ()) / depth;", + " };", + " for (let side = 0; side < TRUNK_SIDES; side++) {", + " const angle = ((side + 0.5) / TRUNK_SIDES) * Math.PI * 2;", + " const x = copy.x + Math.cos(angle) * radius;", + " const y = copy.y + Math.sin(angle) * radius;", + " // Starting a bit under the ground, which can be sloped.", + " setVertex(firstVertex + side * 2, x, y, copy.z - radius);", + " setVertex(firstVertex + side * 2 + 1, x, y, top);", + " }", + " const center = firstVertex + TRUNK_SIDES * 2;", + " setVertex(center, copy.x, copy.y, top);", + " for (let side = 0; side < TRUNK_SIDES; side++) {", + " const bottom = firstVertex + side * 2;", + " const nextBottom = firstVertex + ((side + 1) % TRUNK_SIDES) * 2;", + " // Triangles face out of the prism (vertices at odd indices are at the top).", + " indices.set([bottom, nextBottom, bottom + 1, nextBottom, nextBottom + 1, bottom + 1], indexOffset);", + " indices.set([center, bottom + 1, nextBottom + 1], indexOffset + 6);", + " indexOffset += 9;", + " }", + " });", + " if (copies.length === 0) {", + " // Without triangles, the physics engine and navigation meshes would use", + " // the object box: a tiny triangle under the object is used instead.", + " return { positions: new Float32Array([0, 0, -1, 0.0001, 0, -1, 0, 0.0001, -1]), indices: new Uint32Array([0, 2, 1]) };", + " }", + " return { positions, indices };", + " }", + "", + " dispose() {", + " this.isDisposed = true;", + " if (typeof this.object.setSurface === 'function' && this.object.getSurface() === this.surface) {", + " this.object.setSurface(null);", + " }", + " this._disposeChunks(this.chunks);", + " this._disposeModel();", + " this.group.removeFromParent();", + " }", + "}", + "", + "const scatterToolIcons = {", + " Add: svgIcon(", + " ''", + " ),", + " Erase: svgIcon(", + " ''", + " ),", + "};", + "", + "/**", + " * The brushes shown in the scene editor when scattered models are selected,", + " * to add or erase copies. Without the needed editor functions (older GDevelop", + " * versions), nothing is shown: copies can still be painted with events.", + " */", + "class ScatterEditorTools {", + " constructor() {", + " /** '' when no brush is used. */", + " this.toolName = '';", + " this.sizePercent = 30;", + " this.strengthPercent = 50;", + " /** @type {Scatter | null} */", + " this.scatter = null;", + " /** @type {gdjs.InGameEditor | null} */", + " this.editor = null;", + " this.isStroking = false;", + " this.lastStrokeTime = 0;", + " /** @type {Array<{mask: DensityMask, baseMask: DensityMask}>} */", + " this.undoSteps = [];", + " this.raycaster = new THREE.Raycaster();", + " this.ndc = new THREE.Vector2();", + " this._groundAtCursor = { ground: null, z: 0 };", + " /** A circle following the ground under the cursor. */", + " this.brushRing = new THREE.LineLoop(", + " new THREE.BufferGeometry().setAttribute('position', new THREE.BufferAttribute(new Float32Array(64 * 3), 3)),", + " new THREE.LineBasicMaterial({ color: 0xffffff, depthTest: false, transparent: true })", + " );", + " this.brushRing.renderOrder = 1000;", + " this.brushRing.frustumCulled = false;", + " this.brushRing.raycast = () => {};", + " }", + "", + " /** @param {gdjs.InGameEditor} editor */", + " update(editor) {", + " if (typeof editor.showToolbar !== 'function') return;", + " this.editor = editor;", + " const selectedObjects = editor.getSelectedObjects();", + " const scatter =", + " selectedObjects.length === 1 && selectedObjects[0].__terrainScatter ? selectedObjects[0].__terrainScatter : null;", + " if (scatter !== this.scatter) {", + " this._endStroke();", + " this.scatter = scatter;", + " this.undoSteps.length = 0;", + " }", + " if (scatter) editor.showToolbar('ScatteredModels', this._getToolbarItems());", + " const game = editor.getRuntimeGame();", + " if (!scatter || !this.toolName) {", + " this.brushRing.removeFromParent();", + " return;", + " }", + " const inputManager = game.getInputManager();", + " if (inputManager.wasKeyJustPressed(gdjs.evtTools.input.keysNameToCode.Escape)) {", + " this._endStroke();", + " this.toolName = '';", + " this.brushRing.removeFromParent();", + " return;", + " }", + " editor.captureLeftMouseButton();", + " const hit = this._getPositionUnderCursor(scatter, game);", + " const radius = this._getRadius();", + " this._showBrushRing(scatter, hit, radius);", + "", + " const isPressedForTool =", + " typeof editor.isCapturedLeftMouseButtonPressed === 'function'", + " ? editor.isCapturedLeftMouseButtonPressed()", + " : inputManager.isMouseButtonPressed(0);", + " // The button is released outside the canvas when the mouse is on a toolbar.", + " const isPressed = isPressedForTool && inputManager.isMouseInsideCanvas();", + " if (isPressed && hit && !this.isStroking) {", + " this._startStroke(scatter);", + " }", + " if (this.isStroking && isPressed && hit) {", + " this._applyBrush(scatter, hit, radius);", + " }", + " if (!isPressed) {", + " this._endStroke();", + " }", + " }", + "", + " _getRadius() {", + " const { object } = this.scatter;", + " const maxRadius = Math.max(object.getWidth(), object.getHeight()) / 4;", + " return Math.max(maxRadius * Math.pow(this.sizePercent / 100, 2), 10);", + " }", + "", + " /**", + " * @returns {{x: number, y: number} | null} The position of the scene under the cursor: on the", + " * highest ground hit, or on the plane at the object Z.", + " */", + " _getPositionUnderCursor(scatter, game) {", + " let hit = null;", + " for (const ground of getGrounds(scatter.object.getRuntimeScene())) {", + " if (typeof ground.raycastFromCursor !== 'function') continue;", + " const localHit = ground.raycastFromCursor(game, this.raycaster, this.ndc);", + " if (!localHit) continue;", + " const sceneHit = ground.toScenePosition(localHit);", + " if (!hit || sceneHit.z > hit.z) hit = sceneHit;", + " }", + " if (hit) return hit;", + "", + " const { object } = scatter;", + " const layer = object.getInstanceContainer().getLayer(object.getLayer());", + " const camera = layer.getRenderer().getThreeCamera();", + " if (!camera) return null;", + " const inputManager = game.getInputManager();", + " this.ndc.set(", + " (inputManager.getCursorX() / game.getGameResolutionWidth()) * 2 - 1,", + " -(inputManager.getCursorY() / game.getGameResolutionHeight()) * 2 + 1", + " );", + " this.raycaster.setFromCamera(this.ndc, camera);", + " scatter.group.updateWorldMatrix(true, false);", + " const ray = this.raycaster.ray.clone().applyMatrix4(scatter.group.matrixWorld.clone().invert());", + " const point = ray.intersectPlane(new THREE.Plane(new THREE.Vector3(0, 0, 1), 0), new THREE.Vector3());", + " if (!point) return null;", + " return {", + " x: object.getX() + (point.x / AREA_SIZE) * object.getWidth(),", + " y: object.getY() + (point.y / AREA_SIZE) * object.getHeight(),", + " };", + " }", + "", + " _showBrushRing(scatter, hit, radius) {", + " if (!hit) {", + " this.brushRing.removeFromParent();", + " return;", + " }", + " const { object } = scatter;", + " if (this.brushRing.parent !== scatter.group) scatter.group.add(this.brushRing);", + " const grounds = getGrounds(object.getRuntimeScene());", + " const positions = this.brushRing.geometry.attributes.position;", + " for (let index = 0; index < positions.count; index++) {", + " const angle = (index / positions.count) * Math.PI * 2;", + " const x = hit.x + Math.cos(angle) * radius;", + " const y = hit.y + Math.sin(angle) * radius;", + " const { ground, z } = findHighestGround(grounds, x, y, this._groundAtCursor);", + " positions.setXYZ(", + " index,", + " ((x - object.getX()) / object.getWidth()) * AREA_SIZE,", + " ((y - object.getY()) / object.getHeight()) * AREA_SIZE,", + " (((ground ? z : object.getZ()) + 2 - object.getZ()) / Math.max(object.getDepth(), 0.0001)) * AREA_DEPTH", + " );", + " }", + " positions.needsUpdate = true;", + " }", + "", + " _startStroke(scatter) {", + " this.isStroking = true;", + " this.lastStrokeTime = performance.now();", + " this.undoSteps.push({ mask: scatter.mask.clone(), baseMask: scatter.baseMask });", + " while (this.undoSteps.length > 30) this.undoSteps.shift();", + " }", + "", + " _endStroke() {", + " if (!this.isStroking) return;", + " this.isStroking = false;", + " this._save();", + " }", + "", + " _applyBrush(scatter, hit, radius) {", + " const now = performance.now();", + " const elapsedSeconds = Math.min((now - this.lastStrokeTime) / 1000, 0.1);", + " this.lastStrokeTime = now;", + " const strength = (this.strengthPercent / 100) * elapsedSeconds * 8;", + " scatter.paint(hit.x, hit.y, hit.x, hit.y, radius, this.toolName === 'Add' ? 1 : 0, strength);", + " }", + "", + " _save() {", + " const { scatter, editor } = this;", + " if (!scatter || !editor) return;", + " editor.updateObjectProperties(scatter.object.getName(), { PaintData: scatter.save() });", + " }", + "", + " _undo() {", + " const undoStep = this.undoSteps.pop();", + " // The coverage or the edits may have changed since the stroke.", + " if (!this.scatter || !undoStep || undoStep.baseMask !== this.scatter.baseMask) {", + " this.undoSteps.length = 0;", + " return;", + " }", + " this.scatter.setMask(undoStep.mask);", + " this._save();", + " }", + "", + " /** @returns {Array} */", + " _getToolbarItems() {", + " const toolButton = (toolName, tooltip) => ({", + " type: 'button',", + " id: toolName,", + " tooltip,", + " iconUrl: scatterToolIcons[toolName],", + " isActive: this.toolName === toolName,", + " onClick: () => (this.toolName = this.toolName === toolName ? '' : toolName),", + " });", + " return [", + " toolButton('Add', 'Add copies (drag on the ground)'),", + " toolButton('Erase', 'Erase copies'),", + " { type: 'divider', id: 'BrushDivider' },", + " {", + " type: 'slider',", + " id: 'Size',", + " tooltip: 'Brush size',", + " iconUrl: toolIcons.Size,", + " min: 1,", + " max: 100,", + " value: this.sizePercent,", + " onChange: (value) => (this.sizePercent = value),", + " },", + " {", + " type: 'slider',", + " id: 'Strength',", + " tooltip: 'Brush strength',", + " iconUrl: toolIcons.Strength,", + " min: 5,", + " max: 100,", + " value: this.strengthPercent,", + " onChange: (value) => (this.strengthPercent = value),", + " },", + " {", + " type: 'button',", + " id: 'Undo',", + " tooltip: 'Undo the last stroke on these scattered models',", + " iconUrl: toolIcons.Undo,", + " onClick: () => this._undo(),", + " },", + " ];", + " }", + "}", + "", + "const editorTools = new ScatterEditorTools();", + "if (gdjs.registerInGameEditorPostStepCallback) {", + " gdjs.registerInGameEditorPostStepCallback((editor) => editorTools.update(editor));", + "}", + "", + "gdjs.__terrainScatterExtension = {", + " Scatter,", + " // Exposed for tests.", + " DensityMask,", + " maskCodec,", + " applyEdits,", + "};", + "" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [], + "objectGroups": [] + } + ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "DefineHelperClasses" + }, + { + "functionName": "DefineGrassHelperClasses" + }, + { + "functionName": "DefineScatterHelperClasses" + } + ] + }, + "eventsBasedBehaviors": [], + "eventsBasedObjects": [ + { + "areaMaxX": 4096, + "areaMaxY": 4096, + "areaMaxZ": 600, + "areaMinX": 0, + "areaMinY": 0, + "areaMinZ": 0, + "assetStoreTag": "3d terrains", + "defaultName": "Terrain", + "description": "A ground with hills, mountains or valleys that can be sculpted and painted in the scene editor, or changed with events. Add the 3D physics behavior to it to make objects walk or roll on it.", + "fullName": "3D terrain", + "helpPath": "", + "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLXRlcnJhaW4iIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTQsNkwxMC4yNSwxMUwxMy4xLDE0LjhMMTEuNSwxNkM5LjgxLDEzLjc1IDcsMTAgNywxMEwxLDE4SDIzTDE0LDZaIiAvPjwvc3ZnPg==", + "is3D": true, + "isUsingLegacyInstancesRenderer": true, + "name": "Terrain3D", + "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", + "objects": [ + { + "assetStoreId": "", + "name": "AreaPlaceholder", + "type": "Scene3D::Cube3DObject", + "variables": [], + "effects": [], + "behaviors": [], + "content": { + "width": 4096, + "height": 4096, + "depth": 600, + "enableTextureTransparency": false, + "facesOrientation": "Y", + "backFaceUpThroughWhichAxisRotation": "X", + "materialType": "Basic", + "tint": "255;255;255", + "frontFaceResourceName": "", + "frontFaceVisible": false, + "frontFaceResourceRepeat": false, + "backFaceResourceName": "", + "backFaceVisible": false, + "backFaceResourceRepeat": false, + "leftFaceResourceName": "", + "leftFaceVisible": false, + "leftFaceResourceRepeat": false, + "rightFaceResourceName": "", + "rightFaceVisible": false, + "rightFaceResourceRepeat": false, + "topFaceResourceName": "", + "topFaceVisible": false, + "topFaceResourceRepeat": false, + "bottomFaceResourceName": "", + "bottomFaceVisible": false, + "bottomFaceResourceRepeat": false + } + } + ], + "objectsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "objectName": "AreaPlaceholder" + } + ] + }, + "objectsGroups": [], + "layers": [ + { + "ambientLightColorB": 200, + "ambientLightColorG": 200, + "ambientLightColorR": 200, + "camera2DPlaneMaxDrawingDistance": 5000, + "camera3DFarPlaneDistance": 10000, + "camera3DFieldOfView": 45, + "camera3DNearPlaneDistance": 3, + "cameraType": "", + "followBaseLayerCamera": false, + "isLightingLayer": false, + "isLocked": false, + "name": "", + "renderingType": "", + "visibility": true, + "cameras": [ + { + "defaultSize": true, + "defaultViewport": true, + "height": 0, + "viewportBottom": 1, + "viewportLeft": 0, + "viewportRight": 1, + "viewportTop": 0, + "width": 0 + } + ], + "effects": [] + } + ], + "instances": [ + { + "angle": 0, + "customSize": true, + "depth": 600, + "height": 4096, + "keepRatio": true, + "layer": "", + "name": "AreaPlaceholder", + "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-004096004096", + "width": 4096, + "x": 0, + "y": 0, + "zOrder": 1, + "numberProperties": [], + "stringProperties": [], + "initialVariables": [] + } + ], + "editionSettings": {}, + "eventsFunctions": [ + { + "fullName": "", + "functionType": "Action", + "name": "onCreated", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Terrain3D::DefineHelperClasses" + }, + "parameters": [ + "", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "for (const child of object.getChildrenContainer().getAdhocListOfAllInstances()) {", + " child.deleteFromScene();", + "}", + "if (object.__terrain3D) object.__terrain3D.dispose();", + "object.__terrain3D = new gdjs.__terrain3DExtension.Terrain(object);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "onDestroy", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrain3D.dispose();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "doStepPostEvents", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrain3D.update();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "onHotReloading", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrain3D.onHotReloading();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "description": "Raise the ground around a position (or lower it with a negative height). It's raised the most at the center and less and less up to the radius.", + "fullName": "Raise or lower the ground", + "functionType": "Action", + "group": "Sculpt", + "name": "RaiseGround", + "sentence": "Raise the ground of _PARAM0_ by _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const height = eventsFunctionContext.getArgument(\"Height\");", + "object.__terrain3D.raise(x, y, x, y, radius, height);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Height to add (negative to lower the ground)", + "name": "Height", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Raise the ground along a line, for example for a ridge, or lower it with a negative height to dig a river bed or a valley.", + "fullName": "Raise or lower the ground along a line", + "functionType": "Action", + "group": "Sculpt", + "name": "RaiseGroundAlongLine", + "sentence": "Raise the ground of _PARAM0_ by _PARAM6_ from _PARAM1_; _PARAM2_ to _PARAM3_; _PARAM4_ with a radius of _PARAM5_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x1 = eventsFunctionContext.getArgument(\"X1\");", + "const y1 = eventsFunctionContext.getArgument(\"Y1\");", + "const x2 = eventsFunctionContext.getArgument(\"X2\");", + "const y2 = eventsFunctionContext.getArgument(\"Y2\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const height = eventsFunctionContext.getArgument(\"Height\");", + "object.__terrain3D.raise(x1, y1, x2, y2, radius, height);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the start", + "name": "X1", + "type": "expression" + }, + { + "description": "Y position of the start", + "name": "Y1", + "type": "expression" + }, + { + "description": "X position of the end", + "name": "X2", + "type": "expression" + }, + { + "description": "Y position of the end", + "name": "Y2", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance from the line.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Height to add (negative to lower the ground)", + "name": "Height", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Flatten the ground around a position to a given height, for example to place a building.", + "fullName": "Flatten the ground", + "functionType": "Action", + "group": "Sculpt", + "name": "FlattenGround", + "sentence": "Flatten the ground of _PARAM0_ to a Z of _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const z = eventsFunctionContext.getArgument(\"Z\");", + "object.__terrain3D.flatten(x, y, x, y, radius, z, 1);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Z position of the flat ground", + "name": "Z", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Smooth the ground around a position, softening bumps and cliffs.", + "fullName": "Smooth the ground", + "functionType": "Action", + "group": "Sculpt", + "name": "SmoothGround", + "sentence": "Smooth the ground of _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_ (strength: _PARAM4_)", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const strength = eventsFunctionContext.getArgument(\"Strength\");", + "object.__terrain3D.smooth(x, y, x, y, radius, strength);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Strength (between 0 and 1)", + "name": "Strength", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Paint a layer (like grass, dirt, rock or sand) on the ground around a position.", + "fullName": "Paint a layer", + "functionType": "Action", + "group": "Paint", + "name": "PaintLayer", + "sentence": "Paint layer _PARAM4_ on _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const layer = eventsFunctionContext.getArgument(\"Layer\");", + "object.__terrain3D.paint(x, y, x, y, radius, layer, 1);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Layer (1 to 4)", + "name": "Layer", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Paint a layer on the ground along a line, for example for a path or a road.", + "fullName": "Paint a layer along a line", + "functionType": "Action", + "group": "Paint", + "name": "PaintLayerAlongLine", + "sentence": "Paint layer _PARAM6_ on _PARAM0_ from _PARAM1_; _PARAM2_ to _PARAM3_; _PARAM4_ with a radius of _PARAM5_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x1 = eventsFunctionContext.getArgument(\"X1\");", + "const y1 = eventsFunctionContext.getArgument(\"Y1\");", + "const x2 = eventsFunctionContext.getArgument(\"X2\");", + "const y2 = eventsFunctionContext.getArgument(\"Y2\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const layer = eventsFunctionContext.getArgument(\"Layer\");", + "object.__terrain3D.paint(x1, y1, x2, y2, radius, layer, 1);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the start", + "name": "X1", + "type": "expression" + }, + { + "description": "Y position of the start", + "name": "Y1", + "type": "expression" + }, + { + "description": "X position of the end", + "name": "X2", + "type": "expression" + }, + { + "description": "Y position of the end", + "name": "Y2", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance from the line.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Layer (1 to 4)", + "name": "Layer", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Tint the ground around a position with a color multiplying its layers, like dry grass or darker mud. White removes the tint.", + "fullName": "Tint the ground", + "functionType": "Action", + "group": "Paint", + "name": "TintGround", + "sentence": "Tint the ground of _PARAM0_ with _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const color = gdjs.rgbOrHexToRGBColor(eventsFunctionContext.getArgument(\"Color\"));", + "object.__terrain3D.tint(x, y, x, y, radius, color, 1);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Color", + "name": "Color", + "type": "color" + } + ], + "objectGroups": [] + }, + { + "description": "Make the ground around a position wet (darker and shiny, like after rain or near water) or dry.", + "fullName": "Change the wetness of the ground", + "functionType": "Action", + "group": "Paint", + "name": "WetGround", + "sentence": "Change the wetness of the ground of _PARAM0_ to _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const wetness = eventsFunctionContext.getArgument(\"Wetness\");", + "object.__terrain3D.wet(x, y, x, y, radius, wetness, 1);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Wetness (from 0 for dry to 1)", + "name": "Wetness", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "the wetness of the ground at a position, from 0 (dry) to 1, painted or near the water (wet shore).", + "fullName": "Ground wetness", + "functionType": "ExpressionAndCondition", + "group": "Paint", + "name": "GroundWetness", + "sentence": "the wetness of the ground at _PARAM1_; _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "eventsFunctionContext.returnValue = object.__terrain3D.getWetnessAt(x, y);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "The tint painted on the ground at a position, like \"255;255;255\" (white when there is none).", + "fullName": "Ground tint", + "functionType": "StringExpression", + "group": "Paint", + "name": "GroundTint", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "eventsFunctionContext.returnValue = object.__terrain3D.getTintAt(x, y).join(\";\");" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "string" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Remove the ground in a circle, for the entrance of a tunnel, a cave or a well: there is nothing drawn and no collision there. Fill it again with the \"Fill holes\" action. A height field can't go under itself: put a 3D model (with a collision shape) for the tunnel or the cave under the ground. The physics engine removes the triangles touching a hole, so the hole in the collisions can be up to a cell larger than the one drawn: make the model go a bit further than the hole, or put rocks around the entrance.", + "fullName": "Make a hole", + "functionType": "Action", + "group": "Holes", + "name": "MakeHole", + "sentence": "Make a hole in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "object.__terrain3D.makeHole(x, y, x, y, radius);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "name": "Radius", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Fill the holes in a circle with the layers of the ground around them: the ground is drawn and has collisions again.", + "fullName": "Fill holes", + "functionType": "Action", + "group": "Holes", + "name": "FillHoles", + "sentence": "Fill the holes in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "object.__terrain3D.fillHoles(x, y, x, y, radius);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "name": "Radius", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Make the entrance of a tunnel or a cave on a slope: a hole, with the ground in front of it flattened to the floor of the tunnel.", + "fullName": "Carve an entrance", + "functionType": "Action", + "group": "Holes", + "name": "CarveEntrance", + "sentence": "Carve an entrance in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_, going in at _PARAM4_° with its floor at Z _PARAM5_ and _PARAM6_ pixels of flat ground in front", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const angle = eventsFunctionContext.getArgument(\"Angle\");", + "const floorZ = eventsFunctionContext.getArgument(\"FloorZ\");", + "const length = eventsFunctionContext.getArgument(\"Length\");", + "object.__terrain3D.carveEntrance(x, y, radius, angle, floorZ, length);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "name": "Radius", + "type": "expression" + }, + { + "description": "Direction going into the ground (in degrees)", + "name": "Angle", + "type": "expression" + }, + { + "description": "Z position of the floor", + "name": "FloorZ", + "type": "expression" + }, + { + "description": "Length of the flat ground in front of the entrance (in pixels)", + "name": "Length", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Check if there is a hole in the ground at a position.", + "fullName": "Hole", + "functionType": "Condition", + "group": "Holes", + "name": "IsHole", + "sentence": "There is a hole in _PARAM0_ at _PARAM1_; _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "eventsFunctionContext.returnValue = object.__terrain3D.isHoleAt(x, y);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "The Z position of the ground at a position, or the given value where there is a hole.", + "fullName": "Ground Z position, with holes", + "functionType": "Expression", + "group": "Holes", + "name": "GroundZWithHoles", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const terrain = object.__terrain3D;", + "eventsFunctionContext.returnValue = terrain.isHoleAt(x, y) ? eventsFunctionContext.getArgument(\"ZInHoles\") : terrain.getHeightAt(x, y);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + }, + { + "description": "Value in holes", + "longDescription": "For example, a Z position far under the ground.", + "name": "ZInHoles", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Replace the ground with a generated relief. Painted layers are reset.", + "fullName": "Generate a relief", + "functionType": "Action", + "group": "Generation", + "name": "GenerateRelief", + "sentence": "Generate a relief of _PARAM1_ on _PARAM0_ with seed _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const relief = eventsFunctionContext.getArgument(\"Relief\");", + "const seed = eventsFunctionContext.getArgument(\"Seed\");", + "object.__terrain3D.generate(relief, seed);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "Relief", + "name": "Relief", + "supplementaryInformation": "[\"Flat\", \"Hills\", \"Mountains\", \"Island\"]", + "type": "stringWithSelector" + }, + { + "description": "Seed", + "longDescription": "Each seed gives a different terrain with the same relief.", + "name": "Seed", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Replace the ground with the heights of an image. Painted layers are kept.", + "fullName": "Load a heightmap", + "functionType": "Action", + "group": "Generation", + "name": "LoadHeightmap", + "sentence": "Load the heights of _PARAM0_ from _PARAM1_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const image = eventsFunctionContext.getArgument(\"Image\");", + "object.__terrain3D.loadHeightmap(image);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "Heightmap image", + "longDescription": "Black is the lowest, white the highest. For more precision, store the height on 16 bits in the red (most significant byte) and green channels.", + "name": "Image", + "type": "imageResource" + } + ], + "objectGroups": [] + }, + { + "description": "Move objects up or down so that their bottom is on the ground, under their center. Useful to place trees, rocks or characters on a terrain.", + "fullName": "Put objects on the ground", + "functionType": "Action", + "group": "Position", + "name": "PutOnGround", + "sentence": "Put _PARAM1_ on the ground of _PARAM0_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrain3D.placeOnGround(eventsFunctionContext.getObjects(\"Objects\"));" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "Objects to put on the ground", + "name": "Objects", + "type": "objectList" + } + ], + "objectGroups": [] + }, + { + "description": "the Z position of the ground at a position of the scene.", + "fullName": "Ground Z position", + "functionType": "ExpressionAndCondition", + "group": "Position", + "name": "GroundZ", + "sentence": "the ground Z position at _PARAM1_; _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "eventsFunctionContext.returnValue = object.__terrain3D.getHeightAt(x, y);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "the slope of the ground (angle with the horizontal, in degrees) at a position of the scene.", + "fullName": "Ground slope", + "functionType": "ExpressionAndCondition", + "group": "Position", + "name": "GroundSlope", + "sentence": "the ground slope at _PARAM1_; _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "eventsFunctionContext.returnValue = object.__terrain3D.getSlopeAt(x, y);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "the layer (from 1 to 4) painted the most at a position of the scene. Useful to play footstep sounds or to slow down characters in water or mud.", + "fullName": "Ground layer", + "functionType": "ExpressionAndCondition", + "group": "Paint", + "name": "GroundLayer", + "sentence": "the ground layer at _PARAM1_; _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "eventsFunctionContext.returnValue = object.__terrain3D.getLayerAt(x, y);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + } + ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + }, + { + "folderName": "Sculpt", + "children": [ + { + "functionName": "RaiseGround" + }, + { + "functionName": "RaiseGroundAlongLine" + }, + { + "functionName": "FlattenGround" + }, + { + "functionName": "SmoothGround" + } + ] + }, + { + "folderName": "Paint", + "children": [ + { + "functionName": "PaintLayer" + }, + { + "functionName": "PaintLayerAlongLine" + }, + { + "functionName": "TintGround" + }, + { + "functionName": "WetGround" + }, + { + "functionName": "GroundWetness" + }, + { + "functionName": "GroundTint" + }, + { + "functionName": "GroundLayer" + } + ] + }, + { + "folderName": "Holes", + "children": [ + { + "functionName": "MakeHole" + }, + { + "functionName": "FillHoles" + }, + { + "functionName": "CarveEntrance" + }, + { + "functionName": "IsHole" + }, + { + "functionName": "GroundZWithHoles" + } + ] + }, + { + "folderName": "Generation", + "children": [ + { + "functionName": "GenerateRelief" + }, + { + "functionName": "LoadHeightmap" + } + ] + }, + { + "folderName": "Position", + "children": [ + { + "functionName": "PutOnGround" + }, + { + "functionName": "GroundZ" + }, + { + "functionName": "GroundSlope" + } + ] + } + ] + }, + "propertyDescriptors": [ + { + "value": "Hills", + "type": "Choice", + "label": "Relief", + "description": "The initial shape, before sculpting.", + "group": "Shape", + "choices": [ + { + "label": "Flat", + "value": "Flat" + }, + { + "label": "Hills", + "value": "Hills" + }, + { + "label": "Mountains", + "value": "Mountains" + }, + { + "label": "Island", + "value": "Island" + } + ], + "name": "Relief" + }, + { + "value": "1", + "type": "Number", + "label": "Seed", + "description": "Each seed gives a different terrain with the same relief.", + "group": "Shape", + "name": "Seed" + }, + { + "value": "", + "type": "Resource", + "label": "Heightmap image", + "description": "Optional. Replaces the relief: black is the lowest, white the highest (the object depth).", + "group": "Shape", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "HeightmapImage" + }, + { + "value": "", + "type": "MultilineString", + "label": "Edits", + "description": "Changes of the relief or heightmap, applied in order, as a JSON list, for example [{\"tool\": \"raise\", \"x\": 0.3, \"y\": 0.4, \"radius\": 0.15, \"height\": 0.4}, {\"tool\": \"paint\", \"x\": 0.1, \"y\": 0.9, \"toX\": 0.8, \"toY\": 0.2, \"radius\": 0.02, \"layer\": 2}]. Tools: raise (height added to the ground, negative to lower), flatten (z: the ground is brought to this height, fully at the center, cutting what is above it and filling what is below; with toX and toY, toZ gives the height at the end of the line, for a road going up), smooth (strength), paint (layer, from 1 to 4, and strength), tint (color, like \"120;140;60\", multiplying the layers, white removing the tint, and strength), wetness (amount, from 0 for dry to 1 for wet, and strength), hole (no ground in the radius, for the entrances of tunnels and caves) and fill (the holes in the radius are filled with the layers of the ground around them). x, y and radius are fractions (0 to 1) of the terrain size (x is (scene X - terrain X) / terrain width). height and z are fractions of its depth: the ground at z is at Z = terrain Z + z * terrain depth. Add toX and toY to change the ground along a line (paths, rivers, ridges). Shown in the scene editor.", + "group": "Shape", + "advanced": true, + "name": "Edits" + }, + { + "value": "256", + "type": "Choice", + "label": "Resolution", + "description": "The number of cells on each side of the terrain. Higher is more detailed but slower.", + "group": "Shape", + "choices": [ + { + "label": "64 x 64", + "value": "64" + }, + { + "label": "128 x 128", + "value": "128" + }, + { + "label": "256 x 256", + "value": "256" + }, + { + "label": "512 x 512", + "value": "512" + }, + { + "label": "1024 x 1024", + "value": "1024" + } + ], + "name": "Resolution" + }, + { + "value": "96;160;64", + "type": "Color", + "label": "Layer 1 color (grass)", + "group": "Layers", + "name": "Layer1Color" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 1 texture", + "description": "Optional. Tinted by the layer color.", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer1Texture" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 1 normal map", + "description": "Optional. The bumps of the texture, lit by the lights (an OpenGL normal map, like the textures of ambientCG).", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer1NormalMap" + }, + { + "value": "150;112;72", + "type": "Color", + "label": "Layer 2 color (dirt)", + "group": "Layers", + "name": "Layer2Color" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 2 texture", + "description": "Optional. Tinted by the layer color.", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer2Texture" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 2 normal map", + "description": "Optional. The bumps of the texture, lit by the lights (an OpenGL normal map, like the textures of ambientCG).", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer2NormalMap" + }, + { + "value": "124;118;112", + "type": "Color", + "label": "Layer 3 color (rock)", + "group": "Layers", + "name": "Layer3Color" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 3 texture", + "description": "Optional. Tinted by the layer color.", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer3Texture" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 3 normal map", + "description": "Optional. The bumps of the texture, lit by the lights (an OpenGL normal map, like the textures of ambientCG).", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer3NormalMap" + }, + { + "value": "224;204;148", + "type": "Color", + "label": "Layer 4 color (sand)", + "group": "Layers", + "name": "Layer4Color" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 4 texture", + "description": "Optional. Tinted by the layer color.", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer4Texture" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 4 normal map", + "description": "Optional. The bumps of the texture, lit by the lights (an OpenGL normal map, like the textures of ambientCG).", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer4NormalMap" + }, + { + "value": "1", + "type": "Number", + "label": "Normal map strength", + "description": "How much the normal maps bump the ground: 0 for none, 1 as they are, more to exaggerate them.", + "group": "Layers", + "name": "NormalMapStrength" + }, + { + "value": "256", + "type": "Number", + "unit": "Pixel", + "label": "Texture size", + "description": "The size, in the scene, of one repetition of the layer textures.", + "group": "Layers", + "name": "TextureSize" + }, + { + "value": "0.5", + "type": "Number", + "label": "Height blending", + "description": "Where layers meet, the bright parts of textures (stones, tufts) cover the dark ones (gaps, sand) instead of fading into each other: 0 for none, 1 for the most.", + "group": "Layers", + "name": "HeightBlending" + }, + { + "value": "0.5", + "type": "Number", + "label": "Texture variation", + "description": "Makes the repetition of textures less visible, with patches of bigger and rotated textures and colors slowly varying: 0 for none, 1 for the most.", + "group": "Layers", + "name": "TextureVariation" + }, + { + "value": "false", + "type": "Boolean", + "label": "Wet shore", + "description": "Make the ground wet (darker and shiny) under a Z position, like the sand of a beach near the water.", + "group": "Wetness", + "name": "WetShore" + }, + { + "value": "0", + "type": "Number", + "unit": "Pixel", + "label": "Wet below Z", + "description": "The ground is wet under this Z position (for example, the Z of a 3D water object).", + "group": "Wetness", + "name": "WetShoreZ" + }, + { + "value": "60", + "type": "Number", + "unit": "Pixel", + "label": "Wet shore fade", + "description": "The ground becomes dry over this height above \"Wet below Z\".", + "group": "Wetness", + "name": "WetShoreFade" + }, + { + "value": "3", + "type": "Choice", + "label": "Layer on steep slopes", + "description": "This layer is shown where the ground is steep, whatever is painted.", + "group": "Layers", + "choices": [ + { + "label": "None", + "value": "0" + }, + { + "label": "Layer 1", + "value": "1" + }, + { + "label": "Layer 2", + "value": "2" + }, + { + "label": "Layer 3", + "value": "3" + }, + { + "label": "Layer 4", + "value": "4" + } + ], + "name": "CliffLayer" + }, + { + "value": "45", + "type": "Number", + "unit": "DegreeAngle", + "label": "Steep slope angle", + "group": "Layers", + "name": "CliffAngle" + }, + { + "value": "true", + "type": "Boolean", + "label": "Cast shadows", + "group": "Rendering", + "name": "CastShadow" + }, + { + "value": "true", + "type": "Boolean", + "label": "Receive shadows", + "group": "Rendering", + "name": "ReceiveShadow" + }, + { + "value": "", + "type": "String", + "label": "Sculpted and painted data", + "description": "Written by the sculpt and paint tools of the scene editor, in a compact format not meant to be read or written by hand: use Edits instead. Clear it to go back to the relief, heightmap and edits.", + "advanced": true, + "name": "SculptData" + } + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Shape", + "children": [ + { + "propertyName": "Relief" + }, + { + "propertyName": "Seed" + }, + { + "propertyName": "HeightmapImage" + }, + { + "propertyName": "Edits" + }, + { + "propertyName": "Resolution" + } + ] + }, + { + "folderName": "Layers", + "children": [ + { + "propertyName": "Layer1Color" + }, + { + "propertyName": "Layer1Texture" + }, + { + "propertyName": "Layer1NormalMap" + }, + { + "propertyName": "Layer2Color" + }, + { + "propertyName": "Layer2Texture" + }, + { + "propertyName": "Layer2NormalMap" + }, + { + "propertyName": "Layer3Color" + }, + { + "propertyName": "Layer3Texture" + }, + { + "propertyName": "Layer3NormalMap" + }, + { + "propertyName": "Layer4Color" + }, + { + "propertyName": "Layer4Texture" + }, + { + "propertyName": "Layer4NormalMap" + }, + { + "propertyName": "NormalMapStrength" + }, + { + "propertyName": "TextureSize" + }, + { + "propertyName": "HeightBlending" + }, + { + "propertyName": "TextureVariation" + }, + { + "propertyName": "CliffLayer" + }, + { + "propertyName": "CliffAngle" + } + ] + }, + { + "folderName": "Wetness", + "children": [ + { + "propertyName": "WetShore" + }, + { + "propertyName": "WetShoreZ" + }, + { + "propertyName": "WetShoreFade" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "CastShadow" + }, + { + "propertyName": "ReceiveShadow" + } + ] + }, + { + "propertyName": "SculptData" + } + ] + }, + "variants": [] + }, + { + "areaMaxX": 1000, + "areaMaxY": 1000, + "areaMaxZ": 40, + "areaMinX": 0, + "areaMinY": 0, + "areaMinZ": 0, + "defaultName": "Grass", + "description": "A field of grass blades swaying in the wind and pushed aside by characters. They grow in the area of the object, on the highest 3D terrain at each point (one field can cover several terrains), or at the Z position of the object where there is no terrain.", + "fullName": "Terrain grass", + "helpPath": "", + "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGQ9Ik0zIDIxYzEtNSAxLTkgMC0xMyAyIDMgMyA3IDMgMTN6bTQgMGMwLTYgMS0xMSA0LTE2LTEgNS0xIDEwIDAgMTZ6bTUgMGMwLTUgMi05IDUtMTItMiA0LTIgOC0xIDEyem01IDBjMS00IDItNiA0LTgtMSAzLTEgNS0xIDh6Ii8+PC9zdmc+", + "is3D": true, + "isUsingLegacyInstancesRenderer": true, + "name": "TerrainGrass", + "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/732ef90b7fcf5dd9171fe95d0cf262e09159b487304915a4693baa893d9ce16c_grass.svg", + "objects": [ + { + "assetStoreId": "", + "name": "AreaPlaceholder", + "type": "Scene3D::Cube3DObject", + "variables": [], + "effects": [], + "behaviors": [], + "content": { + "width": 1000, + "height": 1000, + "depth": 40, + "enableTextureTransparency": false, + "facesOrientation": "Y", + "backFaceUpThroughWhichAxisRotation": "X", + "materialType": "Basic", + "tint": "255;255;255", + "frontFaceResourceName": "", + "frontFaceVisible": false, + "frontFaceResourceRepeat": false, + "backFaceResourceName": "", + "backFaceVisible": false, + "backFaceResourceRepeat": false, + "leftFaceResourceName": "", + "leftFaceVisible": false, + "leftFaceResourceRepeat": false, + "rightFaceResourceName": "", + "rightFaceVisible": false, + "rightFaceResourceRepeat": false, + "topFaceResourceName": "", + "topFaceVisible": false, + "topFaceResourceRepeat": false, + "bottomFaceResourceName": "", + "bottomFaceVisible": false, + "bottomFaceResourceRepeat": false + } + } + ], + "objectsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "objectName": "AreaPlaceholder" + } + ] + }, + "objectsGroups": [], + "layers": [ + { + "ambientLightColorB": 200, + "ambientLightColorG": 200, + "ambientLightColorR": 200, + "camera2DPlaneMaxDrawingDistance": 5000, + "camera3DFarPlaneDistance": 10000, + "camera3DFieldOfView": 45, + "camera3DNearPlaneDistance": 3, + "cameraType": "", + "followBaseLayerCamera": false, + "isLightingLayer": false, + "isLocked": false, + "name": "", + "renderingType": "", + "visibility": true, + "cameras": [ + { + "defaultSize": true, + "defaultViewport": true, + "height": 0, + "viewportBottom": 1, + "viewportLeft": 0, + "viewportRight": 1, + "viewportTop": 0, + "width": 0 + } + ], + "effects": [] + } + ], + "instances": [ + { + "angle": 0, + "customSize": true, + "depth": 40, + "height": 1000, + "keepRatio": true, + "layer": "", + "name": "AreaPlaceholder", + "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-001000001000", + "width": 1000, + "x": 0, + "y": 0, + "zOrder": 1, + "numberProperties": [], + "stringProperties": [], + "initialVariables": [] + } + ], + "editionSettings": {}, + "eventsFunctions": [ + { + "fullName": "", + "functionType": "Action", + "name": "onCreated", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Terrain3D::DefineHelperClasses" + }, + "parameters": [ + "", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Terrain3D::DefineGrassHelperClasses" + }, + "parameters": [ + "", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "for (const child of object.getChildrenContainer().getAdhocListOfAllInstances()) {", + " child.deleteFromScene();", + "}", + "if (object.__terrainGrass) object.__terrainGrass.dispose();", + "object.__terrainGrass = new gdjs.__terrainGrassExtension.Grass(object);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "onDestroy", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrainGrass.dispose();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "doStepPostEvents", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const game = runtimeScene.getGame();", + "// Time doesn't pass in the scene editor: real time is used to show the wind.", + "const isInGameEdition = game.isInGameEdition && game.isInGameEdition();", + "const elapsedTime = isInGameEdition ? 1 / 60 : object.getElapsedTime() / 1000;", + "const time = isInGameEdition", + " ? performance.now() / 1000", + " : object.getRuntimeScene().getTimeManager().getTimeFromStart() / 1000;", + "const layer = object.getInstanceContainer().getLayer(object.getLayer());", + "object.__terrainGrass.update(elapsedTime, time, layer.getRenderer().getThreeCamera());" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "onHotReloading", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrainGrass.updateFromProperties();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" + } + ], + "objectGroups": [] + } + ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + } + ] + }, + "propertyDescriptors": [ + { + "value": "60", + "type": "Number", + "label": "Density", + "description": "The number of blades in a square of 100 x 100 pixels.", + "group": "Blades", + "name": "Density" + }, + { + "value": "36", + "type": "Number", + "unit": "Pixel", + "label": "Blade height", + "description": "The average height of blades (each blade is between 60% and 140% of it).", + "group": "Blades", + "name": "BladeHeight" + }, + { + "value": "5", + "type": "Number", + "unit": "Pixel", + "label": "Blade width", + "group": "Blades", + "name": "BladeWidth" + }, + { + "value": "52;104;36", + "type": "Color", + "label": "Base color", + "group": "Blades", + "name": "BaseColor" + }, + { + "value": "150;196;82", + "type": "Color", + "label": "Tip color", + "group": "Blades", + "name": "TipColor" + }, + { + "value": "1", + "type": "Number", + "label": "Seed", + "description": "Each seed places blades differently.", + "group": "Blades", + "name": "Seed" + }, + { + "value": "1", + "type": "Choice", + "label": "Grows on terrain layer", + "description": "On a 3D terrain, blades only grow where this layer is painted. A new terrain has layer 1 painted everywhere.", + "group": "Ground", + "choices": [ + { + "label": "Any", + "value": "0" + }, + { + "label": "Layer 1", + "value": "1" + }, + { + "label": "Layer 2", + "value": "2" + }, + { + "label": "Layer 3", + "value": "3" + }, + { + "label": "Layer 4", + "value": "4" + } + ], + "name": "GroundLayer" + }, + { + "value": "40", + "type": "Number", + "unit": "DegreeAngle", + "label": "Maximum slope", + "description": "No blades grow on steeper ground.", + "group": "Ground", + "name": "MaxSlope" + }, + { + "value": "1", + "type": "Number", + "label": "Wind strength", + "description": "0 for no wind.", + "group": "Wind", + "name": "WindStrength" + }, + { + "value": "2", + "type": "Number", + "label": "Wind speed", + "group": "Wind", + "name": "WindSpeed" + }, + { + "value": "", + "type": "String", + "label": "Object bending the grass", + "description": "The name of an object pushing blades aside when walking through the grass, like the player. The 8 first instances are used.", + "group": "Wind", + "name": "BendingObject" + }, + { + "value": "2500", + "type": "Number", + "unit": "Pixel", + "label": "Fade distance", + "description": "Blades are less and less dense up to this distance from the camera, and hidden after.", + "group": "Rendering", + "name": "FadeDistance" + } + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Blades", + "children": [ + { + "propertyName": "Density" + }, + { + "propertyName": "BladeHeight" + }, + { + "propertyName": "BladeWidth" + }, + { + "propertyName": "BaseColor" + }, + { + "propertyName": "TipColor" + }, + { + "propertyName": "Seed" + } + ] + }, + { + "folderName": "Ground", + "children": [ + { + "propertyName": "GroundLayer" + }, + { + "propertyName": "MaxSlope" + } + ] + }, + { + "folderName": "Wind", + "children": [ + { + "propertyName": "WindStrength" + }, + { + "propertyName": "WindSpeed" + }, + { + "propertyName": "BendingObject" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "FadeDistance" + } + ] + } + ] + }, + "variants": [] + }, + { + "areaMaxX": 2000, + "areaMaxY": 2000, + "areaMaxZ": 200, + "areaMinX": 0, + "areaMinY": 0, + "areaMinZ": 0, + "assetStoreTag": "3d scattered models", + "defaultName": "ScatteredModels", + "description": "Many copies of a 3D model (trees, rocks, bushes, flowers...) placed in the area of the object and drawn all at once, which is much faster than an object for each one. They stand on the highest 3D terrain at each point, or at the Z position of the object where there is no terrain. Paint where they grow with the brushes of the scene editor.", + "fullName": "Scattered models (forests, rocks...)", + "helpPath": "", + "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGQ9Ik0xMCAyMXYtM0gzbDUtNUg1bDUtNUg3bDUtNSA1IDVoLTNsNSA1aC0zbDUgNWgtN3YzeiIvPjwvc3ZnPg==", + "is3D": true, + "isUsingLegacyInstancesRenderer": true, + "name": "ScatteredModels", + "previewIconUrl": "", + "objects": [ + { + "assetStoreId": "", + "name": "AreaPlaceholder", + "type": "Scene3D::Cube3DObject", + "variables": [], + "effects": [], + "behaviors": [], + "content": { + "width": 2000, + "height": 2000, + "depth": 200, + "enableTextureTransparency": false, + "facesOrientation": "Y", + "backFaceUpThroughWhichAxisRotation": "X", + "materialType": "Basic", + "tint": "255;255;255", + "frontFaceResourceName": "", + "frontFaceVisible": false, + "frontFaceResourceRepeat": false, + "backFaceResourceName": "", + "backFaceVisible": false, + "backFaceResourceRepeat": false, + "leftFaceResourceName": "", + "leftFaceVisible": false, + "leftFaceResourceRepeat": false, + "rightFaceResourceName": "", + "rightFaceVisible": false, + "rightFaceResourceRepeat": false, + "topFaceResourceName": "", + "topFaceVisible": false, + "topFaceResourceRepeat": false, + "bottomFaceResourceName": "", + "bottomFaceVisible": false, + "bottomFaceResourceRepeat": false + } + } + ], + "objectsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "objectName": "AreaPlaceholder" + } + ] + }, + "objectsGroups": [], + "layers": [ + { + "ambientLightColorB": 200, + "ambientLightColorG": 200, + "ambientLightColorR": 200, + "camera2DPlaneMaxDrawingDistance": 5000, + "camera3DFarPlaneDistance": 10000, + "camera3DFieldOfView": 45, + "camera3DNearPlaneDistance": 3, + "cameraType": "", + "followBaseLayerCamera": false, + "isLightingLayer": false, + "isLocked": false, + "name": "", + "renderingType": "", + "visibility": true, + "cameras": [ + { + "defaultSize": true, + "defaultViewport": true, + "height": 0, + "viewportBottom": 1, + "viewportLeft": 0, + "viewportRight": 1, + "viewportTop": 0, + "width": 0 + } + ], + "effects": [] + } + ], + "instances": [ + { + "angle": 0, + "customSize": true, + "depth": 200, + "height": 2000, + "keepRatio": true, + "layer": "", + "name": "AreaPlaceholder", + "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-002000002000", + "width": 2000, + "x": 0, + "y": 0, + "zOrder": 1, + "numberProperties": [], + "stringProperties": [], + "initialVariables": [] + } + ], + "editionSettings": {}, + "eventsFunctions": [ + { + "fullName": "", + "functionType": "Action", + "name": "onCreated", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Terrain3D::DefineHelperClasses" + }, + "parameters": [ + "", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Terrain3D::DefineScatterHelperClasses" + }, + "parameters": [ + "", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "for (const child of object.getChildrenContainer().getAdhocListOfAllInstances()) {", + " child.deleteFromScene();", + "}", + "if (object.__terrainScatter) object.__terrainScatter.dispose();", + "object.__terrainScatter = new gdjs.__terrainScatterExtension.Scatter(object);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "onDestroy", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrainScatter.dispose();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "doStepPostEvents", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const game = runtimeScene.getGame();", + "const isInGameEdition = game.isInGameEdition && game.isInGameEdition();", + "const elapsedTime = isInGameEdition ? 1 / 60 : object.getElapsedTime() / 1000;", + "const layer = object.getInstanceContainer().getLayer(object.getLayer());", + "object.__terrainScatter.update(elapsedTime, layer.getRenderer().getThreeCamera());" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "onHotReloading", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrainScatter.onHotReloading();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "description": "Paint the density of copies around a position, up to the density of the object (copies removed with \"Erase copies\" come back). The effect fades out up to the radius.", + "fullName": "Add copies", + "functionType": "Action", + "group": "Paint", + "name": "AddCopies", + "sentence": "Add copies of _PARAM0_ around _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "object.__terrainScatter.addCopies(x, y, radius);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "name": "Radius", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Remove the copies standing in a circle, for example a tree cut by the player (use the position of the nearest copy and a small radius) or a forest cleared for a building. They come back when the scene is restarted.", + "fullName": "Erase copies", + "functionType": "Action", + "group": "Paint", + "name": "EraseCopies", + "sentence": "Erase the copies of _PARAM0_ in a circle at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "object.__terrainScatter.removeCopies(x, y, radius);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "name": "Radius", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "the number of copies placed in the area.", + "fullName": "Number of copies", + "functionType": "ExpressionAndCondition", + "group": "Copies", + "name": "CopyCount", + "sentence": "the number of copies", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "eventsFunctionContext.returnValue = object.__terrainScatter.getCopyCount();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "description": "The X position of the base of the copy nearest to a position (the position itself if there is no copy).", + "fullName": "X position of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyX", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? copy.x : x;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "The Y position of the base of the copy nearest to a position (the position itself if there is no copy).", + "fullName": "Y position of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyY", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? copy.y : y;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "The Z position of the base of the copy nearest to a position (on the ground).", + "fullName": "Z position of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyZ", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? copy.z : object.getZ();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "the distance (on X and Y) from a position to the base of the nearest copy. It's a very big number when there is no copy.", + "fullName": "Distance to the nearest copy", + "functionType": "ExpressionAndCondition", + "group": "Copies", + "name": "NearestCopyDistance", + "sentence": "the distance from _PARAM1_; _PARAM2_ to the nearest copy", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? Math.hypot(copy.x - x, copy.y - y) : Number.MAX_VALUE;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + } + ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + }, + { + "folderName": "Paint", + "children": [ + { + "functionName": "AddCopies" + }, + { + "functionName": "EraseCopies" + } + ] + }, + { + "folderName": "Copies", + "children": [ + { + "functionName": "CopyCount" + }, + { + "functionName": "NearestCopyX" + }, + { + "functionName": "NearestCopyY" + }, + { + "functionName": "NearestCopyZ" + }, + { + "functionName": "NearestCopyDistance" + } + ] + } + ] + }, + "propertyDescriptors": [ + { + "value": "", + "type": "Resource", + "label": "3D model", + "description": "The model copied everywhere (a .glb file). Models standing on the Y axis, like most models of the asset store, are stood up along Z.", + "group": "Model", + "extraInformation": [ + "model3D" + ], + "choices": [], + "name": "Model" + }, + { + "value": "300", + "type": "Number", + "unit": "Pixel", + "label": "Height of a copy", + "description": "Copies keep the proportions of the model.", + "group": "Model", + "name": "Size" + }, + { + "value": "0.8", + "type": "Number", + "label": "Smallest random scale", + "description": "Each copy is scaled at random between the smallest and the largest scale.", + "group": "Model", + "name": "MinScale" + }, + { + "value": "1.2", + "type": "Number", + "label": "Largest random scale", + "group": "Model", + "name": "MaxScale" + }, + { + "value": "true", + "type": "Boolean", + "label": "Random rotation", + "description": "Turn each copy around the Z axis at random.", + "group": "Model", + "name": "RandomRotation" + }, + { + "value": "255;255;255", + "type": "Color", + "label": "Random tint", + "description": "Each copy is tinted between white and this color, at random. White for no tint.", + "group": "Model", + "name": "RandomTint" + }, + { + "value": "0.3", + "type": "Number", + "label": "Density", + "description": "The number of copies in a square of 100 x 100 pixels where the density is fully painted (0.3 is about one copy every 180 pixels). Copies never overlap their neighbors much: they are placed at random in a grid.", + "group": "Placement", + "name": "Density" + }, + { + "value": "1", + "type": "Number", + "label": "Seed", + "description": "Each seed places copies differently.", + "group": "Placement", + "name": "Seed" + }, + { + "value": "Everywhere", + "type": "Choice", + "label": "Copies grow", + "description": "Everywhere in the area, or only where they are painted with the \"Add copies\" brush (or the edits).", + "group": "Placement", + "choices": [ + { + "label": "Everywhere (erase them with the brush)", + "value": "Everywhere" + }, + { + "label": "Only where painted", + "value": "Painted" + } + ], + "name": "Coverage" + }, + { + "value": "1", + "type": "Choice", + "label": "Grows on terrain layer", + "description": "On a 3D terrain, copies only grow where this layer is painted, away from its borders (so that trees don't stand on roads painted with another layer). A new terrain has layer 1 painted everywhere.", + "group": "Ground", + "choices": [ + { + "label": "Any", + "value": "0" + }, + { + "label": "Layer 1", + "value": "1" + }, + { + "label": "Layer 2", + "value": "2" + }, + { + "label": "Layer 3", + "value": "3" + }, + { + "label": "Layer 4", + "value": "4" + } + ], + "name": "GroundLayer" + }, + { + "value": "30", + "type": "Number", + "unit": "DegreeAngle", + "label": "Maximum slope", + "description": "No copies grow on steeper ground.", + "group": "Ground", + "name": "MaxSlope" + }, + { + "value": "-100000", + "type": "Number", + "unit": "Pixel", + "label": "Minimum ground Z", + "description": "No copies grow where the ground is lower (for example, under the water level).", + "group": "Ground", + "name": "MinGroundZ" + }, + { + "value": "100000", + "type": "Number", + "unit": "Pixel", + "label": "Maximum ground Z", + "description": "No copies grow where the ground is higher (for example, above the tree line).", + "group": "Ground", + "name": "MaxGroundZ" + }, + { + "value": "false", + "type": "Boolean", + "label": "Trunk collisions", + "description": "Give each copy a static collision shape around its trunk, for the 3D physics behavior (add it to this object, with the \"Static\" type) and navigation meshes. Without them, these behaviors have no effect on this object.", + "group": "Collisions", + "name": "TrunkCollisions" + }, + { + "value": "30", + "type": "Number", + "unit": "Pixel", + "label": "Trunk diameter", + "description": "The diameter of the collision shape of a copy at the height given in \"Height of a copy\" (it is scaled with the copy).", + "group": "Collisions", + "name": "TrunkDiameter" + }, + { + "value": "8000", + "type": "Number", + "unit": "Pixel", + "label": "Draw distance", + "description": "Copies shrink and are hidden at this distance from the camera.", + "group": "Rendering", + "name": "DrawDistance" + }, + { + "value": "true", + "type": "Boolean", + "label": "Cast shadows", + "group": "Rendering", + "name": "CastShadow" + }, + { + "value": "true", + "type": "Boolean", + "label": "Receive shadows", + "group": "Rendering", + "name": "ReceiveShadow" + }, + { + "value": "", + "type": "MultilineString", + "label": "Edits", + "description": "Where copies are added or erased, applied in order, as a JSON list, for example [{\"tool\": \"add\", \"x\": 0.3, \"y\": 0.4, \"radius\": 0.15}, {\"tool\": \"erase\", \"x\": 0.1, \"y\": 0.9, \"toX\": 0.8, \"toY\": 0.2, \"radius\": 0.02}]. Tools: add (density: from 0 to 1, 1 by default, how dense copies are compared to the Density property) and erase. strength (from 0 to 1) makes a stroke lighter. x, y and radius are fractions (0 to 1) of the object size (x is (scene X - object X) / object width). Add toX and toY to paint along a line (a row of trees, a path through a forest). With \"Copies grow: only where painted\", only the added areas have copies. Shown in the scene editor.", + "group": "Placement", + "advanced": true, + "name": "Edits" + }, + { + "value": "", + "type": "String", + "label": "Painted data", + "description": "Written by the brushes of the scene editor, in a compact format not meant to be read or written by hand: use Edits instead. Clear it to go back to the coverage and the edits.", + "advanced": true, + "name": "PaintData" + } + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Model", + "children": [ + { + "propertyName": "Model" + }, + { + "propertyName": "Size" + }, + { + "propertyName": "MinScale" + }, + { + "propertyName": "MaxScale" + }, + { + "propertyName": "RandomRotation" + }, + { + "propertyName": "RandomTint" + } + ] + }, + { + "folderName": "Placement", + "children": [ + { + "propertyName": "Density" + }, + { + "propertyName": "Seed" + }, + { + "propertyName": "Coverage" + }, + { + "propertyName": "Edits" + } + ] + }, + { + "folderName": "Ground", + "children": [ + { + "propertyName": "GroundLayer" + }, + { + "propertyName": "MaxSlope" + }, + { + "propertyName": "MinGroundZ" + }, + { + "propertyName": "MaxGroundZ" + } + ] + }, + { + "folderName": "Collisions", + "children": [ + { + "propertyName": "TrunkCollisions" + }, + { + "propertyName": "TrunkDiameter" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "DrawDistance" + }, + { + "propertyName": "CastShadow" + }, + { + "propertyName": "ReceiveShadow" + } + ] + }, + { + "propertyName": "PaintData" + } + ] + }, + "variants": [] + } + ] } ], "externalLayouts": [] diff --git a/examples/starting-3d-top-down-rpg/assets/StartingWall.png b/examples/starting-3d-top-down-rpg/assets/StartingWall.png index b3a097887..06278d6e5 100644 Binary files a/examples/starting-3d-top-down-rpg/assets/StartingWall.png and b/examples/starting-3d-top-down-rpg/assets/StartingWall.png differ diff --git a/examples/starting-first-person-farming/assets/StartingWall.png b/examples/starting-first-person-farming/assets/StartingWall.png index b3a097887..06278d6e5 100644 Binary files a/examples/starting-first-person-farming/assets/StartingWall.png and b/examples/starting-first-person-farming/assets/StartingWall.png differ diff --git a/examples/starting-first-person-farming/starting-first-person-farming.json b/examples/starting-first-person-farming/starting-first-person-farming.json index ef18a3283..a4cf37469 100644 --- a/examples/starting-first-person-farming/starting-first-person-farming.json +++ b/examples/starting-first-person-farming/starting-first-person-farming.json @@ -1,7 +1,7 @@ { "firstLayout": "", "gdVersion": { - "build": 283, + "build": 284, "major": 5, "minor": 6, "revision": 0 @@ -345,6 +345,14 @@ "name": "assets/StartingWall.png", "smoothed": true, "userAdded": false + }, + { + "file": "assets/StartingGround.png", + "kind": "image", + "metadata": "", + "name": "assets/StartingGround.png", + "smoothed": true, + "userAdded": false } ] }, @@ -361,12 +369,13 @@ "mangledName": "Game_32Scene", "name": "Game Scene", "r": 255, + "renderer3DWorldScale": 100, "standardSortMethod": true, "stopSoundsOnStartup": true, "title": "", "v": 255, "uiSettings": { - "grid": false, + "grid": true, "gridType": "rectangular", "gridWidth": 64, "gridHeight": 64, @@ -376,7 +385,7 @@ "gridOffsetZ": 0, "gridColor": 10401023, "gridAlpha": 0.8, - "snap": false, + "snap": true, "zoomFactor": 0.34375202416125267, "windowMask": false, "selectedLayer": "", @@ -386,9 +395,6 @@ { "name": "RaycastSurface", "objects": [ - { - "name": "Ground" - }, { "name": "DirtPlot" } @@ -450,24 +456,6 @@ } ], "instances": [ - { - "angle": 0, - "customSize": true, - "depth": 30, - "height": 1728, - "layer": "", - "locked": true, - "name": "Ground", - "persistentUuid": "6a14e5ab-5c31-4692-8cd5-c50937fd60c6", - "width": 1920, - "x": -320, - "y": -576, - "z": -30, - "zOrder": 1, - "numberProperties": [], - "stringProperties": [], - "initialVariables": [] - }, { "angle": 0, "customSize": false, @@ -1614,77 +1602,24 @@ "numberProperties": [], "stringProperties": [], "initialVariables": [] + }, + { + "angle": 0, + "customSize": true, + "height": 1728, + "layer": "", + "name": "Terrain", + "persistentUuid": "07231b93-68ae-47aa-af1e-f7292bddf91f", + "width": 1920, + "x": -320, + "y": -576, + "zOrder": 0, + "numberProperties": [], + "stringProperties": [], + "initialVariables": [] } ], "objects": [ - { - "assetStoreId": "", - "name": "Ground", - "persistentUuid": "afb574c8-6385-4297-bb00-a353b9e5fd2c", - "type": "Scene3D::Cube3DObject", - "variables": [], - "effects": [], - "behaviors": [ - { - "name": "Physics3D", - "type": "Physics3D::Physics3DBehavior", - "object3D": "Object3D", - "bodyType": "Static", - "bullet": false, - "fixedRotation": false, - "shape": "Box", - "shapeOrientation": "Z", - "shapeDimensionA": 0, - "shapeDimensionB": 0, - "shapeDimensionC": 0, - "density": 1, - "friction": 0.3, - "restitution": 0.1, - "linearDamping": 0.1, - "angularDamping": 0.1, - "gravityScale": 1, - "layers": 17, - "masks": 17, - "shapeOffsetX": 0, - "shapeOffsetY": 0, - "shapeOffsetZ": 0, - "massCenterOffsetX": 0, - "massCenterOffsetY": 0, - "massCenterOffsetZ": 0, - "massOverride": 0 - } - ], - "content": { - "width": 1000, - "height": 1000, - "depth": 40, - "enableTextureTransparency": false, - "facesOrientation": "Y", - "frontFaceResourceName": "assets\\StartingGround.png", - "backFaceResourceName": "", - "backFaceUpThroughWhichAxisRotation": "X", - "leftFaceResourceName": "", - "rightFaceResourceName": "", - "topFaceResourceName": "", - "bottomFaceResourceName": "", - "frontFaceVisible": true, - "backFaceVisible": false, - "leftFaceVisible": false, - "rightFaceVisible": false, - "topFaceVisible": false, - "bottomFaceVisible": false, - "frontFaceResourceRepeat": true, - "backFaceResourceRepeat": false, - "leftFaceResourceRepeat": false, - "rightFaceResourceRepeat": false, - "topFaceResourceRepeat": false, - "bottomFaceResourceRepeat": false, - "materialType": "StandardWithoutMetalness", - "isCastingShadow": false, - "isReceivingShadow": true, - "tileScale": 0.2 - } - }, { "assetStoreId": "", "name": "DirtPlot", @@ -3469,6 +3404,59 @@ "isReceivingShadow": true, "tileScale": 0.2 } + }, + { + "assetStoreId": "", + "name": "Terrain", + "persistentUuid": "17bbac29-6ba4-4897-9d22-04f7f9d36a42", + "type": "Terrain3D::Terrain3D", + "variant": "", + "variables": [], + "effects": [], + "behaviors": [ + { + "name": "Physics3D", + "type": "Physics3D::Physics3DBehavior", + "object3D": "Object3D", + "bodyType": "Static", + "bullet": false, + "fixedRotation": false, + "shape": "Box", + "meshShapeResourceName": "", + "shapeOrientation": "Z", + "shapeDimensionA": 0, + "shapeDimensionB": 0, + "shapeDimensionC": 0, + "shapeOffsetX": 0, + "shapeOffsetY": 0, + "shapeOffsetZ": 0, + "massCenterOffsetX": 0, + "massCenterOffsetY": 0, + "massCenterOffsetZ": 0, + "massOverride": 0, + "density": 1, + "friction": 0.3, + "restitution": 0.1, + "linearDamping": 0.1, + "angularDamping": 0.1, + "gravityScale": 1, + "layers": 17, + "masks": 17 + } + ], + "content": { + "SculptData": "2;256;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AQABAQIBAgIAAQEAAQEAAQIBAAICAQEEAQEAVQIAAQIAAQIBAgIDAgIAAQIAARAAAQEAAQECAQABAQIAAQECAQEFBAACAgABAQADAQABAQACAQAFAgAEAQIAAQICAgEDAgIAAQECBgABAwABCAMAAQMCBAMGAwACAQEMAQMHAAICAAEBGhkEAwIABAECAQYDAQAEAQIABAEGAwABAgMCAAECAwIAAgEAAgQFAgQAAQEDAgAEAgMEAAEDAgIAAQUCAggHAAIBAgEAAgQBAQEAVwICAAEBAgIAAgQFAgIAAQ4AAQEAAwEAAQQDAAIFAQICAAECAAEBAQQDAQEAFgIEAQADAgEGAgcCCAEBAAEBBgECAAECBQACDAEJAAEDAgACAR4ZAgUCAgEBAAEBAgADAgECAQQAAQUEAQAEAQABBAMCAAEBAAMBBAEBBgMAAgIAAQMEAQEEAwIBAgABAgMAAQQDAgAFAwIAAgEAXAQAAwEGAwQBAAEEDgECAQABAwQCBQQHAQIGAwIBAQADAwAdAgIAAgEGBgcAAQYBAAEBAAECAgABAQABBAUAAQoDBQEAAwIAAQEEAQABAwIAAQIBAAMBAgACAgMAAQIBAgABAQEEAAQBAgMEAQIAAgEAAwECAgABAQACAQABBAMAAQIAAQMCAgEABgIAAQEBAF8EAgACAQABAgACAgwAAgECAgMAAQEFAgIAAwEAAgEBACICAgABAgQFAgYFBAEAAgIAAQIDAAIEAgMBAwABAgACHBsBAgACAgMEAAIDAAECAAEBAgACAgMAAQICAQACAwIBBgMEAwQFAgACAQABAgEEAQABAQABBgMBAgIDAAECAAEBAgECAAEBAAMBAGICAAIEAgEBAgACDgABBQQBAQQHAQQAAQIAAQMCAQEBACgECAUAAQYDAAEDCAMAAQQDCAUDDAEFAAEDAQIBAhgVAwQBAgEBAAICAAEBAgEAAQIBAAICAQIBAAICAwIBBAMAAQIAAgECAAECAgEDAgEEAwABAgEBBAEABwQFAGcCAgECAAECAAEBAgoBBAABAwIFAAECAAIBAAIDAC0GAQABCAUGBwQCAQYJBAEAAQwCDRAFBQUCBAEBBAwCBAkJAQQCBgcBAgMCAw4PAgECAQICAAIDBgABAQMCAQABAgABAQABAgECAgMCAAEBAgEAAQQDBgMCBQIBAGkCAAECAQQBAAEEAQgEAQABAQMAAQIMDQIAAQMBAC8EAgICAgYJBBQbCAABDg8IFAEkPygBKzoHKwMOHQEEJBcGAw02AysIEhECAgMCEBIjHAgFHx4GHxgBFRgbHBUOERYVEA0KDxQDDQwJCA0KAQMAAQQFAGkEAQIAAgIAAQIMAwIFAQQAAgUIAwMAMgIBBgUIAwABBQoUGQUKBgkDAgQWChkAARliRwUAAQoFEBEFEAsRDhYGIQwLNCMFEgcOCwcGHCMBAAEGAgMAAQIFAQACAQIBBAIKDwIKCwACBAMAAQQAAQMAawIAAQIBAAMIAgUBBAABAQABAwA3AgYHEgABBxQRHgMIDwABFDE+OQNAOzw3QhMaDQgpKAMREhsHDgsOCAUrJgQjCxIRAAICAQABIB8AARoZFhUWERATEA8MCwIBAAEEAwgHAgEAAQIBAGwEAQECAgECCgUBBAQFAgEDAPIBBAACAQIAAQQBBAIFAgABAQEA8gECAgIAAQEAAQIEAgMAAgMA9QECAAICAAEGAAEDAgABAwD2AQIAAQECAgYBAAIDAPgBBAEAAgICAgUA+gEEAAEDBAEBAPoBAgACAgQAAQMA+wECAAICAQD8AQIBAgIBAPwBAgIBAP8BAgCBAgIA/wECAQD/AQIAgQICAIACAgCBBAIAjBoBAIIGAQCABAECAP8BAgCCBAEA/wEBAgD/AQIDAIACAQD/AQMEAP8BAQD/AQEAggICAP0BAQEEAQCAAgEA/gECAQUA/gEBAwgA/AEBAAEDBgD9AQIDAgEBAPwBAQICAwD9AQMEAwYA/AEBBAMCAAECAPsBAgMCAgABBwD7AQECAgIHBgD7AQICAwUEAQD8AQEDBgEBAP0BBAkEBAD7AQEAAQEIAwD8AQMAAgICAgD6AQUEAAEEAwABAQD6AQIBAAEDAgEBAPoBAgIDAAEDAAEEAPoBAwABBAABAQYDAPoBAgMDAggBAwD6AQMAAQYCBwEGAPoBAgQDBwYBAPsBBAUCAgECAwD6AQECAQYCBQEA+gEDBAQDBwgGAPoBBAEFAAEIAQD8AQMEAgEBAPsBAwYAAQEAAgMA/gEBAAEEAPoBBAEDAAECBAEA+gEDAAEEAgIFAQD9AQEBAgD+AQEBAgQDAgIA+AEBAwIEAAIHAgYA9wEBAAEEAAMCAAEBAAECAPYBAQQAAgIBAQECAwQA9gEEAAEBAgEAAgECAQMGAPUBAgECAAEBBAMAAQECAAEDBAD0AQIAAQIDBAMEAwIAAQIBAAECAPQBAgECAQIAAQQDAQIAAQEAAQIA8gECAAIBAgEAAQIBAAEBAgD4AQIDAgEAAQECAwYBAPQBAgEBAgAEAwQAAQMEAAEBAgDzAQIAAQEAAwQDAAEBAgD2AQIDBAABAgABAgEAAQIDAgD0AQEAAQIAAQICAgABAQIBAPcBAQICAQEA+wECAAIDAgIBAgIAAQECAQD1AQIAAQIBAgACAgEEAQEA9AEEBQICAgMAAQIBAgABAQIAAQEA8gEBBgABAQECAAECAAIBAAEBAgDzAQICAwABAgEAAQMAAQYBAAECAQD1AQICAQECAAIFBAIBAPYBAgMCAgIAAwEBAgD3AQIAAgIBAgABAQIBAPYBAgADBAEAAQEA+wECAAICAQABAgEA+AECAQQCAAEBAgEA+QECAAEBAAICAQD7AQIDAgABAQD9AQIAgAIBAIACAgCAAgIAgAICAP8BAgMAgAICAIEEAgCDCAIA5hoDAAICAQABAgIA+QEBAAMCAgEBAAECAAEDBAD0AQQAAgMBAwACAgABAgIBAAICAPUBAgABAQABAwEBAgABBgABAQIA8AEBAgECAQQCAQACAQACBwABAgQAAQIBAgDtAQMCAgIFAAECBAMCAAECAgEDAwABBAQBAOwBAwABBAUEBgEAAQEEAAEBAQABAgABAwABBQABCADrAQcCBAEEAQUEAAICAQYDAgECAgABAQMFAgYCAOgBAwIBAQABAQYCAAIBAgMEAwQCAQACAgMBAwIGAOgBAgEAAwEBAAEBAgQAAQMCAAEBBAEEAAECAQMDAQYCAOYBBAACAQACAQABAgEFAAEEAAEBAAICAAECAAECAAEBAQMGAAEBAgDjAQECAAEEAQAFAgEBAAQBAQABAgABAgIDAQMCBgECAOYBAgABAQAEAgEABAIBAQACAgABAgEDAgMGAQIA4QEBAAECAwICAAYCAQACAQICAQAFAgEBAgcIAQIA4gEBBAEAAQIABAECAgABAQABAQABBAMABAICAwEGCQgBAOABAgACAwADAgABAgECAQACAgEAAgECAAMCAQADAwQHCADfAQIAAQEAAQIBAAIDAgQEAQIAAgICAAECAAMBAAEBAAICAAIBAAEHAN8BCgABAgACAgEAAQIBBQECAQACAQACAQDinQEBAIACAQD/AQUA/wEDAAEBAP0BAQMAAQMA/AEBAwACAgECAPkBAwABAwQAAgECAPkBAwECAQABAQMEAPcBAQMCAQIBAwQDAQYA9QEBAQEAAQIDBAEBAwABBAD1AQUAAgIDBAMBAAEDAAECAgDyAQEFBAACAQABAwMEBQUEAQQA8QEDAQQAAQEBAAEHAgADAwEBAgYA7wEDAgABAwEDAAIEAQIBAAEJBAICAgDtAQEBAAEBAAEBAQIAAQUGAQEAAQIFAAQGAOwBAQMBAAEBAQQBAgIHAgIBAwcCAAEDBgICAOgBAQEDAAEBAgABAQYDAgcCBAEBAQEJAAEEAgMCBAIA5gEBAwEAAgIAAQEEAwEDBgEAAgUAAQoA6aECBAACAQIAAQEBAPkBAgADAQECAgABAQABBAMA9AEDAAIEAgQAAgEAAQEBAgACAQD1AQEAAQIAAQQCAgEAAQUAAQIBAPABAgECAQIDAQIAAgIAAggAAQEDAAEBAgEA7QEEAQEBBgABAQMEAQABAQECBAQAAQMDAgDsAQQAAQMGAwUCAAECAwABAgIAAQEAAQQAAQYAAQcA6wEIAQMCAwIGAwACAQIFBAECAQEAAQIEBgEFAQDoAQQBAgIAAQIFAQACAgEEAwQDAQIAAgEEAgQBBQDoAQECAAMCAgABAgEDAAEEAQABAgMCAwABAQIEBAIFAQDmAQMAAgIAAgIAAQECBgABAwABAgACAQABAQABAQABAgIEBQABAgEA4wECAQABAwIABQECAgAEAgIAAQEAAQEBBAIEAQUCAQDmAQEAAQIABAECAAQBAgIAAgEAAQECBAEEBQIBAOEBAgABAQQBAQAGAQIAAgIBAQIABQECAgEIBwIBAOIBAgMCAAEBAAQCAQEAAQIAAQIAAQMEAAQBAQQCBQoHAgDgAQEAAgQAAwEAAQECAQIAAgECAAICAQADAQIAAwQDCAcA3wEBAAECAAEBAgACBAEDAwIBAAIBAQABAQADAgABAgACAQACAgABCADfAQkAAQEAAgECAAEBAgYCAQIAAgIAAgIA9L8BqgERBw8JCQkHCQcJAwUFBQUDBQACAQEBAQEBAQABAQABAQEABAIAAQIAAgIBAgABAgEAAQQEAQIGBAYCAgoECAIGAgoKBAgGAgIODgABBAMDAQEBIAUCAAIHAQcCAQUBBQABAQUBAAIDAAEDAwEDAQEAAQMDAgMAAQMAAQEBAwIFAgUCAwMCAwABBQACAwABAQABBQABAQMBAAEBAwABAgEHAQACAwEBAgABAwIDAQBVAgIEBAYGCggICggKDhATBgUGAQIAAwICAQABAgACBAQBAQACAQADAgECAAICAAYBAAIDBAECAAEEAAEDAwQAAQUAAgQCCQYBBAUEAQUGAQABBAABCwABDAIBAgABAgIlIAECAAECAQQDAgEBBgIDAQQAAQEAAQEEAAEBAAECAAQBAgECAgMCAgMCAQIBAgIDBAACAwIEBQIAAQIBAgABAgECAQEAAQIAAQIAAQECAAIBAgIDAgIAVQEAAQEAAQECAQEEAQEAAQEAAQ8AAQIAAQIBAgABAgEAAQIBAgIGAwACAQABAgADAgABAgACAgAFAQAEAgEAAQEBAQIEAQEAAQIBBQABBAABBwQAAQQBAwQFBAACAgILAgQIAAIBAAECGRoDBAEABAIBAgUEAgAEAgEABAIFBAABAQQBAAEBBAEAAgIAAgMGAQMAAQIEAQAEAQQDAAEEAQEAAQYBAQcIAAICAQIAAgMCAgIAVwEBAAECAQEAAgMGAQEAAQ0AAQIAAwIAAQMEAAIGAgEBAAEBAAECAgMEAgIAFgEDAgADAQIFAQgBBwICAAECBQIBAAEBBgACCwIKAAEEAQACAh0aAQYBAQICAAECAQADAQIBAgMAAQYDAgAEAgABAwQBAAECAAMCAwICBQQAAgEAAQQDAgIDBAECAQABAQQAAQMEAQAFBAEAAgIAXAMAAwIFBAMCAAEDDQIBAgABBAMBBgMIAgEFBAECAgADBAAdAQEAAgIFBQgAAQUCAAECAAEBAQABAgABAwYAAQkEBgIAAwEAAQIDAgABBAEAAQECAAMCAQACAQQAAQECAQABAgIDAAQCAQQDAgEAAgIAAwIBAQABAgACAgABAwQAAQEAAQQBAQIABgEAAQICAF8DAQACAgABAQACAQsAAgIBAQQAAQIGAQEAAwIAAgICACIBAQABAQMGAQUGAwIAAgEAAQEEAAIDAQQCBAABAQACGxwCAQACAQQDAAIEAAEBAAECAQACAQQAAQEBAgACBAECBQQDBAMGAQACAgABAQIDAgABAgABBQQCAQEEAAEBAAECAQIBAAECAAMCAGIBAAIDAQICAQACDQABBgMCAgMIAgMAAQEAAQQBAgICACgDBwYAAQUEAAEEBwQAAQMEBwYECwIGAAEEAgECARcWBAMCAQICAAIBAAECAQIAAQECAAIBAgECAAIBBAECAwQAAQEAAgIBAAEBAQIEAQIDBAABAQICAwIABwMGAGcBAQIBAAEBAAECAQkCAwABBAEGAAEBAAICAAIEAC0FAgABBwYFCAMBAgUKAwIAAQsBDg8GBgYBAwICAwsBAwoKAgMBBQgCAQQBBA0QAQIBAgEBAAIEBQABAgQBAgABAQABAgABAQIBAQQBAAECAQIAAQMEBQQBBgECAGkBAAEBAgMCAAEDAgcDAgABAgQAAQELDgEAAQQCAC8DAQEBAQUKAxMcBwABDRAHEwIjQCcCLDkILAQNHgIDIxgFBA41BCwHERIBAQQBDxEkGwcGIB0FIBcCFhccGxYNEhUWDw4JEBMEDgsKBw4JAgQAAQMGAGkDAgEAAgEAAQELBAEGAgMAAgYHBAQAMgECBQYHBAABBgkTGgYJBQoEAQMVCRoAARphSAYAAQkGDxIGDwwSDRUFIgsMMyQGEQgNDAgFGyQCAAEFAQQAAQEGAgACAgECAwEJEAEJDAACAwQAAQMAAQQAawEAAQECAAMHAQYCAwABAgABBAA3AQUIEQABCBMSHQQHEAABEzI9OgQ/PDs4QRQZDgcqJwQSERwIDQwNBwYsJQMkDBESAAIBAgABHyAAARkaFRYVEg8UDxALDAECAAEDBAcIAQIAAQECAGwDAgIBAQIBCQYCAwMGAQIEAPIBAwACAgEAAQMCAwEGAQABAgIA8gEBAQEAAQIAAQEDAQQAAgQA9QEBAAIBAAEFAAEEAQABBAD2AQEAAQIBAQUCAAIEAPgBAwIAAgEBAQYA+gEDAAEEAwICAPoBAQACAQMAAQQA+wEBAAIBAgD8AQECAQECAPwBAQECAP8BAQCBAgEA/wEBAgD/AQEAgQIBAIACAQCBBAEAjBoCAIIGAgCABAIBAP8BAQCCBAIA/wECAQD/AQEEAIACAgD/AQQDAP8BAgD/AQIAggIBAP0BAgIDAgCAAgIA/gEBAgYA/gECBAcA/AECAAEEBQD9AQEEAQICAPwBAgEBBAD9AQQDBAUA/AECAwQBAAEBAPsBAQQBAQABCAD7AQIBAQEIBQD7AQEBBAYDAgD8AQIEBQICAP0BAwoDAwD7AQIAAQIHBAD8AQQAAgEBAQD6AQYDAAEDBAABAgD6AQECAAEEAQICAPoBAQEEAAEEAAEDAPoBBAABAwABAgUEAPoBAQQEAQcCBAD6AQQAAQUBCAIFAPoBAQMECAUCAPsBAwYBAQIBBAD6AQIBAgUBBgIA+gEEAwMECAcFAPoBAwIGAAEHAgD8AQQDAQICAPsBBAUAAQIAAgQA/gECAAEDAPoBAwIEAAEBAwIA+gEEAAEDAQEGAgD9AQICAQD+AQICAQMEAQEA+AECBAEDAAIIAQUA9wECAAEDAAMBAAECAAEBAPYBAgMAAgECAgIBBAMA9gEDAAECAQIAAgIBAgQFAPUBAQIBAAECAwQAAQIBAAEEAwD0AQEAAQEEAwQDBAEAAQECAAEBAPQBAQIBAgEAAQMEAgEAAQIAAQEA8gEBAAICAQIAAQECAAECAQD4AQEEAQIAAQIBBAUCAPQBAQICAQAEBAMAAQQDAAECAQDzAQEAAQIAAwMEAAECAQD2AQEEAwABAQABAQIAAQEEAQD0AQIAAQEAAQEBAQABAgECAPcBAgEBAgIA+wEBAAIEAQECAQEAAQIBAgD1AQEAAQECAQACAQIDAgIA9AEDBgEBAQQAAQECAQABAgEAAQIA8gECBQABAgIBAAEBAAICAAECAQDzAQEBBAABAQIAAQQAAQUCAAEBAgD1AQEBAgIBAAIGAwECAPYBAQQBAQEAAwICAQD3AQEAAgECAQABAgECAPYBAQADAwIAAQIA+wEBAAIBAgABAQIA+AEBAgMBAAECAQIA+QEBAAECAAIBAgD7AQEEAQABAgD9AQEAgAICAIACAQCAAgEAgAIBAP8BAQQAgAIBAIEEAQCDCAEA7NoBAgCAAgIA/wEGAP8BBAABAgD9AQIEAAEEAPwBAgQAAgECAQD5AQQAAQQDAAICAQD5AQQCAQIAAQIEAwD3AQIEAQIBAgQDBAIFAPUBAgICAAEBBAMCAgQAAQMA9QEGAAIBBAMEAgABBAABAQEA8gECBgMAAgIAAQQEAwYGAwIDAPEBBAIDAAECAgABCAEAAwQCAgEFAO8BBAEAAQQCBAACAwIBAgABCgMBAQEA7QECAgABAgABAgIBAAEGBQICAAEBBgAEBQDsAQIEAgABAgIDAgEBCAEBAgQIAQABBAUBAQDoAQICBAABAgEAAQIFBAEIAQMCAgICCgABAwEEAQMBAOYBAgQCAAIBAAECAwQCBAUCAAIGAAEJAIGEBACBhAQAgYQEAIGEBACBhAQ=", + "Layer1Texture": "assets/StartingGround.png", + "Layer1Color": "255;255;255", + "HeightBlending": 0.5, + "TextureVariation": 0, + "TextureSize": 128, + "CliffLayer": "3", + "Layer2Texture": "", + "Layer3Texture": "", + "CliffAngle": 45, + "CastShadow": false + } } ], "objectsFolderStructure": { @@ -3481,7 +3469,7 @@ "objectName": "DirtPlot" }, { - "objectName": "Ground" + "objectName": "Terrain" }, { "objectName": "Walls" @@ -11477,7 +11465,6 @@ "name": "PlatformerMultitouchMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -11731,7 +11718,6 @@ "name": "Platformer3DMultitouchMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -11906,7 +11892,6 @@ "name": "Shooter3DMultitouchMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -12070,7 +12055,6 @@ "name": "FirstPersonMultitouchMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -13275,7 +13259,6 @@ "name": "PhysicsCar3DMultitouchMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -13488,7 +13471,6 @@ "name": "TopDownMultitouchMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -22224,7 +22206,6 @@ "name": "Tween3D", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -25461,7 +25442,6 @@ "name": "Wobble", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -35675,7 +35655,6 @@ "name": "ShakeModel3D", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -48376,6 +48355,7802 @@ "variants": [] } ] + }, + { + "author": "", + "category": "General", + "dimension": "3D", + "extensionNamespace": "", + "fullName": "3D terrain", + "gdevelopVersion": ">=5.5.222", + "helpPath": "", + "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLXRlcnJhaW4iIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTQsNkwxMC4yNSwxMUwxMy4xLDE0LjhMMTEuNSwxNkM5LjgxLDEzLjc1IDcsMTAgNywxMEwxLDE4SDIzTDE0LDZaIiAvPjwvc3ZnPg==", + "name": "Terrain3D", + "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", + "shortDescription": "A ground with hills and valleys that can be sculpted and painted in the editor, with collisions.", + "version": "0.1.0", + "description": [ + "A 3D terrain object for open worlds, adventure or racing games: hills, mountains, islands or valleys, with painted layers of grass, dirt, rock or sand.", + "", + "- **Sculpt and paint in the scene editor**: select a terrain to show the tools (raise, lower, smooth, flatten and paint 4 layers).", + "- **Start from a relief** (hills, mountains, island) or **from a heightmap image**.", + "- **Steep slopes automatically show rock** (or any layer).", + "- **Natural borders between layers**: stones and tufts of a texture cover the gaps of the other where layers meet (height blending), and the repetition of textures is made less visible (texture variation).", + "- **Physics**: add the **3D physics** behavior to the terrain. Characters and objects collide with the ground exactly. The terrain is always static.", + "- **Pathfinding**: add the **floor/obstacle for pathfinding (navmesh based)** behavior to the terrain, so that characters with the navmesh pathfinding behavior walk on its hills and around its cliffs (in recent GDevelop versions).", + "- **Shape it with the Edits property**: a list of raise, flatten, smooth and paint strokes written in JSON, shown in the scene editor. It's readable and can be changed by hand or by an AI agent, like the relief, the heightmap and the other properties. Actions only change the terrain during the game.", + "- **Change it during the game** with actions (raise, lower, flatten, smooth, paint, along a line for paths and rivers) and read the ground height, slope or layer with expressions.", + "- **Grass**: add a **terrain grass** object over the terrain. Thousands of blades sway in the wind, grow where a layer (like grass) is painted and where the ground is not too steep, and are pushed aside by characters. Sculpt or paint the terrain and the grass follows.", + "- **Forests, rocks and bushes**: add a **scattered models** object over the terrain. It places hundreds or thousands of copies of a 3D model (trees, rocks, bushes, flowers...) on the ground and draws them all at once, which is much faster than an object for each. Copies grow where a layer is painted, away from steep slopes, and follow the ground when it's sculpted. Add or erase them with the brushes of the scene editor or with the Edits property, give trees trunk collisions, and cut them down with actions. Ready-made forests, rocks and bushes are in the asset store.", + "- **Tunnels and caves**: make holes in the ground (and fill them again) with the brushes, the Edits property or actions, and put a 3D model of the tunnel or the cave (with a mesh collision shape) under the ground. Nothing is drawn in holes, and characters, cars and rays go through them.", + "- **Made for large worlds**: the terrain is split in chunks that are hidden when off-screen and simplified when far away, and collisions are updated only where the ground changes." + ], + "origin": { + "identifier": "Terrain3D", + "name": "gdevelop-extension-store" + }, + "tags": [ + "3d", + "terrain", + "ground", + "heightmap", + "landscape", + "open world", + "grass", + "foliage", + "forest", + "trees", + "tunnel", + "cave" + ], + "authorIds": [], + "dependencies": [], + "globalVariables": [], + "sceneVariables": [], + "eventsFunctions": [ + { + "description": "Define helper classes JavaScript code.", + "fullName": "Define helper classes", + "functionType": "Action", + "name": "DefineHelperClasses", + "private": true, + "sentence": "Define helper classes JavaScript code", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "if (gdjs.__terrain3DExtension) {", + " return;", + "}", + "", + "// Tested by the gameplay tests of the \"3D island terrain\" example (GDevelop-examples repository).", + "", + "const groundsByScene = new WeakMap();", + "/**", + " * The grounds of a scene (the terrains), followed by the terrain grass.", + " *", + " * A ground has: `getVersion()` (changing when the ground changes),", + " * `containsPoint(x, y)`, `getHeightAt(x, y)` (Z position of the ground),", + " * `getSlopeAt(x, y)` (in degrees), `getLayerWeightAt(x, y, layer)` (how", + " * much a painted layer, from 1 to 4, is there, from 0 to 1) and optionally", + " * `getTintAt(x, y)` (the painted tint, as red, green and blue from 0 to 255).", + " * @param {gdjs.RuntimeScene} runtimeScene", + " * @returns {Set}", + " */", + "const getGrounds = (runtimeScene) => {", + " let grounds = groundsByScene.get(runtimeScene);", + " if (!grounds) {", + " grounds = new Set();", + " groundsByScene.set(runtimeScene, grounds);", + " }", + " return grounds;", + "};", + "", + "/**", + " * @param {number} seed", + " * @returns {() => number} Random numbers between 0 (included) and 1 (excluded), the same for the same seed.", + " */", + "const createRandom = (seed) => {", + " let state = seed >>> 0 || 1;", + " return () => {", + " state ^= state << 13;", + " state ^= state >>> 17;", + " state ^= state << 5;", + " return (state >>> 0) / 4294967296;", + " };", + "};", + "", + "/**", + " * Find the highest ground at a position: grounds can overlap (like an island over a sea floor).", + " * @param {Iterable} grounds", + " * @param {{ground: object | null, z: number}} result Set to the ground (null if there is none) and its Z position.", + " */", + "const findHighestGround = (grounds, x, y, result) => {", + " result.ground = null;", + " for (const ground of grounds) {", + " if (!ground.containsPoint(x, y)) continue;", + " const z = ground.getHeightAt(x, y);", + " if (!result.ground || z > result.z) {", + " result.ground = ground;", + " result.z = z;", + " }", + " }", + " return result;", + "};", + "", + "// The grounds or the objects placed on them can change every frame (while", + "// sculpting or moving them in the editor): things are placed again at most this often.", + "const PLACEMENT_DELAY = 0.3;", + "", + "/**", + " * Tells when things placed on the grounds of a scene (grass blades, scattered", + " * models) must be placed again: after the grounds or the given properties", + " * changed, at most every `PLACEMENT_DELAY` seconds.", + " */", + "class GroundPlacement {", + " /** @param {gdjs.RuntimeScene} runtimeScene */", + " constructor(runtimeScene) {", + " this.grounds = getGrounds(runtimeScene);", + " this.placedFrom = '';", + " this.timeSincePlacement = 0;", + " }", + "", + " /**", + " * @param {number} elapsedSeconds", + " * @param {string} propertiesKey Changes when the things must be placed again.", + " */", + " shouldPlaceAgain(elapsedSeconds, propertiesKey) {", + " this.timeSincePlacement += elapsedSeconds;", + " if (this.placedFrom && this.timeSincePlacement < PLACEMENT_DELAY) return false;", + " let placementKey = propertiesKey;", + " for (const ground of this.grounds) placementKey += '|' + ground.getVersion();", + " if (placementKey === this.placedFrom) return false;", + " this.placedFrom = placementKey;", + " this.timeSincePlacement = 0;", + " return true;", + " }", + "", + " placeAgain() {", + " this.placedFrom = '';", + " }", + "}", + "", + "/**", + " * Replace parts of the shader code of a Three.js material, with an error if", + " * a part is missing (for example after an update of Three.js).", + " * @param {string} shaderCode", + " * @param {Array<[string, string]>} replacements The searched code and its replacement.", + " * @param {string} materialName", + " */", + "const patchShaderCode = (shaderCode, replacements, materialName) => {", + " for (const [searchedCode, newCode] of replacements) {", + " if (!shaderCode.includes(searchedCode)) {", + " console.error(`${materialName}: \"${searchedCode}\" was not found in the shader, which won't look as expected.`);", + " }", + " shaderCode = shaderCode.replace(searchedCode, newCode);", + " }", + " return shaderCode;", + "};", + "", + "// Size of the object inner area. Instances are scaled from it, so these only", + "// define the default size of a new terrain.", + "const AREA_SIZE = 4096;", + "const AREA_DEPTH = 600;", + "", + "// Terrains have at most 8 x 8 chunks (so at most 64 draw calls), of at least", + "// 32 x 32 cells.", + "const MIN_CHUNK_CELLS = 32;", + "const MAX_CHUNKS_PER_SIDE = 8;", + "/** @param {number} resolution */", + "const getChunkCells = (resolution) => Math.max(MIN_CHUNK_CELLS, resolution / MAX_CHUNKS_PER_SIDE);", + "// Grid steps of each level of detail, and the distance (in chunk sizes)", + "// from which each one is used.", + "const LOD_STEPS = [1, 2, 4];", + "const LOD_DISTANCES = [0, 3, 6];", + "", + "const LAYER_COUNT = 4;", + "const RESOLUTIONS = [64, 128, 256, 512, 1024];", + "const DEFAULT_RESOLUTION = 256;", + "// Each undo step is a copy of the data: fewer are kept for big terrains.", + "const UNDO_STEPS_SAMPLES = 20 * 257 * 257;", + "", + "const clamp01 = (value) => (value < 0 ? 0 : value > 1 ? 1 : value);", + "const clampInteger = (value, min, max) =>", + " value < min ? min : value > max ? max : Math.floor(value);", + "", + "/**", + " * A rectangle of grid samples, inclusive. Empty when `minI > maxI`.", + " */", + "class SampleRectangle {", + " constructor() {", + " this.clear();", + " }", + "", + " clear() {", + " this.minI = Number.MAX_SAFE_INTEGER;", + " this.minJ = Number.MAX_SAFE_INTEGER;", + " this.maxI = -1;", + " this.maxJ = -1;", + " }", + "", + " isEmpty() {", + " return this.minI > this.maxI;", + " }", + "", + " /** @param {SampleRectangle} other */", + " add(other) {", + " if (other.isEmpty()) return;", + " this.minI = Math.min(this.minI, other.minI);", + " this.minJ = Math.min(this.minJ, other.minJ);", + " this.maxI = Math.max(this.maxI, other.maxI);", + " this.maxJ = Math.max(this.maxJ, other.maxJ);", + " }", + "", + " set(minI, minJ, maxI, maxJ) {", + " this.minI = minI;", + " this.minJ = minJ;", + " this.maxI = maxI;", + " this.maxJ = maxJ;", + " return this;", + " }", + "}", + "", + "/**", + " * The heights (between 0 and 1) and the painted layers weights of a square", + " * grid of `size * size` samples.", + " */", + "class TerrainData {", + " /** @param {number} resolution The number of cells on each side. */", + " constructor(resolution) {", + " this.resolution = resolution;", + " this.size = resolution + 1;", + " this.heights = new Float32Array(this.size * this.size);", + " /** 4 weights (0-255) per sample, one for each layer. */", + " this.splat = new Uint8Array(this.size * this.size * LAYER_COUNT);", + " this.fillLayer(0);", + " /**", + " * 4 bytes per sample: a tint multiplying the layers (red, green and", + " * blue, white for none) and the wetness (0 for dry).", + " */", + " this.colors = new Uint8Array(this.size * this.size * 4);", + " this.clearColors();", + " }", + "", + " clearColors() {", + " this.colors.fill(255);", + " for (let index = 3; index < this.colors.length; index += 4) {", + " this.colors[index] = 0;", + " }", + " }", + "", + " /** @param {number} layer */", + " fillLayer(layer) {", + " this.splat.fill(0);", + " for (let index = layer; index < this.splat.length; index += LAYER_COUNT) {", + " this.splat[index] = 255;", + " }", + " }", + "", + " getHeight(i, j) {", + " const last = this.resolution;", + " const clampedI = i < 0 ? 0 : i > last ? last : i;", + " const clampedJ = j < 0 ? 0 : j > last ? last : j;", + " return this.heights[clampedJ * this.size + clampedI];", + " }", + "", + " /**", + " * @param {number} gridX", + " * @param {number} gridY", + " * @returns {number} The height interpolated on the triangles that are drawn.", + " */", + " sampleHeight(gridX, gridY) {", + " const x = Math.min(Math.max(gridX, 0), this.resolution);", + " const y = Math.min(Math.max(gridY, 0), this.resolution);", + " const i = Math.min(Math.floor(x), this.resolution - 1);", + " const j = Math.min(Math.floor(y), this.resolution - 1);", + " const fx = x - i;", + " const fy = y - j;", + " const topRight = this.getHeight(i + 1, j);", + " const bottomLeft = this.getHeight(i, j + 1);", + " if (fx + fy <= 1) {", + " const topLeft = this.getHeight(i, j);", + " return topLeft + fx * (topRight - topLeft) + fy * (bottomLeft - topLeft);", + " }", + " const bottomRight = this.getHeight(i + 1, j + 1);", + " return (", + " bottomRight +", + " (1 - fx) * (bottomLeft - bottomRight) +", + " (1 - fy) * (topRight - bottomRight)", + " );", + " }", + "", + " /** @returns {number} The weight (0-1) of a layer, interpolated between samples. */", + " sampleLayerWeight(gridX, gridY, layer) {", + " const x = Math.min(Math.max(gridX, 0), this.resolution);", + " const y = Math.min(Math.max(gridY, 0), this.resolution);", + " const i = Math.min(Math.floor(x), this.resolution - 1);", + " const j = Math.min(Math.floor(y), this.resolution - 1);", + " const fx = x - i;", + " const fy = y - j;", + " const weight = (i, j) => this.splat[(j * this.size + i) * LAYER_COUNT + layer];", + " const top = weight(i, j) + fx * (weight(i + 1, j) - weight(i, j));", + " const bottom = weight(i, j + 1) + fx * (weight(i + 1, j + 1) - weight(i, j + 1));", + " return (top + fy * (bottom - top)) / 255;", + " }", + "", + " /**", + " * @param {number} channel 0, 1 and 2 for the tint, 3 for the wetness.", + " * @returns {number} The channel (0-1), interpolated between samples.", + " */", + " sampleColor(gridX, gridY, channel) {", + " const x = Math.min(Math.max(gridX, 0), this.resolution);", + " const y = Math.min(Math.max(gridY, 0), this.resolution);", + " const i = Math.min(Math.floor(x), this.resolution - 1);", + " const j = Math.min(Math.floor(y), this.resolution - 1);", + " const fx = x - i;", + " const fy = y - j;", + " const value = (i, j) => this.colors[(j * this.size + i) * 4 + channel];", + " const top = value(i, j) + fx * (value(i + 1, j) - value(i, j));", + " const bottom = value(i, j + 1) + fx * (value(i + 1, j + 1) - value(i, j + 1));", + " return (top + fy * (bottom - top)) / 255;", + " }", + "", + " /**", + " * @param {number} index The index of a sample.", + " * @returns {boolean} true if no layer is painted on the sample: it's a hole.", + " */", + " isHole(index) {", + " const offset = index * LAYER_COUNT;", + " return (this.splat[offset] | this.splat[offset + 1] | this.splat[offset + 2] | this.splat[offset + 3]) === 0;", + " }", + "", + " /**", + " * @returns {number} The sum of the layer weights (0-1), interpolated between samples: there is a", + " * hole where it's under 0.5, like what is drawn.", + " */", + " sampleSolidity(gridX, gridY) {", + " let solidity = 0;", + " for (let layer = 0; layer < LAYER_COUNT; layer++) solidity += this.sampleLayerWeight(gridX, gridY, layer);", + " return solidity;", + " }", + "", + " /** @returns {number} The index (from 0) of the layer painted the most. */", + " getDominantLayer(gridX, gridY) {", + " const i = clampInteger(Math.round(gridX), 0, this.resolution);", + " const j = clampInteger(Math.round(gridY), 0, this.resolution);", + " const offset = (j * this.size + i) * LAYER_COUNT;", + " let dominantLayer = 0;", + " for (let layer = 1; layer < LAYER_COUNT; layer++) {", + " if (this.splat[offset + layer] > this.splat[offset + dominantLayer]) {", + " dominantLayer = layer;", + " }", + " }", + " return dominantLayer;", + " }", + "", + " /** @param {TerrainData} other */", + " copyFrom(other) {", + " this.heights.set(other.heights);", + " this.splat.set(other.splat);", + " this.colors.set(other.colors);", + " }", + "", + " /**", + " * @param {number} resolution", + " * @returns {TerrainData} A copy of this data with another resolution.", + " */", + " resampled(resolution) {", + " const data = new TerrainData(resolution);", + " const ratio = this.resolution / resolution;", + " for (let j = 0; j < data.size; j++) {", + " for (let i = 0; i < data.size; i++) {", + " const index = j * data.size + i;", + " data.heights[index] = this.sampleHeight(i * ratio, j * ratio);", + " const sourceOffset =", + " (Math.round(j * ratio) * this.size + Math.round(i * ratio)) * LAYER_COUNT;", + " for (let layer = 0; layer < LAYER_COUNT; layer++) {", + " data.splat[index * LAYER_COUNT + layer] = this.splat[sourceOffset + layer];", + " }", + " for (let channel = 0; channel < 4; channel++) {", + " data.colors[index * 4 + channel] = this.colors[sourceOffset + channel];", + " }", + " }", + " }", + " return data;", + " }", + "}", + "", + "/**", + " * Brushes change the samples around a segment, with an effect going smoothly", + " * from full on the segment to none at `radius`. A point is a segment of", + " * length 0. Positions are in scene units relative to the terrain origin.", + " */", + "const brushes = {", + " /**", + " * @param {TerrainData} data", + " * @param {{cellWidth: number, cellHeight: number, ax: number, ay: number, bx: number, by: number, radius: number}} stroke", + " * @param {(sampleIndex: number, weight: number, t: number) => void} apply Called with the weight of", + " * the brush and the position (from 0 to 1) of the nearest point of the segment.", + " * @param {SampleRectangle} changedSamples", + " */", + " forEachSample(data, stroke, apply, changedSamples) {", + " const { cellWidth, cellHeight, ax, ay, bx, by, radius } = stroke;", + " const last = data.resolution;", + " const minI = clampInteger(Math.floor((Math.min(ax, bx) - radius) / cellWidth), 0, last);", + " const maxI = clampInteger(Math.ceil((Math.max(ax, bx) + radius) / cellWidth), 0, last);", + " const minJ = clampInteger(Math.floor((Math.min(ay, by) - radius) / cellHeight), 0, last);", + " const maxJ = clampInteger(Math.ceil((Math.max(ay, by) + radius) / cellHeight), 0, last);", + " changedSamples.set(minI, minJ, maxI, maxJ);", + " if (radius <= 0) return;", + "", + " const abX = bx - ax;", + " const abY = by - ay;", + " const abLengthSquared = abX * abX + abY * abY;", + " const radiusSquared = radius * radius;", + " for (let j = minJ; j <= maxJ; j++) {", + " const y = j * cellHeight;", + " for (let i = minI; i <= maxI; i++) {", + " const x = i * cellWidth;", + " const t =", + " abLengthSquared > 0", + " ? clamp01(((x - ax) * abX + (y - ay) * abY) / abLengthSquared)", + " : 0;", + " const dx = x - (ax + t * abX);", + " const dy = y - (ay + t * abY);", + " const distanceSquared = dx * dx + dy * dy;", + " if (distanceSquared >= radiusSquared) continue;", + " const falloff = 1 - distanceSquared / radiusSquared;", + " apply(j * data.size + i, falloff * falloff, t);", + " }", + " }", + " },", + "", + " raise(data, stroke, amount, changedSamples) {", + " const { heights } = data;", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index, weight) => {", + " heights[index] = clamp01(heights[index] + amount * weight);", + " },", + " changedSamples", + " );", + " },", + "", + " /**", + " * @param {number} targetHeight", + " * @param {number} [endTargetHeight] The target at the end of the segment, for a slope (like a road", + " * going up). The same target as at its start by default.", + " */", + " flatten(data, stroke, targetHeight, strength, changedSamples, endTargetHeight = targetHeight) {", + " const { heights } = data;", + " const startTarget = clamp01(targetHeight);", + " const endTarget = clamp01(endTargetHeight);", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index, weight, t) => {", + " const target = startTarget + (endTarget - startTarget) * t;", + " heights[index] += (target - heights[index]) * clamp01(weight * strength);", + " },", + " changedSamples", + " );", + " },", + "", + " smooth(data, stroke, strength, changedSamples) {", + " const { heights, size } = data;", + " const changes = [];", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index, weight) => {", + " const i = index % size;", + " const j = (index - i) / size;", + " let sum = 0;", + " for (let dj = -1; dj <= 1; dj++) {", + " for (let di = -1; di <= 1; di++) {", + " sum += data.getHeight(i + di, j + dj);", + " }", + " }", + " changes.push(index, heights[index] + (sum / 9 - heights[index]) * clamp01(weight * strength));", + " },", + " changedSamples", + " );", + " // Written after all averages are computed, so that the result", + " // doesn't depend on the order in which samples are visited.", + " for (let index = 0; index < changes.length; index += 2) {", + " heights[changes[index]] = changes[index + 1];", + " }", + " },", + "", + " paint(data, stroke, layer, strength, changedSamples) {", + " const { splat } = data;", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index, weight) => {", + " const amount = clamp01(weight * strength);", + " const offset = index * LAYER_COUNT;", + " for (let otherLayer = 0; otherLayer < LAYER_COUNT; otherLayer++) {", + " const target = otherLayer === layer ? 255 : 0;", + " const value = splat[offset + otherLayer];", + " // Rounded towards the target, so that light strokes still paint.", + " const change = (target - value) * amount;", + " splat[offset + otherLayer] = value + (change > 0 ? Math.ceil(change) : Math.floor(change));", + " }", + " },", + " changedSamples", + " );", + " },", + "", + " /** Remove all the layers, making a hole (`fill` or painting a layer fills it again). */", + " hole(data, stroke, changedSamples) {", + " const { splat } = data;", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index) => {", + " splat.fill(0, index * LAYER_COUNT, (index + 1) * LAYER_COUNT);", + " },", + " changedSamples", + " );", + " },", + "", + " /**", + " * Fill the holes in the radius with the layers of the ground around them:", + " * each pass fills the holes next to the ground with the average of their", + " * neighbors, from the edges of the holes inward.", + " */", + " fill(data, stroke, changedSamples) {", + " const { splat, size, resolution } = data;", + " /** @type {number[]} */", + " let holes = [];", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index) => {", + " if (data.isHole(index)) holes.push(index);", + " },", + " changedSamples", + " );", + " const filledWeights = new Float32Array(holes.length * LAYER_COUNT);", + " while (holes.length > 0) {", + " /** @type {number[]} */", + " const remainingHoles = [];", + " /** @type {number[]} */", + " const filledHoles = [];", + " for (const index of holes) {", + " const i = index % size;", + " const j = (index - i) / size;", + " const weights = filledWeights.subarray(filledHoles.length * LAYER_COUNT, (filledHoles.length + 1) * LAYER_COUNT);", + " weights.fill(0);", + " for (let neighborJ = Math.max(j - 1, 0); neighborJ <= Math.min(j + 1, resolution); neighborJ++) {", + " for (let neighborI = Math.max(i - 1, 0); neighborI <= Math.min(i + 1, resolution); neighborI++) {", + " const neighbor = neighborJ * size + neighborI;", + " if (data.isHole(neighbor)) continue;", + " for (let layer = 0; layer < LAYER_COUNT; layer++) weights[layer] += splat[neighbor * LAYER_COUNT + layer];", + " }", + " }", + " if (weights[0] + weights[1] + weights[2] + weights[3] > 0) filledHoles.push(index);", + " else remainingHoles.push(index);", + " }", + " if (filledHoles.length === 0) {", + " // No ground around (a hole bigger than the brush): the first layer is used.", + " for (const index of remainingHoles) splat[index * LAYER_COUNT] = 255;", + " break;", + " }", + " // Filled after the pass, so that the holes are filled from the edges inward.", + " filledHoles.forEach((index, filledIndex) => {", + " const weights = filledWeights.subarray(filledIndex * LAYER_COUNT, (filledIndex + 1) * LAYER_COUNT);", + " const total = weights[0] + weights[1] + weights[2] + weights[3];", + " for (let layer = 0; layer < LAYER_COUNT; layer++) {", + " splat[index * LAYER_COUNT + layer] = Math.round((weights[layer] / total) * 255);", + " }", + " });", + " holes = remainingHoles;", + " }", + " },", + "", + " /** @param {number[]} color The red, green and blue (0-255) of the tint: white removes it. */", + " tint(data, stroke, color, strength, changedSamples) {", + " brushes.paintColors(data, stroke, 0, color, strength, changedSamples);", + " },", + "", + " /** @param {number} wetness From 0 (dry) to 1. */", + " wet(data, stroke, wetness, strength, changedSamples) {", + " brushes.paintColors(data, stroke, 3, [Math.round(clamp01(wetness) * 255)], strength, changedSamples);", + " },", + "", + " /**", + " * Bring channels of the colors (see `TerrainData.colors`) towards targets.", + " * @param {number} firstChannel", + " * @param {number[]} targets From 0 to 255, for the channels from the first one.", + " */", + " paintColors(data, stroke, firstChannel, targets, strength, changedSamples) {", + " const { colors } = data;", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index, weight) => {", + " const amount = clamp01(weight * strength);", + " for (let channel = 0; channel < targets.length; channel++) {", + " const offset = index * 4 + firstChannel + channel;", + " // Rounded towards the target, so that light strokes still paint.", + " const change = (targets[channel] - colors[offset]) * amount;", + " colors[offset] += change > 0 ? Math.ceil(change) : Math.floor(change);", + " }", + " },", + " changedSamples", + " );", + " },", + "};", + "", + "const relief = (() => {", + " const hash = (x, y, seed) => {", + " let h = Math.imul(x, 374761393) + Math.imul(y, 668265263) + Math.imul(seed, 982451653);", + " h = Math.imul(h ^ (h >>> 13), 1274126177);", + " return ((h ^ (h >>> 16)) >>> 0) / 4294967296;", + " };", + " const valueNoise = (x, y, seed) => {", + " const x0 = Math.floor(x);", + " const y0 = Math.floor(y);", + " const fx = x - x0;", + " const fy = y - y0;", + " const sx = fx * fx * (3 - 2 * fx);", + " const sy = fy * fy * (3 - 2 * fy);", + " const top = hash(x0, y0, seed) + sx * (hash(x0 + 1, y0, seed) - hash(x0, y0, seed));", + " const bottom =", + " hash(x0, y0 + 1, seed) + sx * (hash(x0 + 1, y0 + 1, seed) - hash(x0, y0 + 1, seed));", + " return top + sy * (bottom - top);", + " };", + " /** Fractal noise between 0 and 1. */", + " const fractalNoise = (x, y, seed) => {", + " let value = 0;", + " let amplitude = 0.5;", + " let frequency = 1;", + " for (let octave = 0; octave < 5; octave++) {", + " value += amplitude * valueNoise(x * frequency, y * frequency, seed + octave);", + " amplitude *= 0.5;", + " frequency *= 2;", + " }", + " return value / 0.96875;", + " };", + " const smoothstep = (edge0, edge1, x) => {", + " const t = clamp01((x - edge0) / (edge1 - edge0));", + " return t * t * (3 - 2 * t);", + " };", + " const reliefHeights = {", + " Flat: () => 0,", + " Hills: (u, v, seed) => 0.05 + 0.5 * Math.pow(fractalNoise(u * 3, v * 3, seed), 2),", + " Mountains: (u, v, seed) => {", + " const ridge = 1 - Math.abs(2 * fractalNoise(u * 2.5, v * 2.5, seed) - 1);", + " return 0.9 * Math.pow(ridge, 2.5);", + " },", + " Island: (u, v, seed) => {", + " const distanceToCenter = 2 * Math.hypot(u - 0.5, v - 0.5);", + " const coast = 0.2 * (fractalNoise(u * 4, v * 4, seed + 7) - 0.5);", + " const land = smoothstep(0.95, 0.55, distanceToCenter + coast);", + " return land * (0.12 + 0.6 * Math.pow(fractalNoise(u * 3, v * 3, seed), 1.5));", + " },", + " };", + " const beachLayer = 3;", + "", + " return {", + " /**", + " * @param {TerrainData} data", + " * @param {string} reliefName Flat, Hills, Mountains or Island.", + " * @param {number} seed", + " */", + " generate(data, reliefName, seed) {", + " const getHeight = reliefHeights[reliefName] || reliefHeights.Flat;", + " const integerSeed = Math.floor(seed) | 0;", + " data.fillLayer(0);", + " for (let j = 0; j < data.size; j++) {", + " for (let i = 0; i < data.size; i++) {", + " const index = j * data.size + i;", + " const height = clamp01(", + " getHeight(i / data.resolution, j / data.resolution, integerSeed)", + " );", + " data.heights[index] = height;", + " if (reliefName === 'Island') {", + " const sand = Math.round(255 * smoothstep(0.16, 0.1, height));", + " const offset = index * LAYER_COUNT;", + " data.splat[offset] = 255 - sand;", + " data.splat[offset + beachLayer] = sand;", + " }", + " }", + " }", + " },", + " };", + "})();", + "", + "/**", + " * Edits written in the Edits property: a JSON list of brush strokes, like", + " * `[{\"tool\": \"raise\", \"x\": 0.5, \"y\": 0.5, \"radius\": 0.2, \"height\": 0.3}]`,", + " * applied on the relief or the heightmap. Positions and radius are fractions", + " * of the terrain size, heights and Z positions fractions of its depth: they", + " * are the same for all instances, whatever their size. They are made to be", + " * read and written by people and by AI agents.", + " */", + "const edits = {", + " /**", + " * @param {TerrainData} data", + " * @param {string} text", + " */", + " apply(data, text) {", + " if (!text.trim()) return;", + " let editList;", + " try {", + " editList = JSON.parse(text);", + " } catch (error) {", + " console.warn('The edits of a terrain are not valid JSON: ' + error.message);", + " return;", + " }", + " if (!Array.isArray(editList)) {", + " console.warn('The edits of a terrain must be a JSON list (like [{\"tool\": \"raise\", ...}]).');", + " return;", + " }", + " const numberOr = (value, defaultValue) => (typeof value === 'number' && isFinite(value) ? value : defaultValue);", + " const changedSamples = new SampleRectangle();", + " for (const edit of editList) {", + " if (!edit || typeof edit !== 'object') continue;", + " const x = numberOr(edit.x, 0.5);", + " const y = numberOr(edit.y, 0.5);", + " const stroke = {", + " cellWidth: 1 / data.resolution,", + " cellHeight: 1 / data.resolution,", + " ax: x,", + " ay: y,", + " bx: numberOr(edit.toX, x),", + " by: numberOr(edit.toY, y),", + " radius: numberOr(edit.radius, 0.1),", + " };", + " const strength = clamp01(numberOr(edit.strength, 1));", + " if (edit.tool === 'raise') {", + " brushes.raise(data, stroke, numberOr(edit.height, 0.1), changedSamples);", + " } else if (edit.tool === 'flatten') {", + " const z = numberOr(edit.z, 0);", + " brushes.flatten(data, stroke, z, strength, changedSamples, numberOr(edit.toZ, z));", + " } else if (edit.tool === 'smooth') {", + " // Smoothing the same fraction of the terrain at any resolution.", + " const passes = Math.max(Math.round(((1 + 9 * strength) * data.resolution) / 256), 1);", + " for (let pass = 0; pass < passes; pass++) brushes.smooth(data, stroke, 1, changedSamples);", + " } else if (edit.tool === 'paint') {", + " const layer = clampInteger(numberOr(edit.layer, 1) - 1, 0, LAYER_COUNT - 1);", + " brushes.paint(data, stroke, layer, strength, changedSamples);", + " } else if (edit.tool === 'tint') {", + " const color = gdjs.rgbOrHexToRGBColor(typeof edit.color === 'string' ? edit.color : '255;255;255');", + " brushes.tint(data, stroke, color, strength, changedSamples);", + " } else if (edit.tool === 'wetness') {", + " brushes.wet(data, stroke, numberOr(edit.amount, 1), strength, changedSamples);", + " } else if (edit.tool === 'hole') {", + " brushes.hole(data, stroke, changedSamples);", + " } else if (edit.tool === 'fill') {", + " brushes.fill(data, stroke, changedSamples);", + " } else {", + " console.warn('Unknown tool in the edits of a terrain: \"' + edit.tool + '\" (use raise, flatten, smooth, paint, tint, wetness, hole or fill).');", + " }", + " }", + " },", + "};", + "", + "/**", + " * @param {gdjs.RuntimeGame} game", + " * @param {string} imageResourceName", + " * @returns {CanvasImageSource | null} The image, or null if it's not loaded (yet).", + " */", + "const getLoadedImage = (game, imageResourceName) => {", + " const imageManager = game.getImageManager();", + " if (typeof imageManager.getImageSource === 'function') return imageManager.getImageSource(imageResourceName);", + " // Older GDevelop versions: a 192 x 192 placeholder is given for images that are not loaded.", + " const texture = imageManager.getPIXITexture(imageResourceName);", + " return texture && texture !== imageManager.getInvalidPIXITexture() ? texture.baseTexture.getDrawableSource() : null;", + "};", + "", + "/**", + " * Loads heights from an image: black is the lowest, white the highest.", + " * Heights have 16 bits of precision when the red channel holds the most", + " * significant byte and the green one the least significant (a gray image", + " * works as expected too).", + " * @param {TerrainData} data", + " * @param {gdjs.RuntimeGame} game", + " * @param {string} imageResourceName", + " * @returns {boolean} true if the image could be read.", + " */", + "const loadHeightsFromImage = (data, game, imageResourceName) => {", + " const source = getLoadedImage(game, imageResourceName);", + " if (!source) return false;", + "", + " const canvas = document.createElement('canvas');", + " canvas.width = source.width;", + " canvas.height = source.height;", + " const context = canvas.getContext('2d', { willReadFrequently: true });", + " if (!context) return false;", + " context.drawImage(source, 0, 0);", + " const pixels = context.getImageData(0, 0, source.width, source.height).data;", + " // Pixels are decoded before being interpolated, to keep the 16 bits.", + " const pixelHeight = (x, y) => {", + " const offset = (y * source.width + x) * 4;", + " return (256 * pixels[offset] + pixels[offset + 1]) / 65535;", + " };", + " for (let j = 0; j < data.size; j++) {", + " const y = (j / data.resolution) * (source.height - 1);", + " const y0 = Math.floor(y);", + " const y1 = Math.min(y0 + 1, source.height - 1);", + " for (let i = 0; i < data.size; i++) {", + " const x = (i / data.resolution) * (source.width - 1);", + " const x0 = Math.floor(x);", + " const x1 = Math.min(x0 + 1, source.width - 1);", + " const top = pixelHeight(x0, y0) + (x - x0) * (pixelHeight(x1, y0) - pixelHeight(x0, y0));", + " const bottom = pixelHeight(x0, y1) + (x - x0) * (pixelHeight(x1, y1) - pixelHeight(x0, y1));", + " data.heights[j * data.size + i] = top + (y - y0) * (bottom - top);", + " }", + " }", + " return true;", + "};", + "", + "/**", + " * Stores the data in a string. Only the differences with the base data (from", + " * the relief or the heightmap) are stored: heights on 16 bits, layer weights", + " * on 8 bits, then (since format 2) the tint and wetness on 8 bits. Each value is predicted from its neighbors and the", + " * prediction errors are written as variable length integers, with runs of", + " * zeros collapsed. Untouched or smoothly changed areas take almost no space.", + " */", + "const codec = (() => {", + " const formatVersion = '2';", + " // Format 1 has no tint and wetness.", + " const readableFormatVersions = ['1', '2'];", + "", + " class ByteWriter {", + " constructor() {", + " this.bytes = new Uint8Array(1024);", + " this.length = 0;", + " }", + " writeVarUint(value) {", + " if (this.length + 5 > this.bytes.length) {", + " const bytes = new Uint8Array(this.bytes.length * 2);", + " bytes.set(this.bytes);", + " this.bytes = bytes;", + " }", + " while (value >= 0x80) {", + " this.bytes[this.length++] = (value & 0x7f) | 0x80;", + " value >>>= 7;", + " }", + " this.bytes[this.length++] = value;", + " }", + " }", + "", + " class ByteReader {", + " /** @param {Uint8Array} bytes */", + " constructor(bytes) {", + " this.bytes = bytes;", + " this.position = 0;", + " }", + " readVarUint() {", + " let value = 0;", + " let shift = 0;", + " let byte;", + " do {", + " if (this.position >= this.bytes.length) throw new Error('Truncated terrain data.');", + " byte = this.bytes[this.position++];", + " value += (byte & 0x7f) * Math.pow(2, shift);", + " shift += 7;", + " } while (byte & 0x80);", + " return value;", + " }", + " }", + "", + " /** Predicts a value from the left, top and top-left ones. */", + " const predict = (values, size, index) => {", + " const i = index % size;", + " if (index < size) return i === 0 ? 0 : values[index - 1];", + " if (i === 0) return values[index - size];", + " return values[index - 1] + values[index - size] - values[index - size - 1];", + " };", + "", + " /**", + " * @param {ByteWriter} writer", + " * @param {Int32Array} values A grid of `size * size` values.", + " */", + " const writeGrid = (writer, values, size) => {", + " let zeroRun = 0;", + " for (let index = 0; index < values.length; index++) {", + " const error = values[index] - predict(values, size, index);", + " if (error === 0) {", + " zeroRun++;", + " continue;", + " }", + " if (zeroRun > 0) {", + " writer.writeVarUint(0);", + " writer.writeVarUint(zeroRun);", + " zeroRun = 0;", + " }", + " // Zigzag: small negative and positive numbers get small codes.", + " writer.writeVarUint(error > 0 ? error * 2 : -error * 2 - 1);", + " }", + " if (zeroRun > 0) {", + " writer.writeVarUint(0);", + " writer.writeVarUint(zeroRun);", + " }", + " };", + "", + " /** @param {ByteReader} reader */", + " const readGrid = (reader, size) => {", + " const values = new Int32Array(size * size);", + " let zeroRun = 0;", + " for (let index = 0; index < values.length; index++) {", + " let error = 0;", + " if (zeroRun > 0) {", + " zeroRun--;", + " } else {", + " const code = reader.readVarUint();", + " if (code === 0) {", + " zeroRun = reader.readVarUint() - 1;", + " } else {", + " error = code % 2 === 0 ? code / 2 : -(code + 1) / 2;", + " }", + " }", + " values[index] = predict(values, size, index) + error;", + " }", + " return values;", + " };", + "", + " const toBase64 = (bytes, length) => {", + " let binary = '';", + " for (let offset = 0; offset < length; offset += 0x8000) {", + " binary += String.fromCharCode.apply(null, bytes.subarray(offset, Math.min(offset + 0x8000, length)));", + " }", + " return btoa(binary);", + " };", + "", + " const fromBase64 = (base64) => {", + " const binary = atob(base64);", + " const bytes = new Uint8Array(binary.length);", + " for (let index = 0; index < binary.length; index++) {", + " bytes[index] = binary.charCodeAt(index);", + " }", + " return bytes;", + " };", + "", + " return {", + " /**", + " * @param {TerrainData} data", + " * @param {TerrainData} baseData The data without changes, with the same resolution.", + " */", + " encode(data, baseData) {", + " const writer = new ByteWriter();", + " const values = new Int32Array(data.heights.length);", + " for (let index = 0; index < values.length; index++) {", + " values[index] = Math.round((data.heights[index] - baseData.heights[index]) * 65535);", + " }", + " writeGrid(writer, values, data.size);", + " for (let layer = 0; layer < LAYER_COUNT; layer++) {", + " for (let index = 0; index < values.length; index++) {", + " const offset = index * LAYER_COUNT + layer;", + " values[index] = data.splat[offset] - baseData.splat[offset];", + " }", + " writeGrid(writer, values, data.size);", + " }", + " // Not painted, these grids are zeros: a few bytes each.", + " for (let channel = 0; channel < 4; channel++) {", + " for (let index = 0; index < values.length; index++) {", + " const offset = index * 4 + channel;", + " values[index] = data.colors[offset] - baseData.colors[offset];", + " }", + " writeGrid(writer, values, data.size);", + " }", + " return formatVersion + ';' + data.resolution + ';' + toBase64(writer.bytes, writer.length);", + " },", + "", + " /**", + " * Store grids of integers in a string, like the terrain data. Used by other objects (the scattered models).", + " * @param {Int32Array[]} grids Grids of `size * size` values.", + " * @param {number} size", + " */", + " encodeGrids(grids, size) {", + " const writer = new ByteWriter();", + " for (const grid of grids) writeGrid(writer, grid, size);", + " return toBase64(writer.bytes, writer.length);", + " },", + "", + " /**", + " * @param {string} base64 A string from `encodeGrids`.", + " * @param {number} size", + " * @param {number} count The number of grids.", + " * @returns {Int32Array[]} The grids (an error is thrown if the string is not valid).", + " */", + " decodeGrids(base64, size, count) {", + " const reader = new ByteReader(fromBase64(base64));", + " return Array.from({ length: count }, () => readGrid(reader, size));", + " },", + "", + " /**", + " * @param {string} text", + " * @param {(resolution: number) => TerrainData} createBaseData", + " * @returns {TerrainData | null} null if the text is empty or not valid.", + " */", + " decode(text, createBaseData) {", + " if (!text) return null;", + " const parts = text.split(';');", + " const resolution = parseInt(parts[1], 10);", + " if (readableFormatVersions.indexOf(parts[0]) === -1 || RESOLUTIONS.indexOf(resolution) === -1) {", + " return null;", + " }", + " try {", + " const reader = new ByteReader(fromBase64(parts[2]));", + " const data = createBaseData(resolution);", + " const heightChanges = readGrid(reader, data.size);", + " for (let index = 0; index < heightChanges.length; index++) {", + " data.heights[index] = clamp01(data.heights[index] + heightChanges[index] / 65535);", + " }", + " for (let layer = 0; layer < LAYER_COUNT; layer++) {", + " const weightChanges = readGrid(reader, data.size);", + " for (let index = 0; index < weightChanges.length; index++) {", + " const offset = index * LAYER_COUNT + layer;", + " // The base may have changed since the data was saved.", + " data.splat[offset] = Math.min(Math.max(data.splat[offset] + weightChanges[index], 0), 255);", + " }", + " }", + " if (parts[0] !== '1') {", + " for (let channel = 0; channel < 4; channel++) {", + " const colorChanges = readGrid(reader, data.size);", + " for (let index = 0; index < colorChanges.length; index++) {", + " const offset = index * 4 + channel;", + " data.colors[offset] = Math.min(Math.max(data.colors[offset] + colorChanges[index], 0), 255);", + " }", + " }", + " }", + " return data;", + " } catch (error) {", + " console.error('Terrain data could not be read:', error);", + " return null;", + " }", + " },", + " };", + "})();", + "", + "const lodIndices = new Map();", + "/**", + " * The triangles of a chunk, shared by all chunks of the same size. Chunk", + " * borders have a skirt going down, hiding the gaps between chunks drawn", + " * with different levels of detail.", + " * @param {number} chunkCells", + " * @param {number} step", + " */", + "const getLodIndex = (chunkCells, step) => {", + " const key = chunkCells + ';' + step;", + " const existingIndex = lodIndices.get(key);", + " if (existingIndex) return existingIndex;", + "", + " const sideVertices = chunkCells + 1;", + " const gridVertices = sideVertices * sideVertices;", + " const indices = [];", + " const grid = (i, j) => j * sideVertices + i;", + " for (let j = 0; j < chunkCells; j += step) {", + " for (let i = 0; i < chunkCells; i += step) {", + " const topLeft = grid(i, j);", + " const topRight = grid(i + step, j);", + " const bottomLeft = grid(i, j + step);", + " const bottomRight = grid(i + step, j + step);", + " indices.push(topLeft, topRight, bottomLeft, topRight, bottomRight, bottomLeft);", + " }", + " }", + " const borders = [", + " (k) => grid(k, 0),", + " (k) => grid(k, chunkCells),", + " (k) => grid(0, k),", + " (k) => grid(chunkCells, k),", + " ];", + " borders.forEach((borderVertex, border) => {", + " const skirtVertex = (k) => gridVertices + border * sideVertices + k;", + " for (let k = 0; k < chunkCells; k += step) {", + " const a = borderVertex(k);", + " const b = borderVertex(k + step);", + " const skirtA = skirtVertex(k);", + " const skirtB = skirtVertex(k + step);", + " // Both faces, as a skirt can be seen from either side.", + " indices.push(a, b, skirtB, a, skirtB, skirtA, a, skirtB, b, a, skirtA, skirtB);", + " }", + " });", + " const index = new THREE.BufferAttribute(new Uint16Array(indices), 1);", + " lodIndices.set(key, index);", + " return index;", + "};", + "", + "const vertexShaderDeclarations = `", + "varying vec2 vTerrainLocalXY;", + "varying float vTerrainLocalZ;", + "uniform float terrainWorldScale;", + "varying vec2 vTerrainWorldXY;", + "varying float vTerrainUp;", + "`;", + "const vertexShaderCode = `", + "vTerrainLocalXY = position.xy;", + "vTerrainLocalZ = position.z;", + "vTerrainWorldXY = (modelMatrix * vec4(transformed, 1.0)).xy * terrainWorldScale;", + "vTerrainUp = normalize(transpose(inverse(mat3(modelMatrix))) * objectNormal).z;", + "`;", + "const fragmentShaderDeclarations = `", + "uniform sampler2D terrainSplatMap;", + "uniform vec2 terrainSplatUvScaleAndOffset;", + "uniform vec3 terrainLayerColors[4];", + "uniform sampler2D terrainLayerMap0;", + "uniform sampler2D terrainLayerMap1;", + "uniform sampler2D terrainLayerMap2;", + "uniform sampler2D terrainLayerMap3;", + "uniform sampler2D terrainLayerNormalMap0;", + "uniform sampler2D terrainLayerNormalMap1;", + "uniform sampler2D terrainLayerNormalMap2;", + "uniform sampler2D terrainLayerNormalMap3;", + "uniform float terrainHasNormalMaps;", + "uniform float terrainNormalMapStrength;", + "uniform float terrainTextureSize;", + "uniform float terrainHeightBlending;", + "uniform float terrainTextureVariation;", + "uniform vec4 terrainCliffLayer;", + "uniform vec2 terrainCliffCosines;", + "uniform vec4 terrainBrush;", + "uniform sampler2D terrainColorMap;", + "uniform vec3 terrainWetShore;", + "varying vec2 vTerrainLocalXY;", + "varying float vTerrainLocalZ;", + "varying vec2 vTerrainWorldXY;", + "varying float vTerrainUp;", + "", + "float terrainHash(vec2 p) {", + " return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);", + "}", + "// The rotation of the copies of textures used to make repetition less visible.", + "const mat2 terrainCopyRotation = mat2(0.8, -0.6, 0.6, 0.8);", + "/** A bump of a normal map (from -1 to 1), mixed with its rotated copy in patches. */", + "vec3 terrainSampleNormal(sampler2D normalMap, vec2 uv, vec2 uv2, float patches) {", + " vec3 bump = texture2D(normalMap, uv).xyz * 2.0 - 1.0;", + " if (patches <= 0.0) return bump;", + " vec3 copyBump = texture2D(normalMap, uv2).xyz * 2.0 - 1.0;", + " // The bumps of the copy are rotated back with the inverse rotation.", + " copyBump.xy = transpose(terrainCopyRotation) * copyBump.xy;", + " return mix(bump, copyBump, patches);", + "}", + "float terrainNoise(vec2 p) {", + " vec2 i = floor(p);", + " vec2 f = fract(p);", + " vec2 u = f * f * (3.0 - 2.0 * f);", + " return mix(", + " mix(terrainHash(i), terrainHash(i + vec2(1.0, 0.0)), u.x),", + " mix(terrainHash(i + vec2(0.0, 1.0)), terrainHash(i + vec2(1.0, 1.0)), u.x),", + " u.y", + " );", + "}", + "`;", + "// Holes are samples without any painted layer: the interpolated sum of the", + "// weights gives smooth edges. Only compiled for terrains having holes, as", + "// discard makes drawing slower on some GPUs.", + "const holeShaderCode = `", + "#ifdef TERRAIN_HOLES", + "if (dot(texture2D(terrainSplatMap, vTerrainLocalXY * terrainSplatUvScaleAndOffset.x + terrainSplatUvScaleAndOffset.y), vec4(1.0)) < 0.5) discard;", + "#endif", + "`;", + "// For the shadows: the depth of holes is not drawn.", + "const holeDepthShaderDeclarations = `", + "uniform sampler2D terrainSplatMap;", + "uniform vec2 terrainSplatUvScaleAndOffset;", + "varying vec2 vTerrainLocalXY;", + "`;", + "", + "const fragmentShaderCode = holeShaderCode + `", + "// Borders between layers are made irregular and sharper, to look painted.", + "vec2 terrainNoisePosition = vTerrainWorldXY / 24.0;", + "vec4 terrainJitter = vec4(", + " terrainNoise(terrainNoisePosition),", + " terrainNoise(terrainNoisePosition + 19.1),", + " terrainNoise(terrainNoisePosition + 37.7),", + " terrainNoise(terrainNoisePosition + 53.3)", + ");", + "// Layer weights are in a texture, so that painted details stay the same", + "// whatever the level of detail of the ground.", + "vec2 terrainSplatUv = vTerrainLocalXY * terrainSplatUvScaleAndOffset.x + terrainSplatUvScaleAndOffset.y;", + "vec4 terrainWeights = texture2D(terrainSplatMap, terrainSplatUv) * (0.7 + 0.6 * terrainJitter);", + "terrainWeights = terrainWeights * terrainWeights;", + "terrainWeights /= max(dot(terrainWeights, vec4(1.0)), 0.00001);", + "float terrainCliff = 1.0 - smoothstep(terrainCliffCosines.x, terrainCliffCosines.y, vTerrainUp);", + "terrainWeights = mix(terrainWeights, terrainCliffLayer, terrainCliff * dot(terrainCliffLayer, vec4(1.0)));", + "", + "vec2 terrainUv = vTerrainWorldXY / terrainTextureSize;", + "vec3 terrainTexture0 = texture2D(terrainLayerMap0, terrainUv).rgb;", + "vec3 terrainTexture1 = texture2D(terrainLayerMap1, terrainUv).rgb;", + "vec3 terrainTexture2 = texture2D(terrainLayerMap2, terrainUv).rgb;", + "vec3 terrainTexture3 = texture2D(terrainLayerMap3, terrainUv).rgb;", + "", + "// Less visible repetition: in irregular patches, textures are replaced by a", + "// rotated and shifted copy of themselves, and colors slowly vary.", + "float terrainPatches = 0.0;", + "vec2 terrainUv2 = terrainUv;", + "float terrainColorVariation = 1.0;", + "if (terrainTextureVariation > 0.0) {", + " // Up to half of the ground is covered by the copies, each patch showing", + " // only one of them (mixing them would make them blurry).", + " float terrainPatchThreshold = 1.0 - 0.5 * terrainTextureVariation;", + " terrainPatches = smoothstep(terrainPatchThreshold - 0.05, terrainPatchThreshold + 0.05, terrainNoise(terrainUv * 0.23 + 7.3));", + " terrainUv2 = terrainCopyRotation * terrainUv + vec2(0.31, 0.73);", + " terrainTexture0 = mix(terrainTexture0, texture2D(terrainLayerMap0, terrainUv2).rgb, terrainPatches);", + " terrainTexture1 = mix(terrainTexture1, texture2D(terrainLayerMap1, terrainUv2).rgb, terrainPatches);", + " terrainTexture2 = mix(terrainTexture2, texture2D(terrainLayerMap2, terrainUv2).rgb, terrainPatches);", + " terrainTexture3 = mix(terrainTexture3, texture2D(terrainLayerMap3, terrainUv2).rgb, terrainPatches);", + " terrainColorVariation += terrainTextureVariation * 0.3 *", + " (terrainNoise(terrainUv * 0.11) + 0.5 * terrainNoise(terrainUv * 0.37 + 3.1) - 0.75);", + "}", + "", + "// Height-based blending: the brightness of a texture, compared to its", + "// average, is used as its height. Where layers meet, the high parts (stones,", + "// tufts) cover the low parts (sand, gaps) instead of fading into each other.", + "if (terrainHeightBlending > 0.0) {", + " const vec3 terrainLuminance = vec3(0.299, 0.587, 0.114);", + " vec4 terrainHeights = vec4(", + " dot(terrainTexture0 - textureLod(terrainLayerMap0, vec2(0.5), 16.0).rgb, terrainLuminance),", + " dot(terrainTexture1 - textureLod(terrainLayerMap1, vec2(0.5), 16.0).rgb, terrainLuminance),", + " dot(terrainTexture2 - textureLod(terrainLayerMap2, vec2(0.5), 16.0).rgb, terrainLuminance),", + " dot(terrainTexture3 - textureLod(terrainLayerMap3, vec2(0.5), 16.0).rgb, terrainLuminance)", + " );", + " vec4 terrainHeightWeights = terrainWeights + terrainHeights;", + " float terrainTop = max(max(terrainHeightWeights.x, terrainHeightWeights.y), max(terrainHeightWeights.z, terrainHeightWeights.w));", + " // Only painted layers (and the cliff layer) can show.", + " vec4 terrainHeightBlend = max(terrainHeightWeights - terrainTop + 0.1, 0.0) * step(0.004, terrainWeights);", + " terrainHeightBlend /= max(dot(terrainHeightBlend, vec4(1.0)), 0.00001);", + " terrainWeights = mix(terrainWeights, terrainHeightBlend, terrainHeightBlending);", + "}", + "", + "vec3 terrainColor = terrainColorVariation * (", + " terrainWeights.x * terrainLayerColors[0] * terrainTexture0 +", + " terrainWeights.y * terrainLayerColors[1] * terrainTexture1 +", + " terrainWeights.z * terrainLayerColors[2] * terrainTexture2 +", + " terrainWeights.w * terrainLayerColors[3] * terrainTexture3);", + "", + "// The painted tint multiplies all layers. Wet ground is darker (and", + "// shinier, see the roughness).", + "vec4 terrainTintAndWetness = texture2D(terrainColorMap, terrainSplatUv);", + "float terrainWetness = max(", + " terrainTintAndWetness.a,", + " terrainWetShore.z * (1.0 - smoothstep(terrainWetShore.x, terrainWetShore.x + terrainWetShore.y, vTerrainLocalZ))", + ");", + "terrainColor *= terrainTintAndWetness.rgb * (1.0 - 0.4 * terrainWetness);", + "diffuseColor.rgb *= terrainColor;", + "", + "if (terrainBrush.w > 0.5) {", + " float brushDistance = distance(vTerrainWorldXY, terrainBrush.xy);", + " float ringWidth = max(terrainBrush.z * 0.04, 1.5);", + " float ring = 1.0 - smoothstep(0.0, ringWidth, abs(brushDistance - terrainBrush.z));", + " float disc = brushDistance < terrainBrush.z ? 0.12 : 0.0;", + " diffuseColor.rgb = mix(diffuseColor.rgb, vec3(1.0), max(ring * 0.85, disc));", + "}", + "`;", + "", + "const fragmentShaderRoughnessCode = `", + "roughnessFactor = mix(roughnessFactor, 0.3, terrainWetness);", + "`;", + "", + "const fragmentShaderNormalCode = `", + "if (terrainHasNormalMaps > 0.5) {", + " vec3 terrainMapNormal =", + " terrainWeights.x * terrainSampleNormal(terrainLayerNormalMap0, terrainUv, terrainUv2, terrainPatches) +", + " terrainWeights.y * terrainSampleNormal(terrainLayerNormalMap1, terrainUv, terrainUv2, terrainPatches) +", + " terrainWeights.z * terrainSampleNormal(terrainLayerNormalMap2, terrainUv, terrainUv2, terrainPatches) +", + " terrainWeights.w * terrainSampleNormal(terrainLayerNormalMap3, terrainUv, terrainUv2, terrainPatches);", + " terrainMapNormal.xy *= terrainNormalMapStrength;", + " // The X and Y of textures follow the X and Y of the 3D world, projected on", + " // the ground (the camera space of the normal is used).", + " vec3 terrainTangent = (viewMatrix * vec4(1.0, 0.0, 0.0, 0.0)).xyz;", + " terrainTangent -= normal * dot(normal, terrainTangent);", + " // A ground facing the X axis (a rotated terrain) uses the Y axis instead.", + " if (dot(terrainTangent, terrainTangent) < 0.000001) {", + " terrainTangent = (viewMatrix * vec4(0.0, 1.0, 0.0, 0.0)).xyz;", + " terrainTangent -= normal * dot(normal, terrainTangent);", + " }", + " terrainTangent = normalize(terrainTangent);", + " vec3 terrainBitangent = cross(normal, terrainTangent);", + " normal = normalize(mat3(terrainTangent, terrainBitangent, normal) * terrainMapNormal);", + "}", + "`;", + "", + "let whiteTexture = null;", + "const getWhiteTexture = () => {", + " if (!whiteTexture) {", + " whiteTexture = new THREE.DataTexture(new Uint8Array([255, 255, 255, 255]), 1, 1);", + " whiteTexture.needsUpdate = true;", + " }", + " return whiteTexture;", + "};", + "", + "let flatNormalTexture = null;", + "const getFlatNormalTexture = () => {", + " if (!flatNormalTexture) {", + " flatNormalTexture = new THREE.DataTexture(new Uint8Array([128, 128, 255, 255]), 1, 1);", + " flatNormalTexture.needsUpdate = true;", + " }", + " return flatNormalTexture;", + "};", + "", + "const layerTextures = new WeakMap();", + "const layerNormalMaps = new WeakMap();", + "/**", + " * Textures of the image manager have no mipmaps: they would shimmer when", + " * repeated in the distance.", + " * @param {gdjs.RuntimeGame} game", + " * @param {string} resourceName", + " * @param {boolean} isNormalMap Normal maps are directions, not colors.", + " */", + "const getLayerTexture = (game, resourceName, isNormalMap) => {", + " if (!resourceName) return isNormalMap ? getFlatNormalTexture() : getWhiteTexture();", + " const imageTexture = game.getImageManager().getThreeTexture(resourceName);", + " const textures = isNormalMap ? layerNormalMaps : layerTextures;", + " let texture = textures.get(imageTexture);", + " if (!texture) {", + " texture = imageTexture.clone();", + " if (isNormalMap) texture.colorSpace = THREE.NoColorSpace;", + " texture.wrapS = THREE.RepeatWrapping;", + " texture.wrapT = THREE.RepeatWrapping;", + " texture.minFilter = THREE.LinearMipmapLinearFilter;", + " texture.generateMipmaps = true;", + " texture.anisotropy = 4;", + " texture.needsUpdate = true;", + " textures.set(imageTexture, texture);", + " }", + " return texture;", + "};", + "", + "/**", + " * A texture of 4 bytes per sample of a grid of `size * size` samples (layer", + " * weights, tint and wetness...), interpolated between samples.", + " * @param {Uint8Array} bytes", + " * @param {number} size", + " */", + "const createGridTexture = (bytes, size) => {", + " const texture = new THREE.DataTexture(bytes, size, size);", + " texture.magFilter = THREE.LinearFilter;", + " texture.minFilter = THREE.LinearFilter;", + " texture.userData.isUploaded = false;", + " texture.onUpdate = () => {", + " texture.userData.isUploaded = true;", + " };", + " return texture;", + "};", + "", + "/**", + " * Send the rows of the changed samples to the GPU, or the whole texture", + " * before its first upload (and in older GDevelop versions).", + " * @param {THREE.DataTexture} texture", + " * @param {SampleRectangle} samples", + " */", + "const uploadGridTextureRows = (texture, samples) => {", + " if (texture.userData.isUploaded && typeof texture.addUpdateRange === 'function') {", + " const size = texture.image.width;", + " for (let j = samples.minJ; j <= samples.maxJ; j++) {", + " texture.addUpdateRange((j * size + samples.minI) * 4, (samples.maxI - samples.minI + 1) * 4);", + " }", + " }", + " texture.needsUpdate = true;", + "};", + "", + "class TerrainRenderer {", + " /** @param {Terrain} terrain */", + " constructor(terrain) {", + " this.terrain = terrain;", + " this.group = new THREE.Group();", + " terrain.object.get3DRendererObject().add(this.group);", + " /** @type {Array<{mesh: THREE.Mesh, geometry: THREE.BufferGeometry, firstI: number, firstJ: number, center: THREE.Vector3, lod: number}>} */", + " this.chunks = [];", + " this.chunksPerSide = 0;", + " this.chunkCells = 0;", + " this.chunkSideVertices = 0;", + " this.chunkGridVertices = 0;", + " this.chunkVertices = 0;", + " /** @type {THREE.DataTexture | null} */", + " this.splatTexture = null;", + " /** @type {THREE.DataTexture | null} */", + " this.colorTexture = null;", + " this.uniforms = {", + " terrainSplatMap: { value: getWhiteTexture() },", + " terrainSplatUvScaleAndOffset: { value: new THREE.Vector2() },", + " terrainLayerColors: { value: [new THREE.Color(), new THREE.Color(), new THREE.Color(), new THREE.Color()] },", + " terrainLayerMap0: { value: getWhiteTexture() },", + " terrainLayerMap1: { value: getWhiteTexture() },", + " terrainLayerMap2: { value: getWhiteTexture() },", + " terrainLayerMap3: { value: getWhiteTexture() },", + " terrainLayerNormalMap0: { value: getFlatNormalTexture() },", + " terrainLayerNormalMap1: { value: getFlatNormalTexture() },", + " terrainLayerNormalMap2: { value: getFlatNormalTexture() },", + " terrainLayerNormalMap3: { value: getFlatNormalTexture() },", + " terrainHasNormalMaps: { value: 0 },", + " terrainNormalMapStrength: { value: 1 },", + " terrainTextureSize: { value: 256 },", + " terrainHeightBlending: { value: 0 },", + " terrainTextureVariation: { value: 0 },", + " terrainCliffLayer: { value: new THREE.Vector4() },", + " terrainCliffCosines: { value: new THREE.Vector2() },", + " terrainBrush: { value: new THREE.Vector4() },", + " terrainColorMap: { value: getWhiteTexture() },", + " // Z (in the terrain group) under which the ground is wet, fade height, and 1 if it's used.", + " terrainWetShore: { value: new THREE.Vector3() },", + " // Scene units by 3D world unit.", + " terrainWorldScale: { value: 1 },", + " };", + " this.material = new THREE.MeshStandardMaterial({ roughness: 1, metalness: 0 });", + " this.material.onBeforeCompile = (shader) => {", + " Object.assign(shader.uniforms, this.uniforms);", + " shader.vertexShader = patchShaderCode(shader.vertexShader, [", + " ['#include ', '#include \\n' + vertexShaderDeclarations],", + " ['#include ', '#include \\n' + vertexShaderCode],", + " ], 'Terrain3D');", + " shader.fragmentShader = patchShaderCode(shader.fragmentShader, [", + " ['#include ', '#include \\n' + fragmentShaderDeclarations],", + " ['#include ', fragmentShaderCode],", + " ['#include ', '#include \\n' + fragmentShaderRoughnessCode],", + " ['#include ', '#include \\n' + fragmentShaderNormalCode],", + " ], 'Terrain3D');", + " };", + " this.material.customProgramCacheKey = () => (this.hasHoles ? 'Terrain3DWithHoles' : 'Terrain3D');", + " this.hasHoles = false;", + " // Without them, holes would cast shadows.", + " this.depthMaterial = this._createHoleDepthMaterial(new THREE.MeshDepthMaterial());", + " this.distanceMaterial = this._createHoleDepthMaterial(new THREE.MeshDistanceMaterial());", + " this._cameraPosition = new THREE.Vector3();", + " this._chunkPosition = new THREE.Vector3();", + " this._scale = new THREE.Vector3();", + " }", + "", + " /**", + " * @param {THREE.MeshDepthMaterial | THREE.MeshDistanceMaterial} material", + " * @returns The material, drawing nothing in holes.", + " */", + " _createHoleDepthMaterial(material) {", + " material.defines = { TERRAIN_HOLES: '' };", + " material.onBeforeCompile = (shader) => {", + " Object.assign(shader.uniforms, this.uniforms);", + " shader.vertexShader = patchShaderCode(shader.vertexShader, [", + " ['#include ', '#include \\nvarying vec2 vTerrainLocalXY;'],", + " ['#include ', '#include \\nvTerrainLocalXY = position.xy;'],", + " ], 'Terrain3D shadows');", + " shader.fragmentShader = patchShaderCode(shader.fragmentShader, [", + " ['#include ', '#include \\n' + holeDepthShaderDeclarations],", + " ['#include ', holeShaderCode + '#include '],", + " ], 'Terrain3D shadows');", + " };", + " material.customProgramCacheKey = () => 'Terrain3DHoles';", + " return material;", + " }", + "", + " /**", + " * Use the shaders drawing holes, or the faster ones without them.", + " * @param {boolean} hasHoles", + " */", + " setHasHoles(hasHoles) {", + " if (hasHoles === this.hasHoles) return;", + " this.hasHoles = hasHoles;", + " // The material has its own defines (STANDARD).", + " if (hasHoles) this.material.defines.TERRAIN_HOLES = '';", + " else delete this.material.defines.TERRAIN_HOLES;", + " this.material.needsUpdate = true;", + " for (const chunk of this.chunks) this._setChunkShadowMaterials(chunk.mesh);", + " }", + "", + " /** @param {THREE.Mesh} mesh */", + " _setChunkShadowMaterials(mesh) {", + " mesh.customDepthMaterial = this.hasHoles ? this.depthMaterial : undefined;", + " mesh.customDistanceMaterial = this.hasHoles ? this.distanceMaterial : undefined;", + " }", + "", + " /** Recreate all the chunks, for example after the resolution changed. */", + " rebuild() {", + " this.disposeChunks();", + " const { data } = this.terrain;", + " this.chunkCells = getChunkCells(data.resolution);", + " this.chunkSideVertices = this.chunkCells + 1;", + " this.chunkGridVertices = this.chunkSideVertices * this.chunkSideVertices;", + " this.chunkVertices = this.chunkGridVertices + 4 * this.chunkSideVertices;", + " this.chunksPerSide = data.resolution / this.chunkCells;", + " const cellSize = AREA_SIZE / data.resolution;", + " // Sample centers are at the texel centers.", + " this.splatTexture = createGridTexture(data.splat, data.size);", + " this.uniforms.terrainSplatMap.value = this.splatTexture;", + " this.colorTexture = createGridTexture(data.colors, data.size);", + " // Tints are colors chosen like the layer colors.", + " this.colorTexture.colorSpace = THREE.SRGBColorSpace;", + " this.uniforms.terrainColorMap.value = this.colorTexture;", + " this.uniforms.terrainSplatUvScaleAndOffset.value.set(1 / (cellSize * data.size), 0.5 / data.size);", + " for (let chunkJ = 0; chunkJ < this.chunksPerSide; chunkJ++) {", + " for (let chunkI = 0; chunkI < this.chunksPerSide; chunkI++) {", + " const firstI = chunkI * this.chunkCells;", + " const firstJ = chunkJ * this.chunkCells;", + " const positions = new Float32Array(this.chunkVertices * 3);", + " for (let j = 0; j < this.chunkSideVertices; j++) {", + " for (let i = 0; i < this.chunkSideVertices; i++) {", + " const vertex = j * this.chunkSideVertices + i;", + " positions[vertex * 3] = (firstI + i) * cellSize;", + " positions[vertex * 3 + 1] = (firstJ + j) * cellSize;", + " }", + " }", + " // Skirt vertices are below border vertices.", + " this._forEachSkirtVertex((skirtVertex, borderVertex) => {", + " positions[skirtVertex * 3] = positions[borderVertex * 3];", + " positions[skirtVertex * 3 + 1] = positions[borderVertex * 3 + 1];", + " });", + " const geometry = new THREE.BufferGeometry();", + " geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));", + " geometry.setAttribute('normal', new THREE.BufferAttribute(new Float32Array(this.chunkVertices * 3), 3));", + " geometry.setIndex(getLodIndex(this.chunkCells, LOD_STEPS[0]));", + " geometry.boundingBox = new THREE.Box3();", + " geometry.boundingSphere = new THREE.Sphere();", + " const mesh = new THREE.Mesh(geometry, this.material);", + " mesh.matrixAutoUpdate = false;", + " // Rays hit the ground at its full resolution, whatever the level of", + " // detail drawn, and go through holes.", + " const chunkCells = this.chunkCells;", + " const terrainData = () => this.terrain.data;", + " const localPoint = new THREE.Vector3();", + " mesh.raycast = function (raycaster, intersects) {", + " const drawnIndex = geometry.index;", + " const firstIntersection = intersects.length;", + " geometry.setIndex(getLodIndex(chunkCells, LOD_STEPS[0]));", + " THREE.Mesh.prototype.raycast.call(this, raycaster, intersects);", + " geometry.setIndex(drawnIndex);", + " const data = terrainData();", + " const cellSize = AREA_SIZE / data.resolution;", + " for (let index = intersects.length - 1; index >= firstIntersection; index--) {", + " this.worldToLocal(localPoint.copy(intersects[index].point));", + " if (data.sampleSolidity(localPoint.x / cellSize, localPoint.y / cellSize) < 0.5) {", + " intersects.splice(index, 1);", + " }", + " }", + " };", + " this._setChunkShadowMaterials(mesh);", + " this.group.add(mesh);", + " this.chunks.push({", + " mesh,", + " geometry,", + " firstI,", + " firstJ,", + " center: new THREE.Vector3(", + " (firstI + this.chunkCells / 2) * cellSize,", + " (firstJ + this.chunkCells / 2) * cellSize,", + " 0", + " ),", + " lod: 0,", + " });", + " }", + " }", + " this.updateSamples(new SampleRectangle().set(0, 0, data.resolution, data.resolution), true);", + " this.updateMaterial();", + " }", + "", + " _forEachSkirtVertex(callback) {", + " for (let k = 0; k < this.chunkSideVertices; k++) {", + " callback(this.chunkGridVertices + k, k);", + " callback(this.chunkGridVertices + this.chunkSideVertices + k, this.chunkCells * this.chunkSideVertices + k);", + " callback(this.chunkGridVertices + 2 * this.chunkSideVertices + k, k * this.chunkSideVertices);", + " callback(", + " this.chunkGridVertices + 3 * this.chunkSideVertices + k,", + " k * this.chunkSideVertices + this.chunkCells", + " );", + " }", + " }", + "", + " /**", + " * Skirts go down only to the lowest point a coarser neighbor chunk can", + " * draw along the border, so that they can't be seen through holes.", + " * @param {Float32Array} positions", + " */", + " _updateSkirtHeights(positions) {", + " const coarsestStep = LOD_STEPS[LOD_STEPS.length - 1];", + " const borderVertices = [", + " (k) => k,", + " (k) => this.chunkCells * this.chunkSideVertices + k,", + " (k) => k * this.chunkSideVertices,", + " (k) => k * this.chunkSideVertices + this.chunkCells,", + " ];", + " borderVertices.forEach((borderVertex, border) => {", + " for (let k = 0; k < this.chunkSideVertices; k++) {", + " let minZ = Infinity;", + " const from = Math.max(k - coarsestStep, 0);", + " const to = Math.min(k + coarsestStep, this.chunkCells);", + " for (let other = from; other <= to; other++) {", + " minZ = Math.min(minZ, positions[borderVertex(other) * 3 + 2]);", + " }", + " const skirtVertex = this.chunkGridVertices + border * this.chunkSideVertices + k;", + " positions[skirtVertex * 3 + 2] = minZ;", + " }", + " });", + " }", + "", + " /**", + " * Update the vertices of the chunks sharing the given samples.", + " * @param {SampleRectangle} samples", + " * @param {boolean} hasPaintChanged", + " */", + " updateSamples(samples, hasPaintChanged) {", + " if (samples.isEmpty()) return;", + " const { data } = this.terrain;", + " const cellSize = AREA_SIZE / data.resolution;", + " const slopeFactor = AREA_DEPTH / cellSize;", + " // Normals of samples next to changed ones change too.", + " const minI = Math.max(samples.minI - 1, 0);", + " const minJ = Math.max(samples.minJ - 1, 0);", + " const maxI = Math.min(samples.maxI + 1, data.resolution);", + " const maxJ = Math.min(samples.maxJ + 1, data.resolution);", + " const firstChunkI = Math.max(Math.ceil(minI / this.chunkCells) - 1, 0);", + " const firstChunkJ = Math.max(Math.ceil(minJ / this.chunkCells) - 1, 0);", + " const lastChunkI = Math.min(Math.floor(maxI / this.chunkCells), this.chunksPerSide - 1);", + " const lastChunkJ = Math.min(Math.floor(maxJ / this.chunkCells), this.chunksPerSide - 1);", + "", + " for (let chunkJ = firstChunkJ; chunkJ <= lastChunkJ; chunkJ++) {", + " for (let chunkI = firstChunkI; chunkI <= lastChunkI; chunkI++) {", + " const chunk = this.chunks[chunkJ * this.chunksPerSide + chunkI];", + " const positions = chunk.geometry.attributes.position.array;", + " const normals = chunk.geometry.attributes.normal.array;", + " const fromI = Math.max(minI - chunk.firstI, 0);", + " const toI = Math.min(maxI - chunk.firstI, this.chunkCells);", + " const fromJ = Math.max(minJ - chunk.firstJ, 0);", + " const toJ = Math.min(maxJ - chunk.firstJ, this.chunkCells);", + " for (let j = fromJ; j <= toJ; j++) {", + " const gridJ = chunk.firstJ + j;", + " for (let i = fromI; i <= toI; i++) {", + " const gridI = chunk.firstI + i;", + " const vertex = j * this.chunkSideVertices + i;", + " const sample = gridJ * data.size + gridI;", + " positions[vertex * 3 + 2] = data.heights[sample] * AREA_DEPTH;", + "", + " const leftI = Math.max(gridI - 1, 0);", + " const rightI = Math.min(gridI + 1, data.resolution);", + " const upJ = Math.max(gridJ - 1, 0);", + " const downJ = Math.min(gridJ + 1, data.resolution);", + " const nx =", + " ((data.getHeight(leftI, gridJ) - data.getHeight(rightI, gridJ)) * slopeFactor) /", + " (rightI - leftI);", + " const ny =", + " ((data.getHeight(gridI, upJ) - data.getHeight(gridI, downJ)) * slopeFactor) /", + " (downJ - upJ);", + " const inverseLength = 1 / Math.sqrt(nx * nx + ny * ny + 1);", + " normals[vertex * 3] = nx * inverseLength;", + " normals[vertex * 3 + 1] = ny * inverseLength;", + " normals[vertex * 3 + 2] = inverseLength;", + " }", + " }", + " this._forEachSkirtVertex((skirtVertex, borderVertex) => {", + " for (let axis = 0; axis < 3; axis++) {", + " normals[skirtVertex * 3 + axis] = normals[borderVertex * 3 + axis];", + " }", + " });", + " this._updateSkirtHeights(positions);", + " let maxZ = 0;", + " for (let vertex = 0; vertex < this.chunkGridVertices; vertex++) {", + " maxZ = Math.max(maxZ, positions[vertex * 3 + 2]);", + " }", + " const chunkSize = this.chunkCells * cellSize;", + " chunk.geometry.boundingBox.min.set(chunk.firstI * cellSize, chunk.firstJ * cellSize, 0);", + " chunk.geometry.boundingBox.max.set(", + " chunk.firstI * cellSize + chunkSize,", + " chunk.firstJ * cellSize + chunkSize,", + " maxZ", + " );", + " chunk.geometry.boundingBox.getBoundingSphere(chunk.geometry.boundingSphere);", + " chunk.center.z = maxZ / 2;", + " chunk.geometry.attributes.position.needsUpdate = true;", + " chunk.geometry.attributes.normal.needsUpdate = true;", + " }", + " }", + " if (hasPaintChanged) {", + " // The data may have been replaced by one with the same resolution.", + " this.splatTexture.image.data = data.splat;", + " uploadGridTextureRows(this.splatTexture, samples);", + " this.colorTexture.image.data = data.colors;", + " uploadGridTextureRows(this.colorTexture, samples);", + " }", + " }", + "", + " updateMaterial() {", + " const { object } = this.terrain;", + " const game = object.getRuntimeScene().getGame();", + " const colors = [object._getLayer1Color(), object._getLayer2Color(), object._getLayer3Color(), object._getLayer4Color()];", + " const textures = [", + " object._getLayer1Texture(),", + " object._getLayer2Texture(),", + " object._getLayer3Texture(),", + " object._getLayer4Texture(),", + " ];", + " const normalMaps = [", + " object._getLayer1NormalMap(),", + " object._getLayer2NormalMap(),", + " object._getLayer3NormalMap(),", + " object._getLayer4NormalMap(),", + " ];", + " for (let layer = 0; layer < LAYER_COUNT; layer++) {", + " this.uniforms.terrainLayerColors.value[layer].set(gdjs.rgbOrHexStringToNumber(colors[layer]));", + " this.uniforms['terrainLayerMap' + layer].value = getLayerTexture(game, textures[layer], false);", + " this.uniforms['terrainLayerNormalMap' + layer].value = getLayerTexture(game, normalMaps[layer], true);", + " }", + " this.uniforms.terrainHasNormalMaps.value = normalMaps.some((normalMap) => !!normalMap) ? 1 : 0;", + " this.uniforms.terrainNormalMapStrength.value = Math.max(object._getNormalMapStrength(), 0);", + " this.uniforms.terrainTextureSize.value = Math.max(object._getTextureSize(), 1);", + " this.uniforms.terrainHeightBlending.value = clamp01(object._getHeightBlending());", + " this.uniforms.terrainTextureVariation.value = clamp01(object._getTextureVariation());", + "", + " const cliffLayer = parseInt(object._getCliffLayer(), 10);", + " const cliffAngle = gdjs.toRad(object._getCliffAngle());", + " const cliffBlendAngle = gdjs.toRad(4);", + " this.uniforms.terrainCliffLayer.value.set(", + " cliffLayer === 1 ? 1 : 0,", + " cliffLayer === 2 ? 1 : 0,", + " cliffLayer === 3 ? 1 : 0,", + " cliffLayer === 4 ? 1 : 0", + " );", + " this.uniforms.terrainCliffCosines.value.set(", + " Math.cos(cliffAngle + cliffBlendAngle),", + " Math.cos(cliffAngle - cliffBlendAngle)", + " );", + "", + " const castShadow = object._getCastShadow();", + " const receiveShadow = object._getReceiveShadow();", + " for (const chunk of this.chunks) {", + " chunk.mesh.castShadow = castShadow;", + " chunk.mesh.receiveShadow = receiveShadow;", + " }", + " }", + "", + " /**", + " * Show the brush circle of the editor.", + " * @param {THREE.Vector3 | null} localPosition", + " * @param {number} radius In scene units.", + " */", + " setBrush(localPosition, radius) {", + " const brush = this.uniforms.terrainBrush.value;", + " if (!localPosition) {", + " brush.w = 0;", + " return;", + " }", + " this.group.updateWorldMatrix(true, false);", + " const worldPosition = this._chunkPosition.copy(localPosition).applyMatrix4(this.group.matrixWorld);", + " const worldScale = this.uniforms.terrainWorldScale.value;", + " brush.set(worldPosition.x * worldScale, worldPosition.y * worldScale, radius, 1);", + " }", + "", + " /** @param {THREE.Camera | null} camera */", + " updateLevelsOfDetail(camera) {", + " if (!camera || !this.group.visible) return;", + " const matrixWorld = this.group.matrixWorld;", + " this._cameraPosition.setFromMatrixPosition(camera.matrixWorld);", + " this._scale.setFromMatrixScale(matrixWorld);", + " const cellSize = AREA_SIZE / this.terrain.data.resolution;", + " const chunkWorldSize = this.chunkCells * cellSize * Math.max(this._scale.x, this._scale.y);", + " for (const chunk of this.chunks) {", + " const distance =", + " this._chunkPosition.copy(chunk.center).applyMatrix4(matrixWorld).distanceTo(this._cameraPosition) /", + " chunkWorldSize;", + " let lod = LOD_DISTANCES.length - 1;", + " while (lod > 0 && distance < LOD_DISTANCES[lod]) lod--;", + " if (lod !== chunk.lod) {", + " chunk.lod = lod;", + " chunk.geometry.setIndex(getLodIndex(this.chunkCells, LOD_STEPS[lod]));", + " }", + " }", + " }", + "", + " disposeChunks() {", + " for (const chunk of this.chunks) {", + " chunk.mesh.removeFromParent();", + " // The index is shared by all chunks: it must stay on the GPU.", + " chunk.geometry.setIndex(null);", + " chunk.geometry.dispose();", + " }", + " this.chunks.length = 0;", + " if (this.splatTexture) this.splatTexture.dispose();", + " this.splatTexture = null;", + " if (this.colorTexture) this.colorTexture.dispose();", + " this.colorTexture = null;", + " }", + "", + " dispose() {", + " this.disposeChunks();", + " this.material.dispose();", + " this.depthMaterial.dispose();", + " this.distanceMaterial.dispose();", + " this.group.removeFromParent();", + " }", + "}", + "", + "/**", + " * Makes a Physics3D behavior use a height field shape following the terrain.", + " * Changes of heights are applied to the shape without recreating it.", + " * @implements {gdjs.Physics3DRuntimeBehavior.BodyUpdater}", + " */", + "class TerrainBodyUpdater {", + " /**", + " * @param {Terrain} terrain", + " * @param {gdjs.Physics3DRuntimeBehavior} physics", + " */", + " constructor(terrain, physics) {", + " this.terrain = terrain;", + " this.physics = physics;", + " this.defaultBodyUpdater = physics.bodyUpdater;", + " this.changedSamples = new SampleRectangle();", + " /** @type {Jolt.HeightFieldShape | null} */", + " this.heightField = null;", + " this.heightScale = 1;", + " this.heightsBufferPointer = 0;", + " this.heightsBufferLength = 0;", + " this.bodyX = 0;", + " this.bodyY = 0;", + " this.bodyZ = 0;", + " this.bodyHeight = 0;", + " physics.bodyType = 'Static';", + " physics.bodyUpdater = this;", + " }", + "", + " createShape() {", + " const { data, object } = this.terrain;", + " const worldInvScale = this.physics._sharedData.worldInvScale;", + " const { size } = data;", + " this.heightScale = object.getDepth() * worldInvScale;", + "", + " // Changes of heights (SetHeights) break collisions unless the sample", + " // count is a power of 2: extra samples have no collision.", + " const sampleCount = Math.pow(2, Math.ceil(Math.log2(size)));", + " const settings = new Jolt.HeightFieldShapeSettings();", + " settings.mScale.Set(", + " (object.getWidth() / data.resolution) * worldInvScale,", + " 1,", + " (object.getHeight() / data.resolution) * worldInvScale", + " );", + " settings.mSampleCount = sampleCount;", + " settings.mBlockSize = 4;", + " // Allows later changes of heights on the whole depth of the object.", + " settings.mMinHeightValue = 0;", + " settings.mMaxHeightValue = this.heightScale;", + " settings.mHeightSamples.resize(sampleCount * sampleCount);", + " const samples = new Float32Array(", + " Jolt.HEAPF32.buffer,", + " Jolt.getPointer(settings.mHeightSamples.data()),", + " sampleCount * sampleCount", + " );", + " const noCollision = Jolt.HeightFieldShapeConstantValues.prototype.cNoCollisionValue;", + " samples.fill(noCollision);", + " // Height fields go along X and Z: rows are flipped so that, once", + " // rotated to have heights along Z, rows go along Y.", + " for (let j = 0; j < size; j++) {", + " const row = (size - 1 - j) * sampleCount;", + " for (let i = 0; i < size; i++) {", + " samples[row + i] = this._getSampleHeight(j * size + i, noCollision);", + " }", + " }", + " const shape = settings.Create().Get();", + " Jolt.destroy(settings);", + " this.heightField = Jolt.castObject(shape, Jolt.HeightFieldShape);", + " this.changedSamples.clear();", + " return shape;", + " }", + "", + " /** @returns {number} The height of a sample in the shape, or no collision in a hole. */", + " _getSampleHeight(index, noCollision) {", + " const height = this.terrain.surfaceHeights[index];", + " return Number.isNaN(height) ? noCollision : height * this.heightScale;", + " }", + "", + " _getBodyPosition() {", + " const { object } = this.terrain;", + " const worldInvScale = this.physics._sharedData.worldInvScale;", + " this.bodyX = object.getX();", + " this.bodyY = object.getY();", + " this.bodyZ = object.getZ();", + " // The origin of the height field is at the bottom of the terrain (see createShape).", + " this.bodyHeight = object.getHeight();", + " return this.physics._sharedData.getRVec3(", + " object.getX() * worldInvScale,", + " (object.getY() + object.getHeight()) * worldInvScale,", + " object.getZ() * worldInvScale", + " );", + " }", + "", + " _getBodyRotation() {", + " // A quarter turn around X: heights go along Z.", + " return this.physics._sharedData.getQuat(Math.SQRT1_2, 0, 0, Math.SQRT1_2);", + " }", + "", + " createAndAddBody() {", + " const { physics } = this;", + " const { bodyInterface } = physics._sharedData;", + " const shape = this.createShape();", + " const settings = new Jolt.BodyCreationSettings(", + " shape,", + " this._getBodyPosition(),", + " this._getBodyRotation(),", + " Jolt.EMotionType_Static,", + " physics.getBodyLayer()", + " );", + " settings.mFriction = physics.friction;", + " settings.mRestitution = physics.restitution;", + " const body = bodyInterface.CreateBody(settings);", + " Jolt.destroy(settings);", + " bodyInterface.AddBody(body.GetID(), Jolt.EActivation_DontActivate);", + " return body;", + " }", + "", + " updateObjectFromBody() {", + " // The terrain is static: the body never moves it.", + " }", + "", + " updateBodyFromObject() {", + " const { physics } = this;", + " const body = physics._body;", + " if (!body) return;", + " const { object } = this.terrain;", + " const { bodyInterface } = physics._sharedData;", + " if (", + " object.getX() !== this.bodyX ||", + " object.getY() !== this.bodyY ||", + " object.getZ() !== this.bodyZ ||", + " object.getHeight() !== this.bodyHeight", + " ) {", + " bodyInterface.SetPositionAndRotation(", + " body.GetID(),", + " this._getBodyPosition(),", + " this._getBodyRotation(),", + " Jolt.EActivation_DontActivate", + " );", + " }", + " if (!this.changedSamples.isEmpty()) {", + " this._applyChangedHeights(body);", + " }", + " }", + "", + " _applyChangedHeights(body) {", + " const { data } = this.terrain;", + " const { size } = data;", + " const { minI, minJ, maxI, maxJ } = this.changedSamples;", + " // Jolt only changes blocks of samples. Rows are flipped and extra", + " // samples have no collision (see createShape).", + " const blockSize = this.heightField.GetBlockSize();", + " const sampleCount = this.heightField.GetSampleCount();", + " const alignDown = (value) => Math.floor(value / blockSize) * blockSize;", + " const alignUp = (value) => Math.min(Math.ceil(value / blockSize) * blockSize, sampleCount);", + " const firstX = alignDown(minI);", + " const firstRow = alignDown(size - 1 - maxJ);", + " const width = alignUp(maxI + 1) - firstX;", + " const height = alignUp(size - minJ) - firstRow;", + " if (width * height > this.heightsBufferLength) {", + " if (this.heightsBufferPointer) Jolt._webidl_free(this.heightsBufferPointer);", + " this.heightsBufferLength = width * height;", + " this.heightsBufferPointer = Jolt._webidl_malloc(this.heightsBufferLength * 4);", + " }", + " const heights = new Float32Array(Jolt.HEAPF32.buffer, this.heightsBufferPointer, width * height);", + " const noCollision = Jolt.HeightFieldShapeConstantValues.prototype.cNoCollisionValue;", + " for (let row = 0; row < height; row++) {", + " const j = size - 1 - (firstRow + row);", + " for (let column = 0; column < width; column++) {", + " const i = firstX + column;", + " heights[row * width + column] =", + " i < size && j >= 0 ? this._getSampleHeight(j * size + i, noCollision) : noCollision;", + " }", + " }", + " const sharedData = this.physics._sharedData;", + " this.heightField.SetHeights(", + " firstX,", + " firstRow,", + " width,", + " height,", + " this.heightsBufferPointer,", + " width,", + " sharedData.jolt.GetTempAllocator()", + " );", + " sharedData.bodyInterface.NotifyShapeChanged(", + " body.GetID(),", + " sharedData.getVec3(0, 0, 0),", + " false,", + " Jolt.EActivation_DontActivate", + " );", + " this._wakeUpBodiesOnChangedSamples();", + " this.changedSamples.clear();", + " }", + "", + " /** Bodies sleeping on the changed area would otherwise float or sink. */", + " _wakeUpBodiesOnChangedSamples() {", + " const { object, data } = this.terrain;", + " const worldInvScale = this.physics._sharedData.worldInvScale;", + " const cellWidth = object.getWidth() / data.resolution;", + " const cellHeight = object.getHeight() / data.resolution;", + " const { minI, minJ, maxI, maxJ } = this.changedSamples;", + " const min = new Jolt.Vec3(", + " (object.getX() + (minI - 1) * cellWidth) * worldInvScale,", + " (object.getY() + (minJ - 1) * cellHeight) * worldInvScale,", + " (object.getZ() - 1) * worldInvScale", + " );", + " const max = new Jolt.Vec3(", + " (object.getX() + (maxI + 1) * cellWidth) * worldInvScale,", + " (object.getY() + (maxJ + 1) * cellHeight) * worldInvScale,", + " (object.getZ() + object.getDepth() + 1) * worldInvScale", + " );", + " const box = new Jolt.AABox(min, max);", + " const broadPhaseLayerFilter = new Jolt.BroadPhaseLayerFilter();", + " const objectLayerFilter = new Jolt.ObjectLayerFilter();", + " this.physics._sharedData.bodyInterface.ActivateBodiesInAABox(box, broadPhaseLayerFilter, objectLayerFilter);", + " Jolt.destroy(min);", + " Jolt.destroy(max);", + " Jolt.destroy(box);", + " Jolt.destroy(broadPhaseLayerFilter);", + " Jolt.destroy(objectLayerFilter);", + " }", + "", + " recreateShape() {", + " const body = this.physics._body;", + " if (!body) return;", + " this.physics._sharedData.bodyInterface.SetShape(", + " body.GetID(),", + " this.createShape(),", + " false,", + " Jolt.EActivation_DontActivate", + " );", + " const { resolution } = this.terrain.data;", + " this.changedSamples.set(0, 0, resolution, resolution);", + " this._wakeUpBodiesOnChangedSamples();", + " this.changedSamples.clear();", + " }", + "", + " destroyBody() {", + " this.defaultBodyUpdater.destroyBody();", + " this.heightField = null;", + " }", + "", + " dispose() {", + " if (this.heightsBufferPointer) Jolt._webidl_free(this.heightsBufferPointer);", + " this.heightsBufferPointer = 0;", + " this.heightsBufferLength = 0;", + " }", + "}", + "", + "/** @param {TerrainData} data */", + "const copyTerrainData = (data) => {", + " const copy = new TerrainData(data.resolution);", + " copy.copyFrom(data);", + " return copy;", + "};", + "", + "// Generating a relief or reading an image is long for big terrains, and done", + "// for every instance and every hot-reload: the results are kept.", + "const baseDataCache = new Map();", + "const MAX_CACHED_BASE_DATA = 8;", + "// Changes of the shape of a terrain kept for consumers of its surface that", + "// missed some versions: older changes update the whole surface.", + "const MAX_SURFACE_CHANGES = 64;", + "// Unique among all terrains: a terrain replaced by another one never has its version.", + "let lastGroundVersion = 0;", + "", + "/**", + " * The terrain of a Terrain3D object: its data, how it's drawn and its", + " * surface (for collisions and navigation meshes). Positions are in the scene.", + " */", + "class Terrain {", + " /** @param {gdjs.CustomRuntimeObject3D} object */", + " constructor(object) {", + " this.object = object;", + " /** @type {TerrainData} */", + " this.data = new TerrainData(DEFAULT_RESOLUTION);", + " /** The data before any sculpting or painting, from the relief or heightmap. */", + " this.baseData = this.data;", + " this.renderer = new TerrainRenderer(this);", + " /** @type {TerrainBodyUpdater[]} */", + " this.bodyUpdaters = [];", + " this.changedSamples = new SampleRectangle();", + " this.hasPaintChanged = false;", + " /** Increased each time the ground changes, for objects following it (like grass). */", + " this.version = 0;", + " /** Increased each time the shape of the ground changes (not its paint or position). */", + " this.surfaceVersion = 0;", + " /**", + " * The last changes of the shape of the ground, for `getChangedArea`.", + " * @type {Array<{version: number, samples: SampleRectangle}>}", + " */", + " this.surfaceChanges = [];", + " /** The changes since this version are all in `surfaceChanges`. */", + " this.surfaceChangesStartVersion = 0;", + " this._stroke = { cellWidth: 1, cellHeight: 1, ax: 0, ay: 0, bx: 0, by: 0, radius: 0 };", + " this._strokeChangedSamples = new SampleRectangle();", + " /** The properties the data was loaded from, to reload it only if they change. */", + " this.loadedFrom = '';", + " /** The heightmap image being loaded: the relief is used until it is. */", + " this.loadingHeightmapImage = '';", + " this.isDisposed = false;", + " /** The last value of the sculpt data property read or saved. */", + " this.sculptData = '';", + " /**", + " * Values saved by the editor tools: the editor sends them back with a", + " * hot-reload, possibly after newer ones were saved.", + " * @type {string[]}", + " */", + " this.savedSculptData = [];", + " /** The heights of the surface: the heights of the data, and NaN in holes. */", + " this.surfaceHeights = new Float32Array(0);", + " this.loadFromProperties();", + " this._transform = this._getTransform();", + " getGrounds(object.getRuntimeScene()).add(this);", + " /**", + " * The shape of the ground, for the physics engine and navigation meshes.", + " * @type {gdjs.Surface}", + " */", + " this.surface = {", + " getVersion: () => this.surfaceVersion,", + " getChangedArea: (sinceVersion) => this.getChangedArea(sinceVersion),", + " getTriangles: () => null,", + " getHeightField: () => ({ columns: this.data.size, rows: this.data.size, heights: this.surfaceHeights }),", + " };", + " if (typeof object.setSurface === 'function') {", + " object.setSurface(this.surface);", + " } else if (gdjs.Physics3DRuntimeBehavior) {", + " // Older GDevelop versions don't use surfaces.", + " for (const behavior of object._behaviors) {", + " if (behavior instanceof gdjs.Physics3DRuntimeBehavior) {", + " this.bodyUpdaters.push(new TerrainBodyUpdater(this, behavior));", + " }", + " }", + " }", + " }", + "", + " _getSourceProperties() {", + " const { object } = this;", + " return [", + " object._getResolution(),", + " object._getRelief(),", + " object._getSeed(),", + " object._getHeightmapImage(),", + " object._getEdits(),", + " ].join('|');", + " }", + "", + " loadFromProperties() {", + " const { object } = this;", + " const resolution = parseInt(object._getResolution(), 10);", + " const validResolution = RESOLUTIONS.indexOf(resolution) !== -1 ? resolution : DEFAULT_RESOLUTION;", + " this.sculptData = object._getSculptData();", + " this.loadedFrom = this._getSourceProperties();", + "", + " this.baseData = this._getBaseData(validResolution);", + " let data = codec.decode(this.sculptData, (resolution) => copyTerrainData(this._getBaseData(resolution)));", + " if (data && data.resolution !== validResolution) {", + " data = data.resampled(validResolution);", + " }", + " this.setData(data || copyTerrainData(this.baseData));", + " }", + "", + " /**", + " * @param {number} resolution", + " * @returns {TerrainData} The data from the relief or the heightmap, shared: it must not be changed.", + " */", + " _getBaseData(resolution) {", + " const { object } = this;", + " const key = [resolution, object._getRelief(), object._getSeed(), object._getHeightmapImage(), object._getEdits()].join('|');", + " const cachedData = baseDataCache.get(key);", + " if (cachedData) return cachedData;", + "", + " const data = new TerrainData(resolution);", + " const heightmapImage = object._getHeightmapImage();", + " const isHeightmapLoaded =", + " !!heightmapImage && loadHeightsFromImage(data, object.getRuntimeScene().getGame(), heightmapImage);", + " if (!isHeightmapLoaded) {", + " relief.generate(data, object._getRelief(), object._getSeed());", + " if (heightmapImage) this._reloadOnceLoaded(heightmapImage);", + " }", + " edits.apply(data, object._getEdits());", + " // Not kept if the image is missing: it may be loaded later.", + " if (isHeightmapLoaded || !heightmapImage) baseDataCache.set(key, data);", + " if (baseDataCache.size > MAX_CACHED_BASE_DATA) {", + " baseDataCache.delete(baseDataCache.keys().next().value);", + " }", + " return data;", + " }", + "", + " /** @param {TerrainData} data */", + " setData(data) {", + " this.version = ++lastGroundVersion;", + " this.surfaceVersion = this.version;", + " this.surfaceChanges.length = 0;", + " this.surfaceChangesStartVersion = this.version;", + " const hasSameResolution = data.resolution === this.data.resolution && this.renderer.chunks.length > 0;", + " this.data = data;", + " const allSamples = new SampleRectangle().set(0, 0, data.resolution, data.resolution);", + " this.surfaceHeights = new Float32Array(data.heights.length);", + " this.renderer.setHasHoles(false);", + " this._updateSurfaceHeights(allSamples);", + " if (hasSameResolution) {", + " this.renderer.updateSamples(allSamples, true);", + " } else {", + " this.renderer.rebuild();", + " }", + " for (const bodyUpdater of this.bodyUpdaters) {", + " bodyUpdater.recreateShape();", + " }", + " }", + "", + " onHotReloading() {", + " const sculptData = this.object._getSculptData();", + " const savedIndex = this.savedSculptData.indexOf(sculptData);", + " if (savedIndex !== -1 && this._getSourceProperties() === this.loadedFrom) {", + " // The terrain already has this data, or newer changes.", + " this.savedSculptData.splice(0, savedIndex + 1);", + " } else if (sculptData !== this.sculptData || this._getSourceProperties() !== this.loadedFrom) {", + " this.savedSculptData.length = 0;", + " this.loadFromProperties();", + " }", + " this.renderer.updateMaterial();", + " }", + "", + " /** @returns {string} The data to save in the sculpt data property. */", + " save() {", + " this.sculptData = codec.encode(this.data, this.baseData);", + " this.savedSculptData.push(this.sculptData);", + " return this.sculptData;", + " }", + "", + " getCellWidth() {", + " return this.object.getWidth() / this.data.resolution;", + " }", + "", + " getCellHeight() {", + " return this.object.getHeight() / this.data.resolution;", + " }", + "", + " _toGridX(x) {", + " return (x - this.object.getX()) / this.getCellWidth();", + " }", + "", + " _toGridY(y) {", + " return (y - this.object.getY()) / this.getCellHeight();", + " }", + "", + " /** @returns {number} The Z position of the ground. */", + " getHeightAt(x, y) {", + " return this.object.getZ() + this.data.sampleHeight(this._toGridX(x), this._toGridY(y)) * this.object.getDepth();", + " }", + "", + " /** @returns {number} The angle of the ground with the horizontal, in degrees. */", + " getSlopeAt(x, y) {", + " const gridX = this._toGridX(x);", + " const gridY = this._toGridY(y);", + " const { data } = this;", + " const depth = this.object.getDepth();", + " const slopeX = ((data.sampleHeight(gridX + 0.5, gridY) - data.sampleHeight(gridX - 0.5, gridY)) * depth) / this.getCellWidth();", + " const slopeY = ((data.sampleHeight(gridX, gridY + 0.5) - data.sampleHeight(gridX, gridY - 0.5)) * depth) / this.getCellHeight();", + " return gdjs.toDegrees(Math.atan(Math.hypot(slopeX, slopeY)));", + " }", + "", + " /** @returns {number} The layer painted the most, from 1 to 4. */", + " getLayerAt(x, y) {", + " return this.data.getDominantLayer(this._toGridX(x), this._toGridY(y)) + 1;", + " }", + "", + " /**", + " * @param {number} layer From 1 to 4.", + " * @returns {number} How much the layer is painted, from 0 to 1.", + " */", + " getLayerWeightAt(x, y, layer) {", + " const layerIndex = clampInteger(layer - 1, 0, LAYER_COUNT - 1);", + " return this.data.sampleLayerWeight(this._toGridX(x), this._toGridY(y), layerIndex);", + " }", + "", + " /** @returns {number[]} The painted tint: red, green and blue, from 0 to 255 (white when there is none). */", + " getTintAt(x, y) {", + " const gridX = this._toGridX(x);", + " const gridY = this._toGridY(y);", + " return [0, 1, 2].map((channel) => Math.round(this.data.sampleColor(gridX, gridY, channel) * 255));", + " }", + "", + " /** @returns {number} How wet the ground is, from 0 (dry) to 1, painted or near the water (see the wet shore). */", + " getWetnessAt(x, y) {", + " const paintedWetness = this.data.sampleColor(this._toGridX(x), this._toGridY(y), 3);", + " const { object } = this;", + " if (!object._getWetShore()) return paintedWetness;", + " const fade = Math.max(object._getWetShoreFade(), 0.001);", + " const shoreWetness = clamp01((object._getWetShoreZ() + fade - this.getHeightAt(x, y)) / fade);", + " // Like the smoothstep of the shader.", + " return Math.max(paintedWetness, shoreWetness * shoreWetness * (3 - 2 * shoreWetness));", + " }", + "", + " _isInArea(x, y) {", + " const { object } = this;", + " return (", + " x >= object.getX() &&", + " x <= object.getX() + object.getWidth() &&", + " y >= object.getY() &&", + " y <= object.getY() + object.getHeight()", + " );", + " }", + "", + " /** @returns {boolean} true if the position is above or under the terrain, and not in a hole. */", + " containsPoint(x, y) {", + " return this._isInArea(x, y) && this.data.sampleSolidity(this._toGridX(x), this._toGridY(y)) >= 0.5;", + " }", + "", + " /** @returns {boolean} true if there is a hole in the terrain at the position, like what is drawn. */", + " isHoleAt(x, y) {", + " return this._isInArea(x, y) && this.data.sampleSolidity(this._toGridX(x), this._toGridY(y)) < 0.5;", + " }", + "", + " _getStroke(x1, y1, x2, y2, radius) {", + " const stroke = this._stroke;", + " stroke.cellWidth = this.getCellWidth();", + " stroke.cellHeight = this.getCellHeight();", + " stroke.ax = x1 - this.object.getX();", + " stroke.ay = y1 - this.object.getY();", + " stroke.bx = x2 - this.object.getX();", + " stroke.by = y2 - this.object.getY();", + " stroke.radius = Math.max(radius, 0);", + " return stroke;", + " }", + "", + " /**", + " * @param {SampleRectangle} samples", + " * @param {boolean} haveHeightsChanged false when only the paint changed.", + " */", + " _onSamplesChanged(samples, haveHeightsChanged) {", + " if (samples.isEmpty()) return;", + " this.version = ++lastGroundVersion;", + " this.changedSamples.add(samples);", + " // Painting changes the surface when it makes holes or fills them.", + " const hasSurfaceChanged = this._updateSurfaceHeights(samples);", + " if (!haveHeightsChanged && !hasSurfaceChanged) return;", + " this.surfaceVersion = this.version;", + " this.surfaceChanges.push({ version: this.version, samples: new SampleRectangle().set(samples.minI, samples.minJ, samples.maxI, samples.maxJ) });", + " if (this.surfaceChanges.length > MAX_SURFACE_CHANGES) {", + " this.surfaceChangesStartVersion = this.surfaceChanges.shift().version;", + " }", + " for (const bodyUpdater of this.bodyUpdaters) {", + " bodyUpdater.changedSamples.add(samples);", + " }", + " }", + "", + " /**", + " * @param {SampleRectangle} samples", + " * @returns {boolean} true if heights of the surface changed, or holes were made or filled.", + " */", + " _updateSurfaceHeights(samples) {", + " const { data, surfaceHeights } = this;", + " let hasChanged = false;", + " let hasHoles = false;", + " for (let j = samples.minJ; j <= samples.maxJ; j++) {", + " for (let i = samples.minI; i <= samples.maxI; i++) {", + " const index = j * data.size + i;", + " const isHole = data.isHole(index);", + " const height = isHole ? NaN : data.heights[index];", + " const previousHeight = surfaceHeights[index];", + " if (isHole) {", + " hasHoles = true;", + " if (Number.isNaN(previousHeight)) continue;", + " } else if (height === previousHeight) {", + " continue;", + " }", + " surfaceHeights[index] = height;", + " hasChanged = true;", + " }", + " }", + " // Filled holes keep the shaders drawing holes until the data is loaded again.", + " if (hasHoles) this.renderer.setHasHoles(true);", + " return hasChanged;", + " }", + "", + " /** @param {number} height In scene units, negative to lower the ground. */", + " raise(x1, y1, x2, y2, radius, height) {", + " const samples = this._strokeChangedSamples;", + " brushes.raise(this.data, this._getStroke(x1, y1, x2, y2, radius), height / this.object.getDepth(), samples);", + " this._onSamplesChanged(samples, true);", + " }", + "", + " /** @param {number} z The Z position to flatten the ground to. */", + " /** @param {number} [toZ] The Z position at the end of the segment, for a slope. The same as z by default. */", + " flatten(x1, y1, x2, y2, radius, z, strength, toZ = z) {", + " const samples = this._strokeChangedSamples;", + " const toHeight = (value) => (value - this.object.getZ()) / this.object.getDepth();", + " brushes.flatten(this.data, this._getStroke(x1, y1, x2, y2, radius), toHeight(z), strength, samples, toHeight(toZ));", + " this._onSamplesChanged(samples, true);", + " }", + "", + " smooth(x1, y1, x2, y2, radius, strength) {", + " const samples = this._strokeChangedSamples;", + " brushes.smooth(this.data, this._getStroke(x1, y1, x2, y2, radius), strength, samples);", + " this._onSamplesChanged(samples, true);", + " }", + "", + " /** @param {number} layer From 1 to 4. */", + " paint(x1, y1, x2, y2, radius, layer, strength) {", + " const layerIndex = clampInteger(layer - 1, 0, LAYER_COUNT - 1);", + " const samples = this._strokeChangedSamples;", + " brushes.paint(this.data, this._getStroke(x1, y1, x2, y2, radius), layerIndex, strength, samples);", + " this.hasPaintChanged = true;", + " this._onSamplesChanged(samples, false);", + " }", + "", + " /** @param {number[]} color The red, green and blue (0-255) of the tint: white removes it. */", + " tint(x1, y1, x2, y2, radius, color, strength) {", + " const samples = this._strokeChangedSamples;", + " brushes.tint(this.data, this._getStroke(x1, y1, x2, y2, radius), color, strength, samples);", + " this.hasPaintChanged = true;", + " this._onSamplesChanged(samples, false);", + " }", + "", + " /** @param {number} wetness From 0 (dry) to 1. */", + " wet(x1, y1, x2, y2, radius, wetness, strength) {", + " const samples = this._strokeChangedSamples;", + " brushes.wet(this.data, this._getStroke(x1, y1, x2, y2, radius), wetness, strength, samples);", + " this.hasPaintChanged = true;", + " this._onSamplesChanged(samples, false);", + " }", + "", + " /** Make a hole around a segment (`fillHoles` fills it again). */", + " makeHole(x1, y1, x2, y2, radius) {", + " const samples = this._strokeChangedSamples;", + " brushes.hole(this.data, this._getStroke(x1, y1, x2, y2, radius), samples);", + " this.hasPaintChanged = true;", + " this._onSamplesChanged(samples, false);", + " }", + "", + " /** Fill the holes around a segment with the layers of the ground around them. */", + " fillHoles(x1, y1, x2, y2, radius) {", + " const samples = this._strokeChangedSamples;", + " brushes.fill(this.data, this._getStroke(x1, y1, x2, y2, radius), samples);", + " this.hasPaintChanged = true;", + " this._onSamplesChanged(samples, false);", + " }", + "", + " /**", + " * Make the entrance of a tunnel or a cave: a hole, with the ground in", + " * front of it flattened to the floor of the tunnel.", + " * @param {number} angle The direction going into the ground, in degrees.", + " * @param {number} floorZ", + " * @param {number} length The length of the flat ground in front of the entrance.", + " */", + " carveEntrance(x, y, radius, angle, floorZ, length) {", + " const directionX = Math.cos(gdjs.toRad(angle));", + " const directionY = Math.sin(gdjs.toRad(angle));", + " this.flatten(x - directionX * length, y - directionY * length, x, y, radius * 1.5, floorZ, 1);", + " this.makeHole(x, y, x + directionX * radius, y + directionY * radius, radius);", + " }", + "", + " generate(reliefName, seed) {", + " this.baseData = new TerrainData(this.data.resolution);", + " relief.generate(this.baseData, reliefName, seed);", + " this.setData(copyTerrainData(this.baseData));", + " }", + "", + " loadHeightmap(imageResourceName) {", + " const baseData = new TerrainData(this.data.resolution);", + " if (loadHeightsFromImage(baseData, this.object.getRuntimeScene().getGame(), imageResourceName)) {", + " baseData.splat.set(this.data.splat);", + " baseData.colors.set(this.data.colors);", + " this.baseData = baseData;", + " this.setData(copyTerrainData(baseData));", + " }", + " }", + "", + " /** @param {gdjs.RuntimeObject[]} objects */", + " placeOnGround(objects) {", + " for (const object of objects) {", + " if (object === this.object || !gdjs.Base3DHandler.is3D(object)) continue;", + " const groundZ = this.getHeightAt(object.getCenterXInScene(), object.getCenterYInScene());", + " // The origin of the object is not always at its bottom.", + " object.setZ(groundZ + object.getZ() - object.getUnrotatedAABBMinZ());", + " }", + " }", + "", + " /**", + " * @returns {THREE.Vector3 | null} The point of the ground under the cursor,", + " * in the terrain group coordinates. Holes are hit too, so that brushes can fill them.", + " */", + " raycastFromCursor(game, raycaster, ndc) {", + " const layer = this.object.getInstanceContainer().getLayer(this.object.getLayer());", + " const camera = layer.getRenderer().getThreeCamera();", + " if (!camera) return null;", + " const inputManager = game.getInputManager();", + " ndc.set(", + " (inputManager.getCursorX() / game.getGameResolutionWidth()) * 2 - 1,", + " -(inputManager.getCursorY() / game.getGameResolutionHeight()) * 2 + 1", + " );", + " raycaster.setFromCamera(ndc, camera);", + " this.renderer.group.updateWorldMatrix(true, false);", + " const ray = raycaster.ray.clone().applyMatrix4(this.renderer.group.matrixWorld.clone().invert());", + " ray.direction.normalize();", + "", + " const box = new THREE.Box3(new THREE.Vector3(0, 0, 0), new THREE.Vector3(AREA_SIZE, AREA_SIZE, AREA_DEPTH));", + " const entry = box.containsPoint(ray.origin) ? ray.origin.clone() : ray.intersectBox(box, new THREE.Vector3());", + " if (!entry) return null;", + " const cellSize = AREA_SIZE / this.data.resolution;", + " const groundZAt = (point) => this.data.sampleHeight(point.x / cellSize, point.y / cellSize) * AREA_DEPTH;", + " const step = cellSize / 2;", + " const point = entry.clone();", + " const previousPoint = entry.clone();", + " for (let distance = 0; distance < AREA_SIZE * 2; distance += step) {", + " if (point.z <= groundZAt(point)) {", + " // Refine between the last point above the ground and this one.", + " for (let iteration = 0; iteration < 8; iteration++) {", + " const middle = previousPoint.clone().lerp(point, 0.5);", + " if (middle.z <= groundZAt(middle)) point.copy(middle);", + " else previousPoint.copy(middle);", + " }", + " return point;", + " }", + " if (!box.containsPoint(point)) return null;", + " previousPoint.copy(point);", + " point.addScaledVector(ray.direction, step);", + " }", + " return null;", + " }", + "", + " /**", + " * @param {THREE.Vector3} localPosition In the terrain group coordinates (like from `raycastFromCursor`).", + " * @returns {{x: number, y: number, z: number}} The position in the scene.", + " */", + " toScenePosition(localPosition) {", + " const { object } = this;", + " return {", + " x: object.getX() + (localPosition.x / AREA_SIZE) * object.getWidth(),", + " y: object.getY() + (localPosition.y / AREA_SIZE) * object.getHeight(),", + " z: object.getZ() + (localPosition.z / AREA_DEPTH) * object.getDepth(),", + " };", + " }", + "", + " /** Draw the changes of the ground (collisions are updated by the physics engine). */", + " applyChanges() {", + " this.renderer.updateSamples(this.changedSamples, this.hasPaintChanged);", + " this.changedSamples.clear();", + " this.hasPaintChanged = false;", + " }", + "", + " /** @returns {number} A number changing each time the ground changes (see `getGrounds`). */", + " getVersion() {", + " return this.version;", + " }", + "", + " /**", + " * @param {number} sinceVersion", + " * @returns {gdjs.SurfaceArea | null} The part of the ground whose shape changed since the version (see `gdjs.Surface`).", + " */", + " getChangedArea(sinceVersion) {", + " if (sinceVersion < this.surfaceChangesStartVersion) return null;", + " const samples = new SampleRectangle();", + " for (const change of this.surfaceChanges) {", + " if (change.version > sinceVersion) samples.add(change.samples);", + " }", + " if (samples.isEmpty()) return { minX: 0, minY: 0, maxX: 0, maxY: 0 };", + " const { resolution } = this.data;", + " return {", + " minX: samples.minI / resolution,", + " minY: samples.minJ / resolution,", + " maxX: samples.maxI / resolution,", + " maxY: samples.maxJ / resolution,", + " };", + " }", + "", + " _getTransform() {", + " const { object } = this;", + " return [object.getX(), object.getY(), object.getZ(), object.getWidth(), object.getHeight(), object.getDepth()].join(',');", + " }", + "", + " /** @param {string} imageResourceName A heightmap image that is not loaded yet. */", + " _reloadOnceLoaded(imageResourceName) {", + " if (this.loadingHeightmapImage === imageResourceName) return;", + " this.loadingHeightmapImage = imageResourceName;", + " const game = this.object.getRuntimeScene().getGame();", + " game.getImageManager()", + " .loadResource(imageResourceName)", + " .then(() => {", + " if (this.isDisposed || this.loadingHeightmapImage !== imageResourceName) return;", + " this.loadingHeightmapImage = '';", + " if (getLoadedImage(game, imageResourceName)) this.loadFromProperties();", + " })", + " .catch(() => {});", + " }", + "", + " update() {", + " const transform = this._getTransform();", + " if (transform !== this._transform) {", + " this._transform = transform;", + " this.version = ++lastGroundVersion;", + " }", + " this.applyChanges();", + " this.renderer.uniforms.terrainWorldScale.value = getWorldScale(this.object);", + " this._updateWetShore();", + " const layer = this.object.getInstanceContainer().getLayer(this.object.getLayer());", + " this.renderer.updateLevelsOfDetail(layer.getRenderer().getThreeCamera());", + " }", + "", + " _updateWetShore() {", + " const { object } = this;", + " // In the coordinates of the terrain group, like the vertices.", + " const depthScale = AREA_DEPTH / Math.max(object.getDepth(), 0.0001);", + " this.renderer.uniforms.terrainWetShore.value.set(", + " (object._getWetShoreZ() - object.getZ()) * depthScale,", + " Math.max(object._getWetShoreFade(), 0.001) * depthScale,", + " object._getWetShore() ? 1 : 0", + " );", + " }", + "", + " dispose() {", + " this.isDisposed = true;", + " getGrounds(this.object.getRuntimeScene()).delete(this);", + " if (typeof this.object.setSurface === 'function' && this.object.getSurface() === this.surface) {", + " this.object.setSurface(null);", + " }", + " this.renderer.dispose();", + " for (const bodyUpdater of this.bodyUpdaters) {", + " bodyUpdater.dispose();", + " }", + " }", + "}", + "", + "/**", + " * @param {gdjs.RuntimeObject} object", + " * @returns {number} The number of scene units by 3D world unit (1 in GDevelop versions before the world scale).", + " */", + "const getWorldScale = (object) => {", + " const scene = object.getRuntimeScene();", + " return typeof scene.getRenderer3DWorldScale === 'function' ? scene.getRenderer3DWorldScale() : 1;", + "};", + "", + "const averageColors = new WeakMap();", + "let white = null;", + "/**", + " * @param {THREE.Texture} texture", + " * @returns {THREE.Color} The average color of the texture (white when it's not loaded).", + " */", + "const getAverageColor = (texture) => {", + " const cachedColor = averageColors.get(texture);", + " if (cachedColor) return cachedColor;", + " const image = texture.image;", + " const isDrawable =", + " typeof document !== 'undefined' &&", + " ((image instanceof HTMLImageElement && image.complete && image.naturalWidth > 0) ||", + " image instanceof HTMLCanvasElement ||", + " (typeof ImageBitmap !== 'undefined' && image instanceof ImageBitmap));", + " if (!isDrawable) return white || (white = new THREE.Color(1, 1, 1));", + " const size = 8;", + " const canvas = document.createElement('canvas');", + " canvas.width = size;", + " canvas.height = size;", + " const context = canvas.getContext('2d');", + " context.drawImage(image, 0, 0, size, size);", + " const pixels = context.getImageData(0, 0, size, size).data;", + " let red = 0;", + " let green = 0;", + " let blue = 0;", + " for (let index = 0; index < pixels.length; index += 4) {", + " red += pixels[index];", + " green += pixels[index + 1];", + " blue += pixels[index + 2];", + " }", + " const count = size * size * 255;", + " const color = new THREE.Color().setRGB(red / count, green / count, blue / count, THREE.SRGBColorSpace);", + " averageColors.set(texture, color);", + " return color;", + "};", + "", + "const svgIcon = (content) =>", + " 'data:image/svg+xml,' +", + " encodeURIComponent('' + content + '');", + "const strokes = 'fill=\"none\" stroke=\"#000\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"';", + "const toolIcons = {", + " Raise: svgIcon(''),", + " Lower: svgIcon(''),", + " Smooth: svgIcon(''),", + " Flatten: svgIcon(''),", + " Size: svgIcon(''),", + " Strength: svgIcon(''),", + " Undo: svgIcon(''),", + " Hole: svgIcon(''),", + " FillHoles: svgIcon(''),", + " Wetness: svgIcon(''),", + "};", + "// Tints for the color brush when the editor has no color picker (older GDevelop versions).", + "const tintPresets = ['#e2c879', '#a5c45a', '#8a6a48', '#c98a62', '#9a9a92', '#ffffff'];", + "", + "/**", + " * The sculpt and paint tools shown in the scene editor when a terrain is", + " * selected. Without the needed editor functions (older GDevelop versions),", + " * nothing is shown: the terrain can still be changed with events.", + " */", + "class TerrainEditorTools {", + " constructor() {", + " /** '' when no brush is used. */", + " this.toolName = '';", + " this.sizePercent = 30;", + " this.strengthPercent = 50;", + " /** @type {Terrain | null} */", + " this.terrain = null;", + " /** @type {gdjs.InGameEditor | null} */", + " this.editor = null;", + " this.isStroking = false;", + " this.flattenHeight = 0;", + " /** The color of the \"Tint\" brush, as \"#rrggbb\". */", + " this.tintColor = tintPresets[0];", + " this.wetnessPercent = 100;", + " this.lastStrokeTime = 0;", + " /** @type {{data: TerrainData, baseData: TerrainData}[]} */", + " this.undoSteps = [];", + " this.raycaster = new THREE.Raycaster();", + " this.ndc = new THREE.Vector2();", + " this._swatchColor = new THREE.Color();", + " }", + "", + " /** @param {gdjs.InGameEditor} editor */", + " update(editor) {", + " if (typeof editor.showToolbar !== 'function') {", + " // Older GDevelop versions: the terrain can only be changed with events.", + " return;", + " }", + " this.editor = editor;", + " const selectedObjects = editor.getSelectedObjects();", + " const terrain =", + " selectedObjects.length === 1 && selectedObjects[0].__terrain3D ? selectedObjects[0].__terrain3D : null;", + " if (terrain !== this.terrain) {", + " this._endStroke();", + " if (this.terrain) this.terrain.renderer.setBrush(null, 0);", + " this.terrain = terrain;", + " this.undoSteps.length = 0;", + " }", + " if (terrain) editor.showToolbar('Terrain3D', this._getToolbarItems(terrain));", + " const game = editor.getRuntimeGame();", + " if (!terrain || !this.toolName) {", + " if (terrain) terrain.renderer.setBrush(null, 0);", + " return;", + " }", + "", + " const inputManager = game.getInputManager();", + " if (inputManager.wasKeyJustPressed(gdjs.evtTools.input.keysNameToCode.Escape)) {", + " this._endStroke();", + " this.toolName = '';", + " terrain.renderer.setBrush(null, 0);", + " return;", + " }", + " editor.captureLeftMouseButton();", + " const hit = terrain.raycastFromCursor(game, this.raycaster, this.ndc);", + " const radius = this._getRadius();", + " terrain.renderer.setBrush(hit, radius);", + "", + " // Older GDevelop versions don't have it.", + " const isPressedForTool =", + " typeof editor.isCapturedLeftMouseButtonPressed === 'function'", + " ? editor.isCapturedLeftMouseButtonPressed()", + " : inputManager.isMouseButtonPressed(0);", + " // The button is released outside the canvas when the mouse is on a toolbar.", + " const isPressed = isPressedForTool && inputManager.isMouseInsideCanvas();", + " if (isPressed && hit && !this.isStroking) {", + " this._startStroke(terrain, hit);", + " }", + " if (this.isStroking && isPressed && hit) {", + " this._applyBrush(terrain, hit, radius);", + " }", + " if (!isPressed) {", + " this._endStroke();", + " }", + " }", + "", + " _getRadius() {", + " const { object } = this.terrain;", + " const maxRadius = Math.max(object.getWidth(), object.getHeight()) / 4;", + " return Math.max(maxRadius * Math.pow(this.sizePercent / 100, 2), this.terrain.getCellWidth());", + " }", + "", + " _startStroke(terrain, hit) {", + " this.isStroking = true;", + " this.lastStrokeTime = performance.now();", + " this.flattenHeight = terrain.toScenePosition(hit).z;", + " this.undoSteps.push({ data: copyTerrainData(terrain.data), baseData: terrain.baseData });", + " const maxUndoSteps = Math.max(Math.floor(UNDO_STEPS_SAMPLES / terrain.data.heights.length), 2);", + " while (this.undoSteps.length > maxUndoSteps) this.undoSteps.shift();", + " }", + "", + " _endStroke() {", + " if (!this.isStroking) return;", + " this.isStroking = false;", + " this._save();", + " }", + "", + " _applyBrush(terrain, hit, radius) {", + " const now = performance.now();", + " const elapsedSeconds = Math.min((now - this.lastStrokeTime) / 1000, 0.1);", + " this.lastStrokeTime = now;", + " const { x, y } = terrain.toScenePosition(hit);", + " const amount = (this.strengthPercent / 100) * elapsedSeconds;", + " const depth = terrain.object.getDepth();", + " if (this.toolName === 'Raise') terrain.raise(x, y, x, y, radius, amount * depth * 0.5);", + " else if (this.toolName === 'Lower') terrain.raise(x, y, x, y, radius, -amount * depth * 0.5);", + " else if (this.toolName === 'Smooth') terrain.smooth(x, y, x, y, radius, amount * 10);", + " else if (this.toolName === 'Flatten') terrain.flatten(x, y, x, y, radius, this.flattenHeight, amount * 10);", + " else if (this.toolName.startsWith('Paint')) {", + " terrain.paint(x, y, x, y, radius, parseInt(this.toolName.slice(5), 10), amount * 8);", + " } else if (this.toolName === 'Hole') {", + " terrain.makeHole(x, y, x, y, radius);", + " } else if (this.toolName === 'FillHoles') {", + " terrain.fillHoles(x, y, x, y, radius);", + " } else if (this.toolName === 'Tint') {", + " terrain.tint(x, y, x, y, radius, gdjs.rgbOrHexToRGBColor(this.tintColor), amount * 8);", + " } else if (this.toolName === 'Wetness') {", + " terrain.wet(x, y, x, y, radius, this.wetnessPercent / 100, amount * 8);", + " }", + " // Drawn now, as the terrain was updated earlier in this frame.", + " terrain.applyChanges();", + " }", + "", + " _save() {", + " const { terrain, editor } = this;", + " if (!terrain || !editor) return;", + " editor.updateObjectProperties(terrain.object.getName(), { SculptData: terrain.save() });", + " }", + "", + " _undo() {", + " const undoStep = this.undoSteps.pop();", + " // The relief, the edits or the resolution may have changed since the stroke.", + " if (!this.terrain || !undoStep || undoStep.baseData !== this.terrain.baseData) {", + " this.undoSteps.length = 0;", + " return;", + " }", + " this.terrain.setData(undoStep.data);", + " this._save();", + " }", + "", + " /** @returns {Array} The settings of the tint or wetness brush, when it's used. */", + " _getColorToolItems() {", + " const { editor } = this;", + " if (this.toolName === 'Tint') {", + " if (editor && typeof editor.isToolbarItemTypeSupported === 'function' && editor.isToolbarItemTypeSupported('color')) {", + " return [", + " {", + " type: 'color',", + " id: 'TintColor',", + " tooltip: 'Color of the tint',", + " value: this.tintColor,", + " onChange: (color) => (this.tintColor = color),", + " },", + " ];", + " }", + " return tintPresets.map((color) => ({", + " type: 'button',", + " id: 'TintPreset' + color,", + " tooltip: color === '#ffffff' ? 'No tint' : 'Tint with this color',", + " color,", + " isActive: this.tintColor === color,", + " onClick: () => (this.tintColor = color),", + " }));", + " }", + " if (this.toolName === 'Wetness') {", + " return [", + " {", + " type: 'slider',", + " id: 'WetnessAmount',", + " tooltip: 'Wetness',", + " iconUrl: toolIcons.Wetness,", + " min: 0,", + " max: 100,", + " value: this.wetnessPercent,", + " onChange: (value) => (this.wetnessPercent = value),", + " },", + " ];", + " }", + " return [];", + " }", + "", + " /**", + " * @param {Terrain} terrain", + " * @returns {Array} The toolbar for the current state (the editor only", + " * updates what changed).", + " */", + " _getToolbarItems(terrain) {", + " const { object } = terrain;", + " const toolButton = (toolName, tooltip, color) => ({", + " type: 'button',", + " id: toolName,", + " tooltip,", + " iconUrl: toolIcons[toolName],", + " color,", + " isActive: this.toolName === toolName,", + " onClick: () => (this.toolName = this.toolName === toolName ? '' : toolName),", + " });", + " const layerColors = [object._getLayer1Color(), object._getLayer2Color(), object._getLayer3Color(), object._getLayer4Color()];", + " // Layers are shown with their color, tinting their texture.", + " const swatchColors = layerColors.map((color, index) =>", + " this._swatchColor", + " .set(gdjs.rgbOrHexStringToNumber(color))", + " .multiply(getAverageColor(terrain.renderer.uniforms['terrainLayerMap' + index].value))", + " .getHexString()", + " );", + " return [", + " toolButton('Raise', 'Raise the ground (drag on the terrain)'),", + " toolButton('Lower', 'Lower the ground'),", + " toolButton('Smooth', 'Smooth the ground'),", + " toolButton('Flatten', 'Flatten the ground to the height where the drag starts'),", + " { type: 'divider', id: 'PaintDivider' },", + " ...swatchColors.map((swatchColor, index) =>", + " toolButton('Paint' + (index + 1), 'Paint layer ' + (index + 1), '#' + swatchColor)", + " ),", + " toolButton('Hole', 'Make a hole, for the entrance of a tunnel or a cave'),", + " toolButton('FillHoles', 'Fill holes, with the layers of the ground around them'),", + " { type: 'divider', id: 'ColorDivider' },", + " toolButton('Tint', 'Tint the ground (white removes the tint)', this.tintColor),", + " toolButton('Wetness', 'Make the ground wet, or dry with a wetness of 0'),", + " ...this._getColorToolItems(),", + " { type: 'divider', id: 'BrushDivider' },", + " {", + " type: 'slider',", + " id: 'Size',", + " tooltip: 'Brush size',", + " iconUrl: toolIcons.Size,", + " min: 1,", + " max: 100,", + " value: this.sizePercent,", + " onChange: (value) => (this.sizePercent = value),", + " },", + " {", + " type: 'slider',", + " id: 'Strength',", + " tooltip: 'Brush strength',", + " iconUrl: toolIcons.Strength,", + " min: 5,", + " max: 100,", + " value: this.strengthPercent,", + " onChange: (value) => (this.strengthPercent = value),", + " },", + " { type: 'button', id: 'Undo', tooltip: 'Undo the last stroke on this terrain', iconUrl: toolIcons.Undo, onClick: () => this._undo() },", + " ];", + " }", + "}", + "", + "const editorTools = new TerrainEditorTools();", + "if (gdjs.registerInGameEditorPostStepCallback) {", + " gdjs.registerInGameEditorPostStepCallback((editor) => editorTools.update(editor));", + "}", + "", + "gdjs.__terrain3DExtension = {", + " Terrain,", + " // Used by the terrain grass and the scattered models.", + " getWorldScale,", + " getGrounds,", + " patchShaderCode,", + " createRandom,", + " findHighestGround,", + " GroundPlacement,", + " svgIcon,", + " toolIcons,", + " // Exposed for tests.", + " TerrainBodyUpdater,", + " TerrainData,", + " SampleRectangle,", + " brushes,", + " edits,", + " codec,", + " relief,", + "};" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [], + "objectGroups": [] + }, + { + "description": "Define grass helper classes JavaScript code.", + "fullName": "Define grass helper classes", + "functionType": "Action", + "name": "DefineGrassHelperClasses", + "private": true, + "sentence": "Define grass helper classes JavaScript code", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "if (gdjs.__terrainGrassExtension) {", + " return;", + "}", + "// Blades grow on the grounds of the scene: the 3D terrains (see `getGrounds`", + "// in the terrain helper).", + "", + "// Size of the object inner area: instances are scaled from it.", + "const AREA_SIZE = 1000;", + "const AREA_DEPTH = 40;", + "const CHUNK_SIZE = 500;", + "const MAX_BENDING_OBJECTS = 8;", + "", + "/**", + " * A blade: a thin, tapered strip. Its width and height are 1: blades are", + " * scaled by the shader.", + " */", + "const createBladeGeometry = () => {", + " const halfWidths = [0.5, 0.42, 0.28, 0];", + " const heights = [0, 0.35, 0.7, 1];", + " const positions = [];", + " halfWidths.forEach((halfWidth, level) => {", + " positions.push(-halfWidth, 0, heights[level]);", + " if (halfWidth > 0) positions.push(halfWidth, 0, heights[level]);", + " });", + " const indices = [0, 1, 2, 1, 3, 2, 2, 3, 4, 3, 5, 4, 4, 5, 6];", + " const geometry = new THREE.InstancedBufferGeometry();", + " geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));", + " geometry.setAttribute('normal', new THREE.Float32BufferAttribute(new Array(positions.length).fill(0), 3));", + " geometry.setIndex(indices);", + " return geometry;", + "};", + "", + "const vertexShaderDeclarations = `", + "attribute vec4 grassRoot;", + "attribute vec2 grassShape;", + "// The tint of the ground at the root (linear, from 0 to 1).", + "attribute vec3 grassGroundTint;", + "uniform mat3 grassWorldToLocal;", + "uniform float grassWorldScale;", + "uniform float grassBladeWidth;", + "uniform float grassTime;", + "uniform float grassWindStrength;", + "uniform float grassWindSpeed;", + "uniform vec4 grassBenders[${MAX_BENDING_OBJECTS}];", + "uniform vec3 grassBaseColor;", + "uniform vec3 grassTipColor;", + "varying vec3 vGrassColor;", + "`;", + "const vertexShaderPosition = `", + "// Blades are shaped in scene units (with Y flipped like the 3D world), then", + "// moved back in the (scaled) object space.", + "float bladeHeight = position.z;", + "float grassAngle = grassRoot.w;", + "float bladeWidth = position.x * grassBladeWidth;", + "vec3 blade = vec3(", + " bladeWidth * cos(grassAngle),", + " bladeWidth * sin(grassAngle),", + " bladeHeight * grassShape.x", + ");", + "vec3 rootWorld = (modelMatrix * vec4(grassRoot.xyz, 1.0)).xyz * grassWorldScale;", + "// Tips bend more than the base.", + "float bendFactor = bladeHeight * bladeHeight * grassShape.x;", + "float gust = sin(grassTime * grassWindSpeed + rootWorld.x * 0.013 + rootWorld.y * 0.011);", + "float flutter = sin(grassTime * grassWindSpeed * 2.7 + grassShape.y * 40.0) * 0.3;", + "blade.xy += vec2(0.8, 0.5) * (gust + flutter + 0.6) * grassWindStrength * 0.35 * bendFactor;", + "for (int i = 0; i < ${MAX_BENDING_OBJECTS}; i++) {", + " vec4 bender = grassBenders[i];", + " if (bender.z <= 0.0) continue;", + " vec2 away = rootWorld.xy - bender.xy;", + " float distanceToBender = length(away);", + " float push = 1.0 - smoothstep(bender.z * 0.3, bender.z, distanceToBender);", + " if (push > 0.0) {", + " blade.xy += normalize(away + vec2(0.001)) * push * bendFactor * 0.9;", + " blade.z *= 1.0 - push * 0.5;", + " }", + "}", + "vec3 transformed = grassRoot.xyz + grassWorldToLocal * (blade / grassWorldScale);", + "vGrassColor = mix(grassBaseColor, grassTipColor, bladeHeight) * (0.85 + 0.3 * grassShape.y) * grassGroundTint;", + "`;", + "const vertexShaderNormal = `", + "// Blades are lit like the ground below them.", + "transformedNormal = normalize((viewMatrix * vec4(0.0, 0.0, 1.0, 0.0)).xyz);", + "`;", + "const fragmentShaderDeclarations = `", + "varying vec3 vGrassColor;", + "`;", + "const fragmentShaderColor = `", + "diffuseColor.rgb *= vGrassColor;", + "`;", + "// Both faces of blades are lit like the ground (not flipped for back faces).", + "const fragmentShaderNormal = `", + "#include ", + "normal = normalize(vNormal);", + "nonPerturbedNormal = normal;", + "`;", + "", + "const { getWorldScale, patchShaderCode, createRandom, findHighestGround, GroundPlacement } = gdjs.__terrain3DExtension;", + "", + "/**", + " * The grass of a terrain grass object: blades drawn with GPU instancing, in chunks", + " * that are hidden when off-screen and thinned out with the distance. They grow", + " * on the grounds below, like 3D terrains (only where a given layer is painted).", + " */", + "class Grass {", + " /** @param {gdjs.CustomRuntimeObject3D} object */", + " constructor(object) {", + " this.object = object;", + " this.group = new THREE.Group();", + " object.get3DRendererObject().add(this.group);", + " this.bladeGeometry = createBladeGeometry();", + " /** @type {Array<{mesh: THREE.Mesh, blades: number, center: THREE.Vector3}>} */", + " this.chunks = [];", + " this.placement = new GroundPlacement(object.getRuntimeScene());", + " this._groundAtBlade = { ground: null, z: 0 };", + " this.uniforms = {", + " grassWorldToLocal: { value: new THREE.Matrix3() },", + " // Scene units by 3D world unit.", + " grassWorldScale: { value: 1 },", + " grassBladeWidth: { value: 1 },", + " grassTime: { value: 0 },", + " grassWindStrength: { value: 1 },", + " grassWindSpeed: { value: 1 },", + " grassBenders: { value: Array.from({ length: MAX_BENDING_OBJECTS }, () => new THREE.Vector4()) },", + " grassBaseColor: { value: new THREE.Color() },", + " grassTipColor: { value: new THREE.Color() },", + " };", + " this.material = new THREE.MeshStandardMaterial({ roughness: 1, metalness: 0, side: THREE.DoubleSide });", + " this.material.onBeforeCompile = (shader) => {", + " Object.assign(shader.uniforms, this.uniforms);", + " shader.vertexShader = patchShaderCode(shader.vertexShader, [", + " ['#include ', '#include \\n' + vertexShaderDeclarations],", + " ['#include ', vertexShaderPosition],", + " ['#include ', '#include \\n' + vertexShaderNormal],", + " ], 'TerrainGrass');", + " shader.fragmentShader = patchShaderCode(shader.fragmentShader, [", + " ['#include ', '#include \\n' + fragmentShaderDeclarations],", + " ['#include ', '#include \\n' + fragmentShaderColor],", + " ['#include ', fragmentShaderNormal],", + " ], 'TerrainGrass');", + " };", + " this.material.customProgramCacheKey = () => 'TerrainGrass';", + "", + " this._matrix = new THREE.Matrix4();", + " this._cameraPosition = new THREE.Vector3();", + " this._chunkPosition = new THREE.Vector3();", + " this._scale = new THREE.Vector3();", + " this.updateFromProperties();", + " }", + "", + " updateFromProperties() {", + " const { object } = this;", + " this.uniforms.grassBaseColor.value.set(gdjs.rgbOrHexStringToNumber(object._getBaseColor()));", + " this.uniforms.grassTipColor.value.set(gdjs.rgbOrHexStringToNumber(object._getTipColor()));", + " this.uniforms.grassWindStrength.value = object._getWindStrength();", + " this.uniforms.grassWindSpeed.value = object._getWindSpeed();", + " this.uniforms.grassBladeWidth.value = Math.max(object._getBladeWidth(), 0);", + " this.material.needsUpdate = true;", + " this.placement.placeAgain();", + " }", + "", + " _getPlacementKey() {", + " const { object } = this;", + " return [", + " object.getX(), object.getY(), object.getZ(), object.getWidth(), object.getHeight(), object.getDepth(),", + " object._getDensity(), object._getBladeHeight(), object._getGroundLayer(), object._getMaxSlope(),", + " object._getSeed(),", + " ].join('|');", + " }", + "", + " /** Place the blades again, for example after the ground or the object changed. */", + " rebuild() {", + " const grounds = Array.from(this.placement.grounds);", + " for (const chunk of this.chunks) {", + " chunk.mesh.removeFromParent();", + " chunk.mesh.geometry.dispose();", + " }", + " this.chunks.length = 0;", + "", + " const { object } = this;", + " const width = object.getWidth();", + " const height = object.getHeight();", + " const scaleX = width / AREA_SIZE;", + " const scaleY = height / AREA_SIZE;", + " const scaleZ = Math.max(object.getDepth() / AREA_DEPTH, 0.0001);", + " const bladesPerChunk = Math.round((Math.max(object._getDensity(), 0) * CHUNK_SIZE * CHUNK_SIZE) / 10000);", + " const groundLayer = parseInt(object._getGroundLayer(), 10) || 0;", + " const maxSlope = object._getMaxSlope();", + " const chunksX = Math.max(Math.ceil(width / CHUNK_SIZE), 1);", + " const chunksY = Math.max(Math.ceil(height / CHUNK_SIZE), 1);", + " const nextRandom = createRandom(object._getSeed() * 7919 + 1);", + " const bladeHeight = Math.max(object._getBladeHeight(), 0);", + "", + " for (let chunkY = 0; chunkY < chunksY; chunkY++) {", + " for (let chunkX = 0; chunkX < chunksX; chunkX++) {", + " const roots = new Float32Array(bladesPerChunk * 4);", + " const shapes = new Float32Array(bladesPerChunk * 2);", + " const groundTints = new Uint8Array(bladesPerChunk * 3).fill(255);", + " let blades = 0;", + " let minZ = Infinity;", + " let maxZ = -Infinity;", + " for (let attempt = 0; attempt < bladesPerChunk; attempt++) {", + " // In scene units, relative to the object.", + " const x = (chunkX + nextRandom()) * CHUNK_SIZE;", + " const y = (chunkY + nextRandom()) * CHUNK_SIZE;", + " const angle = nextRandom() * Math.PI * 2;", + " const heightFactor = 0.6 + 0.8 * nextRandom();", + " const tint = nextRandom();", + " const growth = nextRandom();", + " if (x > width || y > height) continue;", + " const sceneX = object.getX() + x;", + " const sceneY = object.getY() + y;", + " const { ground, z } = findHighestGround(grounds, sceneX, sceneY, this._groundAtBlade);", + " const groundZ = ground ? z : object.getZ();", + " if (ground) {", + " if (groundLayer > 0 && growth > ground.getLayerWeightAt(sceneX, sceneY, groundLayer)) continue;", + " if (ground.getSlopeAt(sceneX, sceneY) > maxSlope) continue;", + " if (typeof ground.getTintAt === 'function') {", + " const tint = ground.getTintAt(sceneX, sceneY);", + " for (let channel = 0; channel < 3; channel++) {", + " groundTints[blades * 3 + channel] = Math.round(255 * Math.pow(tint[channel] / 255, 2.2));", + " }", + " }", + " }", + " const localZ = (groundZ - object.getZ()) / scaleZ;", + " roots[blades * 4] = x / scaleX;", + " roots[blades * 4 + 1] = y / scaleY;", + " roots[blades * 4 + 2] = localZ;", + " roots[blades * 4 + 3] = angle;", + " shapes[blades * 2] = heightFactor * bladeHeight;", + " shapes[blades * 2 + 1] = tint;", + " minZ = Math.min(minZ, localZ);", + " maxZ = Math.max(maxZ, localZ);", + " blades++;", + " }", + " if (blades === 0) continue;", + "", + " const geometry = new THREE.InstancedBufferGeometry();", + " geometry.index = this.bladeGeometry.index;", + " geometry.setAttribute('position', this.bladeGeometry.attributes.position);", + " geometry.setAttribute('normal', this.bladeGeometry.attributes.normal);", + " geometry.setAttribute('grassRoot', new THREE.InstancedBufferAttribute(roots.subarray(0, blades * 4), 4));", + " geometry.setAttribute('grassShape', new THREE.InstancedBufferAttribute(shapes.subarray(0, blades * 2), 2));", + " geometry.setAttribute('grassGroundTint', new THREE.InstancedBufferAttribute(groundTints.subarray(0, blades * 3), 3, true));", + " geometry.instanceCount = blades;", + " // The furthest tips can go with the wind and the bending objects (see the shader).", + " const maxBladeHeight = bladeHeight * 1.4;", + " const lean =", + " (0.35 * 1.9 * 0.943 * Math.max(object._getWindStrength(), 0) + 0.9) * maxBladeHeight +", + " Math.max(object._getBladeWidth(), 0) / 2;", + " const leanX = lean / scaleX;", + " const leanY = lean / scaleY;", + " geometry.boundingBox = new THREE.Box3(", + " new THREE.Vector3((chunkX * CHUNK_SIZE) / scaleX - leanX, (chunkY * CHUNK_SIZE) / scaleY - leanY, minZ),", + " new THREE.Vector3(", + " Math.min((chunkX + 1) * CHUNK_SIZE, width) / scaleX + leanX,", + " Math.min((chunkY + 1) * CHUNK_SIZE, height) / scaleY + leanY,", + " maxZ + (bladeHeight * 1.4) / scaleZ", + " )", + " );", + " geometry.boundingSphere = geometry.boundingBox.getBoundingSphere(new THREE.Sphere());", + " const mesh = new THREE.Mesh(geometry, this.material);", + " mesh.receiveShadow = true;", + " // Raycasts would only test one blade: the selection plane is used instead.", + " mesh.raycast = () => {};", + " this.group.add(mesh);", + " this.chunks.push({ mesh, blades, center: geometry.boundingSphere.center.clone() });", + " }", + " }", + " }", + "", + " _updateBendingObjects() {", + " const benders = this.uniforms.grassBenders.value;", + " const objectName = this.object._getBendingObject();", + " const bendingObjects = objectName ? this.object.getInstanceContainer().getObjects(objectName) || [] : [];", + " for (let index = 0; index < MAX_BENDING_OBJECTS; index++) {", + " const bendingObject = bendingObjects[index];", + " if (!bendingObject) {", + " benders[index].set(0, 0, 0, 0);", + " continue;", + " }", + " // Blades are pushed in the 3D scene, which is flipped on Y.", + " const radius = Math.max(bendingObject.getWidth(), bendingObject.getHeight()) * 1.2;", + " benders[index].set(bendingObject.getCenterXInScene(), -bendingObject.getCenterYInScene(), radius, 0);", + " }", + " }", + "", + " /**", + " * @param {number} elapsedSeconds", + " * @param {number} time In seconds, the same for all grass objects.", + " */", + " update(elapsedSeconds, time, camera) {", + " if (this.placement.shouldPlaceAgain(elapsedSeconds, this._getPlacementKey())) {", + " this.rebuild();", + " }", + "", + " this.uniforms.grassTime.value = time;", + " this.uniforms.grassWorldScale.value = getWorldScale(this.object);", + " // The object transformation is applied just before rendering, it's needed now.", + " this.object.getRenderer().ensureUpToDate();", + " this.group.updateWorldMatrix(true, false);", + " this.uniforms.grassWorldToLocal.value.setFromMatrix4(this._matrix.copy(this.group.matrixWorld).invert());", + " this._updateBendingObjects();", + " if (!camera) return;", + "", + " // Far chunks are hidden, and thinned out before (in 3D world units).", + " const fadeDistance = Math.max(this.object._getFadeDistance(), 1) / getWorldScale(this.object);", + " this._cameraPosition.setFromMatrixPosition(camera.matrixWorld);", + " for (const chunk of this.chunks) {", + " const distance = this._chunkPosition", + " .copy(chunk.center)", + " .applyMatrix4(this.group.matrixWorld)", + " .distanceTo(this._cameraPosition);", + " const density = gdjs.evtTools.common.clamp((fadeDistance - distance) / (fadeDistance * 0.5), 0, 1);", + " chunk.mesh.visible = density > 0;", + " chunk.mesh.geometry.instanceCount = Math.ceil(chunk.blades * density);", + " }", + " }", + "", + " dispose() {", + " for (const chunk of this.chunks) {", + " chunk.mesh.geometry.dispose();", + " }", + " this.bladeGeometry.dispose();", + " this.material.dispose();", + " this.group.removeFromParent();", + " }", + "}", + "", + "gdjs.__terrainGrassExtension = { Grass };" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [], + "objectGroups": [] + }, + { + "description": "Define scattered models helper classes JavaScript code.", + "fullName": "Define scattered models helper classes", + "functionType": "Action", + "name": "DefineScatterHelperClasses", + "private": true, + "sentence": "Define scattered models helper classes JavaScript code", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "if (gdjs.__terrainScatterExtension) {", + " return;", + "}", + "// Copies grow on the grounds of the scene, like the terrain grass (see", + "// `getGrounds` in the terrain helper).", + "", + "const {", + " getWorldScale,", + " getGrounds,", + " patchShaderCode,", + " createRandom,", + " findHighestGround,", + " GroundPlacement,", + " SampleRectangle,", + " brushes,", + " codec,", + " svgIcon,", + " toolIcons,", + "} = gdjs.__terrain3DExtension;", + "", + "// Size of the object inner area: instances are scaled from it.", + "const AREA_SIZE = 2000;", + "const AREA_DEPTH = 200;", + "// The painted density is stored in a grid of this many cells on each side,", + "// whatever the size of the object.", + "const MASK_RESOLUTION = 256;", + "const MASK_FORMAT_VERSION = '1';", + "// Copies are hidden in chunks: big chunks mean fewer draw calls, small ones", + "// hide more copies out of the screen.", + "const MIN_CHUNK_SIZE = 500;", + "const MAX_CHUNK_SIZE = 2000;", + "// Copies shrink to nothing over this part of the draw distance, instead of popping out.", + "const FADE_PART = 0.15;", + "const MAX_COPIES = 200000;", + "// Prisms of the trunk collisions.", + "const TRUNK_SIDES = 6;", + "", + "const clamp01 = (value) => (value < 0 ? 0 : value > 1 ? 1 : value);", + "", + "/**", + " * @returns {number} A number for a cell of the copies grid, changing with the seed.", + " */", + "const hashCell = (seed, i, j) => {", + " let h = Math.imul(i, 374761393) + Math.imul(j, 668265263) + Math.imul(seed, 982451653);", + " h = Math.imul(h ^ (h >>> 13), 1274126177);", + " return (h ^ (h >>> 16)) >>> 0;", + "};", + "", + "/**", + " * How dense copies are, from 0 to 1, in a grid covering the object: painted", + " * with the editor brushes, the edits or events.", + " */", + "class DensityMask {", + " /** @param {number} fillValue From 0 to 255. */", + " constructor(fillValue) {", + " // `resolution` and `size` let terrain brushes paint it.", + " this.resolution = MASK_RESOLUTION;", + " this.size = MASK_RESOLUTION + 1;", + " this.values = new Uint8Array(this.size * this.size).fill(fillValue);", + " }", + "", + " /**", + " * @param {number} u From 0 to 1, along the object width.", + " * @param {number} v From 0 to 1, along the object height.", + " * @returns {number} The density, from 0 to 1, interpolated between samples.", + " */", + " sample(u, v) {", + " const x = clamp01(u) * this.resolution;", + " const y = clamp01(v) * this.resolution;", + " const i = Math.min(Math.floor(x), this.resolution - 1);", + " const j = Math.min(Math.floor(y), this.resolution - 1);", + " const fx = x - i;", + " const fy = y - j;", + " const value = (i, j) => this.values[j * this.size + i];", + " const top = value(i, j) + fx * (value(i + 1, j) - value(i, j));", + " const bottom = value(i, j + 1) + fx * (value(i + 1, j + 1) - value(i, j + 1));", + " return (top + fy * (bottom - top)) / 255;", + " }", + "", + " /**", + " * Bring the density towards a target around a segment: fully up to 70%", + " * of the radius, then fading out (forests have dense centers and soft edges).", + " * @param {{cellWidth: number, cellHeight: number, ax: number, ay: number, bx: number, by: number, radius: number}} stroke", + " * @param {number} density The target, from 0 to 1.", + " * @param {number} strength From 0 to 1.", + " * @param {SampleRectangle} changedSamples", + " */", + " paint(stroke, density, strength, changedSamples) {", + " const { values } = this;", + " const target = Math.round(clamp01(density) * 255);", + " brushes.forEachSample(", + " this,", + " stroke,", + " (index, falloff) => {", + " // The falloff of terrain brushes is (1 - distance² / radius²)².", + " const weight = clamp01(2 * Math.sqrt(falloff));", + " // Rounded towards the target, so that light strokes still paint.", + " const change = (target - values[index]) * clamp01(weight * strength);", + " values[index] += change > 0 ? Math.ceil(change) : Math.floor(change);", + " },", + " changedSamples", + " );", + " }", + "", + " /** @returns {DensityMask} */", + " clone() {", + " const mask = new DensityMask(0);", + " mask.values.set(this.values);", + " return mask;", + " }", + "}", + "", + "/**", + " * Edits written in the Edits property: a JSON list of brush strokes, like", + " * `[{\"tool\": \"add\", \"x\": 0.5, \"y\": 0.5, \"radius\": 0.2}]`. Positions and", + " * radius are fractions of the object size, like the terrain edits.", + " */", + "const applyEdits = (mask, text) => {", + " if (!text.trim()) return;", + " let editList;", + " try {", + " editList = JSON.parse(text);", + " } catch (error) {", + " console.warn('The edits of scattered models are not valid JSON: ' + error.message);", + " return;", + " }", + " if (!Array.isArray(editList)) {", + " console.warn('The edits of scattered models must be a JSON list (like [{\"tool\": \"add\", ...}]).');", + " return;", + " }", + " const numberOr = (value, defaultValue) => (typeof value === 'number' && isFinite(value) ? value : defaultValue);", + " const changedSamples = new SampleRectangle();", + " for (const edit of editList) {", + " if (!edit || typeof edit !== 'object') continue;", + " const x = numberOr(edit.x, 0.5);", + " const y = numberOr(edit.y, 0.5);", + " const stroke = {", + " cellWidth: 1 / mask.resolution,", + " cellHeight: 1 / mask.resolution,", + " ax: x,", + " ay: y,", + " bx: numberOr(edit.toX, x),", + " by: numberOr(edit.toY, y),", + " radius: numberOr(edit.radius, 0.1),", + " };", + " const strength = clamp01(numberOr(edit.strength, 1));", + " if (edit.tool === 'add') {", + " mask.paint(stroke, numberOr(edit.density, 1), strength, changedSamples);", + " } else if (edit.tool === 'erase') {", + " mask.paint(stroke, 0, strength, changedSamples);", + " } else {", + " console.warn('Unknown tool in the edits of scattered models: \"' + edit.tool + '\" (use add or erase).');", + " }", + " }", + "};", + "", + "/**", + " * The density painted in the editor, stored as its difference with the", + " * density from the properties (the coverage and the edits), like the terrain", + " * sculpt data.", + " */", + "const maskCodec = {", + " /**", + " * @param {DensityMask} mask", + " * @param {DensityMask} baseMask", + " */", + " encode(mask, baseMask) {", + " const changes = new Int32Array(mask.values.length);", + " let hasChanges = false;", + " for (let index = 0; index < changes.length; index++) {", + " changes[index] = mask.values[index] - baseMask.values[index];", + " if (changes[index] !== 0) hasChanges = true;", + " }", + " if (!hasChanges) return '';", + " return MASK_FORMAT_VERSION + ';' + MASK_RESOLUTION + ';' + codec.encodeGrids([changes], mask.size);", + " },", + "", + " /**", + " * @param {string} text", + " * @param {DensityMask} baseMask", + " * @returns {DensityMask | null} null if the text is empty or not valid.", + " */", + " decode(text, baseMask) {", + " if (!text) return null;", + " const parts = text.split(';');", + " if (parts[0] !== MASK_FORMAT_VERSION || parseInt(parts[1], 10) !== MASK_RESOLUTION) return null;", + " try {", + " const mask = baseMask.clone();", + " const [changes] = codec.decodeGrids(parts[2], mask.size, 1);", + " for (let index = 0; index < changes.length; index++) {", + " // The base may have changed since the data was saved.", + " mask.values[index] = Math.min(Math.max(mask.values[index] + changes[index], 0), 255);", + " }", + " return mask;", + " } catch (error) {", + " console.error('The painted data of scattered models could not be read:', error);", + " return null;", + " }", + " },", + "};", + "", + "const fadeShaderDeclarations = `", + "uniform float scatterDrawDistance;", + "`;", + "// Instance matrices are applied after this: copies shrink towards their base.", + "const fadeShaderCode = `", + "#ifdef USE_INSTANCING", + "vec3 scatterCopyPosition = (modelMatrix * instanceMatrix * vec4(0.0, 0.0, 0.0, 1.0)).xyz;", + "transformed *= clamp(", + " (scatterDrawDistance - distance(scatterCopyPosition, cameraPosition)) / (scatterDrawDistance * ${FADE_PART}),", + " 0.0,", + " 1.0", + ");", + "#endif", + "`;", + "", + "const flipTriangles = (geometry) => {", + " const index = geometry.index;", + " for (let offset = 0; offset + 2 < index.count; offset += 3) {", + " const second = index.getX(offset + 1);", + " index.setX(offset + 1, index.getX(offset + 2));", + " index.setX(offset + 2, second);", + " }", + "};", + "", + "/**", + " * The parts of a model, ready to be drawn with instancing: each glTF", + " * primitive with its material. Geometries are moved so that the model base", + " * is at the origin, its height is 1 and it stands along Z (like 3D model", + " * objects with a 90° rotation around X).", + " * @param {THREE.Object3D} modelScene", + " * @param {object} uniforms Uniforms added to the materials.", + " * @returns {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}}", + " */", + "const createModelParts = (modelScene, uniforms) => {", + " modelScene.updateMatrixWorld(true);", + " const rootInverse = modelScene.matrixWorld.clone().invert();", + " const standUp = new THREE.Matrix4().makeRotationX(Math.PI / 2);", + " /** @type {Array<{mesh: THREE.Mesh, matrix: THREE.Matrix4}>} */", + " const meshes = [];", + " const box = new THREE.Box3();", + " modelScene.traverse((node) => {", + " if (!node.isMesh || !node.geometry || !node.geometry.attributes.position) return;", + " const matrix = standUp.clone().multiply(rootInverse).multiply(node.matrixWorld);", + " if (!node.geometry.boundingBox) node.geometry.computeBoundingBox();", + " box.union(node.geometry.boundingBox.clone().applyMatrix4(matrix));", + " meshes.push({ mesh: node, matrix });", + " });", + " if (box.isEmpty()) return { parts: [], width: 0, depth: 0 };", + "", + " const height = Math.max(box.max.z - box.min.z, 0.000001);", + " // The Y axis of the scene goes down: the model is flipped, like 3D model objects.", + " const normalization = new THREE.Matrix4()", + " .makeScale(1 / height, -1 / height, 1 / height)", + " .multiply(", + " new THREE.Matrix4().makeTranslation(", + " -(box.min.x + box.max.x) / 2,", + " -(box.min.y + box.max.y) / 2,", + " -box.min.z", + " )", + " );", + " const parts = meshes.map(({ mesh, matrix }) => {", + " let geometry = mesh.geometry.clone();", + " if (!geometry.index) {", + " const indices = Array.from({ length: geometry.attributes.position.count }, (_, index) => index);", + " geometry.setIndex(indices);", + " }", + " geometry.applyMatrix4(normalization.clone().multiply(matrix));", + " // The flip turned the faces inside out.", + " flipTriangles(geometry);", + " geometry.computeBoundingSphere();", + " geometry.computeBoundingBox();", + " const patchMaterial = (originalMaterial) => {", + " const material = originalMaterial.clone();", + " // Like 3D model objects (\"Standard without metalness\"), which look better without an environment map.", + " if (material.metalness) material.metalness = 0;", + " material.onBeforeCompile = (shader) => {", + " Object.assign(shader.uniforms, uniforms);", + " shader.vertexShader = patchShaderCode(shader.vertexShader, [", + " ['#include ', '#include \\n' + fadeShaderDeclarations],", + " ['#include ', '#include \\n' + fadeShaderCode],", + " ], 'ScatteredModels');", + " };", + " material.customProgramCacheKey = () => 'ScatteredModels';", + " return material;", + " };", + " const material = Array.isArray(mesh.material) ? mesh.material.map(patchMaterial) : patchMaterial(mesh.material);", + " return { geometry, material };", + " });", + " return {", + " parts,", + " width: (box.max.x - box.min.x) / height,", + " depth: (box.max.y - box.min.y) / height,", + " };", + "};", + "", + "/**", + " * A copy of the model: its base position in the scene, its scale (the", + " * height of the copy) and the key of its cell in the grid of copies.", + " * @typedef {{x: number, y: number, z: number, angle: number, height: number, key: number}} Copy", + " */", + "", + "/**", + " * The scattered models of an object: copies of a 3D model placed on the", + " * grounds of the scene, drawn with GPU instancing in chunks that are hidden", + " * when far or off-screen.", + " */", + "class Scatter {", + " /** @param {gdjs.CustomRuntimeObject3D} object */", + " constructor(object) {", + " this.object = object;", + " this.group = new THREE.Group();", + " object.get3DRendererObject().add(this.group);", + " /** @type {Array<{firstCellI: number, firstCellJ: number, meshes: THREE.InstancedMesh[], copies: Copy[], center: THREE.Vector3, radius: number}>} */", + " this.chunks = [];", + " /**", + " * The grid of copies: one copy at most in each cell, cells grouped in chunks.", + " * @type {{spacing: number, cellsPerChunk: number, cellsX: number, cellsY: number} | null}", + " */", + " this.grid = null;", + " this.placement = new GroundPlacement(object.getRuntimeScene());", + " this.uniforms = {", + " // In 3D world units.", + " scatterDrawDistance: { value: 1 },", + " };", + " /** @type {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}} */", + " this.model = { parts: [], width: 0, depth: 0 };", + " this.modelResourceName = '';", + " this.loadingModelResourceName = '';", + " /**", + " * Copies removed by events (for example, cut trees), by key.", + " * @type {Map}", + " */", + " this.removedCopies = new Map();", + " /** Increased each time copies change, for the collisions. */", + " this.version = 0;", + " this.isDisposed = false;", + " /** The last value of the painted data property read or saved. */", + " this.paintData = '';", + " /**", + " * Values saved by the editor tools: the editor sends them back with a", + " * hot-reload, possibly after newer ones were saved.", + " * @type {string[]}", + " */", + " this.savedPaintData = [];", + " this.maskSource = '';", + " /** @type {DensityMask} */", + " this.baseMask = new DensityMask(255);", + " /** @type {DensityMask} */", + " this.mask = this.baseMask;", + " this.changedSamples = new SampleRectangle();", + " this._stroke = { cellWidth: 1, cellHeight: 1, ax: 0, ay: 0, bx: 0, by: 0, radius: 0 };", + " this._groundAtCopy = { ground: null, z: 0 };", + " this._matrix = new THREE.Matrix4();", + " this._raycastSphere = new THREE.Sphere();", + " this._raycastRay = new THREE.Ray();", + " this._raycastBox = new THREE.Box3();", + " this._raycastInverseMatrix = new THREE.Matrix4();", + " this._raycastScale = new THREE.Vector3();", + " /** Placed on a copy to test the triangles of the model. */", + " this._raycastMesh = new THREE.Mesh();", + " /** @type {THREE.Intersection[]} */", + " this._raycastHits = [];", + " this._color = new THREE.Color();", + " this._tint = new THREE.Color();", + " this._cameraPosition = new THREE.Vector3();", + " this._chunkPosition = new THREE.Vector3();", + " /**", + " * Trunks, for the 3D physics behavior and navigation meshes. Always given,", + " * empty without trunk collisions: otherwise these behaviors would use the", + " * object box, covering the whole area.", + " * @type {gdjs.Surface}", + " */", + " this.surface = {", + " getVersion: () => this.version,", + " getChangedArea: () => null,", + " getTriangles: () => this._getTrunkTriangles(),", + " getHeightField: () => null,", + " };", + " /** The trunk collision properties, changing the surface. */", + " this.trunkKey = '';", + " this.loadMaskFromProperties();", + " this.updateFromProperties();", + " // Before the behaviors create their shapes.", + " this._updateSurface();", + " }", + "", + " _getMaskSource() {", + " return this.object._getCoverage() + '|' + this.object._getEdits();", + " }", + "", + " loadMaskFromProperties() {", + " const { object } = this;", + " this.maskSource = this._getMaskSource();", + " this.paintData = object._getPaintData();", + " this.baseMask = new DensityMask(object._getCoverage() === 'Painted' ? 0 : 255);", + " applyEdits(this.baseMask, object._getEdits());", + " this.mask = maskCodec.decode(this.paintData, this.baseMask) || this.baseMask.clone();", + " this.placement.placeAgain();", + " }", + "", + " onHotReloading() {", + " const paintData = this.object._getPaintData();", + " const savedIndex = this.savedPaintData.indexOf(paintData);", + " if (savedIndex !== -1 && this._getMaskSource() === this.maskSource) {", + " // The object already has this data, or newer changes.", + " this.savedPaintData.splice(0, savedIndex + 1);", + " } else if (paintData !== this.paintData || this._getMaskSource() !== this.maskSource) {", + " this.savedPaintData.length = 0;", + " this.loadMaskFromProperties();", + " }", + " this.updateFromProperties();", + " }", + "", + " /** @returns {string} The data to save in the painted data property. */", + " save() {", + " this.paintData = maskCodec.encode(this.mask, this.baseMask);", + " this.savedPaintData.push(this.paintData);", + " return this.paintData;", + " }", + "", + " updateFromProperties() {", + " const { object } = this;", + " const modelResourceName = object._getModel();", + " if (modelResourceName !== this.modelResourceName) {", + " this.modelResourceName = modelResourceName;", + " this._loadModel();", + " }", + " this.placement.placeAgain();", + " }", + "", + " _disposeModel() {", + " for (const part of this.model.parts) {", + " part.geometry.dispose();", + " for (const material of Array.isArray(part.material) ? part.material : [part.material]) {", + " material.dispose();", + " }", + " }", + " this.model = { parts: [], width: 0, depth: 0 };", + " }", + "", + " _loadModel() {", + " this._disposeChunks(this.chunks);", + " this._disposeModel();", + " const resourceName = this.modelResourceName;", + " if (!resourceName) return;", + " const game = this.object.getRuntimeScene().getGame();", + " const modelManager = game.getModel3DManager();", + " const gltf = modelManager.getModel(resourceName);", + " if (gltf === modelManager._invalidModel) {", + " // Not loaded yet, for example a model just chosen in the editor.", + " if (this.loadingModelResourceName === resourceName) return;", + " this.loadingModelResourceName = resourceName;", + " game.getResourceLoader()", + " .loadResource(resourceName)", + " .then(() => {", + " if (this.isDisposed || this.modelResourceName !== resourceName) return;", + " this.loadingModelResourceName = '';", + " if (modelManager.getModel(resourceName) !== modelManager._invalidModel) {", + " this._loadModel();", + " this.placement.placeAgain();", + " }", + " })", + " .catch(() => {});", + " return;", + " }", + " this.model = createModelParts(gltf.scene, this.uniforms);", + " }", + "", + " _getPlacementKey() {", + " const { object } = this;", + " return [", + " object.getX(), object.getY(), object.getZ(), object.getWidth(), object.getHeight(), object.getDepth(),", + " object._getDensity(), object._getSize(), object._getMinScale(), object._getMaxScale(),", + " object._getRandomRotation(), object._getRandomTint(), object._getGroundLayer(), object._getMaxSlope(),", + " object._getMinGroundZ(), object._getMaxGroundZ(), object._getSeed(), object._getDrawDistance(),", + " object._getCastShadow(), object._getReceiveShadow(), this.model.parts.length,", + " ].join('|');", + " }", + "", + " _getChunkSize() {", + " return Math.min(Math.max(this.object._getDrawDistance() / 4, MIN_CHUNK_SIZE), MAX_CHUNK_SIZE);", + " }", + "", + " /** @returns {number} The distance between copies (one copy per cell of this size at most). */", + " _getSpacing() {", + " const { object } = this;", + " const density = Math.max(object._getDensity(), 0);", + " if (density <= 0) return 0;", + " const spacing = 100 / Math.sqrt(density);", + " // Too many cells would freeze the game: the density is lowered.", + " const cellCount = (object.getWidth() / spacing) * (object.getHeight() / spacing);", + " return cellCount > MAX_COPIES ? spacing * Math.sqrt(cellCount / MAX_COPIES) : spacing;", + " }", + "", + " /**", + " * Find the copy of a cell of the grid, if there is one.", + " * @returns {Copy | null}", + " */", + " _placeCopy(cellI, cellJ, spacing, grounds) {", + " const { object } = this;", + " const width = object.getWidth();", + " const height = object.getHeight();", + " const nextRandom = createRandom(hashCell(Math.floor(object._getSeed()), cellI, cellJ));", + " // Always drawn in the same order, so that a copy stays the same when properties change.", + " const x = (cellI + nextRandom()) * spacing;", + " const y = (cellJ + nextRandom()) * spacing;", + " const presence = nextRandom();", + " const growth = nextRandom();", + " const scaleFactor = nextRandom();", + " const angle = nextRandom() * Math.PI * 2;", + " if (x > width || y > height) return null;", + " if (presence >= this.mask.sample(x / width, y / height)) return null;", + " const key = cellJ * 65536 + cellI;", + " if (this.removedCopies.has(key)) return null;", + "", + " const sceneX = object.getX() + x;", + " const sceneY = object.getY() + y;", + " const { ground, z } = findHighestGround(grounds, sceneX, sceneY, this._groundAtCopy);", + " const groundZ = ground ? z : object.getZ();", + " if (groundZ < object._getMinGroundZ() || groundZ > object._getMaxGroundZ()) return null;", + " const copyHeight =", + " Math.max(object._getSize(), 0) *", + " (object._getMinScale() + (object._getMaxScale() - object._getMinScale()) * scaleFactor);", + " if (ground) {", + " if (ground.getSlopeAt(sceneX, sceneY) > object._getMaxSlope()) return null;", + " const groundLayer = parseInt(object._getGroundLayer(), 10) || 0;", + " if (groundLayer > 0) {", + " if (growth > ground.getLayerWeightAt(sceneX, sceneY, groundLayer)) return null;", + " // Copies don't stand at the border of their layer, so that trees don't cover roads.", + " const margin = (copyHeight * Math.max(this.model.width, this.model.depth)) / 4;", + " for (const [offsetX, offsetY] of [[margin, 0], [-margin, 0], [0, margin], [0, -margin]]) {", + " if (ground.getLayerWeightAt(sceneX + offsetX, sceneY + offsetY, groundLayer) < 0.5) return null;", + " }", + " }", + " }", + " return {", + " x: sceneX,", + " y: sceneY,", + " z: groundZ,", + " angle: object._getRandomRotation() ? angle : 0,", + " height: copyHeight,", + " key,", + " };", + " }", + "", + " _disposeChunks(chunks) {", + " for (const chunk of chunks) {", + " for (const mesh of chunk.meshes) {", + " mesh.removeFromParent();", + " // Geometries and materials are the ones of the model.", + " mesh.dispose();", + " }", + " }", + " if (chunks === this.chunks) this.chunks = [];", + " }", + "", + " /** Place the copies again, for example after the ground or the object changed. */", + " rebuild() {", + " this._disposeChunks(this.chunks);", + " this.version++;", + " const { object } = this;", + " const spacing = this._getSpacing();", + " if (spacing <= 0 || this.model.parts.length === 0) {", + " this.grid = null;", + " return;", + " }", + " const cellsPerChunk = Math.max(Math.round(this._getChunkSize() / spacing), 1);", + " const grid = {", + " spacing,", + " cellsPerChunk,", + " cellsX: Math.ceil(object.getWidth() / spacing),", + " cellsY: Math.ceil(object.getHeight() / spacing),", + " };", + " this.grid = grid;", + " for (let firstCellJ = 0; firstCellJ < grid.cellsY; firstCellJ += cellsPerChunk) {", + " for (let firstCellI = 0; firstCellI < grid.cellsX; firstCellI += cellsPerChunk) {", + " this._createChunk(firstCellI, firstCellJ);", + " }", + " }", + " }", + "", + " /**", + " * Place again the copies of the chunks touching a rectangle of the scene,", + " * for example after painting.", + " */", + " _rebuildArea(minX, minY, maxX, maxY) {", + " const { grid, object } = this;", + " if (!grid) return;", + " const chunkSize = grid.spacing * grid.cellsPerChunk;", + " const firstChunkI = Math.max(Math.floor((minX - object.getX()) / chunkSize), 0);", + " const firstChunkJ = Math.max(Math.floor((minY - object.getY()) / chunkSize), 0);", + " const lastChunkI = Math.min(Math.floor((maxX - object.getX()) / chunkSize), Math.ceil(grid.cellsX / grid.cellsPerChunk) - 1);", + " const lastChunkJ = Math.min(Math.floor((maxY - object.getY()) / chunkSize), Math.ceil(grid.cellsY / grid.cellsPerChunk) - 1);", + " if (firstChunkI > lastChunkI || firstChunkJ > lastChunkJ) return;", + " const isInArea = (chunk) => {", + " const chunkI = chunk.firstCellI / grid.cellsPerChunk;", + " const chunkJ = chunk.firstCellJ / grid.cellsPerChunk;", + " return chunkI >= firstChunkI && chunkI <= lastChunkI && chunkJ >= firstChunkJ && chunkJ <= lastChunkJ;", + " };", + " this._disposeChunks(this.chunks.filter(isInArea));", + " this.chunks = this.chunks.filter((chunk) => !isInArea(chunk));", + " for (let chunkJ = firstChunkJ; chunkJ <= lastChunkJ; chunkJ++) {", + " for (let chunkI = firstChunkI; chunkI <= lastChunkI; chunkI++) {", + " this._createChunk(chunkI * grid.cellsPerChunk, chunkJ * grid.cellsPerChunk);", + " }", + " }", + " this.version++;", + " }", + "", + " _createChunk(firstCellI, firstCellJ) {", + " const { grid } = this;", + " const grounds = Array.from(this.placement.grounds);", + " /** @type {Copy[]} */", + " const copies = [];", + " for (let cellJ = firstCellJ; cellJ < Math.min(firstCellJ + grid.cellsPerChunk, grid.cellsY); cellJ++) {", + " for (let cellI = firstCellI; cellI < Math.min(firstCellI + grid.cellsPerChunk, grid.cellsX); cellI++) {", + " const copy = this._placeCopy(cellI, cellJ, grid.spacing, grounds);", + " if (copy) copies.push(copy);", + " }", + " }", + " if (copies.length === 0) return;", + "", + " const { object } = this;", + " // Copies are placed in scene units, and the object group is scaled", + " // from its inner area: they are scaled back to keep their proportions.", + " const scaleX = Math.max(object.getWidth() / AREA_SIZE, 0.0001);", + " const scaleY = Math.max(object.getHeight() / AREA_SIZE, 0.0001);", + " const scaleZ = Math.max(object.getDepth() / AREA_DEPTH, 0.0001);", + " const matrices = new Float32Array(copies.length * 16);", + " const tintColor = gdjs.rgbOrHexStringToNumber(object._getRandomTint());", + " const hasTint = tintColor !== 0xffffff;", + " const colors = hasTint ? new Float32Array(copies.length * 3) : null;", + " this._tint.setHex(tintColor);", + " const min = new THREE.Vector3(Infinity, Infinity, Infinity);", + " const max = new THREE.Vector3(-Infinity, -Infinity, -Infinity);", + " copies.forEach((copy, index) => {", + " const cos = Math.cos(copy.angle) * copy.height;", + " const sin = Math.sin(copy.angle) * copy.height;", + " const localX = (copy.x - object.getX()) / scaleX;", + " const localY = (copy.y - object.getY()) / scaleY;", + " const localZ = (copy.z - object.getZ()) / scaleZ;", + " this._matrix.set(", + " cos / scaleX, -sin / scaleX, 0, localX,", + " sin / scaleY, cos / scaleY, 0, localY,", + " 0, 0, copy.height / scaleZ, localZ,", + " 0, 0, 0, 1", + " );", + " this._matrix.toArray(matrices, index * 16);", + " if (colors) {", + " // The tint of each copy is between white and the tint color.", + " this._color.setRGB(1, 1, 1).lerp(this._tint, createRandom(copy.key + 1)());", + " this._color.toArray(colors, index * 3);", + " }", + " const reach = (copy.height * Math.max(this.model.width, this.model.depth)) / 2;", + " min.min(this._chunkPosition.set(copy.x - reach, copy.y - reach, copy.z));", + " max.max(this._chunkPosition.set(copy.x + reach, copy.y + reach, copy.z + copy.height));", + " });", + " const instanceMatrix = new THREE.InstancedBufferAttribute(matrices, 16);", + " const instanceColor = colors ? new THREE.InstancedBufferAttribute(colors, 3) : null;", + " const castShadow = object._getCastShadow();", + " const receiveShadow = object._getReceiveShadow();", + " const meshes = this.model.parts.map((part) => {", + " const mesh = new THREE.InstancedMesh(part.geometry, part.material, 0);", + " // All the parts of the model share the positions of the copies.", + " mesh.instanceMatrix = instanceMatrix;", + " mesh.instanceColor = instanceColor;", + " mesh.count = copies.length;", + " mesh.computeBoundingSphere();", + " mesh.castShadow = castShadow;", + " mesh.receiveShadow = receiveShadow;", + " // The first mesh tests all the parts of the copies.", + " mesh.raycast = () => {};", + " this.group.add(mesh);", + " return mesh;", + " });", + " const center = new THREE.Vector3(", + " ((min.x + max.x) / 2 - object.getX()) / scaleX,", + " ((min.y + max.y) / 2 - object.getY()) / scaleY,", + " ((min.z + max.z) / 2 - object.getZ()) / scaleZ", + " );", + " const chunk = { firstCellI, firstCellJ, meshes, copies, center, radius: min.distanceTo(max) / 2 };", + " // Used by the scene editor to select the object when a copy is clicked.", + " meshes[0].raycast = (raycaster, intersects) => this._raycastChunk(chunk, raycaster, intersects);", + " this.chunks.push(chunk);", + " }", + "", + " /**", + " * Add the hits of a ray on the copies of a chunk, like they are drawn. There", + " * can be thousands of copies: only the ones whose box is crossed by the ray", + " * are tested with the triangles of the model.", + " * @param {{meshes: THREE.InstancedMesh[], copies: Copy[], center: THREE.Vector3, radius: number}} chunk", + " * @param {THREE.Raycaster} raycaster", + " * @param {THREE.Intersection[]} intersects", + " */", + " _raycastChunk(chunk, raycaster, intersects) {", + " const { object, group } = this;", + " this._raycastSphere.center.copy(chunk.center).applyMatrix4(group.matrixWorld);", + " this._raycastSphere.radius = chunk.radius / getWorldScale(object);", + " if (!raycaster.ray.intersectsSphere(this._raycastSphere)) return;", + "", + " // The ray in the scene coordinates, like the copies.", + " const scale = this._raycastScale.set(", + " Math.max(object.getWidth() / AREA_SIZE, 0.0001),", + " Math.max(object.getHeight() / AREA_SIZE, 0.0001),", + " Math.max(object.getDepth() / AREA_DEPTH, 0.0001)", + " );", + " const ray = this._raycastRay.copy(raycaster.ray).applyMatrix4(this._raycastInverseMatrix.copy(group.matrixWorld).invert());", + " ray.origin.multiply(scale).add(this._chunkPosition.set(object.getX(), object.getY(), object.getZ()));", + " ray.direction.multiply(scale);", + "", + " const reachPerHeight = Math.max(this.model.width, this.model.depth) / 2;", + " const instanceMatrices = chunk.meshes[0].instanceMatrix.array;", + " chunk.copies.forEach((copy, index) => {", + " const reach = copy.height * reachPerHeight;", + " this._raycastBox.min.set(copy.x - reach, copy.y - reach, copy.z);", + " this._raycastBox.max.set(copy.x + reach, copy.y + reach, copy.z + copy.height);", + " if (!ray.intersectsBox(this._raycastBox)) return;", + " this._raycastMesh.matrixWorld.fromArray(instanceMatrices, index * 16).premultiply(group.matrixWorld);", + " for (const mesh of chunk.meshes) {", + " this._raycastMesh.geometry = mesh.geometry;", + " this._raycastMesh.material = mesh.material;", + " this._raycastHits.length = 0;", + " this._raycastMesh.raycast(raycaster, this._raycastHits);", + " for (const hit of this._raycastHits) {", + " hit.object = mesh;", + " hit.instanceId = index;", + " intersects.push(hit);", + " }", + " }", + " });", + " }", + "", + " /** @param {THREE.Camera | null} camera */", + " _hideFarChunks(camera) {", + " if (!camera) return;", + " const worldScale = getWorldScale(this.object);", + " const drawDistance = Math.max(this.object._getDrawDistance(), 1) / worldScale;", + " this.uniforms.scatterDrawDistance.value = drawDistance;", + " this._cameraPosition.setFromMatrixPosition(camera.matrixWorld);", + " for (const chunk of this.chunks) {", + " const distance = this._chunkPosition", + " .copy(chunk.center)", + " .applyMatrix4(this.group.matrixWorld)", + " .distanceTo(this._cameraPosition);", + " const isVisible = distance - chunk.radius / worldScale < drawDistance;", + " for (const mesh of chunk.meshes) mesh.visible = isVisible;", + " }", + " }", + "", + " /**", + " * @param {number} elapsedSeconds", + " * @param {THREE.Camera | null} camera", + " */", + " update(elapsedSeconds, camera) {", + " if (this.placement.shouldPlaceAgain(elapsedSeconds, this._getPlacementKey())) {", + " this.rebuild();", + " }", + " if (!this.changedSamples.isEmpty()) {", + " const { object } = this;", + " const sampleWidth = object.getWidth() / MASK_RESOLUTION;", + " const sampleHeight = object.getHeight() / MASK_RESOLUTION;", + " const { minI, minJ, maxI, maxJ } = this.changedSamples;", + " // Copies between samples use the density of the samples around them.", + " this._rebuildArea(", + " object.getX() + (minI - 1) * sampleWidth,", + " object.getY() + (minJ - 1) * sampleHeight,", + " object.getX() + (maxI + 1) * sampleWidth,", + " object.getY() + (maxJ + 1) * sampleHeight", + " );", + " this.changedSamples.clear();", + " }", + " // The object transformation is applied just before rendering, it's needed now.", + " this.object.getRenderer().ensureUpToDate();", + " this.group.updateWorldMatrix(true, false);", + " this._hideFarChunks(camera);", + " this._updateSurface();", + " }", + "", + " _updateSurface() {", + " const { object } = this;", + " if (typeof object.setSurface !== 'function') return;", + " const trunkKey = object._getTrunkCollisions() + '|' + object._getTrunkDiameter();", + " if (trunkKey !== this.trunkKey) {", + " this.trunkKey = trunkKey;", + " this.version++;", + " }", + " if (object.getSurface() !== this.surface) object.setSurface(this.surface);", + " }", + "", + " /** @returns {Copy[]} */", + " getCopies() {", + " const copies = [];", + " for (const chunk of this.chunks) copies.push(...chunk.copies);", + " return copies;", + " }", + "", + " /**", + " * @returns {Copy | null} The copy whose base is the nearest to a position (on X and Y), if any.", + " */", + " findNearestCopy(x, y) {", + " let nearestCopy = null;", + " let nearestDistanceSquared = Infinity;", + " for (const chunk of this.chunks) {", + " for (const copy of chunk.copies) {", + " const distanceSquared = (copy.x - x) * (copy.x - x) + (copy.y - y) * (copy.y - y);", + " if (distanceSquared < nearestDistanceSquared) {", + " nearestDistanceSquared = distanceSquared;", + " nearestCopy = copy;", + " }", + " }", + " }", + " return nearestCopy;", + " }", + "", + " getCopyCount() {", + " let count = 0;", + " for (const chunk of this.chunks) count += chunk.copies.length;", + " return count;", + " }", + "", + " /** Remove the copies whose base is in a circle (they come back when the scene is restarted). */", + " removeCopies(x, y, radius) {", + " let hasRemovedCopies = false;", + " for (const chunk of this.chunks) {", + " for (const copy of chunk.copies) {", + " if ((copy.x - x) * (copy.x - x) + (copy.y - y) * (copy.y - y) <= radius * radius) {", + " this.removedCopies.set(copy.key, copy);", + " hasRemovedCopies = true;", + " }", + " }", + " }", + " if (hasRemovedCopies) this._rebuildArea(x - radius, y - radius, x + radius, y + radius);", + " }", + "", + " /** Paint the full density around a position, and bring back the copies removed there. */", + " addCopies(x, y, radius) {", + " for (const [key, copy] of this.removedCopies) {", + " if ((copy.x - x) * (copy.x - x) + (copy.y - y) * (copy.y - y) <= radius * radius) {", + " this.removedCopies.delete(key);", + " }", + " }", + " this.paint(x, y, x, y, radius, 1, 1);", + " }", + "", + " _getStroke(x1, y1, x2, y2, radius) {", + " const { object } = this;", + " const stroke = this._stroke;", + " stroke.cellWidth = object.getWidth() / MASK_RESOLUTION;", + " stroke.cellHeight = object.getHeight() / MASK_RESOLUTION;", + " stroke.ax = x1 - object.getX();", + " stroke.ay = y1 - object.getY();", + " stroke.bx = x2 - object.getX();", + " stroke.by = y2 - object.getY();", + " stroke.radius = Math.max(radius, 0);", + " return stroke;", + " }", + "", + " /**", + " * Paint the density around a segment (copies are placed again at the next frame).", + " * @param {number} density From 0 (erase) to 1 (as dense as the Density property).", + " * @param {number} strength From 0 to 1.", + " */", + " paint(x1, y1, x2, y2, radius, density, strength) {", + " const changedSamples = new SampleRectangle();", + " this.mask.paint(this._getStroke(x1, y1, x2, y2, radius), density, strength, changedSamples);", + " this.changedSamples.add(changedSamples);", + " }", + "", + " /** @param {DensityMask} mask */", + " setMask(mask) {", + " this.mask = mask;", + " this.rebuild();", + " }", + "", + " /**", + " * Prisms around the trunks, from 0 to 1 in the object box (see `gdjs.Surface`).", + " * @returns {gdjs.SurfaceTriangles}", + " */", + " _getTrunkTriangles() {", + " const { object } = this;", + " const copies = object._getTrunkCollisions() ? this.getCopies() : [];", + " const verticesPerCopy = TRUNK_SIDES * 2 + 1;", + " const positions = new Float32Array(copies.length * verticesPerCopy * 3);", + " const indices = new Uint32Array(copies.length * TRUNK_SIDES * 3 * 3);", + " const width = object.getWidth() || 1;", + " const height = object.getHeight() || 1;", + " const depth = object.getDepth() || 1;", + " let indexOffset = 0;", + " copies.forEach((copy, copyIndex) => {", + " const radius = (Math.max(object._getTrunkDiameter(), 0) / 2) * (copy.height / Math.max(object._getSize(), 1));", + " const top = copy.z + copy.height * 0.8;", + " const firstVertex = copyIndex * verticesPerCopy;", + " const setVertex = (vertex, x, y, z) => {", + " positions[vertex * 3] = (x - object.getX()) / width;", + " positions[vertex * 3 + 1] = (y - object.getY()) / height;", + " positions[vertex * 3 + 2] = (z - object.getZ()) / depth;", + " };", + " for (let side = 0; side < TRUNK_SIDES; side++) {", + " const angle = ((side + 0.5) / TRUNK_SIDES) * Math.PI * 2;", + " const x = copy.x + Math.cos(angle) * radius;", + " const y = copy.y + Math.sin(angle) * radius;", + " // Starting a bit under the ground, which can be sloped.", + " setVertex(firstVertex + side * 2, x, y, copy.z - radius);", + " setVertex(firstVertex + side * 2 + 1, x, y, top);", + " }", + " const center = firstVertex + TRUNK_SIDES * 2;", + " setVertex(center, copy.x, copy.y, top);", + " for (let side = 0; side < TRUNK_SIDES; side++) {", + " const bottom = firstVertex + side * 2;", + " const nextBottom = firstVertex + ((side + 1) % TRUNK_SIDES) * 2;", + " // Triangles face out of the prism (vertices at odd indices are at the top).", + " indices.set([bottom, nextBottom, bottom + 1, nextBottom, nextBottom + 1, bottom + 1], indexOffset);", + " indices.set([center, bottom + 1, nextBottom + 1], indexOffset + 6);", + " indexOffset += 9;", + " }", + " });", + " if (copies.length === 0) {", + " // Without triangles, the physics engine and navigation meshes would use", + " // the object box: a tiny triangle under the object is used instead.", + " return { positions: new Float32Array([0, 0, -1, 0.0001, 0, -1, 0, 0.0001, -1]), indices: new Uint32Array([0, 2, 1]) };", + " }", + " return { positions, indices };", + " }", + "", + " dispose() {", + " this.isDisposed = true;", + " if (typeof this.object.setSurface === 'function' && this.object.getSurface() === this.surface) {", + " this.object.setSurface(null);", + " }", + " this._disposeChunks(this.chunks);", + " this._disposeModel();", + " this.group.removeFromParent();", + " }", + "}", + "", + "const scatterToolIcons = {", + " Add: svgIcon(", + " ''", + " ),", + " Erase: svgIcon(", + " ''", + " ),", + "};", + "", + "/**", + " * The brushes shown in the scene editor when scattered models are selected,", + " * to add or erase copies. Without the needed editor functions (older GDevelop", + " * versions), nothing is shown: copies can still be painted with events.", + " */", + "class ScatterEditorTools {", + " constructor() {", + " /** '' when no brush is used. */", + " this.toolName = '';", + " this.sizePercent = 30;", + " this.strengthPercent = 50;", + " /** @type {Scatter | null} */", + " this.scatter = null;", + " /** @type {gdjs.InGameEditor | null} */", + " this.editor = null;", + " this.isStroking = false;", + " this.lastStrokeTime = 0;", + " /** @type {Array<{mask: DensityMask, baseMask: DensityMask}>} */", + " this.undoSteps = [];", + " this.raycaster = new THREE.Raycaster();", + " this.ndc = new THREE.Vector2();", + " this._groundAtCursor = { ground: null, z: 0 };", + " /** A circle following the ground under the cursor. */", + " this.brushRing = new THREE.LineLoop(", + " new THREE.BufferGeometry().setAttribute('position', new THREE.BufferAttribute(new Float32Array(64 * 3), 3)),", + " new THREE.LineBasicMaterial({ color: 0xffffff, depthTest: false, transparent: true })", + " );", + " this.brushRing.renderOrder = 1000;", + " this.brushRing.frustumCulled = false;", + " this.brushRing.raycast = () => {};", + " }", + "", + " /** @param {gdjs.InGameEditor} editor */", + " update(editor) {", + " if (typeof editor.showToolbar !== 'function') return;", + " this.editor = editor;", + " const selectedObjects = editor.getSelectedObjects();", + " const scatter =", + " selectedObjects.length === 1 && selectedObjects[0].__terrainScatter ? selectedObjects[0].__terrainScatter : null;", + " if (scatter !== this.scatter) {", + " this._endStroke();", + " this.scatter = scatter;", + " this.undoSteps.length = 0;", + " }", + " if (scatter) editor.showToolbar('ScatteredModels', this._getToolbarItems());", + " const game = editor.getRuntimeGame();", + " if (!scatter || !this.toolName) {", + " this.brushRing.removeFromParent();", + " return;", + " }", + " const inputManager = game.getInputManager();", + " if (inputManager.wasKeyJustPressed(gdjs.evtTools.input.keysNameToCode.Escape)) {", + " this._endStroke();", + " this.toolName = '';", + " this.brushRing.removeFromParent();", + " return;", + " }", + " editor.captureLeftMouseButton();", + " const hit = this._getPositionUnderCursor(scatter, game);", + " const radius = this._getRadius();", + " this._showBrushRing(scatter, hit, radius);", + "", + " const isPressedForTool =", + " typeof editor.isCapturedLeftMouseButtonPressed === 'function'", + " ? editor.isCapturedLeftMouseButtonPressed()", + " : inputManager.isMouseButtonPressed(0);", + " // The button is released outside the canvas when the mouse is on a toolbar.", + " const isPressed = isPressedForTool && inputManager.isMouseInsideCanvas();", + " if (isPressed && hit && !this.isStroking) {", + " this._startStroke(scatter);", + " }", + " if (this.isStroking && isPressed && hit) {", + " this._applyBrush(scatter, hit, radius);", + " }", + " if (!isPressed) {", + " this._endStroke();", + " }", + " }", + "", + " _getRadius() {", + " const { object } = this.scatter;", + " const maxRadius = Math.max(object.getWidth(), object.getHeight()) / 4;", + " return Math.max(maxRadius * Math.pow(this.sizePercent / 100, 2), 10);", + " }", + "", + " /**", + " * @returns {{x: number, y: number} | null} The position of the scene under the cursor: on the", + " * highest ground hit, or on the plane at the object Z.", + " */", + " _getPositionUnderCursor(scatter, game) {", + " let hit = null;", + " for (const ground of getGrounds(scatter.object.getRuntimeScene())) {", + " if (typeof ground.raycastFromCursor !== 'function') continue;", + " const localHit = ground.raycastFromCursor(game, this.raycaster, this.ndc);", + " if (!localHit) continue;", + " const sceneHit = ground.toScenePosition(localHit);", + " if (!hit || sceneHit.z > hit.z) hit = sceneHit;", + " }", + " if (hit) return hit;", + "", + " const { object } = scatter;", + " const layer = object.getInstanceContainer().getLayer(object.getLayer());", + " const camera = layer.getRenderer().getThreeCamera();", + " if (!camera) return null;", + " const inputManager = game.getInputManager();", + " this.ndc.set(", + " (inputManager.getCursorX() / game.getGameResolutionWidth()) * 2 - 1,", + " -(inputManager.getCursorY() / game.getGameResolutionHeight()) * 2 + 1", + " );", + " this.raycaster.setFromCamera(this.ndc, camera);", + " scatter.group.updateWorldMatrix(true, false);", + " const ray = this.raycaster.ray.clone().applyMatrix4(scatter.group.matrixWorld.clone().invert());", + " const point = ray.intersectPlane(new THREE.Plane(new THREE.Vector3(0, 0, 1), 0), new THREE.Vector3());", + " if (!point) return null;", + " return {", + " x: object.getX() + (point.x / AREA_SIZE) * object.getWidth(),", + " y: object.getY() + (point.y / AREA_SIZE) * object.getHeight(),", + " };", + " }", + "", + " _showBrushRing(scatter, hit, radius) {", + " if (!hit) {", + " this.brushRing.removeFromParent();", + " return;", + " }", + " const { object } = scatter;", + " if (this.brushRing.parent !== scatter.group) scatter.group.add(this.brushRing);", + " const grounds = getGrounds(object.getRuntimeScene());", + " const positions = this.brushRing.geometry.attributes.position;", + " for (let index = 0; index < positions.count; index++) {", + " const angle = (index / positions.count) * Math.PI * 2;", + " const x = hit.x + Math.cos(angle) * radius;", + " const y = hit.y + Math.sin(angle) * radius;", + " const { ground, z } = findHighestGround(grounds, x, y, this._groundAtCursor);", + " positions.setXYZ(", + " index,", + " ((x - object.getX()) / object.getWidth()) * AREA_SIZE,", + " ((y - object.getY()) / object.getHeight()) * AREA_SIZE,", + " (((ground ? z : object.getZ()) + 2 - object.getZ()) / Math.max(object.getDepth(), 0.0001)) * AREA_DEPTH", + " );", + " }", + " positions.needsUpdate = true;", + " }", + "", + " _startStroke(scatter) {", + " this.isStroking = true;", + " this.lastStrokeTime = performance.now();", + " this.undoSteps.push({ mask: scatter.mask.clone(), baseMask: scatter.baseMask });", + " while (this.undoSteps.length > 30) this.undoSteps.shift();", + " }", + "", + " _endStroke() {", + " if (!this.isStroking) return;", + " this.isStroking = false;", + " this._save();", + " }", + "", + " _applyBrush(scatter, hit, radius) {", + " const now = performance.now();", + " const elapsedSeconds = Math.min((now - this.lastStrokeTime) / 1000, 0.1);", + " this.lastStrokeTime = now;", + " const strength = (this.strengthPercent / 100) * elapsedSeconds * 8;", + " scatter.paint(hit.x, hit.y, hit.x, hit.y, radius, this.toolName === 'Add' ? 1 : 0, strength);", + " }", + "", + " _save() {", + " const { scatter, editor } = this;", + " if (!scatter || !editor) return;", + " editor.updateObjectProperties(scatter.object.getName(), { PaintData: scatter.save() });", + " }", + "", + " _undo() {", + " const undoStep = this.undoSteps.pop();", + " // The coverage or the edits may have changed since the stroke.", + " if (!this.scatter || !undoStep || undoStep.baseMask !== this.scatter.baseMask) {", + " this.undoSteps.length = 0;", + " return;", + " }", + " this.scatter.setMask(undoStep.mask);", + " this._save();", + " }", + "", + " /** @returns {Array} */", + " _getToolbarItems() {", + " const toolButton = (toolName, tooltip) => ({", + " type: 'button',", + " id: toolName,", + " tooltip,", + " iconUrl: scatterToolIcons[toolName],", + " isActive: this.toolName === toolName,", + " onClick: () => (this.toolName = this.toolName === toolName ? '' : toolName),", + " });", + " return [", + " toolButton('Add', 'Add copies (drag on the ground)'),", + " toolButton('Erase', 'Erase copies'),", + " { type: 'divider', id: 'BrushDivider' },", + " {", + " type: 'slider',", + " id: 'Size',", + " tooltip: 'Brush size',", + " iconUrl: toolIcons.Size,", + " min: 1,", + " max: 100,", + " value: this.sizePercent,", + " onChange: (value) => (this.sizePercent = value),", + " },", + " {", + " type: 'slider',", + " id: 'Strength',", + " tooltip: 'Brush strength',", + " iconUrl: toolIcons.Strength,", + " min: 5,", + " max: 100,", + " value: this.strengthPercent,", + " onChange: (value) => (this.strengthPercent = value),", + " },", + " {", + " type: 'button',", + " id: 'Undo',", + " tooltip: 'Undo the last stroke on these scattered models',", + " iconUrl: toolIcons.Undo,", + " onClick: () => this._undo(),", + " },", + " ];", + " }", + "}", + "", + "const editorTools = new ScatterEditorTools();", + "if (gdjs.registerInGameEditorPostStepCallback) {", + " gdjs.registerInGameEditorPostStepCallback((editor) => editorTools.update(editor));", + "}", + "", + "gdjs.__terrainScatterExtension = {", + " Scatter,", + " // Exposed for tests.", + " DensityMask,", + " maskCodec,", + " applyEdits,", + "};", + "" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [], + "objectGroups": [] + } + ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "DefineHelperClasses" + }, + { + "functionName": "DefineGrassHelperClasses" + }, + { + "functionName": "DefineScatterHelperClasses" + } + ] + }, + "eventsBasedBehaviors": [], + "eventsBasedObjects": [ + { + "areaMaxX": 4096, + "areaMaxY": 4096, + "areaMaxZ": 600, + "areaMinX": 0, + "areaMinY": 0, + "areaMinZ": 0, + "assetStoreTag": "3d terrains", + "defaultName": "Terrain", + "description": "A ground with hills, mountains or valleys that can be sculpted and painted in the scene editor, or changed with events. Add the 3D physics behavior to it to make objects walk or roll on it.", + "fullName": "3D terrain", + "helpPath": "", + "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLXRlcnJhaW4iIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTQsNkwxMC4yNSwxMUwxMy4xLDE0LjhMMTEuNSwxNkM5LjgxLDEzLjc1IDcsMTAgNywxMEwxLDE4SDIzTDE0LDZaIiAvPjwvc3ZnPg==", + "is3D": true, + "isUsingLegacyInstancesRenderer": true, + "name": "Terrain3D", + "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", + "objects": [ + { + "assetStoreId": "", + "name": "AreaPlaceholder", + "type": "Scene3D::Cube3DObject", + "variables": [], + "effects": [], + "behaviors": [], + "content": { + "width": 4096, + "height": 4096, + "depth": 600, + "enableTextureTransparency": false, + "facesOrientation": "Y", + "backFaceUpThroughWhichAxisRotation": "X", + "materialType": "Basic", + "tint": "255;255;255", + "frontFaceResourceName": "", + "frontFaceVisible": false, + "frontFaceResourceRepeat": false, + "backFaceResourceName": "", + "backFaceVisible": false, + "backFaceResourceRepeat": false, + "leftFaceResourceName": "", + "leftFaceVisible": false, + "leftFaceResourceRepeat": false, + "rightFaceResourceName": "", + "rightFaceVisible": false, + "rightFaceResourceRepeat": false, + "topFaceResourceName": "", + "topFaceVisible": false, + "topFaceResourceRepeat": false, + "bottomFaceResourceName": "", + "bottomFaceVisible": false, + "bottomFaceResourceRepeat": false + } + } + ], + "objectsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "objectName": "AreaPlaceholder" + } + ] + }, + "objectsGroups": [], + "layers": [ + { + "ambientLightColorB": 200, + "ambientLightColorG": 200, + "ambientLightColorR": 200, + "camera2DPlaneMaxDrawingDistance": 5000, + "camera3DFarPlaneDistance": 10000, + "camera3DFieldOfView": 45, + "camera3DNearPlaneDistance": 3, + "cameraType": "", + "followBaseLayerCamera": false, + "isLightingLayer": false, + "isLocked": false, + "name": "", + "renderingType": "", + "visibility": true, + "cameras": [ + { + "defaultSize": true, + "defaultViewport": true, + "height": 0, + "viewportBottom": 1, + "viewportLeft": 0, + "viewportRight": 1, + "viewportTop": 0, + "width": 0 + } + ], + "effects": [] + } + ], + "instances": [ + { + "angle": 0, + "customSize": true, + "depth": 600, + "height": 4096, + "keepRatio": true, + "layer": "", + "name": "AreaPlaceholder", + "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-004096004096", + "width": 4096, + "x": 0, + "y": 0, + "zOrder": 1, + "numberProperties": [], + "stringProperties": [], + "initialVariables": [] + } + ], + "editionSettings": {}, + "eventsFunctions": [ + { + "fullName": "", + "functionType": "Action", + "name": "onCreated", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Terrain3D::DefineHelperClasses" + }, + "parameters": [ + "", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "for (const child of object.getChildrenContainer().getAdhocListOfAllInstances()) {", + " child.deleteFromScene();", + "}", + "if (object.__terrain3D) object.__terrain3D.dispose();", + "object.__terrain3D = new gdjs.__terrain3DExtension.Terrain(object);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "onDestroy", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrain3D.dispose();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "doStepPostEvents", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrain3D.update();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "onHotReloading", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrain3D.onHotReloading();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "description": "Raise the ground around a position (or lower it with a negative height). It's raised the most at the center and less and less up to the radius.", + "fullName": "Raise or lower the ground", + "functionType": "Action", + "group": "Sculpt", + "name": "RaiseGround", + "sentence": "Raise the ground of _PARAM0_ by _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const height = eventsFunctionContext.getArgument(\"Height\");", + "object.__terrain3D.raise(x, y, x, y, radius, height);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Height to add (negative to lower the ground)", + "name": "Height", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Raise the ground along a line, for example for a ridge, or lower it with a negative height to dig a river bed or a valley.", + "fullName": "Raise or lower the ground along a line", + "functionType": "Action", + "group": "Sculpt", + "name": "RaiseGroundAlongLine", + "sentence": "Raise the ground of _PARAM0_ by _PARAM6_ from _PARAM1_; _PARAM2_ to _PARAM3_; _PARAM4_ with a radius of _PARAM5_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x1 = eventsFunctionContext.getArgument(\"X1\");", + "const y1 = eventsFunctionContext.getArgument(\"Y1\");", + "const x2 = eventsFunctionContext.getArgument(\"X2\");", + "const y2 = eventsFunctionContext.getArgument(\"Y2\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const height = eventsFunctionContext.getArgument(\"Height\");", + "object.__terrain3D.raise(x1, y1, x2, y2, radius, height);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the start", + "name": "X1", + "type": "expression" + }, + { + "description": "Y position of the start", + "name": "Y1", + "type": "expression" + }, + { + "description": "X position of the end", + "name": "X2", + "type": "expression" + }, + { + "description": "Y position of the end", + "name": "Y2", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance from the line.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Height to add (negative to lower the ground)", + "name": "Height", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Flatten the ground around a position to a given height, for example to place a building.", + "fullName": "Flatten the ground", + "functionType": "Action", + "group": "Sculpt", + "name": "FlattenGround", + "sentence": "Flatten the ground of _PARAM0_ to a Z of _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const z = eventsFunctionContext.getArgument(\"Z\");", + "object.__terrain3D.flatten(x, y, x, y, radius, z, 1);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Z position of the flat ground", + "name": "Z", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Smooth the ground around a position, softening bumps and cliffs.", + "fullName": "Smooth the ground", + "functionType": "Action", + "group": "Sculpt", + "name": "SmoothGround", + "sentence": "Smooth the ground of _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_ (strength: _PARAM4_)", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const strength = eventsFunctionContext.getArgument(\"Strength\");", + "object.__terrain3D.smooth(x, y, x, y, radius, strength);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Strength (between 0 and 1)", + "name": "Strength", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Paint a layer (like grass, dirt, rock or sand) on the ground around a position.", + "fullName": "Paint a layer", + "functionType": "Action", + "group": "Paint", + "name": "PaintLayer", + "sentence": "Paint layer _PARAM4_ on _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const layer = eventsFunctionContext.getArgument(\"Layer\");", + "object.__terrain3D.paint(x, y, x, y, radius, layer, 1);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Layer (1 to 4)", + "name": "Layer", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Paint a layer on the ground along a line, for example for a path or a road.", + "fullName": "Paint a layer along a line", + "functionType": "Action", + "group": "Paint", + "name": "PaintLayerAlongLine", + "sentence": "Paint layer _PARAM6_ on _PARAM0_ from _PARAM1_; _PARAM2_ to _PARAM3_; _PARAM4_ with a radius of _PARAM5_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x1 = eventsFunctionContext.getArgument(\"X1\");", + "const y1 = eventsFunctionContext.getArgument(\"Y1\");", + "const x2 = eventsFunctionContext.getArgument(\"X2\");", + "const y2 = eventsFunctionContext.getArgument(\"Y2\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const layer = eventsFunctionContext.getArgument(\"Layer\");", + "object.__terrain3D.paint(x1, y1, x2, y2, radius, layer, 1);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the start", + "name": "X1", + "type": "expression" + }, + { + "description": "Y position of the start", + "name": "Y1", + "type": "expression" + }, + { + "description": "X position of the end", + "name": "X2", + "type": "expression" + }, + { + "description": "Y position of the end", + "name": "Y2", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance from the line.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Layer (1 to 4)", + "name": "Layer", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Tint the ground around a position with a color multiplying its layers, like dry grass or darker mud. White removes the tint.", + "fullName": "Tint the ground", + "functionType": "Action", + "group": "Paint", + "name": "TintGround", + "sentence": "Tint the ground of _PARAM0_ with _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const color = gdjs.rgbOrHexToRGBColor(eventsFunctionContext.getArgument(\"Color\"));", + "object.__terrain3D.tint(x, y, x, y, radius, color, 1);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Color", + "name": "Color", + "type": "color" + } + ], + "objectGroups": [] + }, + { + "description": "Make the ground around a position wet (darker and shiny, like after rain or near water) or dry.", + "fullName": "Change the wetness of the ground", + "functionType": "Action", + "group": "Paint", + "name": "WetGround", + "sentence": "Change the wetness of the ground of _PARAM0_ to _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const wetness = eventsFunctionContext.getArgument(\"Wetness\");", + "object.__terrain3D.wet(x, y, x, y, radius, wetness, 1);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Wetness (from 0 for dry to 1)", + "name": "Wetness", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "the wetness of the ground at a position, from 0 (dry) to 1, painted or near the water (wet shore).", + "fullName": "Ground wetness", + "functionType": "ExpressionAndCondition", + "group": "Paint", + "name": "GroundWetness", + "sentence": "the wetness of the ground at _PARAM1_; _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "eventsFunctionContext.returnValue = object.__terrain3D.getWetnessAt(x, y);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "The tint painted on the ground at a position, like \"255;255;255\" (white when there is none).", + "fullName": "Ground tint", + "functionType": "StringExpression", + "group": "Paint", + "name": "GroundTint", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "eventsFunctionContext.returnValue = object.__terrain3D.getTintAt(x, y).join(\";\");" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "string" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Remove the ground in a circle, for the entrance of a tunnel, a cave or a well: there is nothing drawn and no collision there. Fill it again with the \"Fill holes\" action. A height field can't go under itself: put a 3D model (with a collision shape) for the tunnel or the cave under the ground. The physics engine removes the triangles touching a hole, so the hole in the collisions can be up to a cell larger than the one drawn: make the model go a bit further than the hole, or put rocks around the entrance.", + "fullName": "Make a hole", + "functionType": "Action", + "group": "Holes", + "name": "MakeHole", + "sentence": "Make a hole in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "object.__terrain3D.makeHole(x, y, x, y, radius);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "name": "Radius", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Fill the holes in a circle with the layers of the ground around them: the ground is drawn and has collisions again.", + "fullName": "Fill holes", + "functionType": "Action", + "group": "Holes", + "name": "FillHoles", + "sentence": "Fill the holes in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "object.__terrain3D.fillHoles(x, y, x, y, radius);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "name": "Radius", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Make the entrance of a tunnel or a cave on a slope: a hole, with the ground in front of it flattened to the floor of the tunnel.", + "fullName": "Carve an entrance", + "functionType": "Action", + "group": "Holes", + "name": "CarveEntrance", + "sentence": "Carve an entrance in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_, going in at _PARAM4_° with its floor at Z _PARAM5_ and _PARAM6_ pixels of flat ground in front", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const angle = eventsFunctionContext.getArgument(\"Angle\");", + "const floorZ = eventsFunctionContext.getArgument(\"FloorZ\");", + "const length = eventsFunctionContext.getArgument(\"Length\");", + "object.__terrain3D.carveEntrance(x, y, radius, angle, floorZ, length);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "name": "Radius", + "type": "expression" + }, + { + "description": "Direction going into the ground (in degrees)", + "name": "Angle", + "type": "expression" + }, + { + "description": "Z position of the floor", + "name": "FloorZ", + "type": "expression" + }, + { + "description": "Length of the flat ground in front of the entrance (in pixels)", + "name": "Length", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Check if there is a hole in the ground at a position.", + "fullName": "Hole", + "functionType": "Condition", + "group": "Holes", + "name": "IsHole", + "sentence": "There is a hole in _PARAM0_ at _PARAM1_; _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "eventsFunctionContext.returnValue = object.__terrain3D.isHoleAt(x, y);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "The Z position of the ground at a position, or the given value where there is a hole.", + "fullName": "Ground Z position, with holes", + "functionType": "Expression", + "group": "Holes", + "name": "GroundZWithHoles", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const terrain = object.__terrain3D;", + "eventsFunctionContext.returnValue = terrain.isHoleAt(x, y) ? eventsFunctionContext.getArgument(\"ZInHoles\") : terrain.getHeightAt(x, y);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + }, + { + "description": "Value in holes", + "longDescription": "For example, a Z position far under the ground.", + "name": "ZInHoles", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Replace the ground with a generated relief. Painted layers are reset.", + "fullName": "Generate a relief", + "functionType": "Action", + "group": "Generation", + "name": "GenerateRelief", + "sentence": "Generate a relief of _PARAM1_ on _PARAM0_ with seed _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const relief = eventsFunctionContext.getArgument(\"Relief\");", + "const seed = eventsFunctionContext.getArgument(\"Seed\");", + "object.__terrain3D.generate(relief, seed);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "Relief", + "name": "Relief", + "supplementaryInformation": "[\"Flat\", \"Hills\", \"Mountains\", \"Island\"]", + "type": "stringWithSelector" + }, + { + "description": "Seed", + "longDescription": "Each seed gives a different terrain with the same relief.", + "name": "Seed", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Replace the ground with the heights of an image. Painted layers are kept.", + "fullName": "Load a heightmap", + "functionType": "Action", + "group": "Generation", + "name": "LoadHeightmap", + "sentence": "Load the heights of _PARAM0_ from _PARAM1_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const image = eventsFunctionContext.getArgument(\"Image\");", + "object.__terrain3D.loadHeightmap(image);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "Heightmap image", + "longDescription": "Black is the lowest, white the highest. For more precision, store the height on 16 bits in the red (most significant byte) and green channels.", + "name": "Image", + "type": "imageResource" + } + ], + "objectGroups": [] + }, + { + "description": "Move objects up or down so that their bottom is on the ground, under their center. Useful to place trees, rocks or characters on a terrain.", + "fullName": "Put objects on the ground", + "functionType": "Action", + "group": "Position", + "name": "PutOnGround", + "sentence": "Put _PARAM1_ on the ground of _PARAM0_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrain3D.placeOnGround(eventsFunctionContext.getObjects(\"Objects\"));" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "Objects to put on the ground", + "name": "Objects", + "type": "objectList" + } + ], + "objectGroups": [] + }, + { + "description": "the Z position of the ground at a position of the scene.", + "fullName": "Ground Z position", + "functionType": "ExpressionAndCondition", + "group": "Position", + "name": "GroundZ", + "sentence": "the ground Z position at _PARAM1_; _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "eventsFunctionContext.returnValue = object.__terrain3D.getHeightAt(x, y);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "the slope of the ground (angle with the horizontal, in degrees) at a position of the scene.", + "fullName": "Ground slope", + "functionType": "ExpressionAndCondition", + "group": "Position", + "name": "GroundSlope", + "sentence": "the ground slope at _PARAM1_; _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "eventsFunctionContext.returnValue = object.__terrain3D.getSlopeAt(x, y);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "the layer (from 1 to 4) painted the most at a position of the scene. Useful to play footstep sounds or to slow down characters in water or mud.", + "fullName": "Ground layer", + "functionType": "ExpressionAndCondition", + "group": "Paint", + "name": "GroundLayer", + "sentence": "the ground layer at _PARAM1_; _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "eventsFunctionContext.returnValue = object.__terrain3D.getLayerAt(x, y);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + } + ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + }, + { + "folderName": "Sculpt", + "children": [ + { + "functionName": "RaiseGround" + }, + { + "functionName": "RaiseGroundAlongLine" + }, + { + "functionName": "FlattenGround" + }, + { + "functionName": "SmoothGround" + } + ] + }, + { + "folderName": "Paint", + "children": [ + { + "functionName": "PaintLayer" + }, + { + "functionName": "PaintLayerAlongLine" + }, + { + "functionName": "TintGround" + }, + { + "functionName": "WetGround" + }, + { + "functionName": "GroundWetness" + }, + { + "functionName": "GroundTint" + }, + { + "functionName": "GroundLayer" + } + ] + }, + { + "folderName": "Holes", + "children": [ + { + "functionName": "MakeHole" + }, + { + "functionName": "FillHoles" + }, + { + "functionName": "CarveEntrance" + }, + { + "functionName": "IsHole" + }, + { + "functionName": "GroundZWithHoles" + } + ] + }, + { + "folderName": "Generation", + "children": [ + { + "functionName": "GenerateRelief" + }, + { + "functionName": "LoadHeightmap" + } + ] + }, + { + "folderName": "Position", + "children": [ + { + "functionName": "PutOnGround" + }, + { + "functionName": "GroundZ" + }, + { + "functionName": "GroundSlope" + } + ] + } + ] + }, + "propertyDescriptors": [ + { + "value": "Hills", + "type": "Choice", + "label": "Relief", + "description": "The initial shape, before sculpting.", + "group": "Shape", + "choices": [ + { + "label": "Flat", + "value": "Flat" + }, + { + "label": "Hills", + "value": "Hills" + }, + { + "label": "Mountains", + "value": "Mountains" + }, + { + "label": "Island", + "value": "Island" + } + ], + "name": "Relief" + }, + { + "value": "1", + "type": "Number", + "label": "Seed", + "description": "Each seed gives a different terrain with the same relief.", + "group": "Shape", + "name": "Seed" + }, + { + "value": "", + "type": "Resource", + "label": "Heightmap image", + "description": "Optional. Replaces the relief: black is the lowest, white the highest (the object depth).", + "group": "Shape", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "HeightmapImage" + }, + { + "value": "", + "type": "MultilineString", + "label": "Edits", + "description": "Changes of the relief or heightmap, applied in order, as a JSON list, for example [{\"tool\": \"raise\", \"x\": 0.3, \"y\": 0.4, \"radius\": 0.15, \"height\": 0.4}, {\"tool\": \"paint\", \"x\": 0.1, \"y\": 0.9, \"toX\": 0.8, \"toY\": 0.2, \"radius\": 0.02, \"layer\": 2}]. Tools: raise (height added to the ground, negative to lower), flatten (z: the ground is brought to this height, fully at the center, cutting what is above it and filling what is below; with toX and toY, toZ gives the height at the end of the line, for a road going up), smooth (strength), paint (layer, from 1 to 4, and strength), tint (color, like \"120;140;60\", multiplying the layers, white removing the tint, and strength), wetness (amount, from 0 for dry to 1 for wet, and strength), hole (no ground in the radius, for the entrances of tunnels and caves) and fill (the holes in the radius are filled with the layers of the ground around them). x, y and radius are fractions (0 to 1) of the terrain size (x is (scene X - terrain X) / terrain width). height and z are fractions of its depth: the ground at z is at Z = terrain Z + z * terrain depth. Add toX and toY to change the ground along a line (paths, rivers, ridges). Shown in the scene editor.", + "group": "Shape", + "advanced": true, + "name": "Edits" + }, + { + "value": "256", + "type": "Choice", + "label": "Resolution", + "description": "The number of cells on each side of the terrain. Higher is more detailed but slower.", + "group": "Shape", + "choices": [ + { + "label": "64 x 64", + "value": "64" + }, + { + "label": "128 x 128", + "value": "128" + }, + { + "label": "256 x 256", + "value": "256" + }, + { + "label": "512 x 512", + "value": "512" + }, + { + "label": "1024 x 1024", + "value": "1024" + } + ], + "name": "Resolution" + }, + { + "value": "96;160;64", + "type": "Color", + "label": "Layer 1 color (grass)", + "group": "Layers", + "name": "Layer1Color" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 1 texture", + "description": "Optional. Tinted by the layer color.", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer1Texture" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 1 normal map", + "description": "Optional. The bumps of the texture, lit by the lights (an OpenGL normal map, like the textures of ambientCG).", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer1NormalMap" + }, + { + "value": "150;112;72", + "type": "Color", + "label": "Layer 2 color (dirt)", + "group": "Layers", + "name": "Layer2Color" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 2 texture", + "description": "Optional. Tinted by the layer color.", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer2Texture" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 2 normal map", + "description": "Optional. The bumps of the texture, lit by the lights (an OpenGL normal map, like the textures of ambientCG).", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer2NormalMap" + }, + { + "value": "124;118;112", + "type": "Color", + "label": "Layer 3 color (rock)", + "group": "Layers", + "name": "Layer3Color" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 3 texture", + "description": "Optional. Tinted by the layer color.", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer3Texture" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 3 normal map", + "description": "Optional. The bumps of the texture, lit by the lights (an OpenGL normal map, like the textures of ambientCG).", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer3NormalMap" + }, + { + "value": "224;204;148", + "type": "Color", + "label": "Layer 4 color (sand)", + "group": "Layers", + "name": "Layer4Color" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 4 texture", + "description": "Optional. Tinted by the layer color.", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer4Texture" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 4 normal map", + "description": "Optional. The bumps of the texture, lit by the lights (an OpenGL normal map, like the textures of ambientCG).", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer4NormalMap" + }, + { + "value": "1", + "type": "Number", + "label": "Normal map strength", + "description": "How much the normal maps bump the ground: 0 for none, 1 as they are, more to exaggerate them.", + "group": "Layers", + "name": "NormalMapStrength" + }, + { + "value": "256", + "type": "Number", + "unit": "Pixel", + "label": "Texture size", + "description": "The size, in the scene, of one repetition of the layer textures.", + "group": "Layers", + "name": "TextureSize" + }, + { + "value": "0.5", + "type": "Number", + "label": "Height blending", + "description": "Where layers meet, the bright parts of textures (stones, tufts) cover the dark ones (gaps, sand) instead of fading into each other: 0 for none, 1 for the most.", + "group": "Layers", + "name": "HeightBlending" + }, + { + "value": "0.5", + "type": "Number", + "label": "Texture variation", + "description": "Makes the repetition of textures less visible, with patches of bigger and rotated textures and colors slowly varying: 0 for none, 1 for the most.", + "group": "Layers", + "name": "TextureVariation" + }, + { + "value": "false", + "type": "Boolean", + "label": "Wet shore", + "description": "Make the ground wet (darker and shiny) under a Z position, like the sand of a beach near the water.", + "group": "Wetness", + "name": "WetShore" + }, + { + "value": "0", + "type": "Number", + "unit": "Pixel", + "label": "Wet below Z", + "description": "The ground is wet under this Z position (for example, the Z of a 3D water object).", + "group": "Wetness", + "name": "WetShoreZ" + }, + { + "value": "60", + "type": "Number", + "unit": "Pixel", + "label": "Wet shore fade", + "description": "The ground becomes dry over this height above \"Wet below Z\".", + "group": "Wetness", + "name": "WetShoreFade" + }, + { + "value": "3", + "type": "Choice", + "label": "Layer on steep slopes", + "description": "This layer is shown where the ground is steep, whatever is painted.", + "group": "Layers", + "choices": [ + { + "label": "None", + "value": "0" + }, + { + "label": "Layer 1", + "value": "1" + }, + { + "label": "Layer 2", + "value": "2" + }, + { + "label": "Layer 3", + "value": "3" + }, + { + "label": "Layer 4", + "value": "4" + } + ], + "name": "CliffLayer" + }, + { + "value": "45", + "type": "Number", + "unit": "DegreeAngle", + "label": "Steep slope angle", + "group": "Layers", + "name": "CliffAngle" + }, + { + "value": "true", + "type": "Boolean", + "label": "Cast shadows", + "group": "Rendering", + "name": "CastShadow" + }, + { + "value": "true", + "type": "Boolean", + "label": "Receive shadows", + "group": "Rendering", + "name": "ReceiveShadow" + }, + { + "value": "", + "type": "String", + "label": "Sculpted and painted data", + "description": "Written by the sculpt and paint tools of the scene editor, in a compact format not meant to be read or written by hand: use Edits instead. Clear it to go back to the relief, heightmap and edits.", + "advanced": true, + "name": "SculptData" + } + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Shape", + "children": [ + { + "propertyName": "Relief" + }, + { + "propertyName": "Seed" + }, + { + "propertyName": "HeightmapImage" + }, + { + "propertyName": "Edits" + }, + { + "propertyName": "Resolution" + } + ] + }, + { + "folderName": "Layers", + "children": [ + { + "propertyName": "Layer1Color" + }, + { + "propertyName": "Layer1Texture" + }, + { + "propertyName": "Layer1NormalMap" + }, + { + "propertyName": "Layer2Color" + }, + { + "propertyName": "Layer2Texture" + }, + { + "propertyName": "Layer2NormalMap" + }, + { + "propertyName": "Layer3Color" + }, + { + "propertyName": "Layer3Texture" + }, + { + "propertyName": "Layer3NormalMap" + }, + { + "propertyName": "Layer4Color" + }, + { + "propertyName": "Layer4Texture" + }, + { + "propertyName": "Layer4NormalMap" + }, + { + "propertyName": "NormalMapStrength" + }, + { + "propertyName": "TextureSize" + }, + { + "propertyName": "HeightBlending" + }, + { + "propertyName": "TextureVariation" + }, + { + "propertyName": "CliffLayer" + }, + { + "propertyName": "CliffAngle" + } + ] + }, + { + "folderName": "Wetness", + "children": [ + { + "propertyName": "WetShore" + }, + { + "propertyName": "WetShoreZ" + }, + { + "propertyName": "WetShoreFade" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "CastShadow" + }, + { + "propertyName": "ReceiveShadow" + } + ] + }, + { + "propertyName": "SculptData" + } + ] + }, + "variants": [] + }, + { + "areaMaxX": 1000, + "areaMaxY": 1000, + "areaMaxZ": 40, + "areaMinX": 0, + "areaMinY": 0, + "areaMinZ": 0, + "defaultName": "Grass", + "description": "A field of grass blades swaying in the wind and pushed aside by characters. They grow in the area of the object, on the highest 3D terrain at each point (one field can cover several terrains), or at the Z position of the object where there is no terrain.", + "fullName": "Terrain grass", + "helpPath": "", + "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGQ9Ik0zIDIxYzEtNSAxLTkgMC0xMyAyIDMgMyA3IDMgMTN6bTQgMGMwLTYgMS0xMSA0LTE2LTEgNS0xIDEwIDAgMTZ6bTUgMGMwLTUgMi05IDUtMTItMiA0LTIgOC0xIDEyem01IDBjMS00IDItNiA0LTgtMSAzLTEgNS0xIDh6Ii8+PC9zdmc+", + "is3D": true, + "isUsingLegacyInstancesRenderer": true, + "name": "TerrainGrass", + "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/732ef90b7fcf5dd9171fe95d0cf262e09159b487304915a4693baa893d9ce16c_grass.svg", + "objects": [ + { + "assetStoreId": "", + "name": "AreaPlaceholder", + "type": "Scene3D::Cube3DObject", + "variables": [], + "effects": [], + "behaviors": [], + "content": { + "width": 1000, + "height": 1000, + "depth": 40, + "enableTextureTransparency": false, + "facesOrientation": "Y", + "backFaceUpThroughWhichAxisRotation": "X", + "materialType": "Basic", + "tint": "255;255;255", + "frontFaceResourceName": "", + "frontFaceVisible": false, + "frontFaceResourceRepeat": false, + "backFaceResourceName": "", + "backFaceVisible": false, + "backFaceResourceRepeat": false, + "leftFaceResourceName": "", + "leftFaceVisible": false, + "leftFaceResourceRepeat": false, + "rightFaceResourceName": "", + "rightFaceVisible": false, + "rightFaceResourceRepeat": false, + "topFaceResourceName": "", + "topFaceVisible": false, + "topFaceResourceRepeat": false, + "bottomFaceResourceName": "", + "bottomFaceVisible": false, + "bottomFaceResourceRepeat": false + } + } + ], + "objectsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "objectName": "AreaPlaceholder" + } + ] + }, + "objectsGroups": [], + "layers": [ + { + "ambientLightColorB": 200, + "ambientLightColorG": 200, + "ambientLightColorR": 200, + "camera2DPlaneMaxDrawingDistance": 5000, + "camera3DFarPlaneDistance": 10000, + "camera3DFieldOfView": 45, + "camera3DNearPlaneDistance": 3, + "cameraType": "", + "followBaseLayerCamera": false, + "isLightingLayer": false, + "isLocked": false, + "name": "", + "renderingType": "", + "visibility": true, + "cameras": [ + { + "defaultSize": true, + "defaultViewport": true, + "height": 0, + "viewportBottom": 1, + "viewportLeft": 0, + "viewportRight": 1, + "viewportTop": 0, + "width": 0 + } + ], + "effects": [] + } + ], + "instances": [ + { + "angle": 0, + "customSize": true, + "depth": 40, + "height": 1000, + "keepRatio": true, + "layer": "", + "name": "AreaPlaceholder", + "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-001000001000", + "width": 1000, + "x": 0, + "y": 0, + "zOrder": 1, + "numberProperties": [], + "stringProperties": [], + "initialVariables": [] + } + ], + "editionSettings": {}, + "eventsFunctions": [ + { + "fullName": "", + "functionType": "Action", + "name": "onCreated", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Terrain3D::DefineHelperClasses" + }, + "parameters": [ + "", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Terrain3D::DefineGrassHelperClasses" + }, + "parameters": [ + "", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "for (const child of object.getChildrenContainer().getAdhocListOfAllInstances()) {", + " child.deleteFromScene();", + "}", + "if (object.__terrainGrass) object.__terrainGrass.dispose();", + "object.__terrainGrass = new gdjs.__terrainGrassExtension.Grass(object);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "onDestroy", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrainGrass.dispose();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "doStepPostEvents", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const game = runtimeScene.getGame();", + "// Time doesn't pass in the scene editor: real time is used to show the wind.", + "const isInGameEdition = game.isInGameEdition && game.isInGameEdition();", + "const elapsedTime = isInGameEdition ? 1 / 60 : object.getElapsedTime() / 1000;", + "const time = isInGameEdition", + " ? performance.now() / 1000", + " : object.getRuntimeScene().getTimeManager().getTimeFromStart() / 1000;", + "const layer = object.getInstanceContainer().getLayer(object.getLayer());", + "object.__terrainGrass.update(elapsedTime, time, layer.getRenderer().getThreeCamera());" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "onHotReloading", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrainGrass.updateFromProperties();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" + } + ], + "objectGroups": [] + } + ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + } + ] + }, + "propertyDescriptors": [ + { + "value": "60", + "type": "Number", + "label": "Density", + "description": "The number of blades in a square of 100 x 100 pixels.", + "group": "Blades", + "name": "Density" + }, + { + "value": "36", + "type": "Number", + "unit": "Pixel", + "label": "Blade height", + "description": "The average height of blades (each blade is between 60% and 140% of it).", + "group": "Blades", + "name": "BladeHeight" + }, + { + "value": "5", + "type": "Number", + "unit": "Pixel", + "label": "Blade width", + "group": "Blades", + "name": "BladeWidth" + }, + { + "value": "52;104;36", + "type": "Color", + "label": "Base color", + "group": "Blades", + "name": "BaseColor" + }, + { + "value": "150;196;82", + "type": "Color", + "label": "Tip color", + "group": "Blades", + "name": "TipColor" + }, + { + "value": "1", + "type": "Number", + "label": "Seed", + "description": "Each seed places blades differently.", + "group": "Blades", + "name": "Seed" + }, + { + "value": "1", + "type": "Choice", + "label": "Grows on terrain layer", + "description": "On a 3D terrain, blades only grow where this layer is painted. A new terrain has layer 1 painted everywhere.", + "group": "Ground", + "choices": [ + { + "label": "Any", + "value": "0" + }, + { + "label": "Layer 1", + "value": "1" + }, + { + "label": "Layer 2", + "value": "2" + }, + { + "label": "Layer 3", + "value": "3" + }, + { + "label": "Layer 4", + "value": "4" + } + ], + "name": "GroundLayer" + }, + { + "value": "40", + "type": "Number", + "unit": "DegreeAngle", + "label": "Maximum slope", + "description": "No blades grow on steeper ground.", + "group": "Ground", + "name": "MaxSlope" + }, + { + "value": "1", + "type": "Number", + "label": "Wind strength", + "description": "0 for no wind.", + "group": "Wind", + "name": "WindStrength" + }, + { + "value": "2", + "type": "Number", + "label": "Wind speed", + "group": "Wind", + "name": "WindSpeed" + }, + { + "value": "", + "type": "String", + "label": "Object bending the grass", + "description": "The name of an object pushing blades aside when walking through the grass, like the player. The 8 first instances are used.", + "group": "Wind", + "name": "BendingObject" + }, + { + "value": "2500", + "type": "Number", + "unit": "Pixel", + "label": "Fade distance", + "description": "Blades are less and less dense up to this distance from the camera, and hidden after.", + "group": "Rendering", + "name": "FadeDistance" + } + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Blades", + "children": [ + { + "propertyName": "Density" + }, + { + "propertyName": "BladeHeight" + }, + { + "propertyName": "BladeWidth" + }, + { + "propertyName": "BaseColor" + }, + { + "propertyName": "TipColor" + }, + { + "propertyName": "Seed" + } + ] + }, + { + "folderName": "Ground", + "children": [ + { + "propertyName": "GroundLayer" + }, + { + "propertyName": "MaxSlope" + } + ] + }, + { + "folderName": "Wind", + "children": [ + { + "propertyName": "WindStrength" + }, + { + "propertyName": "WindSpeed" + }, + { + "propertyName": "BendingObject" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "FadeDistance" + } + ] + } + ] + }, + "variants": [] + }, + { + "areaMaxX": 2000, + "areaMaxY": 2000, + "areaMaxZ": 200, + "areaMinX": 0, + "areaMinY": 0, + "areaMinZ": 0, + "assetStoreTag": "3d scattered models", + "defaultName": "ScatteredModels", + "description": "Many copies of a 3D model (trees, rocks, bushes, flowers...) placed in the area of the object and drawn all at once, which is much faster than an object for each one. They stand on the highest 3D terrain at each point, or at the Z position of the object where there is no terrain. Paint where they grow with the brushes of the scene editor.", + "fullName": "Scattered models (forests, rocks...)", + "helpPath": "", + "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGQ9Ik0xMCAyMXYtM0gzbDUtNUg1bDUtNUg3bDUtNSA1IDVoLTNsNSA1aC0zbDUgNWgtN3YzeiIvPjwvc3ZnPg==", + "is3D": true, + "isUsingLegacyInstancesRenderer": true, + "name": "ScatteredModels", + "previewIconUrl": "", + "objects": [ + { + "assetStoreId": "", + "name": "AreaPlaceholder", + "type": "Scene3D::Cube3DObject", + "variables": [], + "effects": [], + "behaviors": [], + "content": { + "width": 2000, + "height": 2000, + "depth": 200, + "enableTextureTransparency": false, + "facesOrientation": "Y", + "backFaceUpThroughWhichAxisRotation": "X", + "materialType": "Basic", + "tint": "255;255;255", + "frontFaceResourceName": "", + "frontFaceVisible": false, + "frontFaceResourceRepeat": false, + "backFaceResourceName": "", + "backFaceVisible": false, + "backFaceResourceRepeat": false, + "leftFaceResourceName": "", + "leftFaceVisible": false, + "leftFaceResourceRepeat": false, + "rightFaceResourceName": "", + "rightFaceVisible": false, + "rightFaceResourceRepeat": false, + "topFaceResourceName": "", + "topFaceVisible": false, + "topFaceResourceRepeat": false, + "bottomFaceResourceName": "", + "bottomFaceVisible": false, + "bottomFaceResourceRepeat": false + } + } + ], + "objectsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "objectName": "AreaPlaceholder" + } + ] + }, + "objectsGroups": [], + "layers": [ + { + "ambientLightColorB": 200, + "ambientLightColorG": 200, + "ambientLightColorR": 200, + "camera2DPlaneMaxDrawingDistance": 5000, + "camera3DFarPlaneDistance": 10000, + "camera3DFieldOfView": 45, + "camera3DNearPlaneDistance": 3, + "cameraType": "", + "followBaseLayerCamera": false, + "isLightingLayer": false, + "isLocked": false, + "name": "", + "renderingType": "", + "visibility": true, + "cameras": [ + { + "defaultSize": true, + "defaultViewport": true, + "height": 0, + "viewportBottom": 1, + "viewportLeft": 0, + "viewportRight": 1, + "viewportTop": 0, + "width": 0 + } + ], + "effects": [] + } + ], + "instances": [ + { + "angle": 0, + "customSize": true, + "depth": 200, + "height": 2000, + "keepRatio": true, + "layer": "", + "name": "AreaPlaceholder", + "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-002000002000", + "width": 2000, + "x": 0, + "y": 0, + "zOrder": 1, + "numberProperties": [], + "stringProperties": [], + "initialVariables": [] + } + ], + "editionSettings": {}, + "eventsFunctions": [ + { + "fullName": "", + "functionType": "Action", + "name": "onCreated", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Terrain3D::DefineHelperClasses" + }, + "parameters": [ + "", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Terrain3D::DefineScatterHelperClasses" + }, + "parameters": [ + "", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "for (const child of object.getChildrenContainer().getAdhocListOfAllInstances()) {", + " child.deleteFromScene();", + "}", + "if (object.__terrainScatter) object.__terrainScatter.dispose();", + "object.__terrainScatter = new gdjs.__terrainScatterExtension.Scatter(object);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "onDestroy", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrainScatter.dispose();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "doStepPostEvents", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const game = runtimeScene.getGame();", + "const isInGameEdition = game.isInGameEdition && game.isInGameEdition();", + "const elapsedTime = isInGameEdition ? 1 / 60 : object.getElapsedTime() / 1000;", + "const layer = object.getInstanceContainer().getLayer(object.getLayer());", + "object.__terrainScatter.update(elapsedTime, layer.getRenderer().getThreeCamera());" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "onHotReloading", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrainScatter.onHotReloading();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "description": "Paint the density of copies around a position, up to the density of the object (copies removed with \"Erase copies\" come back). The effect fades out up to the radius.", + "fullName": "Add copies", + "functionType": "Action", + "group": "Paint", + "name": "AddCopies", + "sentence": "Add copies of _PARAM0_ around _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "object.__terrainScatter.addCopies(x, y, radius);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "name": "Radius", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Remove the copies standing in a circle, for example a tree cut by the player (use the position of the nearest copy and a small radius) or a forest cleared for a building. They come back when the scene is restarted.", + "fullName": "Erase copies", + "functionType": "Action", + "group": "Paint", + "name": "EraseCopies", + "sentence": "Erase the copies of _PARAM0_ in a circle at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "object.__terrainScatter.removeCopies(x, y, radius);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "name": "Radius", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "the number of copies placed in the area.", + "fullName": "Number of copies", + "functionType": "ExpressionAndCondition", + "group": "Copies", + "name": "CopyCount", + "sentence": "the number of copies", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "eventsFunctionContext.returnValue = object.__terrainScatter.getCopyCount();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "description": "The X position of the base of the copy nearest to a position (the position itself if there is no copy).", + "fullName": "X position of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyX", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? copy.x : x;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "The Y position of the base of the copy nearest to a position (the position itself if there is no copy).", + "fullName": "Y position of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyY", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? copy.y : y;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "The Z position of the base of the copy nearest to a position (on the ground).", + "fullName": "Z position of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyZ", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? copy.z : object.getZ();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "the distance (on X and Y) from a position to the base of the nearest copy. It's a very big number when there is no copy.", + "fullName": "Distance to the nearest copy", + "functionType": "ExpressionAndCondition", + "group": "Copies", + "name": "NearestCopyDistance", + "sentence": "the distance from _PARAM1_; _PARAM2_ to the nearest copy", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? Math.hypot(copy.x - x, copy.y - y) : Number.MAX_VALUE;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + } + ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + }, + { + "folderName": "Paint", + "children": [ + { + "functionName": "AddCopies" + }, + { + "functionName": "EraseCopies" + } + ] + }, + { + "folderName": "Copies", + "children": [ + { + "functionName": "CopyCount" + }, + { + "functionName": "NearestCopyX" + }, + { + "functionName": "NearestCopyY" + }, + { + "functionName": "NearestCopyZ" + }, + { + "functionName": "NearestCopyDistance" + } + ] + } + ] + }, + "propertyDescriptors": [ + { + "value": "", + "type": "Resource", + "label": "3D model", + "description": "The model copied everywhere (a .glb file). Models standing on the Y axis, like most models of the asset store, are stood up along Z.", + "group": "Model", + "extraInformation": [ + "model3D" + ], + "choices": [], + "name": "Model" + }, + { + "value": "300", + "type": "Number", + "unit": "Pixel", + "label": "Height of a copy", + "description": "Copies keep the proportions of the model.", + "group": "Model", + "name": "Size" + }, + { + "value": "0.8", + "type": "Number", + "label": "Smallest random scale", + "description": "Each copy is scaled at random between the smallest and the largest scale.", + "group": "Model", + "name": "MinScale" + }, + { + "value": "1.2", + "type": "Number", + "label": "Largest random scale", + "group": "Model", + "name": "MaxScale" + }, + { + "value": "true", + "type": "Boolean", + "label": "Random rotation", + "description": "Turn each copy around the Z axis at random.", + "group": "Model", + "name": "RandomRotation" + }, + { + "value": "255;255;255", + "type": "Color", + "label": "Random tint", + "description": "Each copy is tinted between white and this color, at random. White for no tint.", + "group": "Model", + "name": "RandomTint" + }, + { + "value": "0.3", + "type": "Number", + "label": "Density", + "description": "The number of copies in a square of 100 x 100 pixels where the density is fully painted (0.3 is about one copy every 180 pixels). Copies never overlap their neighbors much: they are placed at random in a grid.", + "group": "Placement", + "name": "Density" + }, + { + "value": "1", + "type": "Number", + "label": "Seed", + "description": "Each seed places copies differently.", + "group": "Placement", + "name": "Seed" + }, + { + "value": "Everywhere", + "type": "Choice", + "label": "Copies grow", + "description": "Everywhere in the area, or only where they are painted with the \"Add copies\" brush (or the edits).", + "group": "Placement", + "choices": [ + { + "label": "Everywhere (erase them with the brush)", + "value": "Everywhere" + }, + { + "label": "Only where painted", + "value": "Painted" + } + ], + "name": "Coverage" + }, + { + "value": "1", + "type": "Choice", + "label": "Grows on terrain layer", + "description": "On a 3D terrain, copies only grow where this layer is painted, away from its borders (so that trees don't stand on roads painted with another layer). A new terrain has layer 1 painted everywhere.", + "group": "Ground", + "choices": [ + { + "label": "Any", + "value": "0" + }, + { + "label": "Layer 1", + "value": "1" + }, + { + "label": "Layer 2", + "value": "2" + }, + { + "label": "Layer 3", + "value": "3" + }, + { + "label": "Layer 4", + "value": "4" + } + ], + "name": "GroundLayer" + }, + { + "value": "30", + "type": "Number", + "unit": "DegreeAngle", + "label": "Maximum slope", + "description": "No copies grow on steeper ground.", + "group": "Ground", + "name": "MaxSlope" + }, + { + "value": "-100000", + "type": "Number", + "unit": "Pixel", + "label": "Minimum ground Z", + "description": "No copies grow where the ground is lower (for example, under the water level).", + "group": "Ground", + "name": "MinGroundZ" + }, + { + "value": "100000", + "type": "Number", + "unit": "Pixel", + "label": "Maximum ground Z", + "description": "No copies grow where the ground is higher (for example, above the tree line).", + "group": "Ground", + "name": "MaxGroundZ" + }, + { + "value": "false", + "type": "Boolean", + "label": "Trunk collisions", + "description": "Give each copy a static collision shape around its trunk, for the 3D physics behavior (add it to this object, with the \"Static\" type) and navigation meshes. Without them, these behaviors have no effect on this object.", + "group": "Collisions", + "name": "TrunkCollisions" + }, + { + "value": "30", + "type": "Number", + "unit": "Pixel", + "label": "Trunk diameter", + "description": "The diameter of the collision shape of a copy at the height given in \"Height of a copy\" (it is scaled with the copy).", + "group": "Collisions", + "name": "TrunkDiameter" + }, + { + "value": "8000", + "type": "Number", + "unit": "Pixel", + "label": "Draw distance", + "description": "Copies shrink and are hidden at this distance from the camera.", + "group": "Rendering", + "name": "DrawDistance" + }, + { + "value": "true", + "type": "Boolean", + "label": "Cast shadows", + "group": "Rendering", + "name": "CastShadow" + }, + { + "value": "true", + "type": "Boolean", + "label": "Receive shadows", + "group": "Rendering", + "name": "ReceiveShadow" + }, + { + "value": "", + "type": "MultilineString", + "label": "Edits", + "description": "Where copies are added or erased, applied in order, as a JSON list, for example [{\"tool\": \"add\", \"x\": 0.3, \"y\": 0.4, \"radius\": 0.15}, {\"tool\": \"erase\", \"x\": 0.1, \"y\": 0.9, \"toX\": 0.8, \"toY\": 0.2, \"radius\": 0.02}]. Tools: add (density: from 0 to 1, 1 by default, how dense copies are compared to the Density property) and erase. strength (from 0 to 1) makes a stroke lighter. x, y and radius are fractions (0 to 1) of the object size (x is (scene X - object X) / object width). Add toX and toY to paint along a line (a row of trees, a path through a forest). With \"Copies grow: only where painted\", only the added areas have copies. Shown in the scene editor.", + "group": "Placement", + "advanced": true, + "name": "Edits" + }, + { + "value": "", + "type": "String", + "label": "Painted data", + "description": "Written by the brushes of the scene editor, in a compact format not meant to be read or written by hand: use Edits instead. Clear it to go back to the coverage and the edits.", + "advanced": true, + "name": "PaintData" + } + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Model", + "children": [ + { + "propertyName": "Model" + }, + { + "propertyName": "Size" + }, + { + "propertyName": "MinScale" + }, + { + "propertyName": "MaxScale" + }, + { + "propertyName": "RandomRotation" + }, + { + "propertyName": "RandomTint" + } + ] + }, + { + "folderName": "Placement", + "children": [ + { + "propertyName": "Density" + }, + { + "propertyName": "Seed" + }, + { + "propertyName": "Coverage" + }, + { + "propertyName": "Edits" + } + ] + }, + { + "folderName": "Ground", + "children": [ + { + "propertyName": "GroundLayer" + }, + { + "propertyName": "MaxSlope" + }, + { + "propertyName": "MinGroundZ" + }, + { + "propertyName": "MaxGroundZ" + } + ] + }, + { + "folderName": "Collisions", + "children": [ + { + "propertyName": "TrunkCollisions" + }, + { + "propertyName": "TrunkDiameter" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "DrawDistance" + }, + { + "propertyName": "CastShadow" + }, + { + "propertyName": "ReceiveShadow" + } + ] + }, + { + "propertyName": "PaintData" + } + ] + }, + "variants": [] + } + ] } ], "externalLayouts": [] diff --git a/examples/starting-first-person-horror/assets/StartingWall.png b/examples/starting-first-person-horror/assets/StartingWall.png index b3a097887..06278d6e5 100644 Binary files a/examples/starting-first-person-horror/assets/StartingWall.png and b/examples/starting-first-person-horror/assets/StartingWall.png differ diff --git a/examples/starting-first-person-horror/starting-first-person-horror.json b/examples/starting-first-person-horror/starting-first-person-horror.json index 1d793791a..4fd6628f3 100644 --- a/examples/starting-first-person-horror/starting-first-person-horror.json +++ b/examples/starting-first-person-horror/starting-first-person-horror.json @@ -1,7 +1,7 @@ { "firstLayout": "", "gdVersion": { - "build": 283, + "build": 284, "major": 5, "minor": 6, "revision": 0 @@ -229,6 +229,14 @@ "metadata": "", "name": "assets/starting_unit_character_orange.glb", "userAdded": false + }, + { + "file": "assets/StartingGround.png", + "kind": "image", + "metadata": "", + "name": "assets/StartingGround.png", + "smoothed": true, + "userAdded": false } ] }, @@ -245,12 +253,13 @@ "mangledName": "Game_32Scene", "name": "Game Scene", "r": 255, + "renderer3DWorldScale": 100, "standardSortMethod": true, "stopSoundsOnStartup": true, "title": "", "v": 255, "uiSettings": { - "grid": false, + "grid": true, "gridType": "rectangular", "gridWidth": 64, "gridHeight": 64, @@ -260,7 +269,7 @@ "gridOffsetZ": 0, "gridColor": 10401023, "gridAlpha": 0.8, - "snap": false, + "snap": true, "zoomFactor": 0.4633111824514261, "windowMask": false, "selectedLayer": "", @@ -278,6 +287,9 @@ }, { "name": "Player" + }, + { + "name": "Terrain" } ] } @@ -291,25 +303,6 @@ } ], "instances": [ - { - "angle": 0, - "customSize": true, - "depth": 30, - "height": 2240, - "keepRatio": true, - "layer": "", - "locked": true, - "name": "Ground", - "persistentUuid": "6a14e5ab-5c31-4692-8cd5-c50937fd60c6", - "width": 2432, - "x": -576, - "y": -832, - "z": -30, - "zOrder": 1, - "numberProperties": [], - "stringProperties": [], - "initialVariables": [] - }, { "angle": 0, "customSize": true, @@ -871,77 +864,25 @@ "numberProperties": [], "stringProperties": [], "initialVariables": [] + }, + { + "angle": 0, + "customSize": true, + "height": 2240, + "layer": "", + "locked": true, + "name": "Terrain", + "persistentUuid": "95fc26d3-0e72-4b00-aa75-7e3cdf257dac", + "width": 2432, + "x": -576, + "y": -832, + "zOrder": 0, + "numberProperties": [], + "stringProperties": [], + "initialVariables": [] } ], "objects": [ - { - "assetStoreId": "", - "name": "Ground", - "persistentUuid": "ca100636-aff6-40f7-893c-942f6dc28a40", - "type": "Scene3D::Cube3DObject", - "variables": [], - "effects": [], - "behaviors": [ - { - "name": "Physics3D", - "type": "Physics3D::Physics3DBehavior", - "object3D": "Object3D", - "bodyType": "Static", - "bullet": false, - "fixedRotation": false, - "shape": "Box", - "shapeOrientation": "Z", - "shapeDimensionA": 0, - "shapeDimensionB": 0, - "shapeDimensionC": 0, - "density": 1, - "friction": 0.3, - "restitution": 0.1, - "linearDamping": 0.1, - "angularDamping": 0.1, - "gravityScale": 1, - "layers": 17, - "masks": 17, - "shapeOffsetX": 0, - "shapeOffsetY": 0, - "shapeOffsetZ": 0, - "massCenterOffsetX": 0, - "massCenterOffsetY": 0, - "massCenterOffsetZ": 0, - "massOverride": 0 - } - ], - "content": { - "width": 1000, - "height": 1000, - "depth": 40, - "enableTextureTransparency": false, - "facesOrientation": "Y", - "frontFaceResourceName": "assets\\StartingGround.png", - "backFaceResourceName": "", - "backFaceUpThroughWhichAxisRotation": "X", - "leftFaceResourceName": "assets\\StartingWall.png", - "rightFaceResourceName": "assets\\StartingWall.png", - "topFaceResourceName": "assets\\StartingWall.png", - "bottomFaceResourceName": "assets\\StartingWall.png", - "frontFaceVisible": true, - "backFaceVisible": false, - "leftFaceVisible": true, - "rightFaceVisible": true, - "topFaceVisible": true, - "bottomFaceVisible": true, - "frontFaceResourceRepeat": true, - "backFaceResourceRepeat": false, - "leftFaceResourceRepeat": false, - "rightFaceResourceRepeat": false, - "topFaceResourceRepeat": false, - "bottomFaceResourceRepeat": false, - "materialType": "StandardWithoutMetalness", - "isCastingShadow": true, - "isReceivingShadow": true, - "tileScale": 0.2 - } - }, { "assetStoreId": "", "name": "Obstacle", @@ -1578,6 +1519,59 @@ "Smoothing": 0.15, "ShadowBias": 0.0001 } + }, + { + "assetStoreId": "", + "name": "Terrain", + "persistentUuid": "ab635fb3-3d9b-42ab-b46f-a0da2516b023", + "type": "Terrain3D::Terrain3D", + "variant": "", + "variables": [], + "effects": [], + "behaviors": [ + { + "name": "Physics3D", + "type": "Physics3D::Physics3DBehavior", + "object3D": "Object3D", + "bodyType": "Static", + "bullet": false, + "fixedRotation": false, + "shape": "Box", + "meshShapeResourceName": "", + "shapeOrientation": "Z", + "shapeDimensionA": 0, + "shapeDimensionB": 0, + "shapeDimensionC": 0, + "shapeOffsetX": 0, + "shapeOffsetY": 0, + "shapeOffsetZ": 0, + "massCenterOffsetX": 0, + "massCenterOffsetY": 0, + "massCenterOffsetZ": 0, + "massOverride": 0, + "density": 1, + "friction": 0.3, + "restitution": 0.1, + "linearDamping": 0.1, + "angularDamping": 0.1, + "gravityScale": 1, + "layers": 17, + "masks": 17 + } + ], + "content": { + "SculptData": "2;256;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AQABAQIBAgIAAQEAAQEAAQIBAAICAQEEAQEAVQIAAQIAAQIBAgIDAgIAAQIAARAAAQEAAQECAQABAQIAAQECAQEFBAACAgABAQADAQABAQACAQAFAgAEAQIAAQICAgEDAgIAAQECBgABAwABCAMAAQMCBAMGAwACAQEMAQMHAAICAAEBGhkEAwIABAECAQYDAQAEAQIABAEGAwABAgMCAAECAwIAAgEAAgQFAgQAAQEDAgAEAgMEAAEDAgIAAQUCAggHAAIBAgEAAgQBAQEAVwICAAEBAgIAAgQFAgIAAQ4AAQEAAwEAAQQDAAIFAQICAAECAAEBAQQDAQEAFgIEAQADAgEGAgcCCAEBAAEBBgECAAECBQACDAEJAAEDAgACAR4ZAgUCAgEBAAEBAgADAgECAQQAAQUEAQAEAQABBAMCAAEBAAMBBAEBBgMAAgIAAQMEAQEEAwIBAgABAgMAAQQDAgAFAwIAAgEAXAQAAwEGAwQBAAEEDgECAQABAwQCBQQHAQIGAwIBAQADAwAdAgIAAgEGBgcAAQYBAAEBAAECAgABAQABBAUAAQoDBQEAAwIAAQEEAQABAwIAAQIBAAMBAgACAgMAAQIBAgABAQEEAAQBAgMEAQIAAgEAAwECAgABAQACAQABBAMAAQIAAQMCAgEABgIAAQEBAF8EAgACAQABAgACAgwAAgECAgMAAQEFAgIAAwEAAgEBACICAgABAgQFAgYFBAEAAgIAAQIDAAIEAgMBAwABAgACHBsBAgACAgMEAAIDAAECAAEBAgACAgMAAQICAQACAwIBBgMEAwQFAgACAQABAgEEAQABAQABBgMBAgIDAAECAAEBAgECAAEBAAMBAGICAAIEAgEBAgACDgABBQQBAQQHAQQAAQIAAQMCAQEBACgECAUAAQYDAAEDCAMAAQQDCAUDDAEFAAEDAQIBAhgVAwQBAgEBAAICAAEBAgEAAQIBAAICAQIBAAICAwIBBAMAAQIAAgECAAECAgEDAgEEAwABAgEBBAEABwQFAGcCAgECAAECAAEBAgoBBAABAwIFAAECAAIBAAIDAC0GAQABCAUGBwQCAQYJBAEAAQwCDRAFBQUCBAEBBAwCBAkJAQQCBgcBAgMCAw4PAgECAQICAAIDBgABAQMCAQABAgABAQABAgECAgMCAAEBAgEAAQQDBgMCBQIBAGkCAAECAQQBAAEEAQgEAQABAQMAAQIMDQIAAQMBAC8EAgICAgYJBBQbCAABDg8IFAEkPygBKzoHKwMOHQEEJBcGAw02AysIEhECAgMCEBIjHAgFHx4GHxgBFRgbHBUOERYVEA0KDxQDDQwJCA0KAQMAAQQFAGkEAQIAAgIAAQIMAwIFAQQAAgUIAwMAMgIBBgUIAwABBQoUGQUKBgkDAgQWChkAARliRwUAAQoFEBEFEAsRDhYGIQwLNCMFEgcOCwcGHCMBAAEGAgMAAQIFAQACAQIBBAIKDwIKCwACBAMAAQQAAQMAawIAAQIBAAMIAgUBBAABAQABAwA3AgYHEgABBxQRHgMIDwABFDE+OQNAOzw3QhMaDQgpKAMREhsHDgsOCAUrJgQjCxIRAAICAQABIB8AARoZFhUWERATEA8MCwIBAAEEAwgHAgEAAQIBAGwEAQECAgECCgUBBAQFAgEDAPIBBAACAQIAAQQBBAIFAgABAQEA8gECAgIAAQEAAQIEAgMAAgMA9QECAAICAAEGAAEDAgABAwD2AQIAAQECAgYBAAIDAPgBBAEAAgICAgUA+gEEAAEDBAEBAPoBAgACAgQAAQMA+wECAAICAQD8AQIBAgIBAPwBAgIBAP8BAgCBAgIA/wECAQD/AQIAgQICAIACAgCBBAIAjBoBAIIGAQCABAECAP8BAgCCBAEA/wEBAgD/AQIDAIACAQD/AQMEAP8BAQD/AQEAggICAP0BAQEEAQCAAgEA/gECAQUA/gEBAwgA/AEBAAEDBgD9AQIDAgEBAPwBAQICAwD9AQMEAwYA/AEBBAMCAAECAPsBAgMCAgABBwD7AQECAgIHBgD7AQICAwUEAQD8AQEDBgEBAP0BBAkEBAD7AQEAAQEIAwD8AQMAAgICAgD6AQUEAAEEAwABAQD6AQIBAAEDAgEBAPoBAgIDAAEDAAEEAPoBAwABBAABAQYDAPoBAgMDAggBAwD6AQMAAQYCBwEGAPoBAgQDBwYBAPsBBAUCAgECAwD6AQECAQYCBQEA+gEDBAQDBwgGAPoBBAEFAAEIAQD8AQMEAgEBAPsBAwYAAQEAAgMA/gEBAAEEAPoBBAEDAAECBAEA+gEDAAEEAgIFAQD9AQEBAgD+AQEBAgQDAgIA+AEBAwIEAAIHAgYA9wEBAAEEAAMCAAEBAAECAPYBAQQAAgIBAQECAwQA9gEEAAEBAgEAAgECAQMGAPUBAgECAAEBBAMAAQECAAEDBAD0AQIAAQIDBAMEAwIAAQIBAAECAPQBAgECAQIAAQQDAQIAAQEAAQIA8gECAAIBAgEAAQIBAAEBAgD4AQIDAgEAAQECAwYBAPQBAgEBAgAEAwQAAQMEAAEBAgDzAQIAAQEAAwQDAAEBAgD2AQIDBAABAgABAgEAAQIDAgD0AQEAAQIAAQICAgABAQIBAPcBAQICAQEA+wECAAIDAgIBAgIAAQECAQD1AQIAAQIBAgACAgEEAQEA9AEEBQICAgMAAQIBAgABAQIAAQEA8gEBBgABAQECAAECAAIBAAEBAgDzAQICAwABAgEAAQMAAQYBAAECAQD1AQICAQECAAIFBAIBAPYBAgMCAgIAAwEBAgD3AQIAAgIBAgABAQIBAPYBAgADBAEAAQEA+wECAAICAQABAgEA+AECAQQCAAEBAgEA+QECAAEBAAICAQD7AQIDAgABAQD9AQIAgAIBAIACAgCAAgIAgAICAP8BAgMAgAICAIEEAgCDCAIA5hoDAAICAQABAgIA+QEBAAMCAgEBAAECAAEDBAD0AQQAAgMBAwACAgABAgIBAAICAPUBAgABAQABAwEBAgABBgABAQIA8AEBAgECAQQCAQACAQACBwABAgQAAQIBAgDtAQMCAgIFAAECBAMCAAECAgEDAwABBAQBAOwBAwABBAUEBgEAAQEEAAEBAQABAgABAwABBQABCADrAQcCBAEEAQUEAAICAQYDAgECAgABAQMFAgYCAOgBAwIBAQABAQYCAAIBAgMEAwQCAQACAgMBAwIGAOgBAgEAAwEBAAEBAgQAAQMCAAEBBAEEAAECAQMDAQYCAOYBBAACAQACAQABAgEFAAEEAAEBAAICAAECAAECAAEBAQMGAAEBAgDjAQECAAEEAQAFAgEBAAQBAQABAgABAgIDAQMCBgECAOYBAgABAQAEAgEABAIBAQACAgABAgEDAgMGAQIA4QEBAAECAwICAAYCAQACAQICAQAFAgEBAgcIAQIA4gEBBAEAAQIABAECAgABAQABAQABBAMABAICAwEGCQgBAOABAgACAwADAgABAgECAQACAgEAAgECAAMCAQADAwQHCADfAQIAAQEAAQIBAAIDAgQEAQIAAgICAAECAAMBAAEBAAICAAIBAAEHAN8BCgABAgACAgEAAQIBBQECAQACAQACAQDinQEBAIACAQD/AQUA/wEDAAEBAP0BAQMAAQMA/AEBAwACAgECAPkBAwABAwQAAgECAPkBAwECAQABAQMEAPcBAQMCAQIBAwQDAQYA9QEBAQEAAQIDBAEBAwABBAD1AQUAAgIDBAMBAAEDAAECAgDyAQEFBAACAQABAwMEBQUEAQQA8QEDAQQAAQEBAAEHAgADAwEBAgYA7wEDAgABAwEDAAIEAQIBAAEJBAICAgDtAQEBAAEBAAEBAQIAAQUGAQEAAQIFAAQGAOwBAQMBAAEBAQQBAgIHAgIBAwcCAAEDBgICAOgBAQEDAAEBAgABAQYDAgcCBAEBAQEJAAEEAgMCBAIA5gEBAwEAAgIAAQEEAwEDBgEAAgUAAQoA6aECBAACAQIAAQEBAPkBAgADAQECAgABAQABBAMA9AEDAAIEAgQAAgEAAQEBAgACAQD1AQEAAQIAAQQCAgEAAQUAAQIBAPABAgECAQIDAQIAAgIAAggAAQEDAAEBAgEA7QEEAQEBBgABAQMEAQABAQECBAQAAQMDAgDsAQQAAQMGAwUCAAECAwABAgIAAQEAAQQAAQYAAQcA6wEIAQMCAwIGAwACAQIFBAECAQEAAQIEBgEFAQDoAQQBAgIAAQIFAQACAgEEAwQDAQIAAgEEAgQBBQDoAQECAAMCAgABAgEDAAEEAQABAgMCAwABAQIEBAIFAQDmAQMAAgIAAgIAAQECBgABAwABAgACAQABAQABAQABAgIEBQABAgEA4wECAQABAwIABQECAgAEAgIAAQEAAQEBBAIEAQUCAQDmAQEAAQIABAECAAQBAgIAAgEAAQECBAEEBQIBAOEBAgABAQQBAQAGAQIAAgIBAQIABQECAgEIBwIBAOIBAgMCAAEBAAQCAQEAAQIAAQIAAQMEAAQBAQQCBQoHAgDgAQEAAgQAAwEAAQECAQIAAgECAAICAQADAQIAAwQDCAcA3wEBAAECAAEBAgACBAEDAwIBAAIBAQABAQADAgABAgACAQACAgABCADfAQkAAQEAAgECAAEBAgYCAQIAAgIAAgIA9L8BqgERBw8JCQkHCQcJAwUFBQUDBQACAQEBAQEBAQABAQABAQEABAIAAQIAAgIBAgABAgEAAQQEAQIGBAYCAgoECAIGAgoKBAgGAgIODgABBAMDAQEBIAUCAAIHAQcCAQUBBQABAQUBAAIDAAEDAwEDAQEAAQMDAgMAAQMAAQEBAwIFAgUCAwMCAwABBQACAwABAQABBQABAQMBAAEBAwABAgEHAQACAwEBAgABAwIDAQBVAgIEBAYGCggICggKDhATBgUGAQIAAwICAQABAgACBAQBAQACAQADAgECAAICAAYBAAIDBAECAAEEAAEDAwQAAQUAAgQCCQYBBAUEAQUGAQABBAABCwABDAIBAgABAgIlIAECAAECAQQDAgEBBgIDAQQAAQEAAQEEAAEBAAECAAQBAgECAgMCAgMCAQIBAgIDBAACAwIEBQIAAQIBAgABAgECAQEAAQIAAQIAAQECAAIBAgIDAgIAVQEAAQEAAQECAQEEAQEAAQEAAQ8AAQIAAQIBAgABAgEAAQIBAgIGAwACAQABAgADAgABAgACAgAFAQAEAgEAAQEBAQIEAQEAAQIBBQABBAABBwQAAQQBAwQFBAACAgILAgQIAAIBAAECGRoDBAEABAIBAgUEAgAEAgEABAIFBAABAQQBAAEBBAEAAgIAAgMGAQMAAQIEAQAEAQQDAAEEAQEAAQYBAQcIAAICAQIAAgMCAgIAVwEBAAECAQEAAgMGAQEAAQ0AAQIAAwIAAQMEAAIGAgEBAAEBAAECAgMEAgIAFgEDAgADAQIFAQgBBwICAAECBQIBAAEBBgACCwIKAAEEAQACAh0aAQYBAQICAAECAQADAQIBAgMAAQYDAgAEAgABAwQBAAECAAMCAwICBQQAAgEAAQQDAgIDBAECAQABAQQAAQMEAQAFBAEAAgIAXAMAAwIFBAMCAAEDDQIBAgABBAMBBgMIAgEFBAECAgADBAAdAQEAAgIFBQgAAQUCAAECAAEBAQABAgABAwYAAQkEBgIAAwEAAQIDAgABBAEAAQECAAMCAQACAQQAAQECAQABAgIDAAQCAQQDAgEAAgIAAwIBAQABAgACAgABAwQAAQEAAQQBAQIABgEAAQICAF8DAQACAgABAQACAQsAAgIBAQQAAQIGAQEAAwIAAgICACIBAQABAQMGAQUGAwIAAgEAAQEEAAIDAQQCBAABAQACGxwCAQACAQQDAAIEAAEBAAECAQACAQQAAQEBAgACBAECBQQDBAMGAQACAgABAQIDAgABAgABBQQCAQEEAAEBAAECAQIBAAECAAMCAGIBAAIDAQICAQACDQABBgMCAgMIAgMAAQEAAQQBAgICACgDBwYAAQUEAAEEBwQAAQMEBwYECwIGAAEEAgECARcWBAMCAQICAAIBAAECAQIAAQECAAIBAgECAAIBBAECAwQAAQEAAgIBAAEBAQIEAQIDBAABAQICAwIABwMGAGcBAQIBAAEBAAECAQkCAwABBAEGAAEBAAICAAIEAC0FAgABBwYFCAMBAgUKAwIAAQsBDg8GBgYBAwICAwsBAwoKAgMBBQgCAQQBBA0QAQIBAgEBAAIEBQABAgQBAgABAQABAgABAQIBAQQBAAECAQIAAQMEBQQBBgECAGkBAAEBAgMCAAEDAgcDAgABAgQAAQELDgEAAQQCAC8DAQEBAQUKAxMcBwABDRAHEwIjQCcCLDkILAQNHgIDIxgFBA41BCwHERIBAQQBDxEkGwcGIB0FIBcCFhccGxYNEhUWDw4JEBMEDgsKBw4JAgQAAQMGAGkDAgEAAgEAAQELBAEGAgMAAgYHBAQAMgECBQYHBAABBgkTGgYJBQoEAQMVCRoAARphSAYAAQkGDxIGDwwSDRUFIgsMMyQGEQgNDAgFGyQCAAEFAQQAAQEGAgACAgECAwEJEAEJDAACAwQAAQMAAQQAawEAAQECAAMHAQYCAwABAgABBAA3AQUIEQABCBMSHQQHEAABEzI9OgQ/PDs4QRQZDgcqJwQSERwIDQwNBwYsJQMkDBESAAIBAgABHyAAARkaFRYVEg8UDxALDAECAAEDBAcIAQIAAQECAGwDAgIBAQIBCQYCAwMGAQIEAPIBAwACAgEAAQMCAwEGAQABAgIA8gEBAQEAAQIAAQEDAQQAAgQA9QEBAAIBAAEFAAEEAQABBAD2AQEAAQIBAQUCAAIEAPgBAwIAAgEBAQYA+gEDAAEEAwICAPoBAQACAQMAAQQA+wEBAAIBAgD8AQECAQECAPwBAQECAP8BAQCBAgEA/wEBAgD/AQEAgQIBAIACAQCBBAEAjBoCAIIGAgCABAIBAP8BAQCCBAIA/wECAQD/AQEEAIACAgD/AQQDAP8BAgD/AQIAggIBAP0BAgIDAgCAAgIA/gEBAgYA/gECBAcA/AECAAEEBQD9AQEEAQICAPwBAgEBBAD9AQQDBAUA/AECAwQBAAEBAPsBAQQBAQABCAD7AQIBAQEIBQD7AQEBBAYDAgD8AQIEBQICAP0BAwoDAwD7AQIAAQIHBAD8AQQAAgEBAQD6AQYDAAEDBAABAgD6AQECAAEEAQICAPoBAQEEAAEEAAEDAPoBBAABAwABAgUEAPoBAQQEAQcCBAD6AQQAAQUBCAIFAPoBAQMECAUCAPsBAwYBAQIBBAD6AQIBAgUBBgIA+gEEAwMECAcFAPoBAwIGAAEHAgD8AQQDAQICAPsBBAUAAQIAAgQA/gECAAEDAPoBAwIEAAEBAwIA+gEEAAEDAQEGAgD9AQICAQD+AQICAQMEAQEA+AECBAEDAAIIAQUA9wECAAEDAAMBAAECAAEBAPYBAgMAAgECAgIBBAMA9gEDAAECAQIAAgIBAgQFAPUBAQIBAAECAwQAAQIBAAEEAwD0AQEAAQEEAwQDBAEAAQECAAEBAPQBAQIBAgEAAQMEAgEAAQIAAQEA8gEBAAICAQIAAQECAAECAQD4AQEEAQIAAQIBBAUCAPQBAQICAQAEBAMAAQQDAAECAQDzAQEAAQIAAwMEAAECAQD2AQEEAwABAQABAQIAAQEEAQD0AQIAAQEAAQEBAQABAgECAPcBAgEBAgIA+wEBAAIEAQECAQEAAQIBAgD1AQEAAQECAQACAQIDAgIA9AEDBgEBAQQAAQECAQABAgEAAQIA8gECBQABAgIBAAEBAAICAAECAQDzAQEBBAABAQIAAQQAAQUCAAEBAgD1AQEBAgIBAAIGAwECAPYBAQQBAQEAAwICAQD3AQEAAgECAQABAgECAPYBAQADAwIAAQIA+wEBAAIBAgABAQIA+AEBAgMBAAECAQIA+QEBAAECAAIBAgD7AQEEAQABAgD9AQEAgAICAIACAQCAAgEAgAIBAP8BAQQAgAIBAIEEAQCDCAEA7NoBAgCAAgIA/wEGAP8BBAABAgD9AQIEAAEEAPwBAgQAAgECAQD5AQQAAQQDAAICAQD5AQQCAQIAAQIEAwD3AQIEAQIBAgQDBAIFAPUBAgICAAEBBAMCAgQAAQMA9QEGAAIBBAMEAgABBAABAQEA8gECBgMAAgIAAQQEAwYGAwIDAPEBBAIDAAECAgABCAEAAwQCAgEFAO8BBAEAAQQCBAACAwIBAgABCgMBAQEA7QECAgABAgABAgIBAAEGBQICAAEBBgAEBQDsAQIEAgABAgIDAgEBCAEBAgQIAQABBAUBAQDoAQICBAABAgEAAQIFBAEIAQMCAgICCgABAwEEAQMBAOYBAgQCAAIBAAECAwQCBAUCAAIGAAEJAIGEBACBhAQAgYQEAIGEBACBhAQ=", + "Layer1Texture": "assets/StartingGround.png", + "Layer1Color": "255;255;255", + "HeightBlending": 0.5, + "TextureVariation": 0, + "TextureSize": 192, + "CliffLayer": "3", + "Layer2Texture": "", + "Layer3Texture": "", + "CliffAngle": 45, + "CastShadow": false + } } ], "objectsFolderStructure": { @@ -1599,7 +1593,7 @@ "objectName": "Obstacle" }, { - "objectName": "Ground" + "objectName": "Terrain" }, { "objectName": "Flashlight" @@ -7804,7 +7798,6 @@ "name": "PlatformerMultitouchMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -8058,7 +8051,6 @@ "name": "Platformer3DMultitouchMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -8233,7 +8225,6 @@ "name": "Shooter3DMultitouchMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -8397,7 +8388,6 @@ "name": "FirstPersonMultitouchMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -9602,7 +9592,6 @@ "name": "PhysicsCar3DMultitouchMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -9815,7 +9804,6 @@ "name": "TopDownMultitouchMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -30070,6 +30058,7802 @@ } ], "eventsBasedObjects": [] + }, + { + "author": "", + "category": "General", + "dimension": "3D", + "extensionNamespace": "", + "fullName": "3D terrain", + "gdevelopVersion": ">=5.5.222", + "helpPath": "", + "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLXRlcnJhaW4iIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTQsNkwxMC4yNSwxMUwxMy4xLDE0LjhMMTEuNSwxNkM5LjgxLDEzLjc1IDcsMTAgNywxMEwxLDE4SDIzTDE0LDZaIiAvPjwvc3ZnPg==", + "name": "Terrain3D", + "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", + "shortDescription": "A ground with hills and valleys that can be sculpted and painted in the editor, with collisions.", + "version": "0.1.0", + "description": [ + "A 3D terrain object for open worlds, adventure or racing games: hills, mountains, islands or valleys, with painted layers of grass, dirt, rock or sand.", + "", + "- **Sculpt and paint in the scene editor**: select a terrain to show the tools (raise, lower, smooth, flatten and paint 4 layers).", + "- **Start from a relief** (hills, mountains, island) or **from a heightmap image**.", + "- **Steep slopes automatically show rock** (or any layer).", + "- **Natural borders between layers**: stones and tufts of a texture cover the gaps of the other where layers meet (height blending), and the repetition of textures is made less visible (texture variation).", + "- **Physics**: add the **3D physics** behavior to the terrain. Characters and objects collide with the ground exactly. The terrain is always static.", + "- **Pathfinding**: add the **floor/obstacle for pathfinding (navmesh based)** behavior to the terrain, so that characters with the navmesh pathfinding behavior walk on its hills and around its cliffs (in recent GDevelop versions).", + "- **Shape it with the Edits property**: a list of raise, flatten, smooth and paint strokes written in JSON, shown in the scene editor. It's readable and can be changed by hand or by an AI agent, like the relief, the heightmap and the other properties. Actions only change the terrain during the game.", + "- **Change it during the game** with actions (raise, lower, flatten, smooth, paint, along a line for paths and rivers) and read the ground height, slope or layer with expressions.", + "- **Grass**: add a **terrain grass** object over the terrain. Thousands of blades sway in the wind, grow where a layer (like grass) is painted and where the ground is not too steep, and are pushed aside by characters. Sculpt or paint the terrain and the grass follows.", + "- **Forests, rocks and bushes**: add a **scattered models** object over the terrain. It places hundreds or thousands of copies of a 3D model (trees, rocks, bushes, flowers...) on the ground and draws them all at once, which is much faster than an object for each. Copies grow where a layer is painted, away from steep slopes, and follow the ground when it's sculpted. Add or erase them with the brushes of the scene editor or with the Edits property, give trees trunk collisions, and cut them down with actions. Ready-made forests, rocks and bushes are in the asset store.", + "- **Tunnels and caves**: make holes in the ground (and fill them again) with the brushes, the Edits property or actions, and put a 3D model of the tunnel or the cave (with a mesh collision shape) under the ground. Nothing is drawn in holes, and characters, cars and rays go through them.", + "- **Made for large worlds**: the terrain is split in chunks that are hidden when off-screen and simplified when far away, and collisions are updated only where the ground changes." + ], + "origin": { + "identifier": "Terrain3D", + "name": "gdevelop-extension-store" + }, + "tags": [ + "3d", + "terrain", + "ground", + "heightmap", + "landscape", + "open world", + "grass", + "foliage", + "forest", + "trees", + "tunnel", + "cave" + ], + "authorIds": [], + "dependencies": [], + "globalVariables": [], + "sceneVariables": [], + "eventsFunctions": [ + { + "description": "Define helper classes JavaScript code.", + "fullName": "Define helper classes", + "functionType": "Action", + "name": "DefineHelperClasses", + "private": true, + "sentence": "Define helper classes JavaScript code", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "if (gdjs.__terrain3DExtension) {", + " return;", + "}", + "", + "// Tested by the gameplay tests of the \"3D island terrain\" example (GDevelop-examples repository).", + "", + "const groundsByScene = new WeakMap();", + "/**", + " * The grounds of a scene (the terrains), followed by the terrain grass.", + " *", + " * A ground has: `getVersion()` (changing when the ground changes),", + " * `containsPoint(x, y)`, `getHeightAt(x, y)` (Z position of the ground),", + " * `getSlopeAt(x, y)` (in degrees), `getLayerWeightAt(x, y, layer)` (how", + " * much a painted layer, from 1 to 4, is there, from 0 to 1) and optionally", + " * `getTintAt(x, y)` (the painted tint, as red, green and blue from 0 to 255).", + " * @param {gdjs.RuntimeScene} runtimeScene", + " * @returns {Set}", + " */", + "const getGrounds = (runtimeScene) => {", + " let grounds = groundsByScene.get(runtimeScene);", + " if (!grounds) {", + " grounds = new Set();", + " groundsByScene.set(runtimeScene, grounds);", + " }", + " return grounds;", + "};", + "", + "/**", + " * @param {number} seed", + " * @returns {() => number} Random numbers between 0 (included) and 1 (excluded), the same for the same seed.", + " */", + "const createRandom = (seed) => {", + " let state = seed >>> 0 || 1;", + " return () => {", + " state ^= state << 13;", + " state ^= state >>> 17;", + " state ^= state << 5;", + " return (state >>> 0) / 4294967296;", + " };", + "};", + "", + "/**", + " * Find the highest ground at a position: grounds can overlap (like an island over a sea floor).", + " * @param {Iterable} grounds", + " * @param {{ground: object | null, z: number}} result Set to the ground (null if there is none) and its Z position.", + " */", + "const findHighestGround = (grounds, x, y, result) => {", + " result.ground = null;", + " for (const ground of grounds) {", + " if (!ground.containsPoint(x, y)) continue;", + " const z = ground.getHeightAt(x, y);", + " if (!result.ground || z > result.z) {", + " result.ground = ground;", + " result.z = z;", + " }", + " }", + " return result;", + "};", + "", + "// The grounds or the objects placed on them can change every frame (while", + "// sculpting or moving them in the editor): things are placed again at most this often.", + "const PLACEMENT_DELAY = 0.3;", + "", + "/**", + " * Tells when things placed on the grounds of a scene (grass blades, scattered", + " * models) must be placed again: after the grounds or the given properties", + " * changed, at most every `PLACEMENT_DELAY` seconds.", + " */", + "class GroundPlacement {", + " /** @param {gdjs.RuntimeScene} runtimeScene */", + " constructor(runtimeScene) {", + " this.grounds = getGrounds(runtimeScene);", + " this.placedFrom = '';", + " this.timeSincePlacement = 0;", + " }", + "", + " /**", + " * @param {number} elapsedSeconds", + " * @param {string} propertiesKey Changes when the things must be placed again.", + " */", + " shouldPlaceAgain(elapsedSeconds, propertiesKey) {", + " this.timeSincePlacement += elapsedSeconds;", + " if (this.placedFrom && this.timeSincePlacement < PLACEMENT_DELAY) return false;", + " let placementKey = propertiesKey;", + " for (const ground of this.grounds) placementKey += '|' + ground.getVersion();", + " if (placementKey === this.placedFrom) return false;", + " this.placedFrom = placementKey;", + " this.timeSincePlacement = 0;", + " return true;", + " }", + "", + " placeAgain() {", + " this.placedFrom = '';", + " }", + "}", + "", + "/**", + " * Replace parts of the shader code of a Three.js material, with an error if", + " * a part is missing (for example after an update of Three.js).", + " * @param {string} shaderCode", + " * @param {Array<[string, string]>} replacements The searched code and its replacement.", + " * @param {string} materialName", + " */", + "const patchShaderCode = (shaderCode, replacements, materialName) => {", + " for (const [searchedCode, newCode] of replacements) {", + " if (!shaderCode.includes(searchedCode)) {", + " console.error(`${materialName}: \"${searchedCode}\" was not found in the shader, which won't look as expected.`);", + " }", + " shaderCode = shaderCode.replace(searchedCode, newCode);", + " }", + " return shaderCode;", + "};", + "", + "// Size of the object inner area. Instances are scaled from it, so these only", + "// define the default size of a new terrain.", + "const AREA_SIZE = 4096;", + "const AREA_DEPTH = 600;", + "", + "// Terrains have at most 8 x 8 chunks (so at most 64 draw calls), of at least", + "// 32 x 32 cells.", + "const MIN_CHUNK_CELLS = 32;", + "const MAX_CHUNKS_PER_SIDE = 8;", + "/** @param {number} resolution */", + "const getChunkCells = (resolution) => Math.max(MIN_CHUNK_CELLS, resolution / MAX_CHUNKS_PER_SIDE);", + "// Grid steps of each level of detail, and the distance (in chunk sizes)", + "// from which each one is used.", + "const LOD_STEPS = [1, 2, 4];", + "const LOD_DISTANCES = [0, 3, 6];", + "", + "const LAYER_COUNT = 4;", + "const RESOLUTIONS = [64, 128, 256, 512, 1024];", + "const DEFAULT_RESOLUTION = 256;", + "// Each undo step is a copy of the data: fewer are kept for big terrains.", + "const UNDO_STEPS_SAMPLES = 20 * 257 * 257;", + "", + "const clamp01 = (value) => (value < 0 ? 0 : value > 1 ? 1 : value);", + "const clampInteger = (value, min, max) =>", + " value < min ? min : value > max ? max : Math.floor(value);", + "", + "/**", + " * A rectangle of grid samples, inclusive. Empty when `minI > maxI`.", + " */", + "class SampleRectangle {", + " constructor() {", + " this.clear();", + " }", + "", + " clear() {", + " this.minI = Number.MAX_SAFE_INTEGER;", + " this.minJ = Number.MAX_SAFE_INTEGER;", + " this.maxI = -1;", + " this.maxJ = -1;", + " }", + "", + " isEmpty() {", + " return this.minI > this.maxI;", + " }", + "", + " /** @param {SampleRectangle} other */", + " add(other) {", + " if (other.isEmpty()) return;", + " this.minI = Math.min(this.minI, other.minI);", + " this.minJ = Math.min(this.minJ, other.minJ);", + " this.maxI = Math.max(this.maxI, other.maxI);", + " this.maxJ = Math.max(this.maxJ, other.maxJ);", + " }", + "", + " set(minI, minJ, maxI, maxJ) {", + " this.minI = minI;", + " this.minJ = minJ;", + " this.maxI = maxI;", + " this.maxJ = maxJ;", + " return this;", + " }", + "}", + "", + "/**", + " * The heights (between 0 and 1) and the painted layers weights of a square", + " * grid of `size * size` samples.", + " */", + "class TerrainData {", + " /** @param {number} resolution The number of cells on each side. */", + " constructor(resolution) {", + " this.resolution = resolution;", + " this.size = resolution + 1;", + " this.heights = new Float32Array(this.size * this.size);", + " /** 4 weights (0-255) per sample, one for each layer. */", + " this.splat = new Uint8Array(this.size * this.size * LAYER_COUNT);", + " this.fillLayer(0);", + " /**", + " * 4 bytes per sample: a tint multiplying the layers (red, green and", + " * blue, white for none) and the wetness (0 for dry).", + " */", + " this.colors = new Uint8Array(this.size * this.size * 4);", + " this.clearColors();", + " }", + "", + " clearColors() {", + " this.colors.fill(255);", + " for (let index = 3; index < this.colors.length; index += 4) {", + " this.colors[index] = 0;", + " }", + " }", + "", + " /** @param {number} layer */", + " fillLayer(layer) {", + " this.splat.fill(0);", + " for (let index = layer; index < this.splat.length; index += LAYER_COUNT) {", + " this.splat[index] = 255;", + " }", + " }", + "", + " getHeight(i, j) {", + " const last = this.resolution;", + " const clampedI = i < 0 ? 0 : i > last ? last : i;", + " const clampedJ = j < 0 ? 0 : j > last ? last : j;", + " return this.heights[clampedJ * this.size + clampedI];", + " }", + "", + " /**", + " * @param {number} gridX", + " * @param {number} gridY", + " * @returns {number} The height interpolated on the triangles that are drawn.", + " */", + " sampleHeight(gridX, gridY) {", + " const x = Math.min(Math.max(gridX, 0), this.resolution);", + " const y = Math.min(Math.max(gridY, 0), this.resolution);", + " const i = Math.min(Math.floor(x), this.resolution - 1);", + " const j = Math.min(Math.floor(y), this.resolution - 1);", + " const fx = x - i;", + " const fy = y - j;", + " const topRight = this.getHeight(i + 1, j);", + " const bottomLeft = this.getHeight(i, j + 1);", + " if (fx + fy <= 1) {", + " const topLeft = this.getHeight(i, j);", + " return topLeft + fx * (topRight - topLeft) + fy * (bottomLeft - topLeft);", + " }", + " const bottomRight = this.getHeight(i + 1, j + 1);", + " return (", + " bottomRight +", + " (1 - fx) * (bottomLeft - bottomRight) +", + " (1 - fy) * (topRight - bottomRight)", + " );", + " }", + "", + " /** @returns {number} The weight (0-1) of a layer, interpolated between samples. */", + " sampleLayerWeight(gridX, gridY, layer) {", + " const x = Math.min(Math.max(gridX, 0), this.resolution);", + " const y = Math.min(Math.max(gridY, 0), this.resolution);", + " const i = Math.min(Math.floor(x), this.resolution - 1);", + " const j = Math.min(Math.floor(y), this.resolution - 1);", + " const fx = x - i;", + " const fy = y - j;", + " const weight = (i, j) => this.splat[(j * this.size + i) * LAYER_COUNT + layer];", + " const top = weight(i, j) + fx * (weight(i + 1, j) - weight(i, j));", + " const bottom = weight(i, j + 1) + fx * (weight(i + 1, j + 1) - weight(i, j + 1));", + " return (top + fy * (bottom - top)) / 255;", + " }", + "", + " /**", + " * @param {number} channel 0, 1 and 2 for the tint, 3 for the wetness.", + " * @returns {number} The channel (0-1), interpolated between samples.", + " */", + " sampleColor(gridX, gridY, channel) {", + " const x = Math.min(Math.max(gridX, 0), this.resolution);", + " const y = Math.min(Math.max(gridY, 0), this.resolution);", + " const i = Math.min(Math.floor(x), this.resolution - 1);", + " const j = Math.min(Math.floor(y), this.resolution - 1);", + " const fx = x - i;", + " const fy = y - j;", + " const value = (i, j) => this.colors[(j * this.size + i) * 4 + channel];", + " const top = value(i, j) + fx * (value(i + 1, j) - value(i, j));", + " const bottom = value(i, j + 1) + fx * (value(i + 1, j + 1) - value(i, j + 1));", + " return (top + fy * (bottom - top)) / 255;", + " }", + "", + " /**", + " * @param {number} index The index of a sample.", + " * @returns {boolean} true if no layer is painted on the sample: it's a hole.", + " */", + " isHole(index) {", + " const offset = index * LAYER_COUNT;", + " return (this.splat[offset] | this.splat[offset + 1] | this.splat[offset + 2] | this.splat[offset + 3]) === 0;", + " }", + "", + " /**", + " * @returns {number} The sum of the layer weights (0-1), interpolated between samples: there is a", + " * hole where it's under 0.5, like what is drawn.", + " */", + " sampleSolidity(gridX, gridY) {", + " let solidity = 0;", + " for (let layer = 0; layer < LAYER_COUNT; layer++) solidity += this.sampleLayerWeight(gridX, gridY, layer);", + " return solidity;", + " }", + "", + " /** @returns {number} The index (from 0) of the layer painted the most. */", + " getDominantLayer(gridX, gridY) {", + " const i = clampInteger(Math.round(gridX), 0, this.resolution);", + " const j = clampInteger(Math.round(gridY), 0, this.resolution);", + " const offset = (j * this.size + i) * LAYER_COUNT;", + " let dominantLayer = 0;", + " for (let layer = 1; layer < LAYER_COUNT; layer++) {", + " if (this.splat[offset + layer] > this.splat[offset + dominantLayer]) {", + " dominantLayer = layer;", + " }", + " }", + " return dominantLayer;", + " }", + "", + " /** @param {TerrainData} other */", + " copyFrom(other) {", + " this.heights.set(other.heights);", + " this.splat.set(other.splat);", + " this.colors.set(other.colors);", + " }", + "", + " /**", + " * @param {number} resolution", + " * @returns {TerrainData} A copy of this data with another resolution.", + " */", + " resampled(resolution) {", + " const data = new TerrainData(resolution);", + " const ratio = this.resolution / resolution;", + " for (let j = 0; j < data.size; j++) {", + " for (let i = 0; i < data.size; i++) {", + " const index = j * data.size + i;", + " data.heights[index] = this.sampleHeight(i * ratio, j * ratio);", + " const sourceOffset =", + " (Math.round(j * ratio) * this.size + Math.round(i * ratio)) * LAYER_COUNT;", + " for (let layer = 0; layer < LAYER_COUNT; layer++) {", + " data.splat[index * LAYER_COUNT + layer] = this.splat[sourceOffset + layer];", + " }", + " for (let channel = 0; channel < 4; channel++) {", + " data.colors[index * 4 + channel] = this.colors[sourceOffset + channel];", + " }", + " }", + " }", + " return data;", + " }", + "}", + "", + "/**", + " * Brushes change the samples around a segment, with an effect going smoothly", + " * from full on the segment to none at `radius`. A point is a segment of", + " * length 0. Positions are in scene units relative to the terrain origin.", + " */", + "const brushes = {", + " /**", + " * @param {TerrainData} data", + " * @param {{cellWidth: number, cellHeight: number, ax: number, ay: number, bx: number, by: number, radius: number}} stroke", + " * @param {(sampleIndex: number, weight: number, t: number) => void} apply Called with the weight of", + " * the brush and the position (from 0 to 1) of the nearest point of the segment.", + " * @param {SampleRectangle} changedSamples", + " */", + " forEachSample(data, stroke, apply, changedSamples) {", + " const { cellWidth, cellHeight, ax, ay, bx, by, radius } = stroke;", + " const last = data.resolution;", + " const minI = clampInteger(Math.floor((Math.min(ax, bx) - radius) / cellWidth), 0, last);", + " const maxI = clampInteger(Math.ceil((Math.max(ax, bx) + radius) / cellWidth), 0, last);", + " const minJ = clampInteger(Math.floor((Math.min(ay, by) - radius) / cellHeight), 0, last);", + " const maxJ = clampInteger(Math.ceil((Math.max(ay, by) + radius) / cellHeight), 0, last);", + " changedSamples.set(minI, minJ, maxI, maxJ);", + " if (radius <= 0) return;", + "", + " const abX = bx - ax;", + " const abY = by - ay;", + " const abLengthSquared = abX * abX + abY * abY;", + " const radiusSquared = radius * radius;", + " for (let j = minJ; j <= maxJ; j++) {", + " const y = j * cellHeight;", + " for (let i = minI; i <= maxI; i++) {", + " const x = i * cellWidth;", + " const t =", + " abLengthSquared > 0", + " ? clamp01(((x - ax) * abX + (y - ay) * abY) / abLengthSquared)", + " : 0;", + " const dx = x - (ax + t * abX);", + " const dy = y - (ay + t * abY);", + " const distanceSquared = dx * dx + dy * dy;", + " if (distanceSquared >= radiusSquared) continue;", + " const falloff = 1 - distanceSquared / radiusSquared;", + " apply(j * data.size + i, falloff * falloff, t);", + " }", + " }", + " },", + "", + " raise(data, stroke, amount, changedSamples) {", + " const { heights } = data;", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index, weight) => {", + " heights[index] = clamp01(heights[index] + amount * weight);", + " },", + " changedSamples", + " );", + " },", + "", + " /**", + " * @param {number} targetHeight", + " * @param {number} [endTargetHeight] The target at the end of the segment, for a slope (like a road", + " * going up). The same target as at its start by default.", + " */", + " flatten(data, stroke, targetHeight, strength, changedSamples, endTargetHeight = targetHeight) {", + " const { heights } = data;", + " const startTarget = clamp01(targetHeight);", + " const endTarget = clamp01(endTargetHeight);", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index, weight, t) => {", + " const target = startTarget + (endTarget - startTarget) * t;", + " heights[index] += (target - heights[index]) * clamp01(weight * strength);", + " },", + " changedSamples", + " );", + " },", + "", + " smooth(data, stroke, strength, changedSamples) {", + " const { heights, size } = data;", + " const changes = [];", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index, weight) => {", + " const i = index % size;", + " const j = (index - i) / size;", + " let sum = 0;", + " for (let dj = -1; dj <= 1; dj++) {", + " for (let di = -1; di <= 1; di++) {", + " sum += data.getHeight(i + di, j + dj);", + " }", + " }", + " changes.push(index, heights[index] + (sum / 9 - heights[index]) * clamp01(weight * strength));", + " },", + " changedSamples", + " );", + " // Written after all averages are computed, so that the result", + " // doesn't depend on the order in which samples are visited.", + " for (let index = 0; index < changes.length; index += 2) {", + " heights[changes[index]] = changes[index + 1];", + " }", + " },", + "", + " paint(data, stroke, layer, strength, changedSamples) {", + " const { splat } = data;", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index, weight) => {", + " const amount = clamp01(weight * strength);", + " const offset = index * LAYER_COUNT;", + " for (let otherLayer = 0; otherLayer < LAYER_COUNT; otherLayer++) {", + " const target = otherLayer === layer ? 255 : 0;", + " const value = splat[offset + otherLayer];", + " // Rounded towards the target, so that light strokes still paint.", + " const change = (target - value) * amount;", + " splat[offset + otherLayer] = value + (change > 0 ? Math.ceil(change) : Math.floor(change));", + " }", + " },", + " changedSamples", + " );", + " },", + "", + " /** Remove all the layers, making a hole (`fill` or painting a layer fills it again). */", + " hole(data, stroke, changedSamples) {", + " const { splat } = data;", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index) => {", + " splat.fill(0, index * LAYER_COUNT, (index + 1) * LAYER_COUNT);", + " },", + " changedSamples", + " );", + " },", + "", + " /**", + " * Fill the holes in the radius with the layers of the ground around them:", + " * each pass fills the holes next to the ground with the average of their", + " * neighbors, from the edges of the holes inward.", + " */", + " fill(data, stroke, changedSamples) {", + " const { splat, size, resolution } = data;", + " /** @type {number[]} */", + " let holes = [];", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index) => {", + " if (data.isHole(index)) holes.push(index);", + " },", + " changedSamples", + " );", + " const filledWeights = new Float32Array(holes.length * LAYER_COUNT);", + " while (holes.length > 0) {", + " /** @type {number[]} */", + " const remainingHoles = [];", + " /** @type {number[]} */", + " const filledHoles = [];", + " for (const index of holes) {", + " const i = index % size;", + " const j = (index - i) / size;", + " const weights = filledWeights.subarray(filledHoles.length * LAYER_COUNT, (filledHoles.length + 1) * LAYER_COUNT);", + " weights.fill(0);", + " for (let neighborJ = Math.max(j - 1, 0); neighborJ <= Math.min(j + 1, resolution); neighborJ++) {", + " for (let neighborI = Math.max(i - 1, 0); neighborI <= Math.min(i + 1, resolution); neighborI++) {", + " const neighbor = neighborJ * size + neighborI;", + " if (data.isHole(neighbor)) continue;", + " for (let layer = 0; layer < LAYER_COUNT; layer++) weights[layer] += splat[neighbor * LAYER_COUNT + layer];", + " }", + " }", + " if (weights[0] + weights[1] + weights[2] + weights[3] > 0) filledHoles.push(index);", + " else remainingHoles.push(index);", + " }", + " if (filledHoles.length === 0) {", + " // No ground around (a hole bigger than the brush): the first layer is used.", + " for (const index of remainingHoles) splat[index * LAYER_COUNT] = 255;", + " break;", + " }", + " // Filled after the pass, so that the holes are filled from the edges inward.", + " filledHoles.forEach((index, filledIndex) => {", + " const weights = filledWeights.subarray(filledIndex * LAYER_COUNT, (filledIndex + 1) * LAYER_COUNT);", + " const total = weights[0] + weights[1] + weights[2] + weights[3];", + " for (let layer = 0; layer < LAYER_COUNT; layer++) {", + " splat[index * LAYER_COUNT + layer] = Math.round((weights[layer] / total) * 255);", + " }", + " });", + " holes = remainingHoles;", + " }", + " },", + "", + " /** @param {number[]} color The red, green and blue (0-255) of the tint: white removes it. */", + " tint(data, stroke, color, strength, changedSamples) {", + " brushes.paintColors(data, stroke, 0, color, strength, changedSamples);", + " },", + "", + " /** @param {number} wetness From 0 (dry) to 1. */", + " wet(data, stroke, wetness, strength, changedSamples) {", + " brushes.paintColors(data, stroke, 3, [Math.round(clamp01(wetness) * 255)], strength, changedSamples);", + " },", + "", + " /**", + " * Bring channels of the colors (see `TerrainData.colors`) towards targets.", + " * @param {number} firstChannel", + " * @param {number[]} targets From 0 to 255, for the channels from the first one.", + " */", + " paintColors(data, stroke, firstChannel, targets, strength, changedSamples) {", + " const { colors } = data;", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index, weight) => {", + " const amount = clamp01(weight * strength);", + " for (let channel = 0; channel < targets.length; channel++) {", + " const offset = index * 4 + firstChannel + channel;", + " // Rounded towards the target, so that light strokes still paint.", + " const change = (targets[channel] - colors[offset]) * amount;", + " colors[offset] += change > 0 ? Math.ceil(change) : Math.floor(change);", + " }", + " },", + " changedSamples", + " );", + " },", + "};", + "", + "const relief = (() => {", + " const hash = (x, y, seed) => {", + " let h = Math.imul(x, 374761393) + Math.imul(y, 668265263) + Math.imul(seed, 982451653);", + " h = Math.imul(h ^ (h >>> 13), 1274126177);", + " return ((h ^ (h >>> 16)) >>> 0) / 4294967296;", + " };", + " const valueNoise = (x, y, seed) => {", + " const x0 = Math.floor(x);", + " const y0 = Math.floor(y);", + " const fx = x - x0;", + " const fy = y - y0;", + " const sx = fx * fx * (3 - 2 * fx);", + " const sy = fy * fy * (3 - 2 * fy);", + " const top = hash(x0, y0, seed) + sx * (hash(x0 + 1, y0, seed) - hash(x0, y0, seed));", + " const bottom =", + " hash(x0, y0 + 1, seed) + sx * (hash(x0 + 1, y0 + 1, seed) - hash(x0, y0 + 1, seed));", + " return top + sy * (bottom - top);", + " };", + " /** Fractal noise between 0 and 1. */", + " const fractalNoise = (x, y, seed) => {", + " let value = 0;", + " let amplitude = 0.5;", + " let frequency = 1;", + " for (let octave = 0; octave < 5; octave++) {", + " value += amplitude * valueNoise(x * frequency, y * frequency, seed + octave);", + " amplitude *= 0.5;", + " frequency *= 2;", + " }", + " return value / 0.96875;", + " };", + " const smoothstep = (edge0, edge1, x) => {", + " const t = clamp01((x - edge0) / (edge1 - edge0));", + " return t * t * (3 - 2 * t);", + " };", + " const reliefHeights = {", + " Flat: () => 0,", + " Hills: (u, v, seed) => 0.05 + 0.5 * Math.pow(fractalNoise(u * 3, v * 3, seed), 2),", + " Mountains: (u, v, seed) => {", + " const ridge = 1 - Math.abs(2 * fractalNoise(u * 2.5, v * 2.5, seed) - 1);", + " return 0.9 * Math.pow(ridge, 2.5);", + " },", + " Island: (u, v, seed) => {", + " const distanceToCenter = 2 * Math.hypot(u - 0.5, v - 0.5);", + " const coast = 0.2 * (fractalNoise(u * 4, v * 4, seed + 7) - 0.5);", + " const land = smoothstep(0.95, 0.55, distanceToCenter + coast);", + " return land * (0.12 + 0.6 * Math.pow(fractalNoise(u * 3, v * 3, seed), 1.5));", + " },", + " };", + " const beachLayer = 3;", + "", + " return {", + " /**", + " * @param {TerrainData} data", + " * @param {string} reliefName Flat, Hills, Mountains or Island.", + " * @param {number} seed", + " */", + " generate(data, reliefName, seed) {", + " const getHeight = reliefHeights[reliefName] || reliefHeights.Flat;", + " const integerSeed = Math.floor(seed) | 0;", + " data.fillLayer(0);", + " for (let j = 0; j < data.size; j++) {", + " for (let i = 0; i < data.size; i++) {", + " const index = j * data.size + i;", + " const height = clamp01(", + " getHeight(i / data.resolution, j / data.resolution, integerSeed)", + " );", + " data.heights[index] = height;", + " if (reliefName === 'Island') {", + " const sand = Math.round(255 * smoothstep(0.16, 0.1, height));", + " const offset = index * LAYER_COUNT;", + " data.splat[offset] = 255 - sand;", + " data.splat[offset + beachLayer] = sand;", + " }", + " }", + " }", + " },", + " };", + "})();", + "", + "/**", + " * Edits written in the Edits property: a JSON list of brush strokes, like", + " * `[{\"tool\": \"raise\", \"x\": 0.5, \"y\": 0.5, \"radius\": 0.2, \"height\": 0.3}]`,", + " * applied on the relief or the heightmap. Positions and radius are fractions", + " * of the terrain size, heights and Z positions fractions of its depth: they", + " * are the same for all instances, whatever their size. They are made to be", + " * read and written by people and by AI agents.", + " */", + "const edits = {", + " /**", + " * @param {TerrainData} data", + " * @param {string} text", + " */", + " apply(data, text) {", + " if (!text.trim()) return;", + " let editList;", + " try {", + " editList = JSON.parse(text);", + " } catch (error) {", + " console.warn('The edits of a terrain are not valid JSON: ' + error.message);", + " return;", + " }", + " if (!Array.isArray(editList)) {", + " console.warn('The edits of a terrain must be a JSON list (like [{\"tool\": \"raise\", ...}]).');", + " return;", + " }", + " const numberOr = (value, defaultValue) => (typeof value === 'number' && isFinite(value) ? value : defaultValue);", + " const changedSamples = new SampleRectangle();", + " for (const edit of editList) {", + " if (!edit || typeof edit !== 'object') continue;", + " const x = numberOr(edit.x, 0.5);", + " const y = numberOr(edit.y, 0.5);", + " const stroke = {", + " cellWidth: 1 / data.resolution,", + " cellHeight: 1 / data.resolution,", + " ax: x,", + " ay: y,", + " bx: numberOr(edit.toX, x),", + " by: numberOr(edit.toY, y),", + " radius: numberOr(edit.radius, 0.1),", + " };", + " const strength = clamp01(numberOr(edit.strength, 1));", + " if (edit.tool === 'raise') {", + " brushes.raise(data, stroke, numberOr(edit.height, 0.1), changedSamples);", + " } else if (edit.tool === 'flatten') {", + " const z = numberOr(edit.z, 0);", + " brushes.flatten(data, stroke, z, strength, changedSamples, numberOr(edit.toZ, z));", + " } else if (edit.tool === 'smooth') {", + " // Smoothing the same fraction of the terrain at any resolution.", + " const passes = Math.max(Math.round(((1 + 9 * strength) * data.resolution) / 256), 1);", + " for (let pass = 0; pass < passes; pass++) brushes.smooth(data, stroke, 1, changedSamples);", + " } else if (edit.tool === 'paint') {", + " const layer = clampInteger(numberOr(edit.layer, 1) - 1, 0, LAYER_COUNT - 1);", + " brushes.paint(data, stroke, layer, strength, changedSamples);", + " } else if (edit.tool === 'tint') {", + " const color = gdjs.rgbOrHexToRGBColor(typeof edit.color === 'string' ? edit.color : '255;255;255');", + " brushes.tint(data, stroke, color, strength, changedSamples);", + " } else if (edit.tool === 'wetness') {", + " brushes.wet(data, stroke, numberOr(edit.amount, 1), strength, changedSamples);", + " } else if (edit.tool === 'hole') {", + " brushes.hole(data, stroke, changedSamples);", + " } else if (edit.tool === 'fill') {", + " brushes.fill(data, stroke, changedSamples);", + " } else {", + " console.warn('Unknown tool in the edits of a terrain: \"' + edit.tool + '\" (use raise, flatten, smooth, paint, tint, wetness, hole or fill).');", + " }", + " }", + " },", + "};", + "", + "/**", + " * @param {gdjs.RuntimeGame} game", + " * @param {string} imageResourceName", + " * @returns {CanvasImageSource | null} The image, or null if it's not loaded (yet).", + " */", + "const getLoadedImage = (game, imageResourceName) => {", + " const imageManager = game.getImageManager();", + " if (typeof imageManager.getImageSource === 'function') return imageManager.getImageSource(imageResourceName);", + " // Older GDevelop versions: a 192 x 192 placeholder is given for images that are not loaded.", + " const texture = imageManager.getPIXITexture(imageResourceName);", + " return texture && texture !== imageManager.getInvalidPIXITexture() ? texture.baseTexture.getDrawableSource() : null;", + "};", + "", + "/**", + " * Loads heights from an image: black is the lowest, white the highest.", + " * Heights have 16 bits of precision when the red channel holds the most", + " * significant byte and the green one the least significant (a gray image", + " * works as expected too).", + " * @param {TerrainData} data", + " * @param {gdjs.RuntimeGame} game", + " * @param {string} imageResourceName", + " * @returns {boolean} true if the image could be read.", + " */", + "const loadHeightsFromImage = (data, game, imageResourceName) => {", + " const source = getLoadedImage(game, imageResourceName);", + " if (!source) return false;", + "", + " const canvas = document.createElement('canvas');", + " canvas.width = source.width;", + " canvas.height = source.height;", + " const context = canvas.getContext('2d', { willReadFrequently: true });", + " if (!context) return false;", + " context.drawImage(source, 0, 0);", + " const pixels = context.getImageData(0, 0, source.width, source.height).data;", + " // Pixels are decoded before being interpolated, to keep the 16 bits.", + " const pixelHeight = (x, y) => {", + " const offset = (y * source.width + x) * 4;", + " return (256 * pixels[offset] + pixels[offset + 1]) / 65535;", + " };", + " for (let j = 0; j < data.size; j++) {", + " const y = (j / data.resolution) * (source.height - 1);", + " const y0 = Math.floor(y);", + " const y1 = Math.min(y0 + 1, source.height - 1);", + " for (let i = 0; i < data.size; i++) {", + " const x = (i / data.resolution) * (source.width - 1);", + " const x0 = Math.floor(x);", + " const x1 = Math.min(x0 + 1, source.width - 1);", + " const top = pixelHeight(x0, y0) + (x - x0) * (pixelHeight(x1, y0) - pixelHeight(x0, y0));", + " const bottom = pixelHeight(x0, y1) + (x - x0) * (pixelHeight(x1, y1) - pixelHeight(x0, y1));", + " data.heights[j * data.size + i] = top + (y - y0) * (bottom - top);", + " }", + " }", + " return true;", + "};", + "", + "/**", + " * Stores the data in a string. Only the differences with the base data (from", + " * the relief or the heightmap) are stored: heights on 16 bits, layer weights", + " * on 8 bits, then (since format 2) the tint and wetness on 8 bits. Each value is predicted from its neighbors and the", + " * prediction errors are written as variable length integers, with runs of", + " * zeros collapsed. Untouched or smoothly changed areas take almost no space.", + " */", + "const codec = (() => {", + " const formatVersion = '2';", + " // Format 1 has no tint and wetness.", + " const readableFormatVersions = ['1', '2'];", + "", + " class ByteWriter {", + " constructor() {", + " this.bytes = new Uint8Array(1024);", + " this.length = 0;", + " }", + " writeVarUint(value) {", + " if (this.length + 5 > this.bytes.length) {", + " const bytes = new Uint8Array(this.bytes.length * 2);", + " bytes.set(this.bytes);", + " this.bytes = bytes;", + " }", + " while (value >= 0x80) {", + " this.bytes[this.length++] = (value & 0x7f) | 0x80;", + " value >>>= 7;", + " }", + " this.bytes[this.length++] = value;", + " }", + " }", + "", + " class ByteReader {", + " /** @param {Uint8Array} bytes */", + " constructor(bytes) {", + " this.bytes = bytes;", + " this.position = 0;", + " }", + " readVarUint() {", + " let value = 0;", + " let shift = 0;", + " let byte;", + " do {", + " if (this.position >= this.bytes.length) throw new Error('Truncated terrain data.');", + " byte = this.bytes[this.position++];", + " value += (byte & 0x7f) * Math.pow(2, shift);", + " shift += 7;", + " } while (byte & 0x80);", + " return value;", + " }", + " }", + "", + " /** Predicts a value from the left, top and top-left ones. */", + " const predict = (values, size, index) => {", + " const i = index % size;", + " if (index < size) return i === 0 ? 0 : values[index - 1];", + " if (i === 0) return values[index - size];", + " return values[index - 1] + values[index - size] - values[index - size - 1];", + " };", + "", + " /**", + " * @param {ByteWriter} writer", + " * @param {Int32Array} values A grid of `size * size` values.", + " */", + " const writeGrid = (writer, values, size) => {", + " let zeroRun = 0;", + " for (let index = 0; index < values.length; index++) {", + " const error = values[index] - predict(values, size, index);", + " if (error === 0) {", + " zeroRun++;", + " continue;", + " }", + " if (zeroRun > 0) {", + " writer.writeVarUint(0);", + " writer.writeVarUint(zeroRun);", + " zeroRun = 0;", + " }", + " // Zigzag: small negative and positive numbers get small codes.", + " writer.writeVarUint(error > 0 ? error * 2 : -error * 2 - 1);", + " }", + " if (zeroRun > 0) {", + " writer.writeVarUint(0);", + " writer.writeVarUint(zeroRun);", + " }", + " };", + "", + " /** @param {ByteReader} reader */", + " const readGrid = (reader, size) => {", + " const values = new Int32Array(size * size);", + " let zeroRun = 0;", + " for (let index = 0; index < values.length; index++) {", + " let error = 0;", + " if (zeroRun > 0) {", + " zeroRun--;", + " } else {", + " const code = reader.readVarUint();", + " if (code === 0) {", + " zeroRun = reader.readVarUint() - 1;", + " } else {", + " error = code % 2 === 0 ? code / 2 : -(code + 1) / 2;", + " }", + " }", + " values[index] = predict(values, size, index) + error;", + " }", + " return values;", + " };", + "", + " const toBase64 = (bytes, length) => {", + " let binary = '';", + " for (let offset = 0; offset < length; offset += 0x8000) {", + " binary += String.fromCharCode.apply(null, bytes.subarray(offset, Math.min(offset + 0x8000, length)));", + " }", + " return btoa(binary);", + " };", + "", + " const fromBase64 = (base64) => {", + " const binary = atob(base64);", + " const bytes = new Uint8Array(binary.length);", + " for (let index = 0; index < binary.length; index++) {", + " bytes[index] = binary.charCodeAt(index);", + " }", + " return bytes;", + " };", + "", + " return {", + " /**", + " * @param {TerrainData} data", + " * @param {TerrainData} baseData The data without changes, with the same resolution.", + " */", + " encode(data, baseData) {", + " const writer = new ByteWriter();", + " const values = new Int32Array(data.heights.length);", + " for (let index = 0; index < values.length; index++) {", + " values[index] = Math.round((data.heights[index] - baseData.heights[index]) * 65535);", + " }", + " writeGrid(writer, values, data.size);", + " for (let layer = 0; layer < LAYER_COUNT; layer++) {", + " for (let index = 0; index < values.length; index++) {", + " const offset = index * LAYER_COUNT + layer;", + " values[index] = data.splat[offset] - baseData.splat[offset];", + " }", + " writeGrid(writer, values, data.size);", + " }", + " // Not painted, these grids are zeros: a few bytes each.", + " for (let channel = 0; channel < 4; channel++) {", + " for (let index = 0; index < values.length; index++) {", + " const offset = index * 4 + channel;", + " values[index] = data.colors[offset] - baseData.colors[offset];", + " }", + " writeGrid(writer, values, data.size);", + " }", + " return formatVersion + ';' + data.resolution + ';' + toBase64(writer.bytes, writer.length);", + " },", + "", + " /**", + " * Store grids of integers in a string, like the terrain data. Used by other objects (the scattered models).", + " * @param {Int32Array[]} grids Grids of `size * size` values.", + " * @param {number} size", + " */", + " encodeGrids(grids, size) {", + " const writer = new ByteWriter();", + " for (const grid of grids) writeGrid(writer, grid, size);", + " return toBase64(writer.bytes, writer.length);", + " },", + "", + " /**", + " * @param {string} base64 A string from `encodeGrids`.", + " * @param {number} size", + " * @param {number} count The number of grids.", + " * @returns {Int32Array[]} The grids (an error is thrown if the string is not valid).", + " */", + " decodeGrids(base64, size, count) {", + " const reader = new ByteReader(fromBase64(base64));", + " return Array.from({ length: count }, () => readGrid(reader, size));", + " },", + "", + " /**", + " * @param {string} text", + " * @param {(resolution: number) => TerrainData} createBaseData", + " * @returns {TerrainData | null} null if the text is empty or not valid.", + " */", + " decode(text, createBaseData) {", + " if (!text) return null;", + " const parts = text.split(';');", + " const resolution = parseInt(parts[1], 10);", + " if (readableFormatVersions.indexOf(parts[0]) === -1 || RESOLUTIONS.indexOf(resolution) === -1) {", + " return null;", + " }", + " try {", + " const reader = new ByteReader(fromBase64(parts[2]));", + " const data = createBaseData(resolution);", + " const heightChanges = readGrid(reader, data.size);", + " for (let index = 0; index < heightChanges.length; index++) {", + " data.heights[index] = clamp01(data.heights[index] + heightChanges[index] / 65535);", + " }", + " for (let layer = 0; layer < LAYER_COUNT; layer++) {", + " const weightChanges = readGrid(reader, data.size);", + " for (let index = 0; index < weightChanges.length; index++) {", + " const offset = index * LAYER_COUNT + layer;", + " // The base may have changed since the data was saved.", + " data.splat[offset] = Math.min(Math.max(data.splat[offset] + weightChanges[index], 0), 255);", + " }", + " }", + " if (parts[0] !== '1') {", + " for (let channel = 0; channel < 4; channel++) {", + " const colorChanges = readGrid(reader, data.size);", + " for (let index = 0; index < colorChanges.length; index++) {", + " const offset = index * 4 + channel;", + " data.colors[offset] = Math.min(Math.max(data.colors[offset] + colorChanges[index], 0), 255);", + " }", + " }", + " }", + " return data;", + " } catch (error) {", + " console.error('Terrain data could not be read:', error);", + " return null;", + " }", + " },", + " };", + "})();", + "", + "const lodIndices = new Map();", + "/**", + " * The triangles of a chunk, shared by all chunks of the same size. Chunk", + " * borders have a skirt going down, hiding the gaps between chunks drawn", + " * with different levels of detail.", + " * @param {number} chunkCells", + " * @param {number} step", + " */", + "const getLodIndex = (chunkCells, step) => {", + " const key = chunkCells + ';' + step;", + " const existingIndex = lodIndices.get(key);", + " if (existingIndex) return existingIndex;", + "", + " const sideVertices = chunkCells + 1;", + " const gridVertices = sideVertices * sideVertices;", + " const indices = [];", + " const grid = (i, j) => j * sideVertices + i;", + " for (let j = 0; j < chunkCells; j += step) {", + " for (let i = 0; i < chunkCells; i += step) {", + " const topLeft = grid(i, j);", + " const topRight = grid(i + step, j);", + " const bottomLeft = grid(i, j + step);", + " const bottomRight = grid(i + step, j + step);", + " indices.push(topLeft, topRight, bottomLeft, topRight, bottomRight, bottomLeft);", + " }", + " }", + " const borders = [", + " (k) => grid(k, 0),", + " (k) => grid(k, chunkCells),", + " (k) => grid(0, k),", + " (k) => grid(chunkCells, k),", + " ];", + " borders.forEach((borderVertex, border) => {", + " const skirtVertex = (k) => gridVertices + border * sideVertices + k;", + " for (let k = 0; k < chunkCells; k += step) {", + " const a = borderVertex(k);", + " const b = borderVertex(k + step);", + " const skirtA = skirtVertex(k);", + " const skirtB = skirtVertex(k + step);", + " // Both faces, as a skirt can be seen from either side.", + " indices.push(a, b, skirtB, a, skirtB, skirtA, a, skirtB, b, a, skirtA, skirtB);", + " }", + " });", + " const index = new THREE.BufferAttribute(new Uint16Array(indices), 1);", + " lodIndices.set(key, index);", + " return index;", + "};", + "", + "const vertexShaderDeclarations = `", + "varying vec2 vTerrainLocalXY;", + "varying float vTerrainLocalZ;", + "uniform float terrainWorldScale;", + "varying vec2 vTerrainWorldXY;", + "varying float vTerrainUp;", + "`;", + "const vertexShaderCode = `", + "vTerrainLocalXY = position.xy;", + "vTerrainLocalZ = position.z;", + "vTerrainWorldXY = (modelMatrix * vec4(transformed, 1.0)).xy * terrainWorldScale;", + "vTerrainUp = normalize(transpose(inverse(mat3(modelMatrix))) * objectNormal).z;", + "`;", + "const fragmentShaderDeclarations = `", + "uniform sampler2D terrainSplatMap;", + "uniform vec2 terrainSplatUvScaleAndOffset;", + "uniform vec3 terrainLayerColors[4];", + "uniform sampler2D terrainLayerMap0;", + "uniform sampler2D terrainLayerMap1;", + "uniform sampler2D terrainLayerMap2;", + "uniform sampler2D terrainLayerMap3;", + "uniform sampler2D terrainLayerNormalMap0;", + "uniform sampler2D terrainLayerNormalMap1;", + "uniform sampler2D terrainLayerNormalMap2;", + "uniform sampler2D terrainLayerNormalMap3;", + "uniform float terrainHasNormalMaps;", + "uniform float terrainNormalMapStrength;", + "uniform float terrainTextureSize;", + "uniform float terrainHeightBlending;", + "uniform float terrainTextureVariation;", + "uniform vec4 terrainCliffLayer;", + "uniform vec2 terrainCliffCosines;", + "uniform vec4 terrainBrush;", + "uniform sampler2D terrainColorMap;", + "uniform vec3 terrainWetShore;", + "varying vec2 vTerrainLocalXY;", + "varying float vTerrainLocalZ;", + "varying vec2 vTerrainWorldXY;", + "varying float vTerrainUp;", + "", + "float terrainHash(vec2 p) {", + " return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);", + "}", + "// The rotation of the copies of textures used to make repetition less visible.", + "const mat2 terrainCopyRotation = mat2(0.8, -0.6, 0.6, 0.8);", + "/** A bump of a normal map (from -1 to 1), mixed with its rotated copy in patches. */", + "vec3 terrainSampleNormal(sampler2D normalMap, vec2 uv, vec2 uv2, float patches) {", + " vec3 bump = texture2D(normalMap, uv).xyz * 2.0 - 1.0;", + " if (patches <= 0.0) return bump;", + " vec3 copyBump = texture2D(normalMap, uv2).xyz * 2.0 - 1.0;", + " // The bumps of the copy are rotated back with the inverse rotation.", + " copyBump.xy = transpose(terrainCopyRotation) * copyBump.xy;", + " return mix(bump, copyBump, patches);", + "}", + "float terrainNoise(vec2 p) {", + " vec2 i = floor(p);", + " vec2 f = fract(p);", + " vec2 u = f * f * (3.0 - 2.0 * f);", + " return mix(", + " mix(terrainHash(i), terrainHash(i + vec2(1.0, 0.0)), u.x),", + " mix(terrainHash(i + vec2(0.0, 1.0)), terrainHash(i + vec2(1.0, 1.0)), u.x),", + " u.y", + " );", + "}", + "`;", + "// Holes are samples without any painted layer: the interpolated sum of the", + "// weights gives smooth edges. Only compiled for terrains having holes, as", + "// discard makes drawing slower on some GPUs.", + "const holeShaderCode = `", + "#ifdef TERRAIN_HOLES", + "if (dot(texture2D(terrainSplatMap, vTerrainLocalXY * terrainSplatUvScaleAndOffset.x + terrainSplatUvScaleAndOffset.y), vec4(1.0)) < 0.5) discard;", + "#endif", + "`;", + "// For the shadows: the depth of holes is not drawn.", + "const holeDepthShaderDeclarations = `", + "uniform sampler2D terrainSplatMap;", + "uniform vec2 terrainSplatUvScaleAndOffset;", + "varying vec2 vTerrainLocalXY;", + "`;", + "", + "const fragmentShaderCode = holeShaderCode + `", + "// Borders between layers are made irregular and sharper, to look painted.", + "vec2 terrainNoisePosition = vTerrainWorldXY / 24.0;", + "vec4 terrainJitter = vec4(", + " terrainNoise(terrainNoisePosition),", + " terrainNoise(terrainNoisePosition + 19.1),", + " terrainNoise(terrainNoisePosition + 37.7),", + " terrainNoise(terrainNoisePosition + 53.3)", + ");", + "// Layer weights are in a texture, so that painted details stay the same", + "// whatever the level of detail of the ground.", + "vec2 terrainSplatUv = vTerrainLocalXY * terrainSplatUvScaleAndOffset.x + terrainSplatUvScaleAndOffset.y;", + "vec4 terrainWeights = texture2D(terrainSplatMap, terrainSplatUv) * (0.7 + 0.6 * terrainJitter);", + "terrainWeights = terrainWeights * terrainWeights;", + "terrainWeights /= max(dot(terrainWeights, vec4(1.0)), 0.00001);", + "float terrainCliff = 1.0 - smoothstep(terrainCliffCosines.x, terrainCliffCosines.y, vTerrainUp);", + "terrainWeights = mix(terrainWeights, terrainCliffLayer, terrainCliff * dot(terrainCliffLayer, vec4(1.0)));", + "", + "vec2 terrainUv = vTerrainWorldXY / terrainTextureSize;", + "vec3 terrainTexture0 = texture2D(terrainLayerMap0, terrainUv).rgb;", + "vec3 terrainTexture1 = texture2D(terrainLayerMap1, terrainUv).rgb;", + "vec3 terrainTexture2 = texture2D(terrainLayerMap2, terrainUv).rgb;", + "vec3 terrainTexture3 = texture2D(terrainLayerMap3, terrainUv).rgb;", + "", + "// Less visible repetition: in irregular patches, textures are replaced by a", + "// rotated and shifted copy of themselves, and colors slowly vary.", + "float terrainPatches = 0.0;", + "vec2 terrainUv2 = terrainUv;", + "float terrainColorVariation = 1.0;", + "if (terrainTextureVariation > 0.0) {", + " // Up to half of the ground is covered by the copies, each patch showing", + " // only one of them (mixing them would make them blurry).", + " float terrainPatchThreshold = 1.0 - 0.5 * terrainTextureVariation;", + " terrainPatches = smoothstep(terrainPatchThreshold - 0.05, terrainPatchThreshold + 0.05, terrainNoise(terrainUv * 0.23 + 7.3));", + " terrainUv2 = terrainCopyRotation * terrainUv + vec2(0.31, 0.73);", + " terrainTexture0 = mix(terrainTexture0, texture2D(terrainLayerMap0, terrainUv2).rgb, terrainPatches);", + " terrainTexture1 = mix(terrainTexture1, texture2D(terrainLayerMap1, terrainUv2).rgb, terrainPatches);", + " terrainTexture2 = mix(terrainTexture2, texture2D(terrainLayerMap2, terrainUv2).rgb, terrainPatches);", + " terrainTexture3 = mix(terrainTexture3, texture2D(terrainLayerMap3, terrainUv2).rgb, terrainPatches);", + " terrainColorVariation += terrainTextureVariation * 0.3 *", + " (terrainNoise(terrainUv * 0.11) + 0.5 * terrainNoise(terrainUv * 0.37 + 3.1) - 0.75);", + "}", + "", + "// Height-based blending: the brightness of a texture, compared to its", + "// average, is used as its height. Where layers meet, the high parts (stones,", + "// tufts) cover the low parts (sand, gaps) instead of fading into each other.", + "if (terrainHeightBlending > 0.0) {", + " const vec3 terrainLuminance = vec3(0.299, 0.587, 0.114);", + " vec4 terrainHeights = vec4(", + " dot(terrainTexture0 - textureLod(terrainLayerMap0, vec2(0.5), 16.0).rgb, terrainLuminance),", + " dot(terrainTexture1 - textureLod(terrainLayerMap1, vec2(0.5), 16.0).rgb, terrainLuminance),", + " dot(terrainTexture2 - textureLod(terrainLayerMap2, vec2(0.5), 16.0).rgb, terrainLuminance),", + " dot(terrainTexture3 - textureLod(terrainLayerMap3, vec2(0.5), 16.0).rgb, terrainLuminance)", + " );", + " vec4 terrainHeightWeights = terrainWeights + terrainHeights;", + " float terrainTop = max(max(terrainHeightWeights.x, terrainHeightWeights.y), max(terrainHeightWeights.z, terrainHeightWeights.w));", + " // Only painted layers (and the cliff layer) can show.", + " vec4 terrainHeightBlend = max(terrainHeightWeights - terrainTop + 0.1, 0.0) * step(0.004, terrainWeights);", + " terrainHeightBlend /= max(dot(terrainHeightBlend, vec4(1.0)), 0.00001);", + " terrainWeights = mix(terrainWeights, terrainHeightBlend, terrainHeightBlending);", + "}", + "", + "vec3 terrainColor = terrainColorVariation * (", + " terrainWeights.x * terrainLayerColors[0] * terrainTexture0 +", + " terrainWeights.y * terrainLayerColors[1] * terrainTexture1 +", + " terrainWeights.z * terrainLayerColors[2] * terrainTexture2 +", + " terrainWeights.w * terrainLayerColors[3] * terrainTexture3);", + "", + "// The painted tint multiplies all layers. Wet ground is darker (and", + "// shinier, see the roughness).", + "vec4 terrainTintAndWetness = texture2D(terrainColorMap, terrainSplatUv);", + "float terrainWetness = max(", + " terrainTintAndWetness.a,", + " terrainWetShore.z * (1.0 - smoothstep(terrainWetShore.x, terrainWetShore.x + terrainWetShore.y, vTerrainLocalZ))", + ");", + "terrainColor *= terrainTintAndWetness.rgb * (1.0 - 0.4 * terrainWetness);", + "diffuseColor.rgb *= terrainColor;", + "", + "if (terrainBrush.w > 0.5) {", + " float brushDistance = distance(vTerrainWorldXY, terrainBrush.xy);", + " float ringWidth = max(terrainBrush.z * 0.04, 1.5);", + " float ring = 1.0 - smoothstep(0.0, ringWidth, abs(brushDistance - terrainBrush.z));", + " float disc = brushDistance < terrainBrush.z ? 0.12 : 0.0;", + " diffuseColor.rgb = mix(diffuseColor.rgb, vec3(1.0), max(ring * 0.85, disc));", + "}", + "`;", + "", + "const fragmentShaderRoughnessCode = `", + "roughnessFactor = mix(roughnessFactor, 0.3, terrainWetness);", + "`;", + "", + "const fragmentShaderNormalCode = `", + "if (terrainHasNormalMaps > 0.5) {", + " vec3 terrainMapNormal =", + " terrainWeights.x * terrainSampleNormal(terrainLayerNormalMap0, terrainUv, terrainUv2, terrainPatches) +", + " terrainWeights.y * terrainSampleNormal(terrainLayerNormalMap1, terrainUv, terrainUv2, terrainPatches) +", + " terrainWeights.z * terrainSampleNormal(terrainLayerNormalMap2, terrainUv, terrainUv2, terrainPatches) +", + " terrainWeights.w * terrainSampleNormal(terrainLayerNormalMap3, terrainUv, terrainUv2, terrainPatches);", + " terrainMapNormal.xy *= terrainNormalMapStrength;", + " // The X and Y of textures follow the X and Y of the 3D world, projected on", + " // the ground (the camera space of the normal is used).", + " vec3 terrainTangent = (viewMatrix * vec4(1.0, 0.0, 0.0, 0.0)).xyz;", + " terrainTangent -= normal * dot(normal, terrainTangent);", + " // A ground facing the X axis (a rotated terrain) uses the Y axis instead.", + " if (dot(terrainTangent, terrainTangent) < 0.000001) {", + " terrainTangent = (viewMatrix * vec4(0.0, 1.0, 0.0, 0.0)).xyz;", + " terrainTangent -= normal * dot(normal, terrainTangent);", + " }", + " terrainTangent = normalize(terrainTangent);", + " vec3 terrainBitangent = cross(normal, terrainTangent);", + " normal = normalize(mat3(terrainTangent, terrainBitangent, normal) * terrainMapNormal);", + "}", + "`;", + "", + "let whiteTexture = null;", + "const getWhiteTexture = () => {", + " if (!whiteTexture) {", + " whiteTexture = new THREE.DataTexture(new Uint8Array([255, 255, 255, 255]), 1, 1);", + " whiteTexture.needsUpdate = true;", + " }", + " return whiteTexture;", + "};", + "", + "let flatNormalTexture = null;", + "const getFlatNormalTexture = () => {", + " if (!flatNormalTexture) {", + " flatNormalTexture = new THREE.DataTexture(new Uint8Array([128, 128, 255, 255]), 1, 1);", + " flatNormalTexture.needsUpdate = true;", + " }", + " return flatNormalTexture;", + "};", + "", + "const layerTextures = new WeakMap();", + "const layerNormalMaps = new WeakMap();", + "/**", + " * Textures of the image manager have no mipmaps: they would shimmer when", + " * repeated in the distance.", + " * @param {gdjs.RuntimeGame} game", + " * @param {string} resourceName", + " * @param {boolean} isNormalMap Normal maps are directions, not colors.", + " */", + "const getLayerTexture = (game, resourceName, isNormalMap) => {", + " if (!resourceName) return isNormalMap ? getFlatNormalTexture() : getWhiteTexture();", + " const imageTexture = game.getImageManager().getThreeTexture(resourceName);", + " const textures = isNormalMap ? layerNormalMaps : layerTextures;", + " let texture = textures.get(imageTexture);", + " if (!texture) {", + " texture = imageTexture.clone();", + " if (isNormalMap) texture.colorSpace = THREE.NoColorSpace;", + " texture.wrapS = THREE.RepeatWrapping;", + " texture.wrapT = THREE.RepeatWrapping;", + " texture.minFilter = THREE.LinearMipmapLinearFilter;", + " texture.generateMipmaps = true;", + " texture.anisotropy = 4;", + " texture.needsUpdate = true;", + " textures.set(imageTexture, texture);", + " }", + " return texture;", + "};", + "", + "/**", + " * A texture of 4 bytes per sample of a grid of `size * size` samples (layer", + " * weights, tint and wetness...), interpolated between samples.", + " * @param {Uint8Array} bytes", + " * @param {number} size", + " */", + "const createGridTexture = (bytes, size) => {", + " const texture = new THREE.DataTexture(bytes, size, size);", + " texture.magFilter = THREE.LinearFilter;", + " texture.minFilter = THREE.LinearFilter;", + " texture.userData.isUploaded = false;", + " texture.onUpdate = () => {", + " texture.userData.isUploaded = true;", + " };", + " return texture;", + "};", + "", + "/**", + " * Send the rows of the changed samples to the GPU, or the whole texture", + " * before its first upload (and in older GDevelop versions).", + " * @param {THREE.DataTexture} texture", + " * @param {SampleRectangle} samples", + " */", + "const uploadGridTextureRows = (texture, samples) => {", + " if (texture.userData.isUploaded && typeof texture.addUpdateRange === 'function') {", + " const size = texture.image.width;", + " for (let j = samples.minJ; j <= samples.maxJ; j++) {", + " texture.addUpdateRange((j * size + samples.minI) * 4, (samples.maxI - samples.minI + 1) * 4);", + " }", + " }", + " texture.needsUpdate = true;", + "};", + "", + "class TerrainRenderer {", + " /** @param {Terrain} terrain */", + " constructor(terrain) {", + " this.terrain = terrain;", + " this.group = new THREE.Group();", + " terrain.object.get3DRendererObject().add(this.group);", + " /** @type {Array<{mesh: THREE.Mesh, geometry: THREE.BufferGeometry, firstI: number, firstJ: number, center: THREE.Vector3, lod: number}>} */", + " this.chunks = [];", + " this.chunksPerSide = 0;", + " this.chunkCells = 0;", + " this.chunkSideVertices = 0;", + " this.chunkGridVertices = 0;", + " this.chunkVertices = 0;", + " /** @type {THREE.DataTexture | null} */", + " this.splatTexture = null;", + " /** @type {THREE.DataTexture | null} */", + " this.colorTexture = null;", + " this.uniforms = {", + " terrainSplatMap: { value: getWhiteTexture() },", + " terrainSplatUvScaleAndOffset: { value: new THREE.Vector2() },", + " terrainLayerColors: { value: [new THREE.Color(), new THREE.Color(), new THREE.Color(), new THREE.Color()] },", + " terrainLayerMap0: { value: getWhiteTexture() },", + " terrainLayerMap1: { value: getWhiteTexture() },", + " terrainLayerMap2: { value: getWhiteTexture() },", + " terrainLayerMap3: { value: getWhiteTexture() },", + " terrainLayerNormalMap0: { value: getFlatNormalTexture() },", + " terrainLayerNormalMap1: { value: getFlatNormalTexture() },", + " terrainLayerNormalMap2: { value: getFlatNormalTexture() },", + " terrainLayerNormalMap3: { value: getFlatNormalTexture() },", + " terrainHasNormalMaps: { value: 0 },", + " terrainNormalMapStrength: { value: 1 },", + " terrainTextureSize: { value: 256 },", + " terrainHeightBlending: { value: 0 },", + " terrainTextureVariation: { value: 0 },", + " terrainCliffLayer: { value: new THREE.Vector4() },", + " terrainCliffCosines: { value: new THREE.Vector2() },", + " terrainBrush: { value: new THREE.Vector4() },", + " terrainColorMap: { value: getWhiteTexture() },", + " // Z (in the terrain group) under which the ground is wet, fade height, and 1 if it's used.", + " terrainWetShore: { value: new THREE.Vector3() },", + " // Scene units by 3D world unit.", + " terrainWorldScale: { value: 1 },", + " };", + " this.material = new THREE.MeshStandardMaterial({ roughness: 1, metalness: 0 });", + " this.material.onBeforeCompile = (shader) => {", + " Object.assign(shader.uniforms, this.uniforms);", + " shader.vertexShader = patchShaderCode(shader.vertexShader, [", + " ['#include ', '#include \\n' + vertexShaderDeclarations],", + " ['#include ', '#include \\n' + vertexShaderCode],", + " ], 'Terrain3D');", + " shader.fragmentShader = patchShaderCode(shader.fragmentShader, [", + " ['#include ', '#include \\n' + fragmentShaderDeclarations],", + " ['#include ', fragmentShaderCode],", + " ['#include ', '#include \\n' + fragmentShaderRoughnessCode],", + " ['#include ', '#include \\n' + fragmentShaderNormalCode],", + " ], 'Terrain3D');", + " };", + " this.material.customProgramCacheKey = () => (this.hasHoles ? 'Terrain3DWithHoles' : 'Terrain3D');", + " this.hasHoles = false;", + " // Without them, holes would cast shadows.", + " this.depthMaterial = this._createHoleDepthMaterial(new THREE.MeshDepthMaterial());", + " this.distanceMaterial = this._createHoleDepthMaterial(new THREE.MeshDistanceMaterial());", + " this._cameraPosition = new THREE.Vector3();", + " this._chunkPosition = new THREE.Vector3();", + " this._scale = new THREE.Vector3();", + " }", + "", + " /**", + " * @param {THREE.MeshDepthMaterial | THREE.MeshDistanceMaterial} material", + " * @returns The material, drawing nothing in holes.", + " */", + " _createHoleDepthMaterial(material) {", + " material.defines = { TERRAIN_HOLES: '' };", + " material.onBeforeCompile = (shader) => {", + " Object.assign(shader.uniforms, this.uniforms);", + " shader.vertexShader = patchShaderCode(shader.vertexShader, [", + " ['#include ', '#include \\nvarying vec2 vTerrainLocalXY;'],", + " ['#include ', '#include \\nvTerrainLocalXY = position.xy;'],", + " ], 'Terrain3D shadows');", + " shader.fragmentShader = patchShaderCode(shader.fragmentShader, [", + " ['#include ', '#include \\n' + holeDepthShaderDeclarations],", + " ['#include ', holeShaderCode + '#include '],", + " ], 'Terrain3D shadows');", + " };", + " material.customProgramCacheKey = () => 'Terrain3DHoles';", + " return material;", + " }", + "", + " /**", + " * Use the shaders drawing holes, or the faster ones without them.", + " * @param {boolean} hasHoles", + " */", + " setHasHoles(hasHoles) {", + " if (hasHoles === this.hasHoles) return;", + " this.hasHoles = hasHoles;", + " // The material has its own defines (STANDARD).", + " if (hasHoles) this.material.defines.TERRAIN_HOLES = '';", + " else delete this.material.defines.TERRAIN_HOLES;", + " this.material.needsUpdate = true;", + " for (const chunk of this.chunks) this._setChunkShadowMaterials(chunk.mesh);", + " }", + "", + " /** @param {THREE.Mesh} mesh */", + " _setChunkShadowMaterials(mesh) {", + " mesh.customDepthMaterial = this.hasHoles ? this.depthMaterial : undefined;", + " mesh.customDistanceMaterial = this.hasHoles ? this.distanceMaterial : undefined;", + " }", + "", + " /** Recreate all the chunks, for example after the resolution changed. */", + " rebuild() {", + " this.disposeChunks();", + " const { data } = this.terrain;", + " this.chunkCells = getChunkCells(data.resolution);", + " this.chunkSideVertices = this.chunkCells + 1;", + " this.chunkGridVertices = this.chunkSideVertices * this.chunkSideVertices;", + " this.chunkVertices = this.chunkGridVertices + 4 * this.chunkSideVertices;", + " this.chunksPerSide = data.resolution / this.chunkCells;", + " const cellSize = AREA_SIZE / data.resolution;", + " // Sample centers are at the texel centers.", + " this.splatTexture = createGridTexture(data.splat, data.size);", + " this.uniforms.terrainSplatMap.value = this.splatTexture;", + " this.colorTexture = createGridTexture(data.colors, data.size);", + " // Tints are colors chosen like the layer colors.", + " this.colorTexture.colorSpace = THREE.SRGBColorSpace;", + " this.uniforms.terrainColorMap.value = this.colorTexture;", + " this.uniforms.terrainSplatUvScaleAndOffset.value.set(1 / (cellSize * data.size), 0.5 / data.size);", + " for (let chunkJ = 0; chunkJ < this.chunksPerSide; chunkJ++) {", + " for (let chunkI = 0; chunkI < this.chunksPerSide; chunkI++) {", + " const firstI = chunkI * this.chunkCells;", + " const firstJ = chunkJ * this.chunkCells;", + " const positions = new Float32Array(this.chunkVertices * 3);", + " for (let j = 0; j < this.chunkSideVertices; j++) {", + " for (let i = 0; i < this.chunkSideVertices; i++) {", + " const vertex = j * this.chunkSideVertices + i;", + " positions[vertex * 3] = (firstI + i) * cellSize;", + " positions[vertex * 3 + 1] = (firstJ + j) * cellSize;", + " }", + " }", + " // Skirt vertices are below border vertices.", + " this._forEachSkirtVertex((skirtVertex, borderVertex) => {", + " positions[skirtVertex * 3] = positions[borderVertex * 3];", + " positions[skirtVertex * 3 + 1] = positions[borderVertex * 3 + 1];", + " });", + " const geometry = new THREE.BufferGeometry();", + " geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));", + " geometry.setAttribute('normal', new THREE.BufferAttribute(new Float32Array(this.chunkVertices * 3), 3));", + " geometry.setIndex(getLodIndex(this.chunkCells, LOD_STEPS[0]));", + " geometry.boundingBox = new THREE.Box3();", + " geometry.boundingSphere = new THREE.Sphere();", + " const mesh = new THREE.Mesh(geometry, this.material);", + " mesh.matrixAutoUpdate = false;", + " // Rays hit the ground at its full resolution, whatever the level of", + " // detail drawn, and go through holes.", + " const chunkCells = this.chunkCells;", + " const terrainData = () => this.terrain.data;", + " const localPoint = new THREE.Vector3();", + " mesh.raycast = function (raycaster, intersects) {", + " const drawnIndex = geometry.index;", + " const firstIntersection = intersects.length;", + " geometry.setIndex(getLodIndex(chunkCells, LOD_STEPS[0]));", + " THREE.Mesh.prototype.raycast.call(this, raycaster, intersects);", + " geometry.setIndex(drawnIndex);", + " const data = terrainData();", + " const cellSize = AREA_SIZE / data.resolution;", + " for (let index = intersects.length - 1; index >= firstIntersection; index--) {", + " this.worldToLocal(localPoint.copy(intersects[index].point));", + " if (data.sampleSolidity(localPoint.x / cellSize, localPoint.y / cellSize) < 0.5) {", + " intersects.splice(index, 1);", + " }", + " }", + " };", + " this._setChunkShadowMaterials(mesh);", + " this.group.add(mesh);", + " this.chunks.push({", + " mesh,", + " geometry,", + " firstI,", + " firstJ,", + " center: new THREE.Vector3(", + " (firstI + this.chunkCells / 2) * cellSize,", + " (firstJ + this.chunkCells / 2) * cellSize,", + " 0", + " ),", + " lod: 0,", + " });", + " }", + " }", + " this.updateSamples(new SampleRectangle().set(0, 0, data.resolution, data.resolution), true);", + " this.updateMaterial();", + " }", + "", + " _forEachSkirtVertex(callback) {", + " for (let k = 0; k < this.chunkSideVertices; k++) {", + " callback(this.chunkGridVertices + k, k);", + " callback(this.chunkGridVertices + this.chunkSideVertices + k, this.chunkCells * this.chunkSideVertices + k);", + " callback(this.chunkGridVertices + 2 * this.chunkSideVertices + k, k * this.chunkSideVertices);", + " callback(", + " this.chunkGridVertices + 3 * this.chunkSideVertices + k,", + " k * this.chunkSideVertices + this.chunkCells", + " );", + " }", + " }", + "", + " /**", + " * Skirts go down only to the lowest point a coarser neighbor chunk can", + " * draw along the border, so that they can't be seen through holes.", + " * @param {Float32Array} positions", + " */", + " _updateSkirtHeights(positions) {", + " const coarsestStep = LOD_STEPS[LOD_STEPS.length - 1];", + " const borderVertices = [", + " (k) => k,", + " (k) => this.chunkCells * this.chunkSideVertices + k,", + " (k) => k * this.chunkSideVertices,", + " (k) => k * this.chunkSideVertices + this.chunkCells,", + " ];", + " borderVertices.forEach((borderVertex, border) => {", + " for (let k = 0; k < this.chunkSideVertices; k++) {", + " let minZ = Infinity;", + " const from = Math.max(k - coarsestStep, 0);", + " const to = Math.min(k + coarsestStep, this.chunkCells);", + " for (let other = from; other <= to; other++) {", + " minZ = Math.min(minZ, positions[borderVertex(other) * 3 + 2]);", + " }", + " const skirtVertex = this.chunkGridVertices + border * this.chunkSideVertices + k;", + " positions[skirtVertex * 3 + 2] = minZ;", + " }", + " });", + " }", + "", + " /**", + " * Update the vertices of the chunks sharing the given samples.", + " * @param {SampleRectangle} samples", + " * @param {boolean} hasPaintChanged", + " */", + " updateSamples(samples, hasPaintChanged) {", + " if (samples.isEmpty()) return;", + " const { data } = this.terrain;", + " const cellSize = AREA_SIZE / data.resolution;", + " const slopeFactor = AREA_DEPTH / cellSize;", + " // Normals of samples next to changed ones change too.", + " const minI = Math.max(samples.minI - 1, 0);", + " const minJ = Math.max(samples.minJ - 1, 0);", + " const maxI = Math.min(samples.maxI + 1, data.resolution);", + " const maxJ = Math.min(samples.maxJ + 1, data.resolution);", + " const firstChunkI = Math.max(Math.ceil(minI / this.chunkCells) - 1, 0);", + " const firstChunkJ = Math.max(Math.ceil(minJ / this.chunkCells) - 1, 0);", + " const lastChunkI = Math.min(Math.floor(maxI / this.chunkCells), this.chunksPerSide - 1);", + " const lastChunkJ = Math.min(Math.floor(maxJ / this.chunkCells), this.chunksPerSide - 1);", + "", + " for (let chunkJ = firstChunkJ; chunkJ <= lastChunkJ; chunkJ++) {", + " for (let chunkI = firstChunkI; chunkI <= lastChunkI; chunkI++) {", + " const chunk = this.chunks[chunkJ * this.chunksPerSide + chunkI];", + " const positions = chunk.geometry.attributes.position.array;", + " const normals = chunk.geometry.attributes.normal.array;", + " const fromI = Math.max(minI - chunk.firstI, 0);", + " const toI = Math.min(maxI - chunk.firstI, this.chunkCells);", + " const fromJ = Math.max(minJ - chunk.firstJ, 0);", + " const toJ = Math.min(maxJ - chunk.firstJ, this.chunkCells);", + " for (let j = fromJ; j <= toJ; j++) {", + " const gridJ = chunk.firstJ + j;", + " for (let i = fromI; i <= toI; i++) {", + " const gridI = chunk.firstI + i;", + " const vertex = j * this.chunkSideVertices + i;", + " const sample = gridJ * data.size + gridI;", + " positions[vertex * 3 + 2] = data.heights[sample] * AREA_DEPTH;", + "", + " const leftI = Math.max(gridI - 1, 0);", + " const rightI = Math.min(gridI + 1, data.resolution);", + " const upJ = Math.max(gridJ - 1, 0);", + " const downJ = Math.min(gridJ + 1, data.resolution);", + " const nx =", + " ((data.getHeight(leftI, gridJ) - data.getHeight(rightI, gridJ)) * slopeFactor) /", + " (rightI - leftI);", + " const ny =", + " ((data.getHeight(gridI, upJ) - data.getHeight(gridI, downJ)) * slopeFactor) /", + " (downJ - upJ);", + " const inverseLength = 1 / Math.sqrt(nx * nx + ny * ny + 1);", + " normals[vertex * 3] = nx * inverseLength;", + " normals[vertex * 3 + 1] = ny * inverseLength;", + " normals[vertex * 3 + 2] = inverseLength;", + " }", + " }", + " this._forEachSkirtVertex((skirtVertex, borderVertex) => {", + " for (let axis = 0; axis < 3; axis++) {", + " normals[skirtVertex * 3 + axis] = normals[borderVertex * 3 + axis];", + " }", + " });", + " this._updateSkirtHeights(positions);", + " let maxZ = 0;", + " for (let vertex = 0; vertex < this.chunkGridVertices; vertex++) {", + " maxZ = Math.max(maxZ, positions[vertex * 3 + 2]);", + " }", + " const chunkSize = this.chunkCells * cellSize;", + " chunk.geometry.boundingBox.min.set(chunk.firstI * cellSize, chunk.firstJ * cellSize, 0);", + " chunk.geometry.boundingBox.max.set(", + " chunk.firstI * cellSize + chunkSize,", + " chunk.firstJ * cellSize + chunkSize,", + " maxZ", + " );", + " chunk.geometry.boundingBox.getBoundingSphere(chunk.geometry.boundingSphere);", + " chunk.center.z = maxZ / 2;", + " chunk.geometry.attributes.position.needsUpdate = true;", + " chunk.geometry.attributes.normal.needsUpdate = true;", + " }", + " }", + " if (hasPaintChanged) {", + " // The data may have been replaced by one with the same resolution.", + " this.splatTexture.image.data = data.splat;", + " uploadGridTextureRows(this.splatTexture, samples);", + " this.colorTexture.image.data = data.colors;", + " uploadGridTextureRows(this.colorTexture, samples);", + " }", + " }", + "", + " updateMaterial() {", + " const { object } = this.terrain;", + " const game = object.getRuntimeScene().getGame();", + " const colors = [object._getLayer1Color(), object._getLayer2Color(), object._getLayer3Color(), object._getLayer4Color()];", + " const textures = [", + " object._getLayer1Texture(),", + " object._getLayer2Texture(),", + " object._getLayer3Texture(),", + " object._getLayer4Texture(),", + " ];", + " const normalMaps = [", + " object._getLayer1NormalMap(),", + " object._getLayer2NormalMap(),", + " object._getLayer3NormalMap(),", + " object._getLayer4NormalMap(),", + " ];", + " for (let layer = 0; layer < LAYER_COUNT; layer++) {", + " this.uniforms.terrainLayerColors.value[layer].set(gdjs.rgbOrHexStringToNumber(colors[layer]));", + " this.uniforms['terrainLayerMap' + layer].value = getLayerTexture(game, textures[layer], false);", + " this.uniforms['terrainLayerNormalMap' + layer].value = getLayerTexture(game, normalMaps[layer], true);", + " }", + " this.uniforms.terrainHasNormalMaps.value = normalMaps.some((normalMap) => !!normalMap) ? 1 : 0;", + " this.uniforms.terrainNormalMapStrength.value = Math.max(object._getNormalMapStrength(), 0);", + " this.uniforms.terrainTextureSize.value = Math.max(object._getTextureSize(), 1);", + " this.uniforms.terrainHeightBlending.value = clamp01(object._getHeightBlending());", + " this.uniforms.terrainTextureVariation.value = clamp01(object._getTextureVariation());", + "", + " const cliffLayer = parseInt(object._getCliffLayer(), 10);", + " const cliffAngle = gdjs.toRad(object._getCliffAngle());", + " const cliffBlendAngle = gdjs.toRad(4);", + " this.uniforms.terrainCliffLayer.value.set(", + " cliffLayer === 1 ? 1 : 0,", + " cliffLayer === 2 ? 1 : 0,", + " cliffLayer === 3 ? 1 : 0,", + " cliffLayer === 4 ? 1 : 0", + " );", + " this.uniforms.terrainCliffCosines.value.set(", + " Math.cos(cliffAngle + cliffBlendAngle),", + " Math.cos(cliffAngle - cliffBlendAngle)", + " );", + "", + " const castShadow = object._getCastShadow();", + " const receiveShadow = object._getReceiveShadow();", + " for (const chunk of this.chunks) {", + " chunk.mesh.castShadow = castShadow;", + " chunk.mesh.receiveShadow = receiveShadow;", + " }", + " }", + "", + " /**", + " * Show the brush circle of the editor.", + " * @param {THREE.Vector3 | null} localPosition", + " * @param {number} radius In scene units.", + " */", + " setBrush(localPosition, radius) {", + " const brush = this.uniforms.terrainBrush.value;", + " if (!localPosition) {", + " brush.w = 0;", + " return;", + " }", + " this.group.updateWorldMatrix(true, false);", + " const worldPosition = this._chunkPosition.copy(localPosition).applyMatrix4(this.group.matrixWorld);", + " const worldScale = this.uniforms.terrainWorldScale.value;", + " brush.set(worldPosition.x * worldScale, worldPosition.y * worldScale, radius, 1);", + " }", + "", + " /** @param {THREE.Camera | null} camera */", + " updateLevelsOfDetail(camera) {", + " if (!camera || !this.group.visible) return;", + " const matrixWorld = this.group.matrixWorld;", + " this._cameraPosition.setFromMatrixPosition(camera.matrixWorld);", + " this._scale.setFromMatrixScale(matrixWorld);", + " const cellSize = AREA_SIZE / this.terrain.data.resolution;", + " const chunkWorldSize = this.chunkCells * cellSize * Math.max(this._scale.x, this._scale.y);", + " for (const chunk of this.chunks) {", + " const distance =", + " this._chunkPosition.copy(chunk.center).applyMatrix4(matrixWorld).distanceTo(this._cameraPosition) /", + " chunkWorldSize;", + " let lod = LOD_DISTANCES.length - 1;", + " while (lod > 0 && distance < LOD_DISTANCES[lod]) lod--;", + " if (lod !== chunk.lod) {", + " chunk.lod = lod;", + " chunk.geometry.setIndex(getLodIndex(this.chunkCells, LOD_STEPS[lod]));", + " }", + " }", + " }", + "", + " disposeChunks() {", + " for (const chunk of this.chunks) {", + " chunk.mesh.removeFromParent();", + " // The index is shared by all chunks: it must stay on the GPU.", + " chunk.geometry.setIndex(null);", + " chunk.geometry.dispose();", + " }", + " this.chunks.length = 0;", + " if (this.splatTexture) this.splatTexture.dispose();", + " this.splatTexture = null;", + " if (this.colorTexture) this.colorTexture.dispose();", + " this.colorTexture = null;", + " }", + "", + " dispose() {", + " this.disposeChunks();", + " this.material.dispose();", + " this.depthMaterial.dispose();", + " this.distanceMaterial.dispose();", + " this.group.removeFromParent();", + " }", + "}", + "", + "/**", + " * Makes a Physics3D behavior use a height field shape following the terrain.", + " * Changes of heights are applied to the shape without recreating it.", + " * @implements {gdjs.Physics3DRuntimeBehavior.BodyUpdater}", + " */", + "class TerrainBodyUpdater {", + " /**", + " * @param {Terrain} terrain", + " * @param {gdjs.Physics3DRuntimeBehavior} physics", + " */", + " constructor(terrain, physics) {", + " this.terrain = terrain;", + " this.physics = physics;", + " this.defaultBodyUpdater = physics.bodyUpdater;", + " this.changedSamples = new SampleRectangle();", + " /** @type {Jolt.HeightFieldShape | null} */", + " this.heightField = null;", + " this.heightScale = 1;", + " this.heightsBufferPointer = 0;", + " this.heightsBufferLength = 0;", + " this.bodyX = 0;", + " this.bodyY = 0;", + " this.bodyZ = 0;", + " this.bodyHeight = 0;", + " physics.bodyType = 'Static';", + " physics.bodyUpdater = this;", + " }", + "", + " createShape() {", + " const { data, object } = this.terrain;", + " const worldInvScale = this.physics._sharedData.worldInvScale;", + " const { size } = data;", + " this.heightScale = object.getDepth() * worldInvScale;", + "", + " // Changes of heights (SetHeights) break collisions unless the sample", + " // count is a power of 2: extra samples have no collision.", + " const sampleCount = Math.pow(2, Math.ceil(Math.log2(size)));", + " const settings = new Jolt.HeightFieldShapeSettings();", + " settings.mScale.Set(", + " (object.getWidth() / data.resolution) * worldInvScale,", + " 1,", + " (object.getHeight() / data.resolution) * worldInvScale", + " );", + " settings.mSampleCount = sampleCount;", + " settings.mBlockSize = 4;", + " // Allows later changes of heights on the whole depth of the object.", + " settings.mMinHeightValue = 0;", + " settings.mMaxHeightValue = this.heightScale;", + " settings.mHeightSamples.resize(sampleCount * sampleCount);", + " const samples = new Float32Array(", + " Jolt.HEAPF32.buffer,", + " Jolt.getPointer(settings.mHeightSamples.data()),", + " sampleCount * sampleCount", + " );", + " const noCollision = Jolt.HeightFieldShapeConstantValues.prototype.cNoCollisionValue;", + " samples.fill(noCollision);", + " // Height fields go along X and Z: rows are flipped so that, once", + " // rotated to have heights along Z, rows go along Y.", + " for (let j = 0; j < size; j++) {", + " const row = (size - 1 - j) * sampleCount;", + " for (let i = 0; i < size; i++) {", + " samples[row + i] = this._getSampleHeight(j * size + i, noCollision);", + " }", + " }", + " const shape = settings.Create().Get();", + " Jolt.destroy(settings);", + " this.heightField = Jolt.castObject(shape, Jolt.HeightFieldShape);", + " this.changedSamples.clear();", + " return shape;", + " }", + "", + " /** @returns {number} The height of a sample in the shape, or no collision in a hole. */", + " _getSampleHeight(index, noCollision) {", + " const height = this.terrain.surfaceHeights[index];", + " return Number.isNaN(height) ? noCollision : height * this.heightScale;", + " }", + "", + " _getBodyPosition() {", + " const { object } = this.terrain;", + " const worldInvScale = this.physics._sharedData.worldInvScale;", + " this.bodyX = object.getX();", + " this.bodyY = object.getY();", + " this.bodyZ = object.getZ();", + " // The origin of the height field is at the bottom of the terrain (see createShape).", + " this.bodyHeight = object.getHeight();", + " return this.physics._sharedData.getRVec3(", + " object.getX() * worldInvScale,", + " (object.getY() + object.getHeight()) * worldInvScale,", + " object.getZ() * worldInvScale", + " );", + " }", + "", + " _getBodyRotation() {", + " // A quarter turn around X: heights go along Z.", + " return this.physics._sharedData.getQuat(Math.SQRT1_2, 0, 0, Math.SQRT1_2);", + " }", + "", + " createAndAddBody() {", + " const { physics } = this;", + " const { bodyInterface } = physics._sharedData;", + " const shape = this.createShape();", + " const settings = new Jolt.BodyCreationSettings(", + " shape,", + " this._getBodyPosition(),", + " this._getBodyRotation(),", + " Jolt.EMotionType_Static,", + " physics.getBodyLayer()", + " );", + " settings.mFriction = physics.friction;", + " settings.mRestitution = physics.restitution;", + " const body = bodyInterface.CreateBody(settings);", + " Jolt.destroy(settings);", + " bodyInterface.AddBody(body.GetID(), Jolt.EActivation_DontActivate);", + " return body;", + " }", + "", + " updateObjectFromBody() {", + " // The terrain is static: the body never moves it.", + " }", + "", + " updateBodyFromObject() {", + " const { physics } = this;", + " const body = physics._body;", + " if (!body) return;", + " const { object } = this.terrain;", + " const { bodyInterface } = physics._sharedData;", + " if (", + " object.getX() !== this.bodyX ||", + " object.getY() !== this.bodyY ||", + " object.getZ() !== this.bodyZ ||", + " object.getHeight() !== this.bodyHeight", + " ) {", + " bodyInterface.SetPositionAndRotation(", + " body.GetID(),", + " this._getBodyPosition(),", + " this._getBodyRotation(),", + " Jolt.EActivation_DontActivate", + " );", + " }", + " if (!this.changedSamples.isEmpty()) {", + " this._applyChangedHeights(body);", + " }", + " }", + "", + " _applyChangedHeights(body) {", + " const { data } = this.terrain;", + " const { size } = data;", + " const { minI, minJ, maxI, maxJ } = this.changedSamples;", + " // Jolt only changes blocks of samples. Rows are flipped and extra", + " // samples have no collision (see createShape).", + " const blockSize = this.heightField.GetBlockSize();", + " const sampleCount = this.heightField.GetSampleCount();", + " const alignDown = (value) => Math.floor(value / blockSize) * blockSize;", + " const alignUp = (value) => Math.min(Math.ceil(value / blockSize) * blockSize, sampleCount);", + " const firstX = alignDown(minI);", + " const firstRow = alignDown(size - 1 - maxJ);", + " const width = alignUp(maxI + 1) - firstX;", + " const height = alignUp(size - minJ) - firstRow;", + " if (width * height > this.heightsBufferLength) {", + " if (this.heightsBufferPointer) Jolt._webidl_free(this.heightsBufferPointer);", + " this.heightsBufferLength = width * height;", + " this.heightsBufferPointer = Jolt._webidl_malloc(this.heightsBufferLength * 4);", + " }", + " const heights = new Float32Array(Jolt.HEAPF32.buffer, this.heightsBufferPointer, width * height);", + " const noCollision = Jolt.HeightFieldShapeConstantValues.prototype.cNoCollisionValue;", + " for (let row = 0; row < height; row++) {", + " const j = size - 1 - (firstRow + row);", + " for (let column = 0; column < width; column++) {", + " const i = firstX + column;", + " heights[row * width + column] =", + " i < size && j >= 0 ? this._getSampleHeight(j * size + i, noCollision) : noCollision;", + " }", + " }", + " const sharedData = this.physics._sharedData;", + " this.heightField.SetHeights(", + " firstX,", + " firstRow,", + " width,", + " height,", + " this.heightsBufferPointer,", + " width,", + " sharedData.jolt.GetTempAllocator()", + " );", + " sharedData.bodyInterface.NotifyShapeChanged(", + " body.GetID(),", + " sharedData.getVec3(0, 0, 0),", + " false,", + " Jolt.EActivation_DontActivate", + " );", + " this._wakeUpBodiesOnChangedSamples();", + " this.changedSamples.clear();", + " }", + "", + " /** Bodies sleeping on the changed area would otherwise float or sink. */", + " _wakeUpBodiesOnChangedSamples() {", + " const { object, data } = this.terrain;", + " const worldInvScale = this.physics._sharedData.worldInvScale;", + " const cellWidth = object.getWidth() / data.resolution;", + " const cellHeight = object.getHeight() / data.resolution;", + " const { minI, minJ, maxI, maxJ } = this.changedSamples;", + " const min = new Jolt.Vec3(", + " (object.getX() + (minI - 1) * cellWidth) * worldInvScale,", + " (object.getY() + (minJ - 1) * cellHeight) * worldInvScale,", + " (object.getZ() - 1) * worldInvScale", + " );", + " const max = new Jolt.Vec3(", + " (object.getX() + (maxI + 1) * cellWidth) * worldInvScale,", + " (object.getY() + (maxJ + 1) * cellHeight) * worldInvScale,", + " (object.getZ() + object.getDepth() + 1) * worldInvScale", + " );", + " const box = new Jolt.AABox(min, max);", + " const broadPhaseLayerFilter = new Jolt.BroadPhaseLayerFilter();", + " const objectLayerFilter = new Jolt.ObjectLayerFilter();", + " this.physics._sharedData.bodyInterface.ActivateBodiesInAABox(box, broadPhaseLayerFilter, objectLayerFilter);", + " Jolt.destroy(min);", + " Jolt.destroy(max);", + " Jolt.destroy(box);", + " Jolt.destroy(broadPhaseLayerFilter);", + " Jolt.destroy(objectLayerFilter);", + " }", + "", + " recreateShape() {", + " const body = this.physics._body;", + " if (!body) return;", + " this.physics._sharedData.bodyInterface.SetShape(", + " body.GetID(),", + " this.createShape(),", + " false,", + " Jolt.EActivation_DontActivate", + " );", + " const { resolution } = this.terrain.data;", + " this.changedSamples.set(0, 0, resolution, resolution);", + " this._wakeUpBodiesOnChangedSamples();", + " this.changedSamples.clear();", + " }", + "", + " destroyBody() {", + " this.defaultBodyUpdater.destroyBody();", + " this.heightField = null;", + " }", + "", + " dispose() {", + " if (this.heightsBufferPointer) Jolt._webidl_free(this.heightsBufferPointer);", + " this.heightsBufferPointer = 0;", + " this.heightsBufferLength = 0;", + " }", + "}", + "", + "/** @param {TerrainData} data */", + "const copyTerrainData = (data) => {", + " const copy = new TerrainData(data.resolution);", + " copy.copyFrom(data);", + " return copy;", + "};", + "", + "// Generating a relief or reading an image is long for big terrains, and done", + "// for every instance and every hot-reload: the results are kept.", + "const baseDataCache = new Map();", + "const MAX_CACHED_BASE_DATA = 8;", + "// Changes of the shape of a terrain kept for consumers of its surface that", + "// missed some versions: older changes update the whole surface.", + "const MAX_SURFACE_CHANGES = 64;", + "// Unique among all terrains: a terrain replaced by another one never has its version.", + "let lastGroundVersion = 0;", + "", + "/**", + " * The terrain of a Terrain3D object: its data, how it's drawn and its", + " * surface (for collisions and navigation meshes). Positions are in the scene.", + " */", + "class Terrain {", + " /** @param {gdjs.CustomRuntimeObject3D} object */", + " constructor(object) {", + " this.object = object;", + " /** @type {TerrainData} */", + " this.data = new TerrainData(DEFAULT_RESOLUTION);", + " /** The data before any sculpting or painting, from the relief or heightmap. */", + " this.baseData = this.data;", + " this.renderer = new TerrainRenderer(this);", + " /** @type {TerrainBodyUpdater[]} */", + " this.bodyUpdaters = [];", + " this.changedSamples = new SampleRectangle();", + " this.hasPaintChanged = false;", + " /** Increased each time the ground changes, for objects following it (like grass). */", + " this.version = 0;", + " /** Increased each time the shape of the ground changes (not its paint or position). */", + " this.surfaceVersion = 0;", + " /**", + " * The last changes of the shape of the ground, for `getChangedArea`.", + " * @type {Array<{version: number, samples: SampleRectangle}>}", + " */", + " this.surfaceChanges = [];", + " /** The changes since this version are all in `surfaceChanges`. */", + " this.surfaceChangesStartVersion = 0;", + " this._stroke = { cellWidth: 1, cellHeight: 1, ax: 0, ay: 0, bx: 0, by: 0, radius: 0 };", + " this._strokeChangedSamples = new SampleRectangle();", + " /** The properties the data was loaded from, to reload it only if they change. */", + " this.loadedFrom = '';", + " /** The heightmap image being loaded: the relief is used until it is. */", + " this.loadingHeightmapImage = '';", + " this.isDisposed = false;", + " /** The last value of the sculpt data property read or saved. */", + " this.sculptData = '';", + " /**", + " * Values saved by the editor tools: the editor sends them back with a", + " * hot-reload, possibly after newer ones were saved.", + " * @type {string[]}", + " */", + " this.savedSculptData = [];", + " /** The heights of the surface: the heights of the data, and NaN in holes. */", + " this.surfaceHeights = new Float32Array(0);", + " this.loadFromProperties();", + " this._transform = this._getTransform();", + " getGrounds(object.getRuntimeScene()).add(this);", + " /**", + " * The shape of the ground, for the physics engine and navigation meshes.", + " * @type {gdjs.Surface}", + " */", + " this.surface = {", + " getVersion: () => this.surfaceVersion,", + " getChangedArea: (sinceVersion) => this.getChangedArea(sinceVersion),", + " getTriangles: () => null,", + " getHeightField: () => ({ columns: this.data.size, rows: this.data.size, heights: this.surfaceHeights }),", + " };", + " if (typeof object.setSurface === 'function') {", + " object.setSurface(this.surface);", + " } else if (gdjs.Physics3DRuntimeBehavior) {", + " // Older GDevelop versions don't use surfaces.", + " for (const behavior of object._behaviors) {", + " if (behavior instanceof gdjs.Physics3DRuntimeBehavior) {", + " this.bodyUpdaters.push(new TerrainBodyUpdater(this, behavior));", + " }", + " }", + " }", + " }", + "", + " _getSourceProperties() {", + " const { object } = this;", + " return [", + " object._getResolution(),", + " object._getRelief(),", + " object._getSeed(),", + " object._getHeightmapImage(),", + " object._getEdits(),", + " ].join('|');", + " }", + "", + " loadFromProperties() {", + " const { object } = this;", + " const resolution = parseInt(object._getResolution(), 10);", + " const validResolution = RESOLUTIONS.indexOf(resolution) !== -1 ? resolution : DEFAULT_RESOLUTION;", + " this.sculptData = object._getSculptData();", + " this.loadedFrom = this._getSourceProperties();", + "", + " this.baseData = this._getBaseData(validResolution);", + " let data = codec.decode(this.sculptData, (resolution) => copyTerrainData(this._getBaseData(resolution)));", + " if (data && data.resolution !== validResolution) {", + " data = data.resampled(validResolution);", + " }", + " this.setData(data || copyTerrainData(this.baseData));", + " }", + "", + " /**", + " * @param {number} resolution", + " * @returns {TerrainData} The data from the relief or the heightmap, shared: it must not be changed.", + " */", + " _getBaseData(resolution) {", + " const { object } = this;", + " const key = [resolution, object._getRelief(), object._getSeed(), object._getHeightmapImage(), object._getEdits()].join('|');", + " const cachedData = baseDataCache.get(key);", + " if (cachedData) return cachedData;", + "", + " const data = new TerrainData(resolution);", + " const heightmapImage = object._getHeightmapImage();", + " const isHeightmapLoaded =", + " !!heightmapImage && loadHeightsFromImage(data, object.getRuntimeScene().getGame(), heightmapImage);", + " if (!isHeightmapLoaded) {", + " relief.generate(data, object._getRelief(), object._getSeed());", + " if (heightmapImage) this._reloadOnceLoaded(heightmapImage);", + " }", + " edits.apply(data, object._getEdits());", + " // Not kept if the image is missing: it may be loaded later.", + " if (isHeightmapLoaded || !heightmapImage) baseDataCache.set(key, data);", + " if (baseDataCache.size > MAX_CACHED_BASE_DATA) {", + " baseDataCache.delete(baseDataCache.keys().next().value);", + " }", + " return data;", + " }", + "", + " /** @param {TerrainData} data */", + " setData(data) {", + " this.version = ++lastGroundVersion;", + " this.surfaceVersion = this.version;", + " this.surfaceChanges.length = 0;", + " this.surfaceChangesStartVersion = this.version;", + " const hasSameResolution = data.resolution === this.data.resolution && this.renderer.chunks.length > 0;", + " this.data = data;", + " const allSamples = new SampleRectangle().set(0, 0, data.resolution, data.resolution);", + " this.surfaceHeights = new Float32Array(data.heights.length);", + " this.renderer.setHasHoles(false);", + " this._updateSurfaceHeights(allSamples);", + " if (hasSameResolution) {", + " this.renderer.updateSamples(allSamples, true);", + " } else {", + " this.renderer.rebuild();", + " }", + " for (const bodyUpdater of this.bodyUpdaters) {", + " bodyUpdater.recreateShape();", + " }", + " }", + "", + " onHotReloading() {", + " const sculptData = this.object._getSculptData();", + " const savedIndex = this.savedSculptData.indexOf(sculptData);", + " if (savedIndex !== -1 && this._getSourceProperties() === this.loadedFrom) {", + " // The terrain already has this data, or newer changes.", + " this.savedSculptData.splice(0, savedIndex + 1);", + " } else if (sculptData !== this.sculptData || this._getSourceProperties() !== this.loadedFrom) {", + " this.savedSculptData.length = 0;", + " this.loadFromProperties();", + " }", + " this.renderer.updateMaterial();", + " }", + "", + " /** @returns {string} The data to save in the sculpt data property. */", + " save() {", + " this.sculptData = codec.encode(this.data, this.baseData);", + " this.savedSculptData.push(this.sculptData);", + " return this.sculptData;", + " }", + "", + " getCellWidth() {", + " return this.object.getWidth() / this.data.resolution;", + " }", + "", + " getCellHeight() {", + " return this.object.getHeight() / this.data.resolution;", + " }", + "", + " _toGridX(x) {", + " return (x - this.object.getX()) / this.getCellWidth();", + " }", + "", + " _toGridY(y) {", + " return (y - this.object.getY()) / this.getCellHeight();", + " }", + "", + " /** @returns {number} The Z position of the ground. */", + " getHeightAt(x, y) {", + " return this.object.getZ() + this.data.sampleHeight(this._toGridX(x), this._toGridY(y)) * this.object.getDepth();", + " }", + "", + " /** @returns {number} The angle of the ground with the horizontal, in degrees. */", + " getSlopeAt(x, y) {", + " const gridX = this._toGridX(x);", + " const gridY = this._toGridY(y);", + " const { data } = this;", + " const depth = this.object.getDepth();", + " const slopeX = ((data.sampleHeight(gridX + 0.5, gridY) - data.sampleHeight(gridX - 0.5, gridY)) * depth) / this.getCellWidth();", + " const slopeY = ((data.sampleHeight(gridX, gridY + 0.5) - data.sampleHeight(gridX, gridY - 0.5)) * depth) / this.getCellHeight();", + " return gdjs.toDegrees(Math.atan(Math.hypot(slopeX, slopeY)));", + " }", + "", + " /** @returns {number} The layer painted the most, from 1 to 4. */", + " getLayerAt(x, y) {", + " return this.data.getDominantLayer(this._toGridX(x), this._toGridY(y)) + 1;", + " }", + "", + " /**", + " * @param {number} layer From 1 to 4.", + " * @returns {number} How much the layer is painted, from 0 to 1.", + " */", + " getLayerWeightAt(x, y, layer) {", + " const layerIndex = clampInteger(layer - 1, 0, LAYER_COUNT - 1);", + " return this.data.sampleLayerWeight(this._toGridX(x), this._toGridY(y), layerIndex);", + " }", + "", + " /** @returns {number[]} The painted tint: red, green and blue, from 0 to 255 (white when there is none). */", + " getTintAt(x, y) {", + " const gridX = this._toGridX(x);", + " const gridY = this._toGridY(y);", + " return [0, 1, 2].map((channel) => Math.round(this.data.sampleColor(gridX, gridY, channel) * 255));", + " }", + "", + " /** @returns {number} How wet the ground is, from 0 (dry) to 1, painted or near the water (see the wet shore). */", + " getWetnessAt(x, y) {", + " const paintedWetness = this.data.sampleColor(this._toGridX(x), this._toGridY(y), 3);", + " const { object } = this;", + " if (!object._getWetShore()) return paintedWetness;", + " const fade = Math.max(object._getWetShoreFade(), 0.001);", + " const shoreWetness = clamp01((object._getWetShoreZ() + fade - this.getHeightAt(x, y)) / fade);", + " // Like the smoothstep of the shader.", + " return Math.max(paintedWetness, shoreWetness * shoreWetness * (3 - 2 * shoreWetness));", + " }", + "", + " _isInArea(x, y) {", + " const { object } = this;", + " return (", + " x >= object.getX() &&", + " x <= object.getX() + object.getWidth() &&", + " y >= object.getY() &&", + " y <= object.getY() + object.getHeight()", + " );", + " }", + "", + " /** @returns {boolean} true if the position is above or under the terrain, and not in a hole. */", + " containsPoint(x, y) {", + " return this._isInArea(x, y) && this.data.sampleSolidity(this._toGridX(x), this._toGridY(y)) >= 0.5;", + " }", + "", + " /** @returns {boolean} true if there is a hole in the terrain at the position, like what is drawn. */", + " isHoleAt(x, y) {", + " return this._isInArea(x, y) && this.data.sampleSolidity(this._toGridX(x), this._toGridY(y)) < 0.5;", + " }", + "", + " _getStroke(x1, y1, x2, y2, radius) {", + " const stroke = this._stroke;", + " stroke.cellWidth = this.getCellWidth();", + " stroke.cellHeight = this.getCellHeight();", + " stroke.ax = x1 - this.object.getX();", + " stroke.ay = y1 - this.object.getY();", + " stroke.bx = x2 - this.object.getX();", + " stroke.by = y2 - this.object.getY();", + " stroke.radius = Math.max(radius, 0);", + " return stroke;", + " }", + "", + " /**", + " * @param {SampleRectangle} samples", + " * @param {boolean} haveHeightsChanged false when only the paint changed.", + " */", + " _onSamplesChanged(samples, haveHeightsChanged) {", + " if (samples.isEmpty()) return;", + " this.version = ++lastGroundVersion;", + " this.changedSamples.add(samples);", + " // Painting changes the surface when it makes holes or fills them.", + " const hasSurfaceChanged = this._updateSurfaceHeights(samples);", + " if (!haveHeightsChanged && !hasSurfaceChanged) return;", + " this.surfaceVersion = this.version;", + " this.surfaceChanges.push({ version: this.version, samples: new SampleRectangle().set(samples.minI, samples.minJ, samples.maxI, samples.maxJ) });", + " if (this.surfaceChanges.length > MAX_SURFACE_CHANGES) {", + " this.surfaceChangesStartVersion = this.surfaceChanges.shift().version;", + " }", + " for (const bodyUpdater of this.bodyUpdaters) {", + " bodyUpdater.changedSamples.add(samples);", + " }", + " }", + "", + " /**", + " * @param {SampleRectangle} samples", + " * @returns {boolean} true if heights of the surface changed, or holes were made or filled.", + " */", + " _updateSurfaceHeights(samples) {", + " const { data, surfaceHeights } = this;", + " let hasChanged = false;", + " let hasHoles = false;", + " for (let j = samples.minJ; j <= samples.maxJ; j++) {", + " for (let i = samples.minI; i <= samples.maxI; i++) {", + " const index = j * data.size + i;", + " const isHole = data.isHole(index);", + " const height = isHole ? NaN : data.heights[index];", + " const previousHeight = surfaceHeights[index];", + " if (isHole) {", + " hasHoles = true;", + " if (Number.isNaN(previousHeight)) continue;", + " } else if (height === previousHeight) {", + " continue;", + " }", + " surfaceHeights[index] = height;", + " hasChanged = true;", + " }", + " }", + " // Filled holes keep the shaders drawing holes until the data is loaded again.", + " if (hasHoles) this.renderer.setHasHoles(true);", + " return hasChanged;", + " }", + "", + " /** @param {number} height In scene units, negative to lower the ground. */", + " raise(x1, y1, x2, y2, radius, height) {", + " const samples = this._strokeChangedSamples;", + " brushes.raise(this.data, this._getStroke(x1, y1, x2, y2, radius), height / this.object.getDepth(), samples);", + " this._onSamplesChanged(samples, true);", + " }", + "", + " /** @param {number} z The Z position to flatten the ground to. */", + " /** @param {number} [toZ] The Z position at the end of the segment, for a slope. The same as z by default. */", + " flatten(x1, y1, x2, y2, radius, z, strength, toZ = z) {", + " const samples = this._strokeChangedSamples;", + " const toHeight = (value) => (value - this.object.getZ()) / this.object.getDepth();", + " brushes.flatten(this.data, this._getStroke(x1, y1, x2, y2, radius), toHeight(z), strength, samples, toHeight(toZ));", + " this._onSamplesChanged(samples, true);", + " }", + "", + " smooth(x1, y1, x2, y2, radius, strength) {", + " const samples = this._strokeChangedSamples;", + " brushes.smooth(this.data, this._getStroke(x1, y1, x2, y2, radius), strength, samples);", + " this._onSamplesChanged(samples, true);", + " }", + "", + " /** @param {number} layer From 1 to 4. */", + " paint(x1, y1, x2, y2, radius, layer, strength) {", + " const layerIndex = clampInteger(layer - 1, 0, LAYER_COUNT - 1);", + " const samples = this._strokeChangedSamples;", + " brushes.paint(this.data, this._getStroke(x1, y1, x2, y2, radius), layerIndex, strength, samples);", + " this.hasPaintChanged = true;", + " this._onSamplesChanged(samples, false);", + " }", + "", + " /** @param {number[]} color The red, green and blue (0-255) of the tint: white removes it. */", + " tint(x1, y1, x2, y2, radius, color, strength) {", + " const samples = this._strokeChangedSamples;", + " brushes.tint(this.data, this._getStroke(x1, y1, x2, y2, radius), color, strength, samples);", + " this.hasPaintChanged = true;", + " this._onSamplesChanged(samples, false);", + " }", + "", + " /** @param {number} wetness From 0 (dry) to 1. */", + " wet(x1, y1, x2, y2, radius, wetness, strength) {", + " const samples = this._strokeChangedSamples;", + " brushes.wet(this.data, this._getStroke(x1, y1, x2, y2, radius), wetness, strength, samples);", + " this.hasPaintChanged = true;", + " this._onSamplesChanged(samples, false);", + " }", + "", + " /** Make a hole around a segment (`fillHoles` fills it again). */", + " makeHole(x1, y1, x2, y2, radius) {", + " const samples = this._strokeChangedSamples;", + " brushes.hole(this.data, this._getStroke(x1, y1, x2, y2, radius), samples);", + " this.hasPaintChanged = true;", + " this._onSamplesChanged(samples, false);", + " }", + "", + " /** Fill the holes around a segment with the layers of the ground around them. */", + " fillHoles(x1, y1, x2, y2, radius) {", + " const samples = this._strokeChangedSamples;", + " brushes.fill(this.data, this._getStroke(x1, y1, x2, y2, radius), samples);", + " this.hasPaintChanged = true;", + " this._onSamplesChanged(samples, false);", + " }", + "", + " /**", + " * Make the entrance of a tunnel or a cave: a hole, with the ground in", + " * front of it flattened to the floor of the tunnel.", + " * @param {number} angle The direction going into the ground, in degrees.", + " * @param {number} floorZ", + " * @param {number} length The length of the flat ground in front of the entrance.", + " */", + " carveEntrance(x, y, radius, angle, floorZ, length) {", + " const directionX = Math.cos(gdjs.toRad(angle));", + " const directionY = Math.sin(gdjs.toRad(angle));", + " this.flatten(x - directionX * length, y - directionY * length, x, y, radius * 1.5, floorZ, 1);", + " this.makeHole(x, y, x + directionX * radius, y + directionY * radius, radius);", + " }", + "", + " generate(reliefName, seed) {", + " this.baseData = new TerrainData(this.data.resolution);", + " relief.generate(this.baseData, reliefName, seed);", + " this.setData(copyTerrainData(this.baseData));", + " }", + "", + " loadHeightmap(imageResourceName) {", + " const baseData = new TerrainData(this.data.resolution);", + " if (loadHeightsFromImage(baseData, this.object.getRuntimeScene().getGame(), imageResourceName)) {", + " baseData.splat.set(this.data.splat);", + " baseData.colors.set(this.data.colors);", + " this.baseData = baseData;", + " this.setData(copyTerrainData(baseData));", + " }", + " }", + "", + " /** @param {gdjs.RuntimeObject[]} objects */", + " placeOnGround(objects) {", + " for (const object of objects) {", + " if (object === this.object || !gdjs.Base3DHandler.is3D(object)) continue;", + " const groundZ = this.getHeightAt(object.getCenterXInScene(), object.getCenterYInScene());", + " // The origin of the object is not always at its bottom.", + " object.setZ(groundZ + object.getZ() - object.getUnrotatedAABBMinZ());", + " }", + " }", + "", + " /**", + " * @returns {THREE.Vector3 | null} The point of the ground under the cursor,", + " * in the terrain group coordinates. Holes are hit too, so that brushes can fill them.", + " */", + " raycastFromCursor(game, raycaster, ndc) {", + " const layer = this.object.getInstanceContainer().getLayer(this.object.getLayer());", + " const camera = layer.getRenderer().getThreeCamera();", + " if (!camera) return null;", + " const inputManager = game.getInputManager();", + " ndc.set(", + " (inputManager.getCursorX() / game.getGameResolutionWidth()) * 2 - 1,", + " -(inputManager.getCursorY() / game.getGameResolutionHeight()) * 2 + 1", + " );", + " raycaster.setFromCamera(ndc, camera);", + " this.renderer.group.updateWorldMatrix(true, false);", + " const ray = raycaster.ray.clone().applyMatrix4(this.renderer.group.matrixWorld.clone().invert());", + " ray.direction.normalize();", + "", + " const box = new THREE.Box3(new THREE.Vector3(0, 0, 0), new THREE.Vector3(AREA_SIZE, AREA_SIZE, AREA_DEPTH));", + " const entry = box.containsPoint(ray.origin) ? ray.origin.clone() : ray.intersectBox(box, new THREE.Vector3());", + " if (!entry) return null;", + " const cellSize = AREA_SIZE / this.data.resolution;", + " const groundZAt = (point) => this.data.sampleHeight(point.x / cellSize, point.y / cellSize) * AREA_DEPTH;", + " const step = cellSize / 2;", + " const point = entry.clone();", + " const previousPoint = entry.clone();", + " for (let distance = 0; distance < AREA_SIZE * 2; distance += step) {", + " if (point.z <= groundZAt(point)) {", + " // Refine between the last point above the ground and this one.", + " for (let iteration = 0; iteration < 8; iteration++) {", + " const middle = previousPoint.clone().lerp(point, 0.5);", + " if (middle.z <= groundZAt(middle)) point.copy(middle);", + " else previousPoint.copy(middle);", + " }", + " return point;", + " }", + " if (!box.containsPoint(point)) return null;", + " previousPoint.copy(point);", + " point.addScaledVector(ray.direction, step);", + " }", + " return null;", + " }", + "", + " /**", + " * @param {THREE.Vector3} localPosition In the terrain group coordinates (like from `raycastFromCursor`).", + " * @returns {{x: number, y: number, z: number}} The position in the scene.", + " */", + " toScenePosition(localPosition) {", + " const { object } = this;", + " return {", + " x: object.getX() + (localPosition.x / AREA_SIZE) * object.getWidth(),", + " y: object.getY() + (localPosition.y / AREA_SIZE) * object.getHeight(),", + " z: object.getZ() + (localPosition.z / AREA_DEPTH) * object.getDepth(),", + " };", + " }", + "", + " /** Draw the changes of the ground (collisions are updated by the physics engine). */", + " applyChanges() {", + " this.renderer.updateSamples(this.changedSamples, this.hasPaintChanged);", + " this.changedSamples.clear();", + " this.hasPaintChanged = false;", + " }", + "", + " /** @returns {number} A number changing each time the ground changes (see `getGrounds`). */", + " getVersion() {", + " return this.version;", + " }", + "", + " /**", + " * @param {number} sinceVersion", + " * @returns {gdjs.SurfaceArea | null} The part of the ground whose shape changed since the version (see `gdjs.Surface`).", + " */", + " getChangedArea(sinceVersion) {", + " if (sinceVersion < this.surfaceChangesStartVersion) return null;", + " const samples = new SampleRectangle();", + " for (const change of this.surfaceChanges) {", + " if (change.version > sinceVersion) samples.add(change.samples);", + " }", + " if (samples.isEmpty()) return { minX: 0, minY: 0, maxX: 0, maxY: 0 };", + " const { resolution } = this.data;", + " return {", + " minX: samples.minI / resolution,", + " minY: samples.minJ / resolution,", + " maxX: samples.maxI / resolution,", + " maxY: samples.maxJ / resolution,", + " };", + " }", + "", + " _getTransform() {", + " const { object } = this;", + " return [object.getX(), object.getY(), object.getZ(), object.getWidth(), object.getHeight(), object.getDepth()].join(',');", + " }", + "", + " /** @param {string} imageResourceName A heightmap image that is not loaded yet. */", + " _reloadOnceLoaded(imageResourceName) {", + " if (this.loadingHeightmapImage === imageResourceName) return;", + " this.loadingHeightmapImage = imageResourceName;", + " const game = this.object.getRuntimeScene().getGame();", + " game.getImageManager()", + " .loadResource(imageResourceName)", + " .then(() => {", + " if (this.isDisposed || this.loadingHeightmapImage !== imageResourceName) return;", + " this.loadingHeightmapImage = '';", + " if (getLoadedImage(game, imageResourceName)) this.loadFromProperties();", + " })", + " .catch(() => {});", + " }", + "", + " update() {", + " const transform = this._getTransform();", + " if (transform !== this._transform) {", + " this._transform = transform;", + " this.version = ++lastGroundVersion;", + " }", + " this.applyChanges();", + " this.renderer.uniforms.terrainWorldScale.value = getWorldScale(this.object);", + " this._updateWetShore();", + " const layer = this.object.getInstanceContainer().getLayer(this.object.getLayer());", + " this.renderer.updateLevelsOfDetail(layer.getRenderer().getThreeCamera());", + " }", + "", + " _updateWetShore() {", + " const { object } = this;", + " // In the coordinates of the terrain group, like the vertices.", + " const depthScale = AREA_DEPTH / Math.max(object.getDepth(), 0.0001);", + " this.renderer.uniforms.terrainWetShore.value.set(", + " (object._getWetShoreZ() - object.getZ()) * depthScale,", + " Math.max(object._getWetShoreFade(), 0.001) * depthScale,", + " object._getWetShore() ? 1 : 0", + " );", + " }", + "", + " dispose() {", + " this.isDisposed = true;", + " getGrounds(this.object.getRuntimeScene()).delete(this);", + " if (typeof this.object.setSurface === 'function' && this.object.getSurface() === this.surface) {", + " this.object.setSurface(null);", + " }", + " this.renderer.dispose();", + " for (const bodyUpdater of this.bodyUpdaters) {", + " bodyUpdater.dispose();", + " }", + " }", + "}", + "", + "/**", + " * @param {gdjs.RuntimeObject} object", + " * @returns {number} The number of scene units by 3D world unit (1 in GDevelop versions before the world scale).", + " */", + "const getWorldScale = (object) => {", + " const scene = object.getRuntimeScene();", + " return typeof scene.getRenderer3DWorldScale === 'function' ? scene.getRenderer3DWorldScale() : 1;", + "};", + "", + "const averageColors = new WeakMap();", + "let white = null;", + "/**", + " * @param {THREE.Texture} texture", + " * @returns {THREE.Color} The average color of the texture (white when it's not loaded).", + " */", + "const getAverageColor = (texture) => {", + " const cachedColor = averageColors.get(texture);", + " if (cachedColor) return cachedColor;", + " const image = texture.image;", + " const isDrawable =", + " typeof document !== 'undefined' &&", + " ((image instanceof HTMLImageElement && image.complete && image.naturalWidth > 0) ||", + " image instanceof HTMLCanvasElement ||", + " (typeof ImageBitmap !== 'undefined' && image instanceof ImageBitmap));", + " if (!isDrawable) return white || (white = new THREE.Color(1, 1, 1));", + " const size = 8;", + " const canvas = document.createElement('canvas');", + " canvas.width = size;", + " canvas.height = size;", + " const context = canvas.getContext('2d');", + " context.drawImage(image, 0, 0, size, size);", + " const pixels = context.getImageData(0, 0, size, size).data;", + " let red = 0;", + " let green = 0;", + " let blue = 0;", + " for (let index = 0; index < pixels.length; index += 4) {", + " red += pixels[index];", + " green += pixels[index + 1];", + " blue += pixels[index + 2];", + " }", + " const count = size * size * 255;", + " const color = new THREE.Color().setRGB(red / count, green / count, blue / count, THREE.SRGBColorSpace);", + " averageColors.set(texture, color);", + " return color;", + "};", + "", + "const svgIcon = (content) =>", + " 'data:image/svg+xml,' +", + " encodeURIComponent('' + content + '');", + "const strokes = 'fill=\"none\" stroke=\"#000\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"';", + "const toolIcons = {", + " Raise: svgIcon(''),", + " Lower: svgIcon(''),", + " Smooth: svgIcon(''),", + " Flatten: svgIcon(''),", + " Size: svgIcon(''),", + " Strength: svgIcon(''),", + " Undo: svgIcon(''),", + " Hole: svgIcon(''),", + " FillHoles: svgIcon(''),", + " Wetness: svgIcon(''),", + "};", + "// Tints for the color brush when the editor has no color picker (older GDevelop versions).", + "const tintPresets = ['#e2c879', '#a5c45a', '#8a6a48', '#c98a62', '#9a9a92', '#ffffff'];", + "", + "/**", + " * The sculpt and paint tools shown in the scene editor when a terrain is", + " * selected. Without the needed editor functions (older GDevelop versions),", + " * nothing is shown: the terrain can still be changed with events.", + " */", + "class TerrainEditorTools {", + " constructor() {", + " /** '' when no brush is used. */", + " this.toolName = '';", + " this.sizePercent = 30;", + " this.strengthPercent = 50;", + " /** @type {Terrain | null} */", + " this.terrain = null;", + " /** @type {gdjs.InGameEditor | null} */", + " this.editor = null;", + " this.isStroking = false;", + " this.flattenHeight = 0;", + " /** The color of the \"Tint\" brush, as \"#rrggbb\". */", + " this.tintColor = tintPresets[0];", + " this.wetnessPercent = 100;", + " this.lastStrokeTime = 0;", + " /** @type {{data: TerrainData, baseData: TerrainData}[]} */", + " this.undoSteps = [];", + " this.raycaster = new THREE.Raycaster();", + " this.ndc = new THREE.Vector2();", + " this._swatchColor = new THREE.Color();", + " }", + "", + " /** @param {gdjs.InGameEditor} editor */", + " update(editor) {", + " if (typeof editor.showToolbar !== 'function') {", + " // Older GDevelop versions: the terrain can only be changed with events.", + " return;", + " }", + " this.editor = editor;", + " const selectedObjects = editor.getSelectedObjects();", + " const terrain =", + " selectedObjects.length === 1 && selectedObjects[0].__terrain3D ? selectedObjects[0].__terrain3D : null;", + " if (terrain !== this.terrain) {", + " this._endStroke();", + " if (this.terrain) this.terrain.renderer.setBrush(null, 0);", + " this.terrain = terrain;", + " this.undoSteps.length = 0;", + " }", + " if (terrain) editor.showToolbar('Terrain3D', this._getToolbarItems(terrain));", + " const game = editor.getRuntimeGame();", + " if (!terrain || !this.toolName) {", + " if (terrain) terrain.renderer.setBrush(null, 0);", + " return;", + " }", + "", + " const inputManager = game.getInputManager();", + " if (inputManager.wasKeyJustPressed(gdjs.evtTools.input.keysNameToCode.Escape)) {", + " this._endStroke();", + " this.toolName = '';", + " terrain.renderer.setBrush(null, 0);", + " return;", + " }", + " editor.captureLeftMouseButton();", + " const hit = terrain.raycastFromCursor(game, this.raycaster, this.ndc);", + " const radius = this._getRadius();", + " terrain.renderer.setBrush(hit, radius);", + "", + " // Older GDevelop versions don't have it.", + " const isPressedForTool =", + " typeof editor.isCapturedLeftMouseButtonPressed === 'function'", + " ? editor.isCapturedLeftMouseButtonPressed()", + " : inputManager.isMouseButtonPressed(0);", + " // The button is released outside the canvas when the mouse is on a toolbar.", + " const isPressed = isPressedForTool && inputManager.isMouseInsideCanvas();", + " if (isPressed && hit && !this.isStroking) {", + " this._startStroke(terrain, hit);", + " }", + " if (this.isStroking && isPressed && hit) {", + " this._applyBrush(terrain, hit, radius);", + " }", + " if (!isPressed) {", + " this._endStroke();", + " }", + " }", + "", + " _getRadius() {", + " const { object } = this.terrain;", + " const maxRadius = Math.max(object.getWidth(), object.getHeight()) / 4;", + " return Math.max(maxRadius * Math.pow(this.sizePercent / 100, 2), this.terrain.getCellWidth());", + " }", + "", + " _startStroke(terrain, hit) {", + " this.isStroking = true;", + " this.lastStrokeTime = performance.now();", + " this.flattenHeight = terrain.toScenePosition(hit).z;", + " this.undoSteps.push({ data: copyTerrainData(terrain.data), baseData: terrain.baseData });", + " const maxUndoSteps = Math.max(Math.floor(UNDO_STEPS_SAMPLES / terrain.data.heights.length), 2);", + " while (this.undoSteps.length > maxUndoSteps) this.undoSteps.shift();", + " }", + "", + " _endStroke() {", + " if (!this.isStroking) return;", + " this.isStroking = false;", + " this._save();", + " }", + "", + " _applyBrush(terrain, hit, radius) {", + " const now = performance.now();", + " const elapsedSeconds = Math.min((now - this.lastStrokeTime) / 1000, 0.1);", + " this.lastStrokeTime = now;", + " const { x, y } = terrain.toScenePosition(hit);", + " const amount = (this.strengthPercent / 100) * elapsedSeconds;", + " const depth = terrain.object.getDepth();", + " if (this.toolName === 'Raise') terrain.raise(x, y, x, y, radius, amount * depth * 0.5);", + " else if (this.toolName === 'Lower') terrain.raise(x, y, x, y, radius, -amount * depth * 0.5);", + " else if (this.toolName === 'Smooth') terrain.smooth(x, y, x, y, radius, amount * 10);", + " else if (this.toolName === 'Flatten') terrain.flatten(x, y, x, y, radius, this.flattenHeight, amount * 10);", + " else if (this.toolName.startsWith('Paint')) {", + " terrain.paint(x, y, x, y, radius, parseInt(this.toolName.slice(5), 10), amount * 8);", + " } else if (this.toolName === 'Hole') {", + " terrain.makeHole(x, y, x, y, radius);", + " } else if (this.toolName === 'FillHoles') {", + " terrain.fillHoles(x, y, x, y, radius);", + " } else if (this.toolName === 'Tint') {", + " terrain.tint(x, y, x, y, radius, gdjs.rgbOrHexToRGBColor(this.tintColor), amount * 8);", + " } else if (this.toolName === 'Wetness') {", + " terrain.wet(x, y, x, y, radius, this.wetnessPercent / 100, amount * 8);", + " }", + " // Drawn now, as the terrain was updated earlier in this frame.", + " terrain.applyChanges();", + " }", + "", + " _save() {", + " const { terrain, editor } = this;", + " if (!terrain || !editor) return;", + " editor.updateObjectProperties(terrain.object.getName(), { SculptData: terrain.save() });", + " }", + "", + " _undo() {", + " const undoStep = this.undoSteps.pop();", + " // The relief, the edits or the resolution may have changed since the stroke.", + " if (!this.terrain || !undoStep || undoStep.baseData !== this.terrain.baseData) {", + " this.undoSteps.length = 0;", + " return;", + " }", + " this.terrain.setData(undoStep.data);", + " this._save();", + " }", + "", + " /** @returns {Array} The settings of the tint or wetness brush, when it's used. */", + " _getColorToolItems() {", + " const { editor } = this;", + " if (this.toolName === 'Tint') {", + " if (editor && typeof editor.isToolbarItemTypeSupported === 'function' && editor.isToolbarItemTypeSupported('color')) {", + " return [", + " {", + " type: 'color',", + " id: 'TintColor',", + " tooltip: 'Color of the tint',", + " value: this.tintColor,", + " onChange: (color) => (this.tintColor = color),", + " },", + " ];", + " }", + " return tintPresets.map((color) => ({", + " type: 'button',", + " id: 'TintPreset' + color,", + " tooltip: color === '#ffffff' ? 'No tint' : 'Tint with this color',", + " color,", + " isActive: this.tintColor === color,", + " onClick: () => (this.tintColor = color),", + " }));", + " }", + " if (this.toolName === 'Wetness') {", + " return [", + " {", + " type: 'slider',", + " id: 'WetnessAmount',", + " tooltip: 'Wetness',", + " iconUrl: toolIcons.Wetness,", + " min: 0,", + " max: 100,", + " value: this.wetnessPercent,", + " onChange: (value) => (this.wetnessPercent = value),", + " },", + " ];", + " }", + " return [];", + " }", + "", + " /**", + " * @param {Terrain} terrain", + " * @returns {Array} The toolbar for the current state (the editor only", + " * updates what changed).", + " */", + " _getToolbarItems(terrain) {", + " const { object } = terrain;", + " const toolButton = (toolName, tooltip, color) => ({", + " type: 'button',", + " id: toolName,", + " tooltip,", + " iconUrl: toolIcons[toolName],", + " color,", + " isActive: this.toolName === toolName,", + " onClick: () => (this.toolName = this.toolName === toolName ? '' : toolName),", + " });", + " const layerColors = [object._getLayer1Color(), object._getLayer2Color(), object._getLayer3Color(), object._getLayer4Color()];", + " // Layers are shown with their color, tinting their texture.", + " const swatchColors = layerColors.map((color, index) =>", + " this._swatchColor", + " .set(gdjs.rgbOrHexStringToNumber(color))", + " .multiply(getAverageColor(terrain.renderer.uniforms['terrainLayerMap' + index].value))", + " .getHexString()", + " );", + " return [", + " toolButton('Raise', 'Raise the ground (drag on the terrain)'),", + " toolButton('Lower', 'Lower the ground'),", + " toolButton('Smooth', 'Smooth the ground'),", + " toolButton('Flatten', 'Flatten the ground to the height where the drag starts'),", + " { type: 'divider', id: 'PaintDivider' },", + " ...swatchColors.map((swatchColor, index) =>", + " toolButton('Paint' + (index + 1), 'Paint layer ' + (index + 1), '#' + swatchColor)", + " ),", + " toolButton('Hole', 'Make a hole, for the entrance of a tunnel or a cave'),", + " toolButton('FillHoles', 'Fill holes, with the layers of the ground around them'),", + " { type: 'divider', id: 'ColorDivider' },", + " toolButton('Tint', 'Tint the ground (white removes the tint)', this.tintColor),", + " toolButton('Wetness', 'Make the ground wet, or dry with a wetness of 0'),", + " ...this._getColorToolItems(),", + " { type: 'divider', id: 'BrushDivider' },", + " {", + " type: 'slider',", + " id: 'Size',", + " tooltip: 'Brush size',", + " iconUrl: toolIcons.Size,", + " min: 1,", + " max: 100,", + " value: this.sizePercent,", + " onChange: (value) => (this.sizePercent = value),", + " },", + " {", + " type: 'slider',", + " id: 'Strength',", + " tooltip: 'Brush strength',", + " iconUrl: toolIcons.Strength,", + " min: 5,", + " max: 100,", + " value: this.strengthPercent,", + " onChange: (value) => (this.strengthPercent = value),", + " },", + " { type: 'button', id: 'Undo', tooltip: 'Undo the last stroke on this terrain', iconUrl: toolIcons.Undo, onClick: () => this._undo() },", + " ];", + " }", + "}", + "", + "const editorTools = new TerrainEditorTools();", + "if (gdjs.registerInGameEditorPostStepCallback) {", + " gdjs.registerInGameEditorPostStepCallback((editor) => editorTools.update(editor));", + "}", + "", + "gdjs.__terrain3DExtension = {", + " Terrain,", + " // Used by the terrain grass and the scattered models.", + " getWorldScale,", + " getGrounds,", + " patchShaderCode,", + " createRandom,", + " findHighestGround,", + " GroundPlacement,", + " svgIcon,", + " toolIcons,", + " // Exposed for tests.", + " TerrainBodyUpdater,", + " TerrainData,", + " SampleRectangle,", + " brushes,", + " edits,", + " codec,", + " relief,", + "};" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [], + "objectGroups": [] + }, + { + "description": "Define grass helper classes JavaScript code.", + "fullName": "Define grass helper classes", + "functionType": "Action", + "name": "DefineGrassHelperClasses", + "private": true, + "sentence": "Define grass helper classes JavaScript code", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "if (gdjs.__terrainGrassExtension) {", + " return;", + "}", + "// Blades grow on the grounds of the scene: the 3D terrains (see `getGrounds`", + "// in the terrain helper).", + "", + "// Size of the object inner area: instances are scaled from it.", + "const AREA_SIZE = 1000;", + "const AREA_DEPTH = 40;", + "const CHUNK_SIZE = 500;", + "const MAX_BENDING_OBJECTS = 8;", + "", + "/**", + " * A blade: a thin, tapered strip. Its width and height are 1: blades are", + " * scaled by the shader.", + " */", + "const createBladeGeometry = () => {", + " const halfWidths = [0.5, 0.42, 0.28, 0];", + " const heights = [0, 0.35, 0.7, 1];", + " const positions = [];", + " halfWidths.forEach((halfWidth, level) => {", + " positions.push(-halfWidth, 0, heights[level]);", + " if (halfWidth > 0) positions.push(halfWidth, 0, heights[level]);", + " });", + " const indices = [0, 1, 2, 1, 3, 2, 2, 3, 4, 3, 5, 4, 4, 5, 6];", + " const geometry = new THREE.InstancedBufferGeometry();", + " geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));", + " geometry.setAttribute('normal', new THREE.Float32BufferAttribute(new Array(positions.length).fill(0), 3));", + " geometry.setIndex(indices);", + " return geometry;", + "};", + "", + "const vertexShaderDeclarations = `", + "attribute vec4 grassRoot;", + "attribute vec2 grassShape;", + "// The tint of the ground at the root (linear, from 0 to 1).", + "attribute vec3 grassGroundTint;", + "uniform mat3 grassWorldToLocal;", + "uniform float grassWorldScale;", + "uniform float grassBladeWidth;", + "uniform float grassTime;", + "uniform float grassWindStrength;", + "uniform float grassWindSpeed;", + "uniform vec4 grassBenders[${MAX_BENDING_OBJECTS}];", + "uniform vec3 grassBaseColor;", + "uniform vec3 grassTipColor;", + "varying vec3 vGrassColor;", + "`;", + "const vertexShaderPosition = `", + "// Blades are shaped in scene units (with Y flipped like the 3D world), then", + "// moved back in the (scaled) object space.", + "float bladeHeight = position.z;", + "float grassAngle = grassRoot.w;", + "float bladeWidth = position.x * grassBladeWidth;", + "vec3 blade = vec3(", + " bladeWidth * cos(grassAngle),", + " bladeWidth * sin(grassAngle),", + " bladeHeight * grassShape.x", + ");", + "vec3 rootWorld = (modelMatrix * vec4(grassRoot.xyz, 1.0)).xyz * grassWorldScale;", + "// Tips bend more than the base.", + "float bendFactor = bladeHeight * bladeHeight * grassShape.x;", + "float gust = sin(grassTime * grassWindSpeed + rootWorld.x * 0.013 + rootWorld.y * 0.011);", + "float flutter = sin(grassTime * grassWindSpeed * 2.7 + grassShape.y * 40.0) * 0.3;", + "blade.xy += vec2(0.8, 0.5) * (gust + flutter + 0.6) * grassWindStrength * 0.35 * bendFactor;", + "for (int i = 0; i < ${MAX_BENDING_OBJECTS}; i++) {", + " vec4 bender = grassBenders[i];", + " if (bender.z <= 0.0) continue;", + " vec2 away = rootWorld.xy - bender.xy;", + " float distanceToBender = length(away);", + " float push = 1.0 - smoothstep(bender.z * 0.3, bender.z, distanceToBender);", + " if (push > 0.0) {", + " blade.xy += normalize(away + vec2(0.001)) * push * bendFactor * 0.9;", + " blade.z *= 1.0 - push * 0.5;", + " }", + "}", + "vec3 transformed = grassRoot.xyz + grassWorldToLocal * (blade / grassWorldScale);", + "vGrassColor = mix(grassBaseColor, grassTipColor, bladeHeight) * (0.85 + 0.3 * grassShape.y) * grassGroundTint;", + "`;", + "const vertexShaderNormal = `", + "// Blades are lit like the ground below them.", + "transformedNormal = normalize((viewMatrix * vec4(0.0, 0.0, 1.0, 0.0)).xyz);", + "`;", + "const fragmentShaderDeclarations = `", + "varying vec3 vGrassColor;", + "`;", + "const fragmentShaderColor = `", + "diffuseColor.rgb *= vGrassColor;", + "`;", + "// Both faces of blades are lit like the ground (not flipped for back faces).", + "const fragmentShaderNormal = `", + "#include ", + "normal = normalize(vNormal);", + "nonPerturbedNormal = normal;", + "`;", + "", + "const { getWorldScale, patchShaderCode, createRandom, findHighestGround, GroundPlacement } = gdjs.__terrain3DExtension;", + "", + "/**", + " * The grass of a terrain grass object: blades drawn with GPU instancing, in chunks", + " * that are hidden when off-screen and thinned out with the distance. They grow", + " * on the grounds below, like 3D terrains (only where a given layer is painted).", + " */", + "class Grass {", + " /** @param {gdjs.CustomRuntimeObject3D} object */", + " constructor(object) {", + " this.object = object;", + " this.group = new THREE.Group();", + " object.get3DRendererObject().add(this.group);", + " this.bladeGeometry = createBladeGeometry();", + " /** @type {Array<{mesh: THREE.Mesh, blades: number, center: THREE.Vector3}>} */", + " this.chunks = [];", + " this.placement = new GroundPlacement(object.getRuntimeScene());", + " this._groundAtBlade = { ground: null, z: 0 };", + " this.uniforms = {", + " grassWorldToLocal: { value: new THREE.Matrix3() },", + " // Scene units by 3D world unit.", + " grassWorldScale: { value: 1 },", + " grassBladeWidth: { value: 1 },", + " grassTime: { value: 0 },", + " grassWindStrength: { value: 1 },", + " grassWindSpeed: { value: 1 },", + " grassBenders: { value: Array.from({ length: MAX_BENDING_OBJECTS }, () => new THREE.Vector4()) },", + " grassBaseColor: { value: new THREE.Color() },", + " grassTipColor: { value: new THREE.Color() },", + " };", + " this.material = new THREE.MeshStandardMaterial({ roughness: 1, metalness: 0, side: THREE.DoubleSide });", + " this.material.onBeforeCompile = (shader) => {", + " Object.assign(shader.uniforms, this.uniforms);", + " shader.vertexShader = patchShaderCode(shader.vertexShader, [", + " ['#include ', '#include \\n' + vertexShaderDeclarations],", + " ['#include ', vertexShaderPosition],", + " ['#include ', '#include \\n' + vertexShaderNormal],", + " ], 'TerrainGrass');", + " shader.fragmentShader = patchShaderCode(shader.fragmentShader, [", + " ['#include ', '#include \\n' + fragmentShaderDeclarations],", + " ['#include ', '#include \\n' + fragmentShaderColor],", + " ['#include ', fragmentShaderNormal],", + " ], 'TerrainGrass');", + " };", + " this.material.customProgramCacheKey = () => 'TerrainGrass';", + "", + " this._matrix = new THREE.Matrix4();", + " this._cameraPosition = new THREE.Vector3();", + " this._chunkPosition = new THREE.Vector3();", + " this._scale = new THREE.Vector3();", + " this.updateFromProperties();", + " }", + "", + " updateFromProperties() {", + " const { object } = this;", + " this.uniforms.grassBaseColor.value.set(gdjs.rgbOrHexStringToNumber(object._getBaseColor()));", + " this.uniforms.grassTipColor.value.set(gdjs.rgbOrHexStringToNumber(object._getTipColor()));", + " this.uniforms.grassWindStrength.value = object._getWindStrength();", + " this.uniforms.grassWindSpeed.value = object._getWindSpeed();", + " this.uniforms.grassBladeWidth.value = Math.max(object._getBladeWidth(), 0);", + " this.material.needsUpdate = true;", + " this.placement.placeAgain();", + " }", + "", + " _getPlacementKey() {", + " const { object } = this;", + " return [", + " object.getX(), object.getY(), object.getZ(), object.getWidth(), object.getHeight(), object.getDepth(),", + " object._getDensity(), object._getBladeHeight(), object._getGroundLayer(), object._getMaxSlope(),", + " object._getSeed(),", + " ].join('|');", + " }", + "", + " /** Place the blades again, for example after the ground or the object changed. */", + " rebuild() {", + " const grounds = Array.from(this.placement.grounds);", + " for (const chunk of this.chunks) {", + " chunk.mesh.removeFromParent();", + " chunk.mesh.geometry.dispose();", + " }", + " this.chunks.length = 0;", + "", + " const { object } = this;", + " const width = object.getWidth();", + " const height = object.getHeight();", + " const scaleX = width / AREA_SIZE;", + " const scaleY = height / AREA_SIZE;", + " const scaleZ = Math.max(object.getDepth() / AREA_DEPTH, 0.0001);", + " const bladesPerChunk = Math.round((Math.max(object._getDensity(), 0) * CHUNK_SIZE * CHUNK_SIZE) / 10000);", + " const groundLayer = parseInt(object._getGroundLayer(), 10) || 0;", + " const maxSlope = object._getMaxSlope();", + " const chunksX = Math.max(Math.ceil(width / CHUNK_SIZE), 1);", + " const chunksY = Math.max(Math.ceil(height / CHUNK_SIZE), 1);", + " const nextRandom = createRandom(object._getSeed() * 7919 + 1);", + " const bladeHeight = Math.max(object._getBladeHeight(), 0);", + "", + " for (let chunkY = 0; chunkY < chunksY; chunkY++) {", + " for (let chunkX = 0; chunkX < chunksX; chunkX++) {", + " const roots = new Float32Array(bladesPerChunk * 4);", + " const shapes = new Float32Array(bladesPerChunk * 2);", + " const groundTints = new Uint8Array(bladesPerChunk * 3).fill(255);", + " let blades = 0;", + " let minZ = Infinity;", + " let maxZ = -Infinity;", + " for (let attempt = 0; attempt < bladesPerChunk; attempt++) {", + " // In scene units, relative to the object.", + " const x = (chunkX + nextRandom()) * CHUNK_SIZE;", + " const y = (chunkY + nextRandom()) * CHUNK_SIZE;", + " const angle = nextRandom() * Math.PI * 2;", + " const heightFactor = 0.6 + 0.8 * nextRandom();", + " const tint = nextRandom();", + " const growth = nextRandom();", + " if (x > width || y > height) continue;", + " const sceneX = object.getX() + x;", + " const sceneY = object.getY() + y;", + " const { ground, z } = findHighestGround(grounds, sceneX, sceneY, this._groundAtBlade);", + " const groundZ = ground ? z : object.getZ();", + " if (ground) {", + " if (groundLayer > 0 && growth > ground.getLayerWeightAt(sceneX, sceneY, groundLayer)) continue;", + " if (ground.getSlopeAt(sceneX, sceneY) > maxSlope) continue;", + " if (typeof ground.getTintAt === 'function') {", + " const tint = ground.getTintAt(sceneX, sceneY);", + " for (let channel = 0; channel < 3; channel++) {", + " groundTints[blades * 3 + channel] = Math.round(255 * Math.pow(tint[channel] / 255, 2.2));", + " }", + " }", + " }", + " const localZ = (groundZ - object.getZ()) / scaleZ;", + " roots[blades * 4] = x / scaleX;", + " roots[blades * 4 + 1] = y / scaleY;", + " roots[blades * 4 + 2] = localZ;", + " roots[blades * 4 + 3] = angle;", + " shapes[blades * 2] = heightFactor * bladeHeight;", + " shapes[blades * 2 + 1] = tint;", + " minZ = Math.min(minZ, localZ);", + " maxZ = Math.max(maxZ, localZ);", + " blades++;", + " }", + " if (blades === 0) continue;", + "", + " const geometry = new THREE.InstancedBufferGeometry();", + " geometry.index = this.bladeGeometry.index;", + " geometry.setAttribute('position', this.bladeGeometry.attributes.position);", + " geometry.setAttribute('normal', this.bladeGeometry.attributes.normal);", + " geometry.setAttribute('grassRoot', new THREE.InstancedBufferAttribute(roots.subarray(0, blades * 4), 4));", + " geometry.setAttribute('grassShape', new THREE.InstancedBufferAttribute(shapes.subarray(0, blades * 2), 2));", + " geometry.setAttribute('grassGroundTint', new THREE.InstancedBufferAttribute(groundTints.subarray(0, blades * 3), 3, true));", + " geometry.instanceCount = blades;", + " // The furthest tips can go with the wind and the bending objects (see the shader).", + " const maxBladeHeight = bladeHeight * 1.4;", + " const lean =", + " (0.35 * 1.9 * 0.943 * Math.max(object._getWindStrength(), 0) + 0.9) * maxBladeHeight +", + " Math.max(object._getBladeWidth(), 0) / 2;", + " const leanX = lean / scaleX;", + " const leanY = lean / scaleY;", + " geometry.boundingBox = new THREE.Box3(", + " new THREE.Vector3((chunkX * CHUNK_SIZE) / scaleX - leanX, (chunkY * CHUNK_SIZE) / scaleY - leanY, minZ),", + " new THREE.Vector3(", + " Math.min((chunkX + 1) * CHUNK_SIZE, width) / scaleX + leanX,", + " Math.min((chunkY + 1) * CHUNK_SIZE, height) / scaleY + leanY,", + " maxZ + (bladeHeight * 1.4) / scaleZ", + " )", + " );", + " geometry.boundingSphere = geometry.boundingBox.getBoundingSphere(new THREE.Sphere());", + " const mesh = new THREE.Mesh(geometry, this.material);", + " mesh.receiveShadow = true;", + " // Raycasts would only test one blade: the selection plane is used instead.", + " mesh.raycast = () => {};", + " this.group.add(mesh);", + " this.chunks.push({ mesh, blades, center: geometry.boundingSphere.center.clone() });", + " }", + " }", + " }", + "", + " _updateBendingObjects() {", + " const benders = this.uniforms.grassBenders.value;", + " const objectName = this.object._getBendingObject();", + " const bendingObjects = objectName ? this.object.getInstanceContainer().getObjects(objectName) || [] : [];", + " for (let index = 0; index < MAX_BENDING_OBJECTS; index++) {", + " const bendingObject = bendingObjects[index];", + " if (!bendingObject) {", + " benders[index].set(0, 0, 0, 0);", + " continue;", + " }", + " // Blades are pushed in the 3D scene, which is flipped on Y.", + " const radius = Math.max(bendingObject.getWidth(), bendingObject.getHeight()) * 1.2;", + " benders[index].set(bendingObject.getCenterXInScene(), -bendingObject.getCenterYInScene(), radius, 0);", + " }", + " }", + "", + " /**", + " * @param {number} elapsedSeconds", + " * @param {number} time In seconds, the same for all grass objects.", + " */", + " update(elapsedSeconds, time, camera) {", + " if (this.placement.shouldPlaceAgain(elapsedSeconds, this._getPlacementKey())) {", + " this.rebuild();", + " }", + "", + " this.uniforms.grassTime.value = time;", + " this.uniforms.grassWorldScale.value = getWorldScale(this.object);", + " // The object transformation is applied just before rendering, it's needed now.", + " this.object.getRenderer().ensureUpToDate();", + " this.group.updateWorldMatrix(true, false);", + " this.uniforms.grassWorldToLocal.value.setFromMatrix4(this._matrix.copy(this.group.matrixWorld).invert());", + " this._updateBendingObjects();", + " if (!camera) return;", + "", + " // Far chunks are hidden, and thinned out before (in 3D world units).", + " const fadeDistance = Math.max(this.object._getFadeDistance(), 1) / getWorldScale(this.object);", + " this._cameraPosition.setFromMatrixPosition(camera.matrixWorld);", + " for (const chunk of this.chunks) {", + " const distance = this._chunkPosition", + " .copy(chunk.center)", + " .applyMatrix4(this.group.matrixWorld)", + " .distanceTo(this._cameraPosition);", + " const density = gdjs.evtTools.common.clamp((fadeDistance - distance) / (fadeDistance * 0.5), 0, 1);", + " chunk.mesh.visible = density > 0;", + " chunk.mesh.geometry.instanceCount = Math.ceil(chunk.blades * density);", + " }", + " }", + "", + " dispose() {", + " for (const chunk of this.chunks) {", + " chunk.mesh.geometry.dispose();", + " }", + " this.bladeGeometry.dispose();", + " this.material.dispose();", + " this.group.removeFromParent();", + " }", + "}", + "", + "gdjs.__terrainGrassExtension = { Grass };" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [], + "objectGroups": [] + }, + { + "description": "Define scattered models helper classes JavaScript code.", + "fullName": "Define scattered models helper classes", + "functionType": "Action", + "name": "DefineScatterHelperClasses", + "private": true, + "sentence": "Define scattered models helper classes JavaScript code", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "if (gdjs.__terrainScatterExtension) {", + " return;", + "}", + "// Copies grow on the grounds of the scene, like the terrain grass (see", + "// `getGrounds` in the terrain helper).", + "", + "const {", + " getWorldScale,", + " getGrounds,", + " patchShaderCode,", + " createRandom,", + " findHighestGround,", + " GroundPlacement,", + " SampleRectangle,", + " brushes,", + " codec,", + " svgIcon,", + " toolIcons,", + "} = gdjs.__terrain3DExtension;", + "", + "// Size of the object inner area: instances are scaled from it.", + "const AREA_SIZE = 2000;", + "const AREA_DEPTH = 200;", + "// The painted density is stored in a grid of this many cells on each side,", + "// whatever the size of the object.", + "const MASK_RESOLUTION = 256;", + "const MASK_FORMAT_VERSION = '1';", + "// Copies are hidden in chunks: big chunks mean fewer draw calls, small ones", + "// hide more copies out of the screen.", + "const MIN_CHUNK_SIZE = 500;", + "const MAX_CHUNK_SIZE = 2000;", + "// Copies shrink to nothing over this part of the draw distance, instead of popping out.", + "const FADE_PART = 0.15;", + "const MAX_COPIES = 200000;", + "// Prisms of the trunk collisions.", + "const TRUNK_SIDES = 6;", + "", + "const clamp01 = (value) => (value < 0 ? 0 : value > 1 ? 1 : value);", + "", + "/**", + " * @returns {number} A number for a cell of the copies grid, changing with the seed.", + " */", + "const hashCell = (seed, i, j) => {", + " let h = Math.imul(i, 374761393) + Math.imul(j, 668265263) + Math.imul(seed, 982451653);", + " h = Math.imul(h ^ (h >>> 13), 1274126177);", + " return (h ^ (h >>> 16)) >>> 0;", + "};", + "", + "/**", + " * How dense copies are, from 0 to 1, in a grid covering the object: painted", + " * with the editor brushes, the edits or events.", + " */", + "class DensityMask {", + " /** @param {number} fillValue From 0 to 255. */", + " constructor(fillValue) {", + " // `resolution` and `size` let terrain brushes paint it.", + " this.resolution = MASK_RESOLUTION;", + " this.size = MASK_RESOLUTION + 1;", + " this.values = new Uint8Array(this.size * this.size).fill(fillValue);", + " }", + "", + " /**", + " * @param {number} u From 0 to 1, along the object width.", + " * @param {number} v From 0 to 1, along the object height.", + " * @returns {number} The density, from 0 to 1, interpolated between samples.", + " */", + " sample(u, v) {", + " const x = clamp01(u) * this.resolution;", + " const y = clamp01(v) * this.resolution;", + " const i = Math.min(Math.floor(x), this.resolution - 1);", + " const j = Math.min(Math.floor(y), this.resolution - 1);", + " const fx = x - i;", + " const fy = y - j;", + " const value = (i, j) => this.values[j * this.size + i];", + " const top = value(i, j) + fx * (value(i + 1, j) - value(i, j));", + " const bottom = value(i, j + 1) + fx * (value(i + 1, j + 1) - value(i, j + 1));", + " return (top + fy * (bottom - top)) / 255;", + " }", + "", + " /**", + " * Bring the density towards a target around a segment: fully up to 70%", + " * of the radius, then fading out (forests have dense centers and soft edges).", + " * @param {{cellWidth: number, cellHeight: number, ax: number, ay: number, bx: number, by: number, radius: number}} stroke", + " * @param {number} density The target, from 0 to 1.", + " * @param {number} strength From 0 to 1.", + " * @param {SampleRectangle} changedSamples", + " */", + " paint(stroke, density, strength, changedSamples) {", + " const { values } = this;", + " const target = Math.round(clamp01(density) * 255);", + " brushes.forEachSample(", + " this,", + " stroke,", + " (index, falloff) => {", + " // The falloff of terrain brushes is (1 - distance² / radius²)².", + " const weight = clamp01(2 * Math.sqrt(falloff));", + " // Rounded towards the target, so that light strokes still paint.", + " const change = (target - values[index]) * clamp01(weight * strength);", + " values[index] += change > 0 ? Math.ceil(change) : Math.floor(change);", + " },", + " changedSamples", + " );", + " }", + "", + " /** @returns {DensityMask} */", + " clone() {", + " const mask = new DensityMask(0);", + " mask.values.set(this.values);", + " return mask;", + " }", + "}", + "", + "/**", + " * Edits written in the Edits property: a JSON list of brush strokes, like", + " * `[{\"tool\": \"add\", \"x\": 0.5, \"y\": 0.5, \"radius\": 0.2}]`. Positions and", + " * radius are fractions of the object size, like the terrain edits.", + " */", + "const applyEdits = (mask, text) => {", + " if (!text.trim()) return;", + " let editList;", + " try {", + " editList = JSON.parse(text);", + " } catch (error) {", + " console.warn('The edits of scattered models are not valid JSON: ' + error.message);", + " return;", + " }", + " if (!Array.isArray(editList)) {", + " console.warn('The edits of scattered models must be a JSON list (like [{\"tool\": \"add\", ...}]).');", + " return;", + " }", + " const numberOr = (value, defaultValue) => (typeof value === 'number' && isFinite(value) ? value : defaultValue);", + " const changedSamples = new SampleRectangle();", + " for (const edit of editList) {", + " if (!edit || typeof edit !== 'object') continue;", + " const x = numberOr(edit.x, 0.5);", + " const y = numberOr(edit.y, 0.5);", + " const stroke = {", + " cellWidth: 1 / mask.resolution,", + " cellHeight: 1 / mask.resolution,", + " ax: x,", + " ay: y,", + " bx: numberOr(edit.toX, x),", + " by: numberOr(edit.toY, y),", + " radius: numberOr(edit.radius, 0.1),", + " };", + " const strength = clamp01(numberOr(edit.strength, 1));", + " if (edit.tool === 'add') {", + " mask.paint(stroke, numberOr(edit.density, 1), strength, changedSamples);", + " } else if (edit.tool === 'erase') {", + " mask.paint(stroke, 0, strength, changedSamples);", + " } else {", + " console.warn('Unknown tool in the edits of scattered models: \"' + edit.tool + '\" (use add or erase).');", + " }", + " }", + "};", + "", + "/**", + " * The density painted in the editor, stored as its difference with the", + " * density from the properties (the coverage and the edits), like the terrain", + " * sculpt data.", + " */", + "const maskCodec = {", + " /**", + " * @param {DensityMask} mask", + " * @param {DensityMask} baseMask", + " */", + " encode(mask, baseMask) {", + " const changes = new Int32Array(mask.values.length);", + " let hasChanges = false;", + " for (let index = 0; index < changes.length; index++) {", + " changes[index] = mask.values[index] - baseMask.values[index];", + " if (changes[index] !== 0) hasChanges = true;", + " }", + " if (!hasChanges) return '';", + " return MASK_FORMAT_VERSION + ';' + MASK_RESOLUTION + ';' + codec.encodeGrids([changes], mask.size);", + " },", + "", + " /**", + " * @param {string} text", + " * @param {DensityMask} baseMask", + " * @returns {DensityMask | null} null if the text is empty or not valid.", + " */", + " decode(text, baseMask) {", + " if (!text) return null;", + " const parts = text.split(';');", + " if (parts[0] !== MASK_FORMAT_VERSION || parseInt(parts[1], 10) !== MASK_RESOLUTION) return null;", + " try {", + " const mask = baseMask.clone();", + " const [changes] = codec.decodeGrids(parts[2], mask.size, 1);", + " for (let index = 0; index < changes.length; index++) {", + " // The base may have changed since the data was saved.", + " mask.values[index] = Math.min(Math.max(mask.values[index] + changes[index], 0), 255);", + " }", + " return mask;", + " } catch (error) {", + " console.error('The painted data of scattered models could not be read:', error);", + " return null;", + " }", + " },", + "};", + "", + "const fadeShaderDeclarations = `", + "uniform float scatterDrawDistance;", + "`;", + "// Instance matrices are applied after this: copies shrink towards their base.", + "const fadeShaderCode = `", + "#ifdef USE_INSTANCING", + "vec3 scatterCopyPosition = (modelMatrix * instanceMatrix * vec4(0.0, 0.0, 0.0, 1.0)).xyz;", + "transformed *= clamp(", + " (scatterDrawDistance - distance(scatterCopyPosition, cameraPosition)) / (scatterDrawDistance * ${FADE_PART}),", + " 0.0,", + " 1.0", + ");", + "#endif", + "`;", + "", + "const flipTriangles = (geometry) => {", + " const index = geometry.index;", + " for (let offset = 0; offset + 2 < index.count; offset += 3) {", + " const second = index.getX(offset + 1);", + " index.setX(offset + 1, index.getX(offset + 2));", + " index.setX(offset + 2, second);", + " }", + "};", + "", + "/**", + " * The parts of a model, ready to be drawn with instancing: each glTF", + " * primitive with its material. Geometries are moved so that the model base", + " * is at the origin, its height is 1 and it stands along Z (like 3D model", + " * objects with a 90° rotation around X).", + " * @param {THREE.Object3D} modelScene", + " * @param {object} uniforms Uniforms added to the materials.", + " * @returns {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}}", + " */", + "const createModelParts = (modelScene, uniforms) => {", + " modelScene.updateMatrixWorld(true);", + " const rootInverse = modelScene.matrixWorld.clone().invert();", + " const standUp = new THREE.Matrix4().makeRotationX(Math.PI / 2);", + " /** @type {Array<{mesh: THREE.Mesh, matrix: THREE.Matrix4}>} */", + " const meshes = [];", + " const box = new THREE.Box3();", + " modelScene.traverse((node) => {", + " if (!node.isMesh || !node.geometry || !node.geometry.attributes.position) return;", + " const matrix = standUp.clone().multiply(rootInverse).multiply(node.matrixWorld);", + " if (!node.geometry.boundingBox) node.geometry.computeBoundingBox();", + " box.union(node.geometry.boundingBox.clone().applyMatrix4(matrix));", + " meshes.push({ mesh: node, matrix });", + " });", + " if (box.isEmpty()) return { parts: [], width: 0, depth: 0 };", + "", + " const height = Math.max(box.max.z - box.min.z, 0.000001);", + " // The Y axis of the scene goes down: the model is flipped, like 3D model objects.", + " const normalization = new THREE.Matrix4()", + " .makeScale(1 / height, -1 / height, 1 / height)", + " .multiply(", + " new THREE.Matrix4().makeTranslation(", + " -(box.min.x + box.max.x) / 2,", + " -(box.min.y + box.max.y) / 2,", + " -box.min.z", + " )", + " );", + " const parts = meshes.map(({ mesh, matrix }) => {", + " let geometry = mesh.geometry.clone();", + " if (!geometry.index) {", + " const indices = Array.from({ length: geometry.attributes.position.count }, (_, index) => index);", + " geometry.setIndex(indices);", + " }", + " geometry.applyMatrix4(normalization.clone().multiply(matrix));", + " // The flip turned the faces inside out.", + " flipTriangles(geometry);", + " geometry.computeBoundingSphere();", + " geometry.computeBoundingBox();", + " const patchMaterial = (originalMaterial) => {", + " const material = originalMaterial.clone();", + " // Like 3D model objects (\"Standard without metalness\"), which look better without an environment map.", + " if (material.metalness) material.metalness = 0;", + " material.onBeforeCompile = (shader) => {", + " Object.assign(shader.uniforms, uniforms);", + " shader.vertexShader = patchShaderCode(shader.vertexShader, [", + " ['#include ', '#include \\n' + fadeShaderDeclarations],", + " ['#include ', '#include \\n' + fadeShaderCode],", + " ], 'ScatteredModels');", + " };", + " material.customProgramCacheKey = () => 'ScatteredModels';", + " return material;", + " };", + " const material = Array.isArray(mesh.material) ? mesh.material.map(patchMaterial) : patchMaterial(mesh.material);", + " return { geometry, material };", + " });", + " return {", + " parts,", + " width: (box.max.x - box.min.x) / height,", + " depth: (box.max.y - box.min.y) / height,", + " };", + "};", + "", + "/**", + " * A copy of the model: its base position in the scene, its scale (the", + " * height of the copy) and the key of its cell in the grid of copies.", + " * @typedef {{x: number, y: number, z: number, angle: number, height: number, key: number}} Copy", + " */", + "", + "/**", + " * The scattered models of an object: copies of a 3D model placed on the", + " * grounds of the scene, drawn with GPU instancing in chunks that are hidden", + " * when far or off-screen.", + " */", + "class Scatter {", + " /** @param {gdjs.CustomRuntimeObject3D} object */", + " constructor(object) {", + " this.object = object;", + " this.group = new THREE.Group();", + " object.get3DRendererObject().add(this.group);", + " /** @type {Array<{firstCellI: number, firstCellJ: number, meshes: THREE.InstancedMesh[], copies: Copy[], center: THREE.Vector3, radius: number}>} */", + " this.chunks = [];", + " /**", + " * The grid of copies: one copy at most in each cell, cells grouped in chunks.", + " * @type {{spacing: number, cellsPerChunk: number, cellsX: number, cellsY: number} | null}", + " */", + " this.grid = null;", + " this.placement = new GroundPlacement(object.getRuntimeScene());", + " this.uniforms = {", + " // In 3D world units.", + " scatterDrawDistance: { value: 1 },", + " };", + " /** @type {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}} */", + " this.model = { parts: [], width: 0, depth: 0 };", + " this.modelResourceName = '';", + " this.loadingModelResourceName = '';", + " /**", + " * Copies removed by events (for example, cut trees), by key.", + " * @type {Map}", + " */", + " this.removedCopies = new Map();", + " /** Increased each time copies change, for the collisions. */", + " this.version = 0;", + " this.isDisposed = false;", + " /** The last value of the painted data property read or saved. */", + " this.paintData = '';", + " /**", + " * Values saved by the editor tools: the editor sends them back with a", + " * hot-reload, possibly after newer ones were saved.", + " * @type {string[]}", + " */", + " this.savedPaintData = [];", + " this.maskSource = '';", + " /** @type {DensityMask} */", + " this.baseMask = new DensityMask(255);", + " /** @type {DensityMask} */", + " this.mask = this.baseMask;", + " this.changedSamples = new SampleRectangle();", + " this._stroke = { cellWidth: 1, cellHeight: 1, ax: 0, ay: 0, bx: 0, by: 0, radius: 0 };", + " this._groundAtCopy = { ground: null, z: 0 };", + " this._matrix = new THREE.Matrix4();", + " this._raycastSphere = new THREE.Sphere();", + " this._raycastRay = new THREE.Ray();", + " this._raycastBox = new THREE.Box3();", + " this._raycastInverseMatrix = new THREE.Matrix4();", + " this._raycastScale = new THREE.Vector3();", + " /** Placed on a copy to test the triangles of the model. */", + " this._raycastMesh = new THREE.Mesh();", + " /** @type {THREE.Intersection[]} */", + " this._raycastHits = [];", + " this._color = new THREE.Color();", + " this._tint = new THREE.Color();", + " this._cameraPosition = new THREE.Vector3();", + " this._chunkPosition = new THREE.Vector3();", + " /**", + " * Trunks, for the 3D physics behavior and navigation meshes. Always given,", + " * empty without trunk collisions: otherwise these behaviors would use the", + " * object box, covering the whole area.", + " * @type {gdjs.Surface}", + " */", + " this.surface = {", + " getVersion: () => this.version,", + " getChangedArea: () => null,", + " getTriangles: () => this._getTrunkTriangles(),", + " getHeightField: () => null,", + " };", + " /** The trunk collision properties, changing the surface. */", + " this.trunkKey = '';", + " this.loadMaskFromProperties();", + " this.updateFromProperties();", + " // Before the behaviors create their shapes.", + " this._updateSurface();", + " }", + "", + " _getMaskSource() {", + " return this.object._getCoverage() + '|' + this.object._getEdits();", + " }", + "", + " loadMaskFromProperties() {", + " const { object } = this;", + " this.maskSource = this._getMaskSource();", + " this.paintData = object._getPaintData();", + " this.baseMask = new DensityMask(object._getCoverage() === 'Painted' ? 0 : 255);", + " applyEdits(this.baseMask, object._getEdits());", + " this.mask = maskCodec.decode(this.paintData, this.baseMask) || this.baseMask.clone();", + " this.placement.placeAgain();", + " }", + "", + " onHotReloading() {", + " const paintData = this.object._getPaintData();", + " const savedIndex = this.savedPaintData.indexOf(paintData);", + " if (savedIndex !== -1 && this._getMaskSource() === this.maskSource) {", + " // The object already has this data, or newer changes.", + " this.savedPaintData.splice(0, savedIndex + 1);", + " } else if (paintData !== this.paintData || this._getMaskSource() !== this.maskSource) {", + " this.savedPaintData.length = 0;", + " this.loadMaskFromProperties();", + " }", + " this.updateFromProperties();", + " }", + "", + " /** @returns {string} The data to save in the painted data property. */", + " save() {", + " this.paintData = maskCodec.encode(this.mask, this.baseMask);", + " this.savedPaintData.push(this.paintData);", + " return this.paintData;", + " }", + "", + " updateFromProperties() {", + " const { object } = this;", + " const modelResourceName = object._getModel();", + " if (modelResourceName !== this.modelResourceName) {", + " this.modelResourceName = modelResourceName;", + " this._loadModel();", + " }", + " this.placement.placeAgain();", + " }", + "", + " _disposeModel() {", + " for (const part of this.model.parts) {", + " part.geometry.dispose();", + " for (const material of Array.isArray(part.material) ? part.material : [part.material]) {", + " material.dispose();", + " }", + " }", + " this.model = { parts: [], width: 0, depth: 0 };", + " }", + "", + " _loadModel() {", + " this._disposeChunks(this.chunks);", + " this._disposeModel();", + " const resourceName = this.modelResourceName;", + " if (!resourceName) return;", + " const game = this.object.getRuntimeScene().getGame();", + " const modelManager = game.getModel3DManager();", + " const gltf = modelManager.getModel(resourceName);", + " if (gltf === modelManager._invalidModel) {", + " // Not loaded yet, for example a model just chosen in the editor.", + " if (this.loadingModelResourceName === resourceName) return;", + " this.loadingModelResourceName = resourceName;", + " game.getResourceLoader()", + " .loadResource(resourceName)", + " .then(() => {", + " if (this.isDisposed || this.modelResourceName !== resourceName) return;", + " this.loadingModelResourceName = '';", + " if (modelManager.getModel(resourceName) !== modelManager._invalidModel) {", + " this._loadModel();", + " this.placement.placeAgain();", + " }", + " })", + " .catch(() => {});", + " return;", + " }", + " this.model = createModelParts(gltf.scene, this.uniforms);", + " }", + "", + " _getPlacementKey() {", + " const { object } = this;", + " return [", + " object.getX(), object.getY(), object.getZ(), object.getWidth(), object.getHeight(), object.getDepth(),", + " object._getDensity(), object._getSize(), object._getMinScale(), object._getMaxScale(),", + " object._getRandomRotation(), object._getRandomTint(), object._getGroundLayer(), object._getMaxSlope(),", + " object._getMinGroundZ(), object._getMaxGroundZ(), object._getSeed(), object._getDrawDistance(),", + " object._getCastShadow(), object._getReceiveShadow(), this.model.parts.length,", + " ].join('|');", + " }", + "", + " _getChunkSize() {", + " return Math.min(Math.max(this.object._getDrawDistance() / 4, MIN_CHUNK_SIZE), MAX_CHUNK_SIZE);", + " }", + "", + " /** @returns {number} The distance between copies (one copy per cell of this size at most). */", + " _getSpacing() {", + " const { object } = this;", + " const density = Math.max(object._getDensity(), 0);", + " if (density <= 0) return 0;", + " const spacing = 100 / Math.sqrt(density);", + " // Too many cells would freeze the game: the density is lowered.", + " const cellCount = (object.getWidth() / spacing) * (object.getHeight() / spacing);", + " return cellCount > MAX_COPIES ? spacing * Math.sqrt(cellCount / MAX_COPIES) : spacing;", + " }", + "", + " /**", + " * Find the copy of a cell of the grid, if there is one.", + " * @returns {Copy | null}", + " */", + " _placeCopy(cellI, cellJ, spacing, grounds) {", + " const { object } = this;", + " const width = object.getWidth();", + " const height = object.getHeight();", + " const nextRandom = createRandom(hashCell(Math.floor(object._getSeed()), cellI, cellJ));", + " // Always drawn in the same order, so that a copy stays the same when properties change.", + " const x = (cellI + nextRandom()) * spacing;", + " const y = (cellJ + nextRandom()) * spacing;", + " const presence = nextRandom();", + " const growth = nextRandom();", + " const scaleFactor = nextRandom();", + " const angle = nextRandom() * Math.PI * 2;", + " if (x > width || y > height) return null;", + " if (presence >= this.mask.sample(x / width, y / height)) return null;", + " const key = cellJ * 65536 + cellI;", + " if (this.removedCopies.has(key)) return null;", + "", + " const sceneX = object.getX() + x;", + " const sceneY = object.getY() + y;", + " const { ground, z } = findHighestGround(grounds, sceneX, sceneY, this._groundAtCopy);", + " const groundZ = ground ? z : object.getZ();", + " if (groundZ < object._getMinGroundZ() || groundZ > object._getMaxGroundZ()) return null;", + " const copyHeight =", + " Math.max(object._getSize(), 0) *", + " (object._getMinScale() + (object._getMaxScale() - object._getMinScale()) * scaleFactor);", + " if (ground) {", + " if (ground.getSlopeAt(sceneX, sceneY) > object._getMaxSlope()) return null;", + " const groundLayer = parseInt(object._getGroundLayer(), 10) || 0;", + " if (groundLayer > 0) {", + " if (growth > ground.getLayerWeightAt(sceneX, sceneY, groundLayer)) return null;", + " // Copies don't stand at the border of their layer, so that trees don't cover roads.", + " const margin = (copyHeight * Math.max(this.model.width, this.model.depth)) / 4;", + " for (const [offsetX, offsetY] of [[margin, 0], [-margin, 0], [0, margin], [0, -margin]]) {", + " if (ground.getLayerWeightAt(sceneX + offsetX, sceneY + offsetY, groundLayer) < 0.5) return null;", + " }", + " }", + " }", + " return {", + " x: sceneX,", + " y: sceneY,", + " z: groundZ,", + " angle: object._getRandomRotation() ? angle : 0,", + " height: copyHeight,", + " key,", + " };", + " }", + "", + " _disposeChunks(chunks) {", + " for (const chunk of chunks) {", + " for (const mesh of chunk.meshes) {", + " mesh.removeFromParent();", + " // Geometries and materials are the ones of the model.", + " mesh.dispose();", + " }", + " }", + " if (chunks === this.chunks) this.chunks = [];", + " }", + "", + " /** Place the copies again, for example after the ground or the object changed. */", + " rebuild() {", + " this._disposeChunks(this.chunks);", + " this.version++;", + " const { object } = this;", + " const spacing = this._getSpacing();", + " if (spacing <= 0 || this.model.parts.length === 0) {", + " this.grid = null;", + " return;", + " }", + " const cellsPerChunk = Math.max(Math.round(this._getChunkSize() / spacing), 1);", + " const grid = {", + " spacing,", + " cellsPerChunk,", + " cellsX: Math.ceil(object.getWidth() / spacing),", + " cellsY: Math.ceil(object.getHeight() / spacing),", + " };", + " this.grid = grid;", + " for (let firstCellJ = 0; firstCellJ < grid.cellsY; firstCellJ += cellsPerChunk) {", + " for (let firstCellI = 0; firstCellI < grid.cellsX; firstCellI += cellsPerChunk) {", + " this._createChunk(firstCellI, firstCellJ);", + " }", + " }", + " }", + "", + " /**", + " * Place again the copies of the chunks touching a rectangle of the scene,", + " * for example after painting.", + " */", + " _rebuildArea(minX, minY, maxX, maxY) {", + " const { grid, object } = this;", + " if (!grid) return;", + " const chunkSize = grid.spacing * grid.cellsPerChunk;", + " const firstChunkI = Math.max(Math.floor((minX - object.getX()) / chunkSize), 0);", + " const firstChunkJ = Math.max(Math.floor((minY - object.getY()) / chunkSize), 0);", + " const lastChunkI = Math.min(Math.floor((maxX - object.getX()) / chunkSize), Math.ceil(grid.cellsX / grid.cellsPerChunk) - 1);", + " const lastChunkJ = Math.min(Math.floor((maxY - object.getY()) / chunkSize), Math.ceil(grid.cellsY / grid.cellsPerChunk) - 1);", + " if (firstChunkI > lastChunkI || firstChunkJ > lastChunkJ) return;", + " const isInArea = (chunk) => {", + " const chunkI = chunk.firstCellI / grid.cellsPerChunk;", + " const chunkJ = chunk.firstCellJ / grid.cellsPerChunk;", + " return chunkI >= firstChunkI && chunkI <= lastChunkI && chunkJ >= firstChunkJ && chunkJ <= lastChunkJ;", + " };", + " this._disposeChunks(this.chunks.filter(isInArea));", + " this.chunks = this.chunks.filter((chunk) => !isInArea(chunk));", + " for (let chunkJ = firstChunkJ; chunkJ <= lastChunkJ; chunkJ++) {", + " for (let chunkI = firstChunkI; chunkI <= lastChunkI; chunkI++) {", + " this._createChunk(chunkI * grid.cellsPerChunk, chunkJ * grid.cellsPerChunk);", + " }", + " }", + " this.version++;", + " }", + "", + " _createChunk(firstCellI, firstCellJ) {", + " const { grid } = this;", + " const grounds = Array.from(this.placement.grounds);", + " /** @type {Copy[]} */", + " const copies = [];", + " for (let cellJ = firstCellJ; cellJ < Math.min(firstCellJ + grid.cellsPerChunk, grid.cellsY); cellJ++) {", + " for (let cellI = firstCellI; cellI < Math.min(firstCellI + grid.cellsPerChunk, grid.cellsX); cellI++) {", + " const copy = this._placeCopy(cellI, cellJ, grid.spacing, grounds);", + " if (copy) copies.push(copy);", + " }", + " }", + " if (copies.length === 0) return;", + "", + " const { object } = this;", + " // Copies are placed in scene units, and the object group is scaled", + " // from its inner area: they are scaled back to keep their proportions.", + " const scaleX = Math.max(object.getWidth() / AREA_SIZE, 0.0001);", + " const scaleY = Math.max(object.getHeight() / AREA_SIZE, 0.0001);", + " const scaleZ = Math.max(object.getDepth() / AREA_DEPTH, 0.0001);", + " const matrices = new Float32Array(copies.length * 16);", + " const tintColor = gdjs.rgbOrHexStringToNumber(object._getRandomTint());", + " const hasTint = tintColor !== 0xffffff;", + " const colors = hasTint ? new Float32Array(copies.length * 3) : null;", + " this._tint.setHex(tintColor);", + " const min = new THREE.Vector3(Infinity, Infinity, Infinity);", + " const max = new THREE.Vector3(-Infinity, -Infinity, -Infinity);", + " copies.forEach((copy, index) => {", + " const cos = Math.cos(copy.angle) * copy.height;", + " const sin = Math.sin(copy.angle) * copy.height;", + " const localX = (copy.x - object.getX()) / scaleX;", + " const localY = (copy.y - object.getY()) / scaleY;", + " const localZ = (copy.z - object.getZ()) / scaleZ;", + " this._matrix.set(", + " cos / scaleX, -sin / scaleX, 0, localX,", + " sin / scaleY, cos / scaleY, 0, localY,", + " 0, 0, copy.height / scaleZ, localZ,", + " 0, 0, 0, 1", + " );", + " this._matrix.toArray(matrices, index * 16);", + " if (colors) {", + " // The tint of each copy is between white and the tint color.", + " this._color.setRGB(1, 1, 1).lerp(this._tint, createRandom(copy.key + 1)());", + " this._color.toArray(colors, index * 3);", + " }", + " const reach = (copy.height * Math.max(this.model.width, this.model.depth)) / 2;", + " min.min(this._chunkPosition.set(copy.x - reach, copy.y - reach, copy.z));", + " max.max(this._chunkPosition.set(copy.x + reach, copy.y + reach, copy.z + copy.height));", + " });", + " const instanceMatrix = new THREE.InstancedBufferAttribute(matrices, 16);", + " const instanceColor = colors ? new THREE.InstancedBufferAttribute(colors, 3) : null;", + " const castShadow = object._getCastShadow();", + " const receiveShadow = object._getReceiveShadow();", + " const meshes = this.model.parts.map((part) => {", + " const mesh = new THREE.InstancedMesh(part.geometry, part.material, 0);", + " // All the parts of the model share the positions of the copies.", + " mesh.instanceMatrix = instanceMatrix;", + " mesh.instanceColor = instanceColor;", + " mesh.count = copies.length;", + " mesh.computeBoundingSphere();", + " mesh.castShadow = castShadow;", + " mesh.receiveShadow = receiveShadow;", + " // The first mesh tests all the parts of the copies.", + " mesh.raycast = () => {};", + " this.group.add(mesh);", + " return mesh;", + " });", + " const center = new THREE.Vector3(", + " ((min.x + max.x) / 2 - object.getX()) / scaleX,", + " ((min.y + max.y) / 2 - object.getY()) / scaleY,", + " ((min.z + max.z) / 2 - object.getZ()) / scaleZ", + " );", + " const chunk = { firstCellI, firstCellJ, meshes, copies, center, radius: min.distanceTo(max) / 2 };", + " // Used by the scene editor to select the object when a copy is clicked.", + " meshes[0].raycast = (raycaster, intersects) => this._raycastChunk(chunk, raycaster, intersects);", + " this.chunks.push(chunk);", + " }", + "", + " /**", + " * Add the hits of a ray on the copies of a chunk, like they are drawn. There", + " * can be thousands of copies: only the ones whose box is crossed by the ray", + " * are tested with the triangles of the model.", + " * @param {{meshes: THREE.InstancedMesh[], copies: Copy[], center: THREE.Vector3, radius: number}} chunk", + " * @param {THREE.Raycaster} raycaster", + " * @param {THREE.Intersection[]} intersects", + " */", + " _raycastChunk(chunk, raycaster, intersects) {", + " const { object, group } = this;", + " this._raycastSphere.center.copy(chunk.center).applyMatrix4(group.matrixWorld);", + " this._raycastSphere.radius = chunk.radius / getWorldScale(object);", + " if (!raycaster.ray.intersectsSphere(this._raycastSphere)) return;", + "", + " // The ray in the scene coordinates, like the copies.", + " const scale = this._raycastScale.set(", + " Math.max(object.getWidth() / AREA_SIZE, 0.0001),", + " Math.max(object.getHeight() / AREA_SIZE, 0.0001),", + " Math.max(object.getDepth() / AREA_DEPTH, 0.0001)", + " );", + " const ray = this._raycastRay.copy(raycaster.ray).applyMatrix4(this._raycastInverseMatrix.copy(group.matrixWorld).invert());", + " ray.origin.multiply(scale).add(this._chunkPosition.set(object.getX(), object.getY(), object.getZ()));", + " ray.direction.multiply(scale);", + "", + " const reachPerHeight = Math.max(this.model.width, this.model.depth) / 2;", + " const instanceMatrices = chunk.meshes[0].instanceMatrix.array;", + " chunk.copies.forEach((copy, index) => {", + " const reach = copy.height * reachPerHeight;", + " this._raycastBox.min.set(copy.x - reach, copy.y - reach, copy.z);", + " this._raycastBox.max.set(copy.x + reach, copy.y + reach, copy.z + copy.height);", + " if (!ray.intersectsBox(this._raycastBox)) return;", + " this._raycastMesh.matrixWorld.fromArray(instanceMatrices, index * 16).premultiply(group.matrixWorld);", + " for (const mesh of chunk.meshes) {", + " this._raycastMesh.geometry = mesh.geometry;", + " this._raycastMesh.material = mesh.material;", + " this._raycastHits.length = 0;", + " this._raycastMesh.raycast(raycaster, this._raycastHits);", + " for (const hit of this._raycastHits) {", + " hit.object = mesh;", + " hit.instanceId = index;", + " intersects.push(hit);", + " }", + " }", + " });", + " }", + "", + " /** @param {THREE.Camera | null} camera */", + " _hideFarChunks(camera) {", + " if (!camera) return;", + " const worldScale = getWorldScale(this.object);", + " const drawDistance = Math.max(this.object._getDrawDistance(), 1) / worldScale;", + " this.uniforms.scatterDrawDistance.value = drawDistance;", + " this._cameraPosition.setFromMatrixPosition(camera.matrixWorld);", + " for (const chunk of this.chunks) {", + " const distance = this._chunkPosition", + " .copy(chunk.center)", + " .applyMatrix4(this.group.matrixWorld)", + " .distanceTo(this._cameraPosition);", + " const isVisible = distance - chunk.radius / worldScale < drawDistance;", + " for (const mesh of chunk.meshes) mesh.visible = isVisible;", + " }", + " }", + "", + " /**", + " * @param {number} elapsedSeconds", + " * @param {THREE.Camera | null} camera", + " */", + " update(elapsedSeconds, camera) {", + " if (this.placement.shouldPlaceAgain(elapsedSeconds, this._getPlacementKey())) {", + " this.rebuild();", + " }", + " if (!this.changedSamples.isEmpty()) {", + " const { object } = this;", + " const sampleWidth = object.getWidth() / MASK_RESOLUTION;", + " const sampleHeight = object.getHeight() / MASK_RESOLUTION;", + " const { minI, minJ, maxI, maxJ } = this.changedSamples;", + " // Copies between samples use the density of the samples around them.", + " this._rebuildArea(", + " object.getX() + (minI - 1) * sampleWidth,", + " object.getY() + (minJ - 1) * sampleHeight,", + " object.getX() + (maxI + 1) * sampleWidth,", + " object.getY() + (maxJ + 1) * sampleHeight", + " );", + " this.changedSamples.clear();", + " }", + " // The object transformation is applied just before rendering, it's needed now.", + " this.object.getRenderer().ensureUpToDate();", + " this.group.updateWorldMatrix(true, false);", + " this._hideFarChunks(camera);", + " this._updateSurface();", + " }", + "", + " _updateSurface() {", + " const { object } = this;", + " if (typeof object.setSurface !== 'function') return;", + " const trunkKey = object._getTrunkCollisions() + '|' + object._getTrunkDiameter();", + " if (trunkKey !== this.trunkKey) {", + " this.trunkKey = trunkKey;", + " this.version++;", + " }", + " if (object.getSurface() !== this.surface) object.setSurface(this.surface);", + " }", + "", + " /** @returns {Copy[]} */", + " getCopies() {", + " const copies = [];", + " for (const chunk of this.chunks) copies.push(...chunk.copies);", + " return copies;", + " }", + "", + " /**", + " * @returns {Copy | null} The copy whose base is the nearest to a position (on X and Y), if any.", + " */", + " findNearestCopy(x, y) {", + " let nearestCopy = null;", + " let nearestDistanceSquared = Infinity;", + " for (const chunk of this.chunks) {", + " for (const copy of chunk.copies) {", + " const distanceSquared = (copy.x - x) * (copy.x - x) + (copy.y - y) * (copy.y - y);", + " if (distanceSquared < nearestDistanceSquared) {", + " nearestDistanceSquared = distanceSquared;", + " nearestCopy = copy;", + " }", + " }", + " }", + " return nearestCopy;", + " }", + "", + " getCopyCount() {", + " let count = 0;", + " for (const chunk of this.chunks) count += chunk.copies.length;", + " return count;", + " }", + "", + " /** Remove the copies whose base is in a circle (they come back when the scene is restarted). */", + " removeCopies(x, y, radius) {", + " let hasRemovedCopies = false;", + " for (const chunk of this.chunks) {", + " for (const copy of chunk.copies) {", + " if ((copy.x - x) * (copy.x - x) + (copy.y - y) * (copy.y - y) <= radius * radius) {", + " this.removedCopies.set(copy.key, copy);", + " hasRemovedCopies = true;", + " }", + " }", + " }", + " if (hasRemovedCopies) this._rebuildArea(x - radius, y - radius, x + radius, y + radius);", + " }", + "", + " /** Paint the full density around a position, and bring back the copies removed there. */", + " addCopies(x, y, radius) {", + " for (const [key, copy] of this.removedCopies) {", + " if ((copy.x - x) * (copy.x - x) + (copy.y - y) * (copy.y - y) <= radius * radius) {", + " this.removedCopies.delete(key);", + " }", + " }", + " this.paint(x, y, x, y, radius, 1, 1);", + " }", + "", + " _getStroke(x1, y1, x2, y2, radius) {", + " const { object } = this;", + " const stroke = this._stroke;", + " stroke.cellWidth = object.getWidth() / MASK_RESOLUTION;", + " stroke.cellHeight = object.getHeight() / MASK_RESOLUTION;", + " stroke.ax = x1 - object.getX();", + " stroke.ay = y1 - object.getY();", + " stroke.bx = x2 - object.getX();", + " stroke.by = y2 - object.getY();", + " stroke.radius = Math.max(radius, 0);", + " return stroke;", + " }", + "", + " /**", + " * Paint the density around a segment (copies are placed again at the next frame).", + " * @param {number} density From 0 (erase) to 1 (as dense as the Density property).", + " * @param {number} strength From 0 to 1.", + " */", + " paint(x1, y1, x2, y2, radius, density, strength) {", + " const changedSamples = new SampleRectangle();", + " this.mask.paint(this._getStroke(x1, y1, x2, y2, radius), density, strength, changedSamples);", + " this.changedSamples.add(changedSamples);", + " }", + "", + " /** @param {DensityMask} mask */", + " setMask(mask) {", + " this.mask = mask;", + " this.rebuild();", + " }", + "", + " /**", + " * Prisms around the trunks, from 0 to 1 in the object box (see `gdjs.Surface`).", + " * @returns {gdjs.SurfaceTriangles}", + " */", + " _getTrunkTriangles() {", + " const { object } = this;", + " const copies = object._getTrunkCollisions() ? this.getCopies() : [];", + " const verticesPerCopy = TRUNK_SIDES * 2 + 1;", + " const positions = new Float32Array(copies.length * verticesPerCopy * 3);", + " const indices = new Uint32Array(copies.length * TRUNK_SIDES * 3 * 3);", + " const width = object.getWidth() || 1;", + " const height = object.getHeight() || 1;", + " const depth = object.getDepth() || 1;", + " let indexOffset = 0;", + " copies.forEach((copy, copyIndex) => {", + " const radius = (Math.max(object._getTrunkDiameter(), 0) / 2) * (copy.height / Math.max(object._getSize(), 1));", + " const top = copy.z + copy.height * 0.8;", + " const firstVertex = copyIndex * verticesPerCopy;", + " const setVertex = (vertex, x, y, z) => {", + " positions[vertex * 3] = (x - object.getX()) / width;", + " positions[vertex * 3 + 1] = (y - object.getY()) / height;", + " positions[vertex * 3 + 2] = (z - object.getZ()) / depth;", + " };", + " for (let side = 0; side < TRUNK_SIDES; side++) {", + " const angle = ((side + 0.5) / TRUNK_SIDES) * Math.PI * 2;", + " const x = copy.x + Math.cos(angle) * radius;", + " const y = copy.y + Math.sin(angle) * radius;", + " // Starting a bit under the ground, which can be sloped.", + " setVertex(firstVertex + side * 2, x, y, copy.z - radius);", + " setVertex(firstVertex + side * 2 + 1, x, y, top);", + " }", + " const center = firstVertex + TRUNK_SIDES * 2;", + " setVertex(center, copy.x, copy.y, top);", + " for (let side = 0; side < TRUNK_SIDES; side++) {", + " const bottom = firstVertex + side * 2;", + " const nextBottom = firstVertex + ((side + 1) % TRUNK_SIDES) * 2;", + " // Triangles face out of the prism (vertices at odd indices are at the top).", + " indices.set([bottom, nextBottom, bottom + 1, nextBottom, nextBottom + 1, bottom + 1], indexOffset);", + " indices.set([center, bottom + 1, nextBottom + 1], indexOffset + 6);", + " indexOffset += 9;", + " }", + " });", + " if (copies.length === 0) {", + " // Without triangles, the physics engine and navigation meshes would use", + " // the object box: a tiny triangle under the object is used instead.", + " return { positions: new Float32Array([0, 0, -1, 0.0001, 0, -1, 0, 0.0001, -1]), indices: new Uint32Array([0, 2, 1]) };", + " }", + " return { positions, indices };", + " }", + "", + " dispose() {", + " this.isDisposed = true;", + " if (typeof this.object.setSurface === 'function' && this.object.getSurface() === this.surface) {", + " this.object.setSurface(null);", + " }", + " this._disposeChunks(this.chunks);", + " this._disposeModel();", + " this.group.removeFromParent();", + " }", + "}", + "", + "const scatterToolIcons = {", + " Add: svgIcon(", + " ''", + " ),", + " Erase: svgIcon(", + " ''", + " ),", + "};", + "", + "/**", + " * The brushes shown in the scene editor when scattered models are selected,", + " * to add or erase copies. Without the needed editor functions (older GDevelop", + " * versions), nothing is shown: copies can still be painted with events.", + " */", + "class ScatterEditorTools {", + " constructor() {", + " /** '' when no brush is used. */", + " this.toolName = '';", + " this.sizePercent = 30;", + " this.strengthPercent = 50;", + " /** @type {Scatter | null} */", + " this.scatter = null;", + " /** @type {gdjs.InGameEditor | null} */", + " this.editor = null;", + " this.isStroking = false;", + " this.lastStrokeTime = 0;", + " /** @type {Array<{mask: DensityMask, baseMask: DensityMask}>} */", + " this.undoSteps = [];", + " this.raycaster = new THREE.Raycaster();", + " this.ndc = new THREE.Vector2();", + " this._groundAtCursor = { ground: null, z: 0 };", + " /** A circle following the ground under the cursor. */", + " this.brushRing = new THREE.LineLoop(", + " new THREE.BufferGeometry().setAttribute('position', new THREE.BufferAttribute(new Float32Array(64 * 3), 3)),", + " new THREE.LineBasicMaterial({ color: 0xffffff, depthTest: false, transparent: true })", + " );", + " this.brushRing.renderOrder = 1000;", + " this.brushRing.frustumCulled = false;", + " this.brushRing.raycast = () => {};", + " }", + "", + " /** @param {gdjs.InGameEditor} editor */", + " update(editor) {", + " if (typeof editor.showToolbar !== 'function') return;", + " this.editor = editor;", + " const selectedObjects = editor.getSelectedObjects();", + " const scatter =", + " selectedObjects.length === 1 && selectedObjects[0].__terrainScatter ? selectedObjects[0].__terrainScatter : null;", + " if (scatter !== this.scatter) {", + " this._endStroke();", + " this.scatter = scatter;", + " this.undoSteps.length = 0;", + " }", + " if (scatter) editor.showToolbar('ScatteredModels', this._getToolbarItems());", + " const game = editor.getRuntimeGame();", + " if (!scatter || !this.toolName) {", + " this.brushRing.removeFromParent();", + " return;", + " }", + " const inputManager = game.getInputManager();", + " if (inputManager.wasKeyJustPressed(gdjs.evtTools.input.keysNameToCode.Escape)) {", + " this._endStroke();", + " this.toolName = '';", + " this.brushRing.removeFromParent();", + " return;", + " }", + " editor.captureLeftMouseButton();", + " const hit = this._getPositionUnderCursor(scatter, game);", + " const radius = this._getRadius();", + " this._showBrushRing(scatter, hit, radius);", + "", + " const isPressedForTool =", + " typeof editor.isCapturedLeftMouseButtonPressed === 'function'", + " ? editor.isCapturedLeftMouseButtonPressed()", + " : inputManager.isMouseButtonPressed(0);", + " // The button is released outside the canvas when the mouse is on a toolbar.", + " const isPressed = isPressedForTool && inputManager.isMouseInsideCanvas();", + " if (isPressed && hit && !this.isStroking) {", + " this._startStroke(scatter);", + " }", + " if (this.isStroking && isPressed && hit) {", + " this._applyBrush(scatter, hit, radius);", + " }", + " if (!isPressed) {", + " this._endStroke();", + " }", + " }", + "", + " _getRadius() {", + " const { object } = this.scatter;", + " const maxRadius = Math.max(object.getWidth(), object.getHeight()) / 4;", + " return Math.max(maxRadius * Math.pow(this.sizePercent / 100, 2), 10);", + " }", + "", + " /**", + " * @returns {{x: number, y: number} | null} The position of the scene under the cursor: on the", + " * highest ground hit, or on the plane at the object Z.", + " */", + " _getPositionUnderCursor(scatter, game) {", + " let hit = null;", + " for (const ground of getGrounds(scatter.object.getRuntimeScene())) {", + " if (typeof ground.raycastFromCursor !== 'function') continue;", + " const localHit = ground.raycastFromCursor(game, this.raycaster, this.ndc);", + " if (!localHit) continue;", + " const sceneHit = ground.toScenePosition(localHit);", + " if (!hit || sceneHit.z > hit.z) hit = sceneHit;", + " }", + " if (hit) return hit;", + "", + " const { object } = scatter;", + " const layer = object.getInstanceContainer().getLayer(object.getLayer());", + " const camera = layer.getRenderer().getThreeCamera();", + " if (!camera) return null;", + " const inputManager = game.getInputManager();", + " this.ndc.set(", + " (inputManager.getCursorX() / game.getGameResolutionWidth()) * 2 - 1,", + " -(inputManager.getCursorY() / game.getGameResolutionHeight()) * 2 + 1", + " );", + " this.raycaster.setFromCamera(this.ndc, camera);", + " scatter.group.updateWorldMatrix(true, false);", + " const ray = this.raycaster.ray.clone().applyMatrix4(scatter.group.matrixWorld.clone().invert());", + " const point = ray.intersectPlane(new THREE.Plane(new THREE.Vector3(0, 0, 1), 0), new THREE.Vector3());", + " if (!point) return null;", + " return {", + " x: object.getX() + (point.x / AREA_SIZE) * object.getWidth(),", + " y: object.getY() + (point.y / AREA_SIZE) * object.getHeight(),", + " };", + " }", + "", + " _showBrushRing(scatter, hit, radius) {", + " if (!hit) {", + " this.brushRing.removeFromParent();", + " return;", + " }", + " const { object } = scatter;", + " if (this.brushRing.parent !== scatter.group) scatter.group.add(this.brushRing);", + " const grounds = getGrounds(object.getRuntimeScene());", + " const positions = this.brushRing.geometry.attributes.position;", + " for (let index = 0; index < positions.count; index++) {", + " const angle = (index / positions.count) * Math.PI * 2;", + " const x = hit.x + Math.cos(angle) * radius;", + " const y = hit.y + Math.sin(angle) * radius;", + " const { ground, z } = findHighestGround(grounds, x, y, this._groundAtCursor);", + " positions.setXYZ(", + " index,", + " ((x - object.getX()) / object.getWidth()) * AREA_SIZE,", + " ((y - object.getY()) / object.getHeight()) * AREA_SIZE,", + " (((ground ? z : object.getZ()) + 2 - object.getZ()) / Math.max(object.getDepth(), 0.0001)) * AREA_DEPTH", + " );", + " }", + " positions.needsUpdate = true;", + " }", + "", + " _startStroke(scatter) {", + " this.isStroking = true;", + " this.lastStrokeTime = performance.now();", + " this.undoSteps.push({ mask: scatter.mask.clone(), baseMask: scatter.baseMask });", + " while (this.undoSteps.length > 30) this.undoSteps.shift();", + " }", + "", + " _endStroke() {", + " if (!this.isStroking) return;", + " this.isStroking = false;", + " this._save();", + " }", + "", + " _applyBrush(scatter, hit, radius) {", + " const now = performance.now();", + " const elapsedSeconds = Math.min((now - this.lastStrokeTime) / 1000, 0.1);", + " this.lastStrokeTime = now;", + " const strength = (this.strengthPercent / 100) * elapsedSeconds * 8;", + " scatter.paint(hit.x, hit.y, hit.x, hit.y, radius, this.toolName === 'Add' ? 1 : 0, strength);", + " }", + "", + " _save() {", + " const { scatter, editor } = this;", + " if (!scatter || !editor) return;", + " editor.updateObjectProperties(scatter.object.getName(), { PaintData: scatter.save() });", + " }", + "", + " _undo() {", + " const undoStep = this.undoSteps.pop();", + " // The coverage or the edits may have changed since the stroke.", + " if (!this.scatter || !undoStep || undoStep.baseMask !== this.scatter.baseMask) {", + " this.undoSteps.length = 0;", + " return;", + " }", + " this.scatter.setMask(undoStep.mask);", + " this._save();", + " }", + "", + " /** @returns {Array} */", + " _getToolbarItems() {", + " const toolButton = (toolName, tooltip) => ({", + " type: 'button',", + " id: toolName,", + " tooltip,", + " iconUrl: scatterToolIcons[toolName],", + " isActive: this.toolName === toolName,", + " onClick: () => (this.toolName = this.toolName === toolName ? '' : toolName),", + " });", + " return [", + " toolButton('Add', 'Add copies (drag on the ground)'),", + " toolButton('Erase', 'Erase copies'),", + " { type: 'divider', id: 'BrushDivider' },", + " {", + " type: 'slider',", + " id: 'Size',", + " tooltip: 'Brush size',", + " iconUrl: toolIcons.Size,", + " min: 1,", + " max: 100,", + " value: this.sizePercent,", + " onChange: (value) => (this.sizePercent = value),", + " },", + " {", + " type: 'slider',", + " id: 'Strength',", + " tooltip: 'Brush strength',", + " iconUrl: toolIcons.Strength,", + " min: 5,", + " max: 100,", + " value: this.strengthPercent,", + " onChange: (value) => (this.strengthPercent = value),", + " },", + " {", + " type: 'button',", + " id: 'Undo',", + " tooltip: 'Undo the last stroke on these scattered models',", + " iconUrl: toolIcons.Undo,", + " onClick: () => this._undo(),", + " },", + " ];", + " }", + "}", + "", + "const editorTools = new ScatterEditorTools();", + "if (gdjs.registerInGameEditorPostStepCallback) {", + " gdjs.registerInGameEditorPostStepCallback((editor) => editorTools.update(editor));", + "}", + "", + "gdjs.__terrainScatterExtension = {", + " Scatter,", + " // Exposed for tests.", + " DensityMask,", + " maskCodec,", + " applyEdits,", + "};", + "" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [], + "objectGroups": [] + } + ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "DefineHelperClasses" + }, + { + "functionName": "DefineGrassHelperClasses" + }, + { + "functionName": "DefineScatterHelperClasses" + } + ] + }, + "eventsBasedBehaviors": [], + "eventsBasedObjects": [ + { + "areaMaxX": 4096, + "areaMaxY": 4096, + "areaMaxZ": 600, + "areaMinX": 0, + "areaMinY": 0, + "areaMinZ": 0, + "assetStoreTag": "3d terrains", + "defaultName": "Terrain", + "description": "A ground with hills, mountains or valleys that can be sculpted and painted in the scene editor, or changed with events. Add the 3D physics behavior to it to make objects walk or roll on it.", + "fullName": "3D terrain", + "helpPath": "", + "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLXRlcnJhaW4iIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTQsNkwxMC4yNSwxMUwxMy4xLDE0LjhMMTEuNSwxNkM5LjgxLDEzLjc1IDcsMTAgNywxMEwxLDE4SDIzTDE0LDZaIiAvPjwvc3ZnPg==", + "is3D": true, + "isUsingLegacyInstancesRenderer": true, + "name": "Terrain3D", + "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", + "objects": [ + { + "assetStoreId": "", + "name": "AreaPlaceholder", + "type": "Scene3D::Cube3DObject", + "variables": [], + "effects": [], + "behaviors": [], + "content": { + "width": 4096, + "height": 4096, + "depth": 600, + "enableTextureTransparency": false, + "facesOrientation": "Y", + "backFaceUpThroughWhichAxisRotation": "X", + "materialType": "Basic", + "tint": "255;255;255", + "frontFaceResourceName": "", + "frontFaceVisible": false, + "frontFaceResourceRepeat": false, + "backFaceResourceName": "", + "backFaceVisible": false, + "backFaceResourceRepeat": false, + "leftFaceResourceName": "", + "leftFaceVisible": false, + "leftFaceResourceRepeat": false, + "rightFaceResourceName": "", + "rightFaceVisible": false, + "rightFaceResourceRepeat": false, + "topFaceResourceName": "", + "topFaceVisible": false, + "topFaceResourceRepeat": false, + "bottomFaceResourceName": "", + "bottomFaceVisible": false, + "bottomFaceResourceRepeat": false + } + } + ], + "objectsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "objectName": "AreaPlaceholder" + } + ] + }, + "objectsGroups": [], + "layers": [ + { + "ambientLightColorB": 200, + "ambientLightColorG": 200, + "ambientLightColorR": 200, + "camera2DPlaneMaxDrawingDistance": 5000, + "camera3DFarPlaneDistance": 10000, + "camera3DFieldOfView": 45, + "camera3DNearPlaneDistance": 3, + "cameraType": "", + "followBaseLayerCamera": false, + "isLightingLayer": false, + "isLocked": false, + "name": "", + "renderingType": "", + "visibility": true, + "cameras": [ + { + "defaultSize": true, + "defaultViewport": true, + "height": 0, + "viewportBottom": 1, + "viewportLeft": 0, + "viewportRight": 1, + "viewportTop": 0, + "width": 0 + } + ], + "effects": [] + } + ], + "instances": [ + { + "angle": 0, + "customSize": true, + "depth": 600, + "height": 4096, + "keepRatio": true, + "layer": "", + "name": "AreaPlaceholder", + "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-004096004096", + "width": 4096, + "x": 0, + "y": 0, + "zOrder": 1, + "numberProperties": [], + "stringProperties": [], + "initialVariables": [] + } + ], + "editionSettings": {}, + "eventsFunctions": [ + { + "fullName": "", + "functionType": "Action", + "name": "onCreated", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Terrain3D::DefineHelperClasses" + }, + "parameters": [ + "", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "for (const child of object.getChildrenContainer().getAdhocListOfAllInstances()) {", + " child.deleteFromScene();", + "}", + "if (object.__terrain3D) object.__terrain3D.dispose();", + "object.__terrain3D = new gdjs.__terrain3DExtension.Terrain(object);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "onDestroy", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrain3D.dispose();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "doStepPostEvents", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrain3D.update();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "onHotReloading", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrain3D.onHotReloading();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "description": "Raise the ground around a position (or lower it with a negative height). It's raised the most at the center and less and less up to the radius.", + "fullName": "Raise or lower the ground", + "functionType": "Action", + "group": "Sculpt", + "name": "RaiseGround", + "sentence": "Raise the ground of _PARAM0_ by _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const height = eventsFunctionContext.getArgument(\"Height\");", + "object.__terrain3D.raise(x, y, x, y, radius, height);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Height to add (negative to lower the ground)", + "name": "Height", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Raise the ground along a line, for example for a ridge, or lower it with a negative height to dig a river bed or a valley.", + "fullName": "Raise or lower the ground along a line", + "functionType": "Action", + "group": "Sculpt", + "name": "RaiseGroundAlongLine", + "sentence": "Raise the ground of _PARAM0_ by _PARAM6_ from _PARAM1_; _PARAM2_ to _PARAM3_; _PARAM4_ with a radius of _PARAM5_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x1 = eventsFunctionContext.getArgument(\"X1\");", + "const y1 = eventsFunctionContext.getArgument(\"Y1\");", + "const x2 = eventsFunctionContext.getArgument(\"X2\");", + "const y2 = eventsFunctionContext.getArgument(\"Y2\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const height = eventsFunctionContext.getArgument(\"Height\");", + "object.__terrain3D.raise(x1, y1, x2, y2, radius, height);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the start", + "name": "X1", + "type": "expression" + }, + { + "description": "Y position of the start", + "name": "Y1", + "type": "expression" + }, + { + "description": "X position of the end", + "name": "X2", + "type": "expression" + }, + { + "description": "Y position of the end", + "name": "Y2", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance from the line.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Height to add (negative to lower the ground)", + "name": "Height", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Flatten the ground around a position to a given height, for example to place a building.", + "fullName": "Flatten the ground", + "functionType": "Action", + "group": "Sculpt", + "name": "FlattenGround", + "sentence": "Flatten the ground of _PARAM0_ to a Z of _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const z = eventsFunctionContext.getArgument(\"Z\");", + "object.__terrain3D.flatten(x, y, x, y, radius, z, 1);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Z position of the flat ground", + "name": "Z", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Smooth the ground around a position, softening bumps and cliffs.", + "fullName": "Smooth the ground", + "functionType": "Action", + "group": "Sculpt", + "name": "SmoothGround", + "sentence": "Smooth the ground of _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_ (strength: _PARAM4_)", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const strength = eventsFunctionContext.getArgument(\"Strength\");", + "object.__terrain3D.smooth(x, y, x, y, radius, strength);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Strength (between 0 and 1)", + "name": "Strength", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Paint a layer (like grass, dirt, rock or sand) on the ground around a position.", + "fullName": "Paint a layer", + "functionType": "Action", + "group": "Paint", + "name": "PaintLayer", + "sentence": "Paint layer _PARAM4_ on _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const layer = eventsFunctionContext.getArgument(\"Layer\");", + "object.__terrain3D.paint(x, y, x, y, radius, layer, 1);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Layer (1 to 4)", + "name": "Layer", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Paint a layer on the ground along a line, for example for a path or a road.", + "fullName": "Paint a layer along a line", + "functionType": "Action", + "group": "Paint", + "name": "PaintLayerAlongLine", + "sentence": "Paint layer _PARAM6_ on _PARAM0_ from _PARAM1_; _PARAM2_ to _PARAM3_; _PARAM4_ with a radius of _PARAM5_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x1 = eventsFunctionContext.getArgument(\"X1\");", + "const y1 = eventsFunctionContext.getArgument(\"Y1\");", + "const x2 = eventsFunctionContext.getArgument(\"X2\");", + "const y2 = eventsFunctionContext.getArgument(\"Y2\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const layer = eventsFunctionContext.getArgument(\"Layer\");", + "object.__terrain3D.paint(x1, y1, x2, y2, radius, layer, 1);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the start", + "name": "X1", + "type": "expression" + }, + { + "description": "Y position of the start", + "name": "Y1", + "type": "expression" + }, + { + "description": "X position of the end", + "name": "X2", + "type": "expression" + }, + { + "description": "Y position of the end", + "name": "Y2", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance from the line.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Layer (1 to 4)", + "name": "Layer", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Tint the ground around a position with a color multiplying its layers, like dry grass or darker mud. White removes the tint.", + "fullName": "Tint the ground", + "functionType": "Action", + "group": "Paint", + "name": "TintGround", + "sentence": "Tint the ground of _PARAM0_ with _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const color = gdjs.rgbOrHexToRGBColor(eventsFunctionContext.getArgument(\"Color\"));", + "object.__terrain3D.tint(x, y, x, y, radius, color, 1);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Color", + "name": "Color", + "type": "color" + } + ], + "objectGroups": [] + }, + { + "description": "Make the ground around a position wet (darker and shiny, like after rain or near water) or dry.", + "fullName": "Change the wetness of the ground", + "functionType": "Action", + "group": "Paint", + "name": "WetGround", + "sentence": "Change the wetness of the ground of _PARAM0_ to _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const wetness = eventsFunctionContext.getArgument(\"Wetness\");", + "object.__terrain3D.wet(x, y, x, y, radius, wetness, 1);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Wetness (from 0 for dry to 1)", + "name": "Wetness", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "the wetness of the ground at a position, from 0 (dry) to 1, painted or near the water (wet shore).", + "fullName": "Ground wetness", + "functionType": "ExpressionAndCondition", + "group": "Paint", + "name": "GroundWetness", + "sentence": "the wetness of the ground at _PARAM1_; _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "eventsFunctionContext.returnValue = object.__terrain3D.getWetnessAt(x, y);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "The tint painted on the ground at a position, like \"255;255;255\" (white when there is none).", + "fullName": "Ground tint", + "functionType": "StringExpression", + "group": "Paint", + "name": "GroundTint", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "eventsFunctionContext.returnValue = object.__terrain3D.getTintAt(x, y).join(\";\");" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "string" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Remove the ground in a circle, for the entrance of a tunnel, a cave or a well: there is nothing drawn and no collision there. Fill it again with the \"Fill holes\" action. A height field can't go under itself: put a 3D model (with a collision shape) for the tunnel or the cave under the ground. The physics engine removes the triangles touching a hole, so the hole in the collisions can be up to a cell larger than the one drawn: make the model go a bit further than the hole, or put rocks around the entrance.", + "fullName": "Make a hole", + "functionType": "Action", + "group": "Holes", + "name": "MakeHole", + "sentence": "Make a hole in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "object.__terrain3D.makeHole(x, y, x, y, radius);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "name": "Radius", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Fill the holes in a circle with the layers of the ground around them: the ground is drawn and has collisions again.", + "fullName": "Fill holes", + "functionType": "Action", + "group": "Holes", + "name": "FillHoles", + "sentence": "Fill the holes in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "object.__terrain3D.fillHoles(x, y, x, y, radius);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "name": "Radius", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Make the entrance of a tunnel or a cave on a slope: a hole, with the ground in front of it flattened to the floor of the tunnel.", + "fullName": "Carve an entrance", + "functionType": "Action", + "group": "Holes", + "name": "CarveEntrance", + "sentence": "Carve an entrance in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_, going in at _PARAM4_° with its floor at Z _PARAM5_ and _PARAM6_ pixels of flat ground in front", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const angle = eventsFunctionContext.getArgument(\"Angle\");", + "const floorZ = eventsFunctionContext.getArgument(\"FloorZ\");", + "const length = eventsFunctionContext.getArgument(\"Length\");", + "object.__terrain3D.carveEntrance(x, y, radius, angle, floorZ, length);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "name": "Radius", + "type": "expression" + }, + { + "description": "Direction going into the ground (in degrees)", + "name": "Angle", + "type": "expression" + }, + { + "description": "Z position of the floor", + "name": "FloorZ", + "type": "expression" + }, + { + "description": "Length of the flat ground in front of the entrance (in pixels)", + "name": "Length", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Check if there is a hole in the ground at a position.", + "fullName": "Hole", + "functionType": "Condition", + "group": "Holes", + "name": "IsHole", + "sentence": "There is a hole in _PARAM0_ at _PARAM1_; _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "eventsFunctionContext.returnValue = object.__terrain3D.isHoleAt(x, y);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "The Z position of the ground at a position, or the given value where there is a hole.", + "fullName": "Ground Z position, with holes", + "functionType": "Expression", + "group": "Holes", + "name": "GroundZWithHoles", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const terrain = object.__terrain3D;", + "eventsFunctionContext.returnValue = terrain.isHoleAt(x, y) ? eventsFunctionContext.getArgument(\"ZInHoles\") : terrain.getHeightAt(x, y);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + }, + { + "description": "Value in holes", + "longDescription": "For example, a Z position far under the ground.", + "name": "ZInHoles", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Replace the ground with a generated relief. Painted layers are reset.", + "fullName": "Generate a relief", + "functionType": "Action", + "group": "Generation", + "name": "GenerateRelief", + "sentence": "Generate a relief of _PARAM1_ on _PARAM0_ with seed _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const relief = eventsFunctionContext.getArgument(\"Relief\");", + "const seed = eventsFunctionContext.getArgument(\"Seed\");", + "object.__terrain3D.generate(relief, seed);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "Relief", + "name": "Relief", + "supplementaryInformation": "[\"Flat\", \"Hills\", \"Mountains\", \"Island\"]", + "type": "stringWithSelector" + }, + { + "description": "Seed", + "longDescription": "Each seed gives a different terrain with the same relief.", + "name": "Seed", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Replace the ground with the heights of an image. Painted layers are kept.", + "fullName": "Load a heightmap", + "functionType": "Action", + "group": "Generation", + "name": "LoadHeightmap", + "sentence": "Load the heights of _PARAM0_ from _PARAM1_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const image = eventsFunctionContext.getArgument(\"Image\");", + "object.__terrain3D.loadHeightmap(image);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "Heightmap image", + "longDescription": "Black is the lowest, white the highest. For more precision, store the height on 16 bits in the red (most significant byte) and green channels.", + "name": "Image", + "type": "imageResource" + } + ], + "objectGroups": [] + }, + { + "description": "Move objects up or down so that their bottom is on the ground, under their center. Useful to place trees, rocks or characters on a terrain.", + "fullName": "Put objects on the ground", + "functionType": "Action", + "group": "Position", + "name": "PutOnGround", + "sentence": "Put _PARAM1_ on the ground of _PARAM0_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrain3D.placeOnGround(eventsFunctionContext.getObjects(\"Objects\"));" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "Objects to put on the ground", + "name": "Objects", + "type": "objectList" + } + ], + "objectGroups": [] + }, + { + "description": "the Z position of the ground at a position of the scene.", + "fullName": "Ground Z position", + "functionType": "ExpressionAndCondition", + "group": "Position", + "name": "GroundZ", + "sentence": "the ground Z position at _PARAM1_; _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "eventsFunctionContext.returnValue = object.__terrain3D.getHeightAt(x, y);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "the slope of the ground (angle with the horizontal, in degrees) at a position of the scene.", + "fullName": "Ground slope", + "functionType": "ExpressionAndCondition", + "group": "Position", + "name": "GroundSlope", + "sentence": "the ground slope at _PARAM1_; _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "eventsFunctionContext.returnValue = object.__terrain3D.getSlopeAt(x, y);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "the layer (from 1 to 4) painted the most at a position of the scene. Useful to play footstep sounds or to slow down characters in water or mud.", + "fullName": "Ground layer", + "functionType": "ExpressionAndCondition", + "group": "Paint", + "name": "GroundLayer", + "sentence": "the ground layer at _PARAM1_; _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "eventsFunctionContext.returnValue = object.__terrain3D.getLayerAt(x, y);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + } + ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + }, + { + "folderName": "Sculpt", + "children": [ + { + "functionName": "RaiseGround" + }, + { + "functionName": "RaiseGroundAlongLine" + }, + { + "functionName": "FlattenGround" + }, + { + "functionName": "SmoothGround" + } + ] + }, + { + "folderName": "Paint", + "children": [ + { + "functionName": "PaintLayer" + }, + { + "functionName": "PaintLayerAlongLine" + }, + { + "functionName": "TintGround" + }, + { + "functionName": "WetGround" + }, + { + "functionName": "GroundWetness" + }, + { + "functionName": "GroundTint" + }, + { + "functionName": "GroundLayer" + } + ] + }, + { + "folderName": "Holes", + "children": [ + { + "functionName": "MakeHole" + }, + { + "functionName": "FillHoles" + }, + { + "functionName": "CarveEntrance" + }, + { + "functionName": "IsHole" + }, + { + "functionName": "GroundZWithHoles" + } + ] + }, + { + "folderName": "Generation", + "children": [ + { + "functionName": "GenerateRelief" + }, + { + "functionName": "LoadHeightmap" + } + ] + }, + { + "folderName": "Position", + "children": [ + { + "functionName": "PutOnGround" + }, + { + "functionName": "GroundZ" + }, + { + "functionName": "GroundSlope" + } + ] + } + ] + }, + "propertyDescriptors": [ + { + "value": "Hills", + "type": "Choice", + "label": "Relief", + "description": "The initial shape, before sculpting.", + "group": "Shape", + "choices": [ + { + "label": "Flat", + "value": "Flat" + }, + { + "label": "Hills", + "value": "Hills" + }, + { + "label": "Mountains", + "value": "Mountains" + }, + { + "label": "Island", + "value": "Island" + } + ], + "name": "Relief" + }, + { + "value": "1", + "type": "Number", + "label": "Seed", + "description": "Each seed gives a different terrain with the same relief.", + "group": "Shape", + "name": "Seed" + }, + { + "value": "", + "type": "Resource", + "label": "Heightmap image", + "description": "Optional. Replaces the relief: black is the lowest, white the highest (the object depth).", + "group": "Shape", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "HeightmapImage" + }, + { + "value": "", + "type": "MultilineString", + "label": "Edits", + "description": "Changes of the relief or heightmap, applied in order, as a JSON list, for example [{\"tool\": \"raise\", \"x\": 0.3, \"y\": 0.4, \"radius\": 0.15, \"height\": 0.4}, {\"tool\": \"paint\", \"x\": 0.1, \"y\": 0.9, \"toX\": 0.8, \"toY\": 0.2, \"radius\": 0.02, \"layer\": 2}]. Tools: raise (height added to the ground, negative to lower), flatten (z: the ground is brought to this height, fully at the center, cutting what is above it and filling what is below; with toX and toY, toZ gives the height at the end of the line, for a road going up), smooth (strength), paint (layer, from 1 to 4, and strength), tint (color, like \"120;140;60\", multiplying the layers, white removing the tint, and strength), wetness (amount, from 0 for dry to 1 for wet, and strength), hole (no ground in the radius, for the entrances of tunnels and caves) and fill (the holes in the radius are filled with the layers of the ground around them). x, y and radius are fractions (0 to 1) of the terrain size (x is (scene X - terrain X) / terrain width). height and z are fractions of its depth: the ground at z is at Z = terrain Z + z * terrain depth. Add toX and toY to change the ground along a line (paths, rivers, ridges). Shown in the scene editor.", + "group": "Shape", + "advanced": true, + "name": "Edits" + }, + { + "value": "256", + "type": "Choice", + "label": "Resolution", + "description": "The number of cells on each side of the terrain. Higher is more detailed but slower.", + "group": "Shape", + "choices": [ + { + "label": "64 x 64", + "value": "64" + }, + { + "label": "128 x 128", + "value": "128" + }, + { + "label": "256 x 256", + "value": "256" + }, + { + "label": "512 x 512", + "value": "512" + }, + { + "label": "1024 x 1024", + "value": "1024" + } + ], + "name": "Resolution" + }, + { + "value": "96;160;64", + "type": "Color", + "label": "Layer 1 color (grass)", + "group": "Layers", + "name": "Layer1Color" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 1 texture", + "description": "Optional. Tinted by the layer color.", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer1Texture" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 1 normal map", + "description": "Optional. The bumps of the texture, lit by the lights (an OpenGL normal map, like the textures of ambientCG).", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer1NormalMap" + }, + { + "value": "150;112;72", + "type": "Color", + "label": "Layer 2 color (dirt)", + "group": "Layers", + "name": "Layer2Color" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 2 texture", + "description": "Optional. Tinted by the layer color.", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer2Texture" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 2 normal map", + "description": "Optional. The bumps of the texture, lit by the lights (an OpenGL normal map, like the textures of ambientCG).", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer2NormalMap" + }, + { + "value": "124;118;112", + "type": "Color", + "label": "Layer 3 color (rock)", + "group": "Layers", + "name": "Layer3Color" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 3 texture", + "description": "Optional. Tinted by the layer color.", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer3Texture" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 3 normal map", + "description": "Optional. The bumps of the texture, lit by the lights (an OpenGL normal map, like the textures of ambientCG).", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer3NormalMap" + }, + { + "value": "224;204;148", + "type": "Color", + "label": "Layer 4 color (sand)", + "group": "Layers", + "name": "Layer4Color" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 4 texture", + "description": "Optional. Tinted by the layer color.", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer4Texture" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 4 normal map", + "description": "Optional. The bumps of the texture, lit by the lights (an OpenGL normal map, like the textures of ambientCG).", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer4NormalMap" + }, + { + "value": "1", + "type": "Number", + "label": "Normal map strength", + "description": "How much the normal maps bump the ground: 0 for none, 1 as they are, more to exaggerate them.", + "group": "Layers", + "name": "NormalMapStrength" + }, + { + "value": "256", + "type": "Number", + "unit": "Pixel", + "label": "Texture size", + "description": "The size, in the scene, of one repetition of the layer textures.", + "group": "Layers", + "name": "TextureSize" + }, + { + "value": "0.5", + "type": "Number", + "label": "Height blending", + "description": "Where layers meet, the bright parts of textures (stones, tufts) cover the dark ones (gaps, sand) instead of fading into each other: 0 for none, 1 for the most.", + "group": "Layers", + "name": "HeightBlending" + }, + { + "value": "0.5", + "type": "Number", + "label": "Texture variation", + "description": "Makes the repetition of textures less visible, with patches of bigger and rotated textures and colors slowly varying: 0 for none, 1 for the most.", + "group": "Layers", + "name": "TextureVariation" + }, + { + "value": "false", + "type": "Boolean", + "label": "Wet shore", + "description": "Make the ground wet (darker and shiny) under a Z position, like the sand of a beach near the water.", + "group": "Wetness", + "name": "WetShore" + }, + { + "value": "0", + "type": "Number", + "unit": "Pixel", + "label": "Wet below Z", + "description": "The ground is wet under this Z position (for example, the Z of a 3D water object).", + "group": "Wetness", + "name": "WetShoreZ" + }, + { + "value": "60", + "type": "Number", + "unit": "Pixel", + "label": "Wet shore fade", + "description": "The ground becomes dry over this height above \"Wet below Z\".", + "group": "Wetness", + "name": "WetShoreFade" + }, + { + "value": "3", + "type": "Choice", + "label": "Layer on steep slopes", + "description": "This layer is shown where the ground is steep, whatever is painted.", + "group": "Layers", + "choices": [ + { + "label": "None", + "value": "0" + }, + { + "label": "Layer 1", + "value": "1" + }, + { + "label": "Layer 2", + "value": "2" + }, + { + "label": "Layer 3", + "value": "3" + }, + { + "label": "Layer 4", + "value": "4" + } + ], + "name": "CliffLayer" + }, + { + "value": "45", + "type": "Number", + "unit": "DegreeAngle", + "label": "Steep slope angle", + "group": "Layers", + "name": "CliffAngle" + }, + { + "value": "true", + "type": "Boolean", + "label": "Cast shadows", + "group": "Rendering", + "name": "CastShadow" + }, + { + "value": "true", + "type": "Boolean", + "label": "Receive shadows", + "group": "Rendering", + "name": "ReceiveShadow" + }, + { + "value": "", + "type": "String", + "label": "Sculpted and painted data", + "description": "Written by the sculpt and paint tools of the scene editor, in a compact format not meant to be read or written by hand: use Edits instead. Clear it to go back to the relief, heightmap and edits.", + "advanced": true, + "name": "SculptData" + } + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Shape", + "children": [ + { + "propertyName": "Relief" + }, + { + "propertyName": "Seed" + }, + { + "propertyName": "HeightmapImage" + }, + { + "propertyName": "Edits" + }, + { + "propertyName": "Resolution" + } + ] + }, + { + "folderName": "Layers", + "children": [ + { + "propertyName": "Layer1Color" + }, + { + "propertyName": "Layer1Texture" + }, + { + "propertyName": "Layer1NormalMap" + }, + { + "propertyName": "Layer2Color" + }, + { + "propertyName": "Layer2Texture" + }, + { + "propertyName": "Layer2NormalMap" + }, + { + "propertyName": "Layer3Color" + }, + { + "propertyName": "Layer3Texture" + }, + { + "propertyName": "Layer3NormalMap" + }, + { + "propertyName": "Layer4Color" + }, + { + "propertyName": "Layer4Texture" + }, + { + "propertyName": "Layer4NormalMap" + }, + { + "propertyName": "NormalMapStrength" + }, + { + "propertyName": "TextureSize" + }, + { + "propertyName": "HeightBlending" + }, + { + "propertyName": "TextureVariation" + }, + { + "propertyName": "CliffLayer" + }, + { + "propertyName": "CliffAngle" + } + ] + }, + { + "folderName": "Wetness", + "children": [ + { + "propertyName": "WetShore" + }, + { + "propertyName": "WetShoreZ" + }, + { + "propertyName": "WetShoreFade" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "CastShadow" + }, + { + "propertyName": "ReceiveShadow" + } + ] + }, + { + "propertyName": "SculptData" + } + ] + }, + "variants": [] + }, + { + "areaMaxX": 1000, + "areaMaxY": 1000, + "areaMaxZ": 40, + "areaMinX": 0, + "areaMinY": 0, + "areaMinZ": 0, + "defaultName": "Grass", + "description": "A field of grass blades swaying in the wind and pushed aside by characters. They grow in the area of the object, on the highest 3D terrain at each point (one field can cover several terrains), or at the Z position of the object where there is no terrain.", + "fullName": "Terrain grass", + "helpPath": "", + "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGQ9Ik0zIDIxYzEtNSAxLTkgMC0xMyAyIDMgMyA3IDMgMTN6bTQgMGMwLTYgMS0xMSA0LTE2LTEgNS0xIDEwIDAgMTZ6bTUgMGMwLTUgMi05IDUtMTItMiA0LTIgOC0xIDEyem01IDBjMS00IDItNiA0LTgtMSAzLTEgNS0xIDh6Ii8+PC9zdmc+", + "is3D": true, + "isUsingLegacyInstancesRenderer": true, + "name": "TerrainGrass", + "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/732ef90b7fcf5dd9171fe95d0cf262e09159b487304915a4693baa893d9ce16c_grass.svg", + "objects": [ + { + "assetStoreId": "", + "name": "AreaPlaceholder", + "type": "Scene3D::Cube3DObject", + "variables": [], + "effects": [], + "behaviors": [], + "content": { + "width": 1000, + "height": 1000, + "depth": 40, + "enableTextureTransparency": false, + "facesOrientation": "Y", + "backFaceUpThroughWhichAxisRotation": "X", + "materialType": "Basic", + "tint": "255;255;255", + "frontFaceResourceName": "", + "frontFaceVisible": false, + "frontFaceResourceRepeat": false, + "backFaceResourceName": "", + "backFaceVisible": false, + "backFaceResourceRepeat": false, + "leftFaceResourceName": "", + "leftFaceVisible": false, + "leftFaceResourceRepeat": false, + "rightFaceResourceName": "", + "rightFaceVisible": false, + "rightFaceResourceRepeat": false, + "topFaceResourceName": "", + "topFaceVisible": false, + "topFaceResourceRepeat": false, + "bottomFaceResourceName": "", + "bottomFaceVisible": false, + "bottomFaceResourceRepeat": false + } + } + ], + "objectsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "objectName": "AreaPlaceholder" + } + ] + }, + "objectsGroups": [], + "layers": [ + { + "ambientLightColorB": 200, + "ambientLightColorG": 200, + "ambientLightColorR": 200, + "camera2DPlaneMaxDrawingDistance": 5000, + "camera3DFarPlaneDistance": 10000, + "camera3DFieldOfView": 45, + "camera3DNearPlaneDistance": 3, + "cameraType": "", + "followBaseLayerCamera": false, + "isLightingLayer": false, + "isLocked": false, + "name": "", + "renderingType": "", + "visibility": true, + "cameras": [ + { + "defaultSize": true, + "defaultViewport": true, + "height": 0, + "viewportBottom": 1, + "viewportLeft": 0, + "viewportRight": 1, + "viewportTop": 0, + "width": 0 + } + ], + "effects": [] + } + ], + "instances": [ + { + "angle": 0, + "customSize": true, + "depth": 40, + "height": 1000, + "keepRatio": true, + "layer": "", + "name": "AreaPlaceholder", + "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-001000001000", + "width": 1000, + "x": 0, + "y": 0, + "zOrder": 1, + "numberProperties": [], + "stringProperties": [], + "initialVariables": [] + } + ], + "editionSettings": {}, + "eventsFunctions": [ + { + "fullName": "", + "functionType": "Action", + "name": "onCreated", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Terrain3D::DefineHelperClasses" + }, + "parameters": [ + "", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Terrain3D::DefineGrassHelperClasses" + }, + "parameters": [ + "", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "for (const child of object.getChildrenContainer().getAdhocListOfAllInstances()) {", + " child.deleteFromScene();", + "}", + "if (object.__terrainGrass) object.__terrainGrass.dispose();", + "object.__terrainGrass = new gdjs.__terrainGrassExtension.Grass(object);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "onDestroy", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrainGrass.dispose();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "doStepPostEvents", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const game = runtimeScene.getGame();", + "// Time doesn't pass in the scene editor: real time is used to show the wind.", + "const isInGameEdition = game.isInGameEdition && game.isInGameEdition();", + "const elapsedTime = isInGameEdition ? 1 / 60 : object.getElapsedTime() / 1000;", + "const time = isInGameEdition", + " ? performance.now() / 1000", + " : object.getRuntimeScene().getTimeManager().getTimeFromStart() / 1000;", + "const layer = object.getInstanceContainer().getLayer(object.getLayer());", + "object.__terrainGrass.update(elapsedTime, time, layer.getRenderer().getThreeCamera());" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "onHotReloading", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrainGrass.updateFromProperties();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" + } + ], + "objectGroups": [] + } + ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + } + ] + }, + "propertyDescriptors": [ + { + "value": "60", + "type": "Number", + "label": "Density", + "description": "The number of blades in a square of 100 x 100 pixels.", + "group": "Blades", + "name": "Density" + }, + { + "value": "36", + "type": "Number", + "unit": "Pixel", + "label": "Blade height", + "description": "The average height of blades (each blade is between 60% and 140% of it).", + "group": "Blades", + "name": "BladeHeight" + }, + { + "value": "5", + "type": "Number", + "unit": "Pixel", + "label": "Blade width", + "group": "Blades", + "name": "BladeWidth" + }, + { + "value": "52;104;36", + "type": "Color", + "label": "Base color", + "group": "Blades", + "name": "BaseColor" + }, + { + "value": "150;196;82", + "type": "Color", + "label": "Tip color", + "group": "Blades", + "name": "TipColor" + }, + { + "value": "1", + "type": "Number", + "label": "Seed", + "description": "Each seed places blades differently.", + "group": "Blades", + "name": "Seed" + }, + { + "value": "1", + "type": "Choice", + "label": "Grows on terrain layer", + "description": "On a 3D terrain, blades only grow where this layer is painted. A new terrain has layer 1 painted everywhere.", + "group": "Ground", + "choices": [ + { + "label": "Any", + "value": "0" + }, + { + "label": "Layer 1", + "value": "1" + }, + { + "label": "Layer 2", + "value": "2" + }, + { + "label": "Layer 3", + "value": "3" + }, + { + "label": "Layer 4", + "value": "4" + } + ], + "name": "GroundLayer" + }, + { + "value": "40", + "type": "Number", + "unit": "DegreeAngle", + "label": "Maximum slope", + "description": "No blades grow on steeper ground.", + "group": "Ground", + "name": "MaxSlope" + }, + { + "value": "1", + "type": "Number", + "label": "Wind strength", + "description": "0 for no wind.", + "group": "Wind", + "name": "WindStrength" + }, + { + "value": "2", + "type": "Number", + "label": "Wind speed", + "group": "Wind", + "name": "WindSpeed" + }, + { + "value": "", + "type": "String", + "label": "Object bending the grass", + "description": "The name of an object pushing blades aside when walking through the grass, like the player. The 8 first instances are used.", + "group": "Wind", + "name": "BendingObject" + }, + { + "value": "2500", + "type": "Number", + "unit": "Pixel", + "label": "Fade distance", + "description": "Blades are less and less dense up to this distance from the camera, and hidden after.", + "group": "Rendering", + "name": "FadeDistance" + } + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Blades", + "children": [ + { + "propertyName": "Density" + }, + { + "propertyName": "BladeHeight" + }, + { + "propertyName": "BladeWidth" + }, + { + "propertyName": "BaseColor" + }, + { + "propertyName": "TipColor" + }, + { + "propertyName": "Seed" + } + ] + }, + { + "folderName": "Ground", + "children": [ + { + "propertyName": "GroundLayer" + }, + { + "propertyName": "MaxSlope" + } + ] + }, + { + "folderName": "Wind", + "children": [ + { + "propertyName": "WindStrength" + }, + { + "propertyName": "WindSpeed" + }, + { + "propertyName": "BendingObject" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "FadeDistance" + } + ] + } + ] + }, + "variants": [] + }, + { + "areaMaxX": 2000, + "areaMaxY": 2000, + "areaMaxZ": 200, + "areaMinX": 0, + "areaMinY": 0, + "areaMinZ": 0, + "assetStoreTag": "3d scattered models", + "defaultName": "ScatteredModels", + "description": "Many copies of a 3D model (trees, rocks, bushes, flowers...) placed in the area of the object and drawn all at once, which is much faster than an object for each one. They stand on the highest 3D terrain at each point, or at the Z position of the object where there is no terrain. Paint where they grow with the brushes of the scene editor.", + "fullName": "Scattered models (forests, rocks...)", + "helpPath": "", + "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGQ9Ik0xMCAyMXYtM0gzbDUtNUg1bDUtNUg3bDUtNSA1IDVoLTNsNSA1aC0zbDUgNWgtN3YzeiIvPjwvc3ZnPg==", + "is3D": true, + "isUsingLegacyInstancesRenderer": true, + "name": "ScatteredModels", + "previewIconUrl": "", + "objects": [ + { + "assetStoreId": "", + "name": "AreaPlaceholder", + "type": "Scene3D::Cube3DObject", + "variables": [], + "effects": [], + "behaviors": [], + "content": { + "width": 2000, + "height": 2000, + "depth": 200, + "enableTextureTransparency": false, + "facesOrientation": "Y", + "backFaceUpThroughWhichAxisRotation": "X", + "materialType": "Basic", + "tint": "255;255;255", + "frontFaceResourceName": "", + "frontFaceVisible": false, + "frontFaceResourceRepeat": false, + "backFaceResourceName": "", + "backFaceVisible": false, + "backFaceResourceRepeat": false, + "leftFaceResourceName": "", + "leftFaceVisible": false, + "leftFaceResourceRepeat": false, + "rightFaceResourceName": "", + "rightFaceVisible": false, + "rightFaceResourceRepeat": false, + "topFaceResourceName": "", + "topFaceVisible": false, + "topFaceResourceRepeat": false, + "bottomFaceResourceName": "", + "bottomFaceVisible": false, + "bottomFaceResourceRepeat": false + } + } + ], + "objectsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "objectName": "AreaPlaceholder" + } + ] + }, + "objectsGroups": [], + "layers": [ + { + "ambientLightColorB": 200, + "ambientLightColorG": 200, + "ambientLightColorR": 200, + "camera2DPlaneMaxDrawingDistance": 5000, + "camera3DFarPlaneDistance": 10000, + "camera3DFieldOfView": 45, + "camera3DNearPlaneDistance": 3, + "cameraType": "", + "followBaseLayerCamera": false, + "isLightingLayer": false, + "isLocked": false, + "name": "", + "renderingType": "", + "visibility": true, + "cameras": [ + { + "defaultSize": true, + "defaultViewport": true, + "height": 0, + "viewportBottom": 1, + "viewportLeft": 0, + "viewportRight": 1, + "viewportTop": 0, + "width": 0 + } + ], + "effects": [] + } + ], + "instances": [ + { + "angle": 0, + "customSize": true, + "depth": 200, + "height": 2000, + "keepRatio": true, + "layer": "", + "name": "AreaPlaceholder", + "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-002000002000", + "width": 2000, + "x": 0, + "y": 0, + "zOrder": 1, + "numberProperties": [], + "stringProperties": [], + "initialVariables": [] + } + ], + "editionSettings": {}, + "eventsFunctions": [ + { + "fullName": "", + "functionType": "Action", + "name": "onCreated", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Terrain3D::DefineHelperClasses" + }, + "parameters": [ + "", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Terrain3D::DefineScatterHelperClasses" + }, + "parameters": [ + "", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "for (const child of object.getChildrenContainer().getAdhocListOfAllInstances()) {", + " child.deleteFromScene();", + "}", + "if (object.__terrainScatter) object.__terrainScatter.dispose();", + "object.__terrainScatter = new gdjs.__terrainScatterExtension.Scatter(object);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "onDestroy", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrainScatter.dispose();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "doStepPostEvents", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const game = runtimeScene.getGame();", + "const isInGameEdition = game.isInGameEdition && game.isInGameEdition();", + "const elapsedTime = isInGameEdition ? 1 / 60 : object.getElapsedTime() / 1000;", + "const layer = object.getInstanceContainer().getLayer(object.getLayer());", + "object.__terrainScatter.update(elapsedTime, layer.getRenderer().getThreeCamera());" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "onHotReloading", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrainScatter.onHotReloading();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "description": "Paint the density of copies around a position, up to the density of the object (copies removed with \"Erase copies\" come back). The effect fades out up to the radius.", + "fullName": "Add copies", + "functionType": "Action", + "group": "Paint", + "name": "AddCopies", + "sentence": "Add copies of _PARAM0_ around _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "object.__terrainScatter.addCopies(x, y, radius);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "name": "Radius", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Remove the copies standing in a circle, for example a tree cut by the player (use the position of the nearest copy and a small radius) or a forest cleared for a building. They come back when the scene is restarted.", + "fullName": "Erase copies", + "functionType": "Action", + "group": "Paint", + "name": "EraseCopies", + "sentence": "Erase the copies of _PARAM0_ in a circle at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "object.__terrainScatter.removeCopies(x, y, radius);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "name": "Radius", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "the number of copies placed in the area.", + "fullName": "Number of copies", + "functionType": "ExpressionAndCondition", + "group": "Copies", + "name": "CopyCount", + "sentence": "the number of copies", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "eventsFunctionContext.returnValue = object.__terrainScatter.getCopyCount();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "description": "The X position of the base of the copy nearest to a position (the position itself if there is no copy).", + "fullName": "X position of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyX", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? copy.x : x;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "The Y position of the base of the copy nearest to a position (the position itself if there is no copy).", + "fullName": "Y position of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyY", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? copy.y : y;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "The Z position of the base of the copy nearest to a position (on the ground).", + "fullName": "Z position of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyZ", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? copy.z : object.getZ();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "the distance (on X and Y) from a position to the base of the nearest copy. It's a very big number when there is no copy.", + "fullName": "Distance to the nearest copy", + "functionType": "ExpressionAndCondition", + "group": "Copies", + "name": "NearestCopyDistance", + "sentence": "the distance from _PARAM1_; _PARAM2_ to the nearest copy", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? Math.hypot(copy.x - x, copy.y - y) : Number.MAX_VALUE;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + } + ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + }, + { + "folderName": "Paint", + "children": [ + { + "functionName": "AddCopies" + }, + { + "functionName": "EraseCopies" + } + ] + }, + { + "folderName": "Copies", + "children": [ + { + "functionName": "CopyCount" + }, + { + "functionName": "NearestCopyX" + }, + { + "functionName": "NearestCopyY" + }, + { + "functionName": "NearestCopyZ" + }, + { + "functionName": "NearestCopyDistance" + } + ] + } + ] + }, + "propertyDescriptors": [ + { + "value": "", + "type": "Resource", + "label": "3D model", + "description": "The model copied everywhere (a .glb file). Models standing on the Y axis, like most models of the asset store, are stood up along Z.", + "group": "Model", + "extraInformation": [ + "model3D" + ], + "choices": [], + "name": "Model" + }, + { + "value": "300", + "type": "Number", + "unit": "Pixel", + "label": "Height of a copy", + "description": "Copies keep the proportions of the model.", + "group": "Model", + "name": "Size" + }, + { + "value": "0.8", + "type": "Number", + "label": "Smallest random scale", + "description": "Each copy is scaled at random between the smallest and the largest scale.", + "group": "Model", + "name": "MinScale" + }, + { + "value": "1.2", + "type": "Number", + "label": "Largest random scale", + "group": "Model", + "name": "MaxScale" + }, + { + "value": "true", + "type": "Boolean", + "label": "Random rotation", + "description": "Turn each copy around the Z axis at random.", + "group": "Model", + "name": "RandomRotation" + }, + { + "value": "255;255;255", + "type": "Color", + "label": "Random tint", + "description": "Each copy is tinted between white and this color, at random. White for no tint.", + "group": "Model", + "name": "RandomTint" + }, + { + "value": "0.3", + "type": "Number", + "label": "Density", + "description": "The number of copies in a square of 100 x 100 pixels where the density is fully painted (0.3 is about one copy every 180 pixels). Copies never overlap their neighbors much: they are placed at random in a grid.", + "group": "Placement", + "name": "Density" + }, + { + "value": "1", + "type": "Number", + "label": "Seed", + "description": "Each seed places copies differently.", + "group": "Placement", + "name": "Seed" + }, + { + "value": "Everywhere", + "type": "Choice", + "label": "Copies grow", + "description": "Everywhere in the area, or only where they are painted with the \"Add copies\" brush (or the edits).", + "group": "Placement", + "choices": [ + { + "label": "Everywhere (erase them with the brush)", + "value": "Everywhere" + }, + { + "label": "Only where painted", + "value": "Painted" + } + ], + "name": "Coverage" + }, + { + "value": "1", + "type": "Choice", + "label": "Grows on terrain layer", + "description": "On a 3D terrain, copies only grow where this layer is painted, away from its borders (so that trees don't stand on roads painted with another layer). A new terrain has layer 1 painted everywhere.", + "group": "Ground", + "choices": [ + { + "label": "Any", + "value": "0" + }, + { + "label": "Layer 1", + "value": "1" + }, + { + "label": "Layer 2", + "value": "2" + }, + { + "label": "Layer 3", + "value": "3" + }, + { + "label": "Layer 4", + "value": "4" + } + ], + "name": "GroundLayer" + }, + { + "value": "30", + "type": "Number", + "unit": "DegreeAngle", + "label": "Maximum slope", + "description": "No copies grow on steeper ground.", + "group": "Ground", + "name": "MaxSlope" + }, + { + "value": "-100000", + "type": "Number", + "unit": "Pixel", + "label": "Minimum ground Z", + "description": "No copies grow where the ground is lower (for example, under the water level).", + "group": "Ground", + "name": "MinGroundZ" + }, + { + "value": "100000", + "type": "Number", + "unit": "Pixel", + "label": "Maximum ground Z", + "description": "No copies grow where the ground is higher (for example, above the tree line).", + "group": "Ground", + "name": "MaxGroundZ" + }, + { + "value": "false", + "type": "Boolean", + "label": "Trunk collisions", + "description": "Give each copy a static collision shape around its trunk, for the 3D physics behavior (add it to this object, with the \"Static\" type) and navigation meshes. Without them, these behaviors have no effect on this object.", + "group": "Collisions", + "name": "TrunkCollisions" + }, + { + "value": "30", + "type": "Number", + "unit": "Pixel", + "label": "Trunk diameter", + "description": "The diameter of the collision shape of a copy at the height given in \"Height of a copy\" (it is scaled with the copy).", + "group": "Collisions", + "name": "TrunkDiameter" + }, + { + "value": "8000", + "type": "Number", + "unit": "Pixel", + "label": "Draw distance", + "description": "Copies shrink and are hidden at this distance from the camera.", + "group": "Rendering", + "name": "DrawDistance" + }, + { + "value": "true", + "type": "Boolean", + "label": "Cast shadows", + "group": "Rendering", + "name": "CastShadow" + }, + { + "value": "true", + "type": "Boolean", + "label": "Receive shadows", + "group": "Rendering", + "name": "ReceiveShadow" + }, + { + "value": "", + "type": "MultilineString", + "label": "Edits", + "description": "Where copies are added or erased, applied in order, as a JSON list, for example [{\"tool\": \"add\", \"x\": 0.3, \"y\": 0.4, \"radius\": 0.15}, {\"tool\": \"erase\", \"x\": 0.1, \"y\": 0.9, \"toX\": 0.8, \"toY\": 0.2, \"radius\": 0.02}]. Tools: add (density: from 0 to 1, 1 by default, how dense copies are compared to the Density property) and erase. strength (from 0 to 1) makes a stroke lighter. x, y and radius are fractions (0 to 1) of the object size (x is (scene X - object X) / object width). Add toX and toY to paint along a line (a row of trees, a path through a forest). With \"Copies grow: only where painted\", only the added areas have copies. Shown in the scene editor.", + "group": "Placement", + "advanced": true, + "name": "Edits" + }, + { + "value": "", + "type": "String", + "label": "Painted data", + "description": "Written by the brushes of the scene editor, in a compact format not meant to be read or written by hand: use Edits instead. Clear it to go back to the coverage and the edits.", + "advanced": true, + "name": "PaintData" + } + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Model", + "children": [ + { + "propertyName": "Model" + }, + { + "propertyName": "Size" + }, + { + "propertyName": "MinScale" + }, + { + "propertyName": "MaxScale" + }, + { + "propertyName": "RandomRotation" + }, + { + "propertyName": "RandomTint" + } + ] + }, + { + "folderName": "Placement", + "children": [ + { + "propertyName": "Density" + }, + { + "propertyName": "Seed" + }, + { + "propertyName": "Coverage" + }, + { + "propertyName": "Edits" + } + ] + }, + { + "folderName": "Ground", + "children": [ + { + "propertyName": "GroundLayer" + }, + { + "propertyName": "MaxSlope" + }, + { + "propertyName": "MinGroundZ" + }, + { + "propertyName": "MaxGroundZ" + } + ] + }, + { + "folderName": "Collisions", + "children": [ + { + "propertyName": "TrunkCollisions" + }, + { + "propertyName": "TrunkDiameter" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "DrawDistance" + }, + { + "propertyName": "CastShadow" + }, + { + "propertyName": "ReceiveShadow" + } + ] + }, + { + "propertyName": "PaintData" + } + ] + }, + "variants": [] + } + ] } ], "externalLayouts": [] diff --git a/examples/starting-first-person-shooter-enemies/assets/StartingWall.png b/examples/starting-first-person-shooter-enemies/assets/StartingWall.png index b3a097887..06278d6e5 100644 Binary files a/examples/starting-first-person-shooter-enemies/assets/StartingWall.png and b/examples/starting-first-person-shooter-enemies/assets/StartingWall.png differ diff --git a/examples/starting-first-person-shooter-enemies/starting-first-person-shooter-enemies.json b/examples/starting-first-person-shooter-enemies/starting-first-person-shooter-enemies.json index 3a6da8536..bd508ab1d 100644 --- a/examples/starting-first-person-shooter-enemies/starting-first-person-shooter-enemies.json +++ b/examples/starting-first-person-shooter-enemies/starting-first-person-shooter-enemies.json @@ -1,7 +1,7 @@ { "firstLayout": "", "gdVersion": { - "build": 283, + "build": 284, "major": 5, "minor": 6, "revision": 0 @@ -262,6 +262,14 @@ "name": "assets/Skybox_Right.png", "smoothed": true, "userAdded": false + }, + { + "file": "assets/StartingGround.png", + "kind": "image", + "metadata": "", + "name": "assets/StartingGround.png", + "smoothed": true, + "userAdded": false } ] }, @@ -278,6 +286,7 @@ "mangledName": "Game_32Scene", "name": "Game Scene", "r": 255, + "renderer3DWorldScale": 100, "standardSortMethod": true, "stopSoundsOnStartup": true, "title": "", @@ -311,6 +320,9 @@ }, { "name": "Player" + }, + { + "name": "Terrain" } ] }, @@ -324,10 +336,10 @@ "name": "PushableBox" }, { - "name": "Ground" + "name": "Enemy" }, { - "name": "Enemy" + "name": "Terrain" } ] } @@ -341,24 +353,6 @@ } ], "instances": [ - { - "angle": 0, - "customSize": true, - "depth": 32, - "height": 2240, - "layer": "", - "locked": true, - "name": "Ground", - "persistentUuid": "6a14e5ab-5c31-4692-8cd5-c50937fd60c6", - "width": 2432, - "x": -576, - "y": -832, - "z": -32, - "zOrder": 1, - "numberProperties": [], - "stringProperties": [], - "initialVariables": [] - }, { "angle": 0, "customSize": false, @@ -951,92 +945,39 @@ "numberProperties": [], "stringProperties": [], "initialVariables": [] + }, + { + "angle": 0, + "customSize": true, + "height": 2240, + "layer": "", + "name": "Terrain", + "persistentUuid": "9697dde6-08c0-4778-bffe-d16474aaae66", + "width": 2432, + "x": -576, + "y": -832, + "zOrder": 0, + "numberProperties": [], + "stringProperties": [], + "initialVariables": [] } ], "objects": [ { "assetStoreId": "", - "name": "Ground", - "persistentUuid": "f46cabd7-51f2-4d49-9571-59176d6eec16", + "name": "Obstacle", + "persistentUuid": "ea4b306a-6d05-44c2-91c9-85749c47de20", "type": "Scene3D::Cube3DObject", "variables": [], "effects": [], "behaviors": [ - { - "name": "Physics3D", - "type": "Physics3D::Physics3DBehavior", - "object3D": "Object3D", - "bodyType": "Static", - "bullet": false, - "fixedRotation": false, - "shape": "Box", - "shapeOrientation": "Z", - "shapeDimensionA": 0, - "shapeDimensionB": 0, - "shapeDimensionC": 0, - "density": 1, - "friction": 0.3, - "restitution": 0.1, - "linearDamping": 0.1, - "angularDamping": 0.1, - "gravityScale": 1, - "layers": 17, - "masks": 17, - "shapeOffsetX": 0, - "shapeOffsetY": 0, - "shapeOffsetZ": 0, - "massCenterOffsetX": 0, - "massCenterOffsetY": 0, - "massCenterOffsetZ": 0, - "massOverride": 0 - }, { "name": "NavMeshObstacle", "type": "NavMeshPathfinding::NavMeshObstacleBehavior", "shape": "Box", "meshShapeResourceName": "", "obstacleOnly": false - } - ], - "content": { - "width": 1000, - "height": 1000, - "depth": 40, - "enableTextureTransparency": false, - "facesOrientation": "Y", - "frontFaceResourceName": "assets\\StartingGround.png", - "backFaceResourceName": "", - "backFaceUpThroughWhichAxisRotation": "X", - "leftFaceResourceName": "", - "rightFaceResourceName": "", - "topFaceResourceName": "", - "bottomFaceResourceName": "", - "frontFaceVisible": true, - "backFaceVisible": false, - "leftFaceVisible": false, - "rightFaceVisible": false, - "topFaceVisible": false, - "bottomFaceVisible": false, - "frontFaceResourceRepeat": true, - "backFaceResourceRepeat": false, - "leftFaceResourceRepeat": false, - "rightFaceResourceRepeat": false, - "topFaceResourceRepeat": false, - "bottomFaceResourceRepeat": false, - "materialType": "StandardWithoutMetalness", - "isCastingShadow": true, - "isReceivingShadow": true, - "tileScale": 0.2 - } - }, - { - "assetStoreId": "", - "name": "Obstacle", - "persistentUuid": "ea4b306a-6d05-44c2-91c9-85749c47de20", - "type": "Scene3D::Cube3DObject", - "variables": [], - "effects": [], - "behaviors": [ + }, { "isFolded": true, "name": "Physics3D", @@ -1065,13 +1006,6 @@ "massCenterOffsetY": 0, "massCenterOffsetZ": 0, "massOverride": 0 - }, - { - "name": "NavMeshObstacle", - "type": "NavMeshPathfinding::NavMeshObstacleBehavior", - "shape": "Box", - "meshShapeResourceName": "", - "obstacleOnly": false } ], "content": { @@ -1113,6 +1047,13 @@ "variables": [], "effects": [], "behaviors": [ + { + "name": "NavMeshObstacle", + "type": "NavMeshPathfinding::NavMeshObstacleBehavior", + "shape": "Box", + "meshShapeResourceName": "", + "obstacleOnly": true + }, { "isFolded": true, "name": "Physics3D", @@ -1141,13 +1082,6 @@ "massCenterOffsetY": 0, "massCenterOffsetZ": 0, "massOverride": 0 - }, - { - "name": "NavMeshObstacle", - "type": "NavMeshPathfinding::NavMeshObstacleBehavior", - "shape": "Box", - "meshShapeResourceName": "", - "obstacleOnly": true } ], "content": { @@ -1875,6 +1809,59 @@ ] } } + }, + { + "assetStoreId": "", + "name": "Terrain", + "persistentUuid": "e790958d-7766-41e2-9d51-7170e042c4a4", + "type": "Terrain3D::Terrain3D", + "variant": "", + "variables": [], + "effects": [], + "behaviors": [ + { + "name": "Physics3D", + "type": "Physics3D::Physics3DBehavior", + "object3D": "Object3D", + "bodyType": "Static", + "bullet": false, + "fixedRotation": false, + "shape": "Box", + "meshShapeResourceName": "", + "shapeOrientation": "Z", + "shapeDimensionA": 0, + "shapeDimensionB": 0, + "shapeDimensionC": 0, + "shapeOffsetX": 0, + "shapeOffsetY": 0, + "shapeOffsetZ": 0, + "massCenterOffsetX": 0, + "massCenterOffsetY": 0, + "massCenterOffsetZ": 0, + "massOverride": 0, + "density": 1, + "friction": 0.3, + "restitution": 0.1, + "linearDamping": 0.1, + "angularDamping": 0.1, + "gravityScale": 1, + "layers": 17, + "masks": 17 + } + ], + "content": { + "SculptData": "2;256;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AQABAQIBAgIAAQEAAQEAAQIBAAICAQEEAQEAVQIAAQIAAQIBAgIDAgIAAQIAARAAAQEAAQECAQABAQIAAQECAQEFBAACAgABAQADAQABAQACAQAFAgAEAQIAAQICAgEDAgIAAQECBgABAwABCAMAAQMCBAMGAwACAQEMAQMHAAICAAEBGhkEAwIABAECAQYDAQAEAQIABAEGAwABAgMCAAECAwIAAgEAAgQFAgQAAQEDAgAEAgMEAAEDAgIAAQUCAggHAAIBAgEAAgQBAQEAVwICAAEBAgIAAgQFAgIAAQ4AAQEAAwEAAQQDAAIFAQICAAECAAEBAQQDAQEAFgIEAQADAgEGAgcCCAEBAAEBBgECAAECBQACDAEJAAEDAgACAR4ZAgUCAgEBAAEBAgADAgECAQQAAQUEAQAEAQABBAMCAAEBAAMBBAEBBgMAAgIAAQMEAQEEAwIBAgABAgMAAQQDAgAFAwIAAgEAXAQAAwEGAwQBAAEEDgECAQABAwQCBQQHAQIGAwIBAQADAwAdAgIAAgEGBgcAAQYBAAEBAAECAgABAQABBAUAAQoDBQEAAwIAAQEEAQABAwIAAQIBAAMBAgACAgMAAQIBAgABAQEEAAQBAgMEAQIAAgEAAwECAgABAQACAQABBAMAAQIAAQMCAgEABgIAAQEBAF8EAgACAQABAgACAgwAAgECAgMAAQEFAgIAAwEAAgEBACICAgABAgQFAgYFBAEAAgIAAQIDAAIEAgMBAwABAgACHBsBAgACAgMEAAIDAAECAAEBAgACAgMAAQICAQACAwIBBgMEAwQFAgACAQABAgEEAQABAQABBgMBAgIDAAECAAEBAgECAAEBAAMBAGICAAIEAgEBAgACDgABBQQBAQQHAQQAAQIAAQMCAQEBACgECAUAAQYDAAEDCAMAAQQDCAUDDAEFAAEDAQIBAhgVAwQBAgEBAAICAAEBAgEAAQIBAAICAQIBAAICAwIBBAMAAQIAAgECAAECAgEDAgEEAwABAgEBBAEABwQFAGcCAgECAAECAAEBAgoBBAABAwIFAAECAAIBAAIDAC0GAQABCAUGBwQCAQYJBAEAAQwCDRAFBQUCBAEBBAwCBAkJAQQCBgcBAgMCAw4PAgECAQICAAIDBgABAQMCAQABAgABAQABAgECAgMCAAEBAgEAAQQDBgMCBQIBAGkCAAECAQQBAAEEAQgEAQABAQMAAQIMDQIAAQMBAC8EAgICAgYJBBQbCAABDg8IFAEkPygBKzoHKwMOHQEEJBcGAw02AysIEhECAgMCEBIjHAgFHx4GHxgBFRgbHBUOERYVEA0KDxQDDQwJCA0KAQMAAQQFAGkEAQIAAgIAAQIMAwIFAQQAAgUIAwMAMgIBBgUIAwABBQoUGQUKBgkDAgQWChkAARliRwUAAQoFEBEFEAsRDhYGIQwLNCMFEgcOCwcGHCMBAAEGAgMAAQIFAQACAQIBBAIKDwIKCwACBAMAAQQAAQMAawIAAQIBAAMIAgUBBAABAQABAwA3AgYHEgABBxQRHgMIDwABFDE+OQNAOzw3QhMaDQgpKAMREhsHDgsOCAUrJgQjCxIRAAICAQABIB8AARoZFhUWERATEA8MCwIBAAEEAwgHAgEAAQIBAGwEAQECAgECCgUBBAQFAgEDAPIBBAACAQIAAQQBBAIFAgABAQEA8gECAgIAAQEAAQIEAgMAAgMA9QECAAICAAEGAAEDAgABAwD2AQIAAQECAgYBAAIDAPgBBAEAAgICAgUA+gEEAAEDBAEBAPoBAgACAgQAAQMA+wECAAICAQD8AQIBAgIBAPwBAgIBAP8BAgCBAgIA/wECAQD/AQIAgQICAIACAgCBBAIAjBoBAIIGAQCABAECAP8BAgCCBAEA/wEBAgD/AQIDAIACAQD/AQMEAP8BAQD/AQEAggICAP0BAQEEAQCAAgEA/gECAQUA/gEBAwgA/AEBAAEDBgD9AQIDAgEBAPwBAQICAwD9AQMEAwYA/AEBBAMCAAECAPsBAgMCAgABBwD7AQECAgIHBgD7AQICAwUEAQD8AQEDBgEBAP0BBAkEBAD7AQEAAQEIAwD8AQMAAgICAgD6AQUEAAEEAwABAQD6AQIBAAEDAgEBAPoBAgIDAAEDAAEEAPoBAwABBAABAQYDAPoBAgMDAggBAwD6AQMAAQYCBwEGAPoBAgQDBwYBAPsBBAUCAgECAwD6AQECAQYCBQEA+gEDBAQDBwgGAPoBBAEFAAEIAQD8AQMEAgEBAPsBAwYAAQEAAgMA/gEBAAEEAPoBBAEDAAECBAEA+gEDAAEEAgIFAQD9AQEBAgD+AQEBAgQDAgIA+AEBAwIEAAIHAgYA9wEBAAEEAAMCAAEBAAECAPYBAQQAAgIBAQECAwQA9gEEAAEBAgEAAgECAQMGAPUBAgECAAEBBAMAAQECAAEDBAD0AQIAAQIDBAMEAwIAAQIBAAECAPQBAgECAQIAAQQDAQIAAQEAAQIA8gECAAIBAgEAAQIBAAEBAgD4AQIDAgEAAQECAwYBAPQBAgEBAgAEAwQAAQMEAAEBAgDzAQIAAQEAAwQDAAEBAgD2AQIDBAABAgABAgEAAQIDAgD0AQEAAQIAAQICAgABAQIBAPcBAQICAQEA+wECAAIDAgIBAgIAAQECAQD1AQIAAQIBAgACAgEEAQEA9AEEBQICAgMAAQIBAgABAQIAAQEA8gEBBgABAQECAAECAAIBAAEBAgDzAQICAwABAgEAAQMAAQYBAAECAQD1AQICAQECAAIFBAIBAPYBAgMCAgIAAwEBAgD3AQIAAgIBAgABAQIBAPYBAgADBAEAAQEA+wECAAICAQABAgEA+AECAQQCAAEBAgEA+QECAAEBAAICAQD7AQIDAgABAQD9AQIAgAIBAIACAgCAAgIAgAICAP8BAgMAgAICAIEEAgCDCAIA5hoDAAICAQABAgIA+QEBAAMCAgEBAAECAAEDBAD0AQQAAgMBAwACAgABAgIBAAICAPUBAgABAQABAwEBAgABBgABAQIA8AEBAgECAQQCAQACAQACBwABAgQAAQIBAgDtAQMCAgIFAAECBAMCAAECAgEDAwABBAQBAOwBAwABBAUEBgEAAQEEAAEBAQABAgABAwABBQABCADrAQcCBAEEAQUEAAICAQYDAgECAgABAQMFAgYCAOgBAwIBAQABAQYCAAIBAgMEAwQCAQACAgMBAwIGAOgBAgEAAwEBAAEBAgQAAQMCAAEBBAEEAAECAQMDAQYCAOYBBAACAQACAQABAgEFAAEEAAEBAAICAAECAAECAAEBAQMGAAEBAgDjAQECAAEEAQAFAgEBAAQBAQABAgABAgIDAQMCBgECAOYBAgABAQAEAgEABAIBAQACAgABAgEDAgMGAQIA4QEBAAECAwICAAYCAQACAQICAQAFAgEBAgcIAQIA4gEBBAEAAQIABAECAgABAQABAQABBAMABAICAwEGCQgBAOABAgACAwADAgABAgECAQACAgEAAgECAAMCAQADAwQHCADfAQIAAQEAAQIBAAIDAgQEAQIAAgICAAECAAMBAAEBAAICAAIBAAEHAN8BCgABAgACAgEAAQIBBQECAQACAQACAQDinQEBAIACAQD/AQUA/wEDAAEBAP0BAQMAAQMA/AEBAwACAgECAPkBAwABAwQAAgECAPkBAwECAQABAQMEAPcBAQMCAQIBAwQDAQYA9QEBAQEAAQIDBAEBAwABBAD1AQUAAgIDBAMBAAEDAAECAgDyAQEFBAACAQABAwMEBQUEAQQA8QEDAQQAAQEBAAEHAgADAwEBAgYA7wEDAgABAwEDAAIEAQIBAAEJBAICAgDtAQEBAAEBAAEBAQIAAQUGAQEAAQIFAAQGAOwBAQMBAAEBAQQBAgIHAgIBAwcCAAEDBgICAOgBAQEDAAEBAgABAQYDAgcCBAEBAQEJAAEEAgMCBAIA5gEBAwEAAgIAAQEEAwEDBgEAAgUAAQoA6aECBAACAQIAAQEBAPkBAgADAQECAgABAQABBAMA9AEDAAIEAgQAAgEAAQEBAgACAQD1AQEAAQIAAQQCAgEAAQUAAQIBAPABAgECAQIDAQIAAgIAAggAAQEDAAEBAgEA7QEEAQEBBgABAQMEAQABAQECBAQAAQMDAgDsAQQAAQMGAwUCAAECAwABAgIAAQEAAQQAAQYAAQcA6wEIAQMCAwIGAwACAQIFBAECAQEAAQIEBgEFAQDoAQQBAgIAAQIFAQACAgEEAwQDAQIAAgEEAgQBBQDoAQECAAMCAgABAgEDAAEEAQABAgMCAwABAQIEBAIFAQDmAQMAAgIAAgIAAQECBgABAwABAgACAQABAQABAQABAgIEBQABAgEA4wECAQABAwIABQECAgAEAgIAAQEAAQEBBAIEAQUCAQDmAQEAAQIABAECAAQBAgIAAgEAAQECBAEEBQIBAOEBAgABAQQBAQAGAQIAAgIBAQIABQECAgEIBwIBAOIBAgMCAAEBAAQCAQEAAQIAAQIAAQMEAAQBAQQCBQoHAgDgAQEAAgQAAwEAAQECAQIAAgECAAICAQADAQIAAwQDCAcA3wEBAAECAAEBAgACBAEDAwIBAAIBAQABAQADAgABAgACAQACAgABCADfAQkAAQEAAgECAAEBAgYCAQIAAgIAAgIA9L8BqgERBw8JCQkHCQcJAwUFBQUDBQACAQEBAQEBAQABAQABAQEABAIAAQIAAgIBAgABAgEAAQQEAQIGBAYCAgoECAIGAgoKBAgGAgIODgABBAMDAQEBIAUCAAIHAQcCAQUBBQABAQUBAAIDAAEDAwEDAQEAAQMDAgMAAQMAAQEBAwIFAgUCAwMCAwABBQACAwABAQABBQABAQMBAAEBAwABAgEHAQACAwEBAgABAwIDAQBVAgIEBAYGCggICggKDhATBgUGAQIAAwICAQABAgACBAQBAQACAQADAgECAAICAAYBAAIDBAECAAEEAAEDAwQAAQUAAgQCCQYBBAUEAQUGAQABBAABCwABDAIBAgABAgIlIAECAAECAQQDAgEBBgIDAQQAAQEAAQEEAAEBAAECAAQBAgECAgMCAgMCAQIBAgIDBAACAwIEBQIAAQIBAgABAgECAQEAAQIAAQIAAQECAAIBAgIDAgIAVQEAAQEAAQECAQEEAQEAAQEAAQ8AAQIAAQIBAgABAgEAAQIBAgIGAwACAQABAgADAgABAgACAgAFAQAEAgEAAQEBAQIEAQEAAQIBBQABBAABBwQAAQQBAwQFBAACAgILAgQIAAIBAAECGRoDBAEABAIBAgUEAgAEAgEABAIFBAABAQQBAAEBBAEAAgIAAgMGAQMAAQIEAQAEAQQDAAEEAQEAAQYBAQcIAAICAQIAAgMCAgIAVwEBAAECAQEAAgMGAQEAAQ0AAQIAAwIAAQMEAAIGAgEBAAEBAAECAgMEAgIAFgEDAgADAQIFAQgBBwICAAECBQIBAAEBBgACCwIKAAEEAQACAh0aAQYBAQICAAECAQADAQIBAgMAAQYDAgAEAgABAwQBAAECAAMCAwICBQQAAgEAAQQDAgIDBAECAQABAQQAAQMEAQAFBAEAAgIAXAMAAwIFBAMCAAEDDQIBAgABBAMBBgMIAgEFBAECAgADBAAdAQEAAgIFBQgAAQUCAAECAAEBAQABAgABAwYAAQkEBgIAAwEAAQIDAgABBAEAAQECAAMCAQACAQQAAQECAQABAgIDAAQCAQQDAgEAAgIAAwIBAQABAgACAgABAwQAAQEAAQQBAQIABgEAAQICAF8DAQACAgABAQACAQsAAgIBAQQAAQIGAQEAAwIAAgICACIBAQABAQMGAQUGAwIAAgEAAQEEAAIDAQQCBAABAQACGxwCAQACAQQDAAIEAAEBAAECAQACAQQAAQEBAgACBAECBQQDBAMGAQACAgABAQIDAgABAgABBQQCAQEEAAEBAAECAQIBAAECAAMCAGIBAAIDAQICAQACDQABBgMCAgMIAgMAAQEAAQQBAgICACgDBwYAAQUEAAEEBwQAAQMEBwYECwIGAAEEAgECARcWBAMCAQICAAIBAAECAQIAAQECAAIBAgECAAIBBAECAwQAAQEAAgIBAAEBAQIEAQIDBAABAQICAwIABwMGAGcBAQIBAAEBAAECAQkCAwABBAEGAAEBAAICAAIEAC0FAgABBwYFCAMBAgUKAwIAAQsBDg8GBgYBAwICAwsBAwoKAgMBBQgCAQQBBA0QAQIBAgEBAAIEBQABAgQBAgABAQABAgABAQIBAQQBAAECAQIAAQMEBQQBBgECAGkBAAEBAgMCAAEDAgcDAgABAgQAAQELDgEAAQQCAC8DAQEBAQUKAxMcBwABDRAHEwIjQCcCLDkILAQNHgIDIxgFBA41BCwHERIBAQQBDxEkGwcGIB0FIBcCFhccGxYNEhUWDw4JEBMEDgsKBw4JAgQAAQMGAGkDAgEAAgEAAQELBAEGAgMAAgYHBAQAMgECBQYHBAABBgkTGgYJBQoEAQMVCRoAARphSAYAAQkGDxIGDwwSDRUFIgsMMyQGEQgNDAgFGyQCAAEFAQQAAQEGAgACAgECAwEJEAEJDAACAwQAAQMAAQQAawEAAQECAAMHAQYCAwABAgABBAA3AQUIEQABCBMSHQQHEAABEzI9OgQ/PDs4QRQZDgcqJwQSERwIDQwNBwYsJQMkDBESAAIBAgABHyAAARkaFRYVEg8UDxALDAECAAEDBAcIAQIAAQECAGwDAgIBAQIBCQYCAwMGAQIEAPIBAwACAgEAAQMCAwEGAQABAgIA8gEBAQEAAQIAAQEDAQQAAgQA9QEBAAIBAAEFAAEEAQABBAD2AQEAAQIBAQUCAAIEAPgBAwIAAgEBAQYA+gEDAAEEAwICAPoBAQACAQMAAQQA+wEBAAIBAgD8AQECAQECAPwBAQECAP8BAQCBAgEA/wEBAgD/AQEAgQIBAIACAQCBBAEAjBoCAIIGAgCABAIBAP8BAQCCBAIA/wECAQD/AQEEAIACAgD/AQQDAP8BAgD/AQIAggIBAP0BAgIDAgCAAgIA/gEBAgYA/gECBAcA/AECAAEEBQD9AQEEAQICAPwBAgEBBAD9AQQDBAUA/AECAwQBAAEBAPsBAQQBAQABCAD7AQIBAQEIBQD7AQEBBAYDAgD8AQIEBQICAP0BAwoDAwD7AQIAAQIHBAD8AQQAAgEBAQD6AQYDAAEDBAABAgD6AQECAAEEAQICAPoBAQEEAAEEAAEDAPoBBAABAwABAgUEAPoBAQQEAQcCBAD6AQQAAQUBCAIFAPoBAQMECAUCAPsBAwYBAQIBBAD6AQIBAgUBBgIA+gEEAwMECAcFAPoBAwIGAAEHAgD8AQQDAQICAPsBBAUAAQIAAgQA/gECAAEDAPoBAwIEAAEBAwIA+gEEAAEDAQEGAgD9AQICAQD+AQICAQMEAQEA+AECBAEDAAIIAQUA9wECAAEDAAMBAAECAAEBAPYBAgMAAgECAgIBBAMA9gEDAAECAQIAAgIBAgQFAPUBAQIBAAECAwQAAQIBAAEEAwD0AQEAAQEEAwQDBAEAAQECAAEBAPQBAQIBAgEAAQMEAgEAAQIAAQEA8gEBAAICAQIAAQECAAECAQD4AQEEAQIAAQIBBAUCAPQBAQICAQAEBAMAAQQDAAECAQDzAQEAAQIAAwMEAAECAQD2AQEEAwABAQABAQIAAQEEAQD0AQIAAQEAAQEBAQABAgECAPcBAgEBAgIA+wEBAAIEAQECAQEAAQIBAgD1AQEAAQECAQACAQIDAgIA9AEDBgEBAQQAAQECAQABAgEAAQIA8gECBQABAgIBAAEBAAICAAECAQDzAQEBBAABAQIAAQQAAQUCAAEBAgD1AQEBAgIBAAIGAwECAPYBAQQBAQEAAwICAQD3AQEAAgECAQABAgECAPYBAQADAwIAAQIA+wEBAAIBAgABAQIA+AEBAgMBAAECAQIA+QEBAAECAAIBAgD7AQEEAQABAgD9AQEAgAICAIACAQCAAgEAgAIBAP8BAQQAgAIBAIEEAQCDCAEA7NoBAgCAAgIA/wEGAP8BBAABAgD9AQIEAAEEAPwBAgQAAgECAQD5AQQAAQQDAAICAQD5AQQCAQIAAQIEAwD3AQIEAQIBAgQDBAIFAPUBAgICAAEBBAMCAgQAAQMA9QEGAAIBBAMEAgABBAABAQEA8gECBgMAAgIAAQQEAwYGAwIDAPEBBAIDAAECAgABCAEAAwQCAgEFAO8BBAEAAQQCBAACAwIBAgABCgMBAQEA7QECAgABAgABAgIBAAEGBQICAAEBBgAEBQDsAQIEAgABAgIDAgEBCAEBAgQIAQABBAUBAQDoAQICBAABAgEAAQIFBAEIAQMCAgICCgABAwEEAQMBAOYBAgQCAAIBAAECAwQCBAUCAAIGAAEJAIGEBACBhAQAgYQEAIGEBACBhAQ=", + "Layer1Texture": "assets/StartingGround.png", + "Layer1Color": "255;255;255", + "HeightBlending": 0.5, + "TextureVariation": 0, + "TextureSize": 128, + "CliffLayer": "3", + "Layer2Texture": "", + "Layer3Texture": "", + "CliffAngle": 45, + "CastShadow": false + } } ], "objectsFolderStructure": { @@ -1916,7 +1903,7 @@ "folderName": "World", "children": [ { - "objectName": "Ground" + "objectName": "Terrain" }, { "objectName": "Obstacle" @@ -1953,7 +1940,6 @@ ], "events": [ { - "folded": false, "type": "BuiltinCommonInstructions::Standard", "conditions": [], "actions": [ @@ -1994,7 +1980,6 @@ ] }, { - "folded": false, "type": "BuiltinCommonInstructions::Standard", "conditions": [], "actions": [ @@ -2057,7 +2042,6 @@ "type": "BuiltinCommonInstructions::Group", "events": [ { - "folded": false, "type": "BuiltinCommonInstructions::Standard", "conditions": [ { @@ -2170,7 +2154,6 @@ ], "events": [ { - "folded": false, "type": "BuiltinCommonInstructions::Standard", "conditions": [ { @@ -2224,7 +2207,6 @@ "comment": "While it walks, the navmesh moves the enemy: its physics body is kinematic, so the player bumps into it without pushing it. When it is shot, it becomes dynamic, to be thrown by the impulse at the impact and to fall." }, { - "folded": false, "type": "BuiltinCommonInstructions::Standard", "conditions": [ { @@ -2332,7 +2314,6 @@ "type": "BuiltinCommonInstructions::Group", "events": [ { - "folded": false, "type": "BuiltinCommonInstructions::Comment", "color": { "b": 109, @@ -2345,7 +2326,6 @@ "comment": "Patrolling logic" }, { - "folded": false, "type": "BuiltinCommonInstructions::ForEach", "object": "Enemy", "conditions": [ @@ -2408,7 +2388,6 @@ ] }, { - "folded": false, "type": "BuiltinCommonInstructions::Standard", "conditions": [ { @@ -2450,7 +2429,6 @@ ], "events": [ { - "folded": false, "type": "BuiltinCommonInstructions::Standard", "conditions": [ { @@ -2502,7 +2480,6 @@ ], "events": [ { - "folded": false, "type": "BuiltinCommonInstructions::Standard", "conditions": [ { @@ -2556,7 +2533,6 @@ ] }, { - "folded": false, "type": "BuiltinCommonInstructions::Standard", "conditions": [ { @@ -2584,7 +2560,6 @@ ] }, { - "folded": false, "type": "BuiltinCommonInstructions::Comment", "color": { "b": 109, @@ -2597,7 +2572,6 @@ "comment": "Chasing logic" }, { - "folded": false, "type": "BuiltinCommonInstructions::ForEach", "object": "Enemy", "conditions": [ @@ -2681,7 +2655,6 @@ ] }, { - "folded": false, "type": "BuiltinCommonInstructions::Standard", "conditions": [ { @@ -2699,7 +2672,6 @@ "actions": [], "events": [ { - "folded": false, "type": "BuiltinCommonInstructions::Standard", "conditions": [ { @@ -2745,8 +2717,8 @@ ] }, { - "folded": false, "type": "BuiltinCommonInstructions::ForEach", + "object": "Enemy", "conditions": [ { "type": { @@ -2803,14 +2775,11 @@ "\"Repath\"" ] } - ], - "object": "Enemy", - "events": [] + ] } ] }, { - "folded": false, "type": "BuiltinCommonInstructions::Comment", "color": { "b": 109, @@ -2823,7 +2792,6 @@ "comment": "Player dies" }, { - "folded": false, "type": "BuiltinCommonInstructions::Standard", "conditions": [ { @@ -2877,7 +2845,6 @@ "type": "BuiltinCommonInstructions::Group", "events": [ { - "folded": false, "type": "BuiltinCommonInstructions::Standard", "conditions": [ { @@ -2893,7 +2860,6 @@ "actions": [], "events": [ { - "folded": false, "type": "BuiltinCommonInstructions::Comment", "color": { "b": 109, @@ -2906,7 +2872,6 @@ "comment": "Set to Touch" }, { - "folded": false, "type": "BuiltinCommonInstructions::Standard", "conditions": [ { @@ -2971,7 +2936,6 @@ ] }, { - "folded": false, "type": "BuiltinCommonInstructions::Comment", "color": { "b": 109, @@ -2984,7 +2948,6 @@ "comment": "Set to Keyboard" }, { - "folded": false, "type": "BuiltinCommonInstructions::Standard", "conditions": [ { @@ -3063,7 +3026,6 @@ "type": "BuiltinCommonInstructions::Group", "events": [ { - "folded": false, "type": "BuiltinCommonInstructions::Standard", "conditions": [ { @@ -3092,7 +3054,6 @@ "actions": [], "events": [ { - "folded": false, "type": "BuiltinCommonInstructions::Standard", "conditions": [ { @@ -3122,7 +3083,6 @@ ] }, { - "folded": false, "type": "BuiltinCommonInstructions::Standard", "conditions": [ { @@ -3144,7 +3104,6 @@ "actions": [], "events": [ { - "folded": false, "type": "BuiltinCommonInstructions::Standard", "conditions": [ { @@ -3212,7 +3171,6 @@ "type": "BuiltinCommonInstructions::Group", "events": [ { - "folded": false, "type": "BuiltinCommonInstructions::Standard", "conditions": [ { @@ -3486,6 +3444,21 @@ "name": "MultitouchButton", "type": "SpriteMultitouchJoystick::MultitouchButton" }, + { + "name": "NavMeshCharacter", + "type": "NavMeshPathfinding::NavMeshCharacterBehavior", + "cellSize": 10, + "cellDepth": 2, + "slopeMaxAngle": 40, + "stairHeightMax": 20, + "walkableRadius": -1, + "speedScaleY": 1, + "walkableDepth": 150 + }, + { + "name": "NavMeshObstacle", + "type": "NavMeshPathfinding::NavMeshObstacleBehavior" + }, { "name": "Object3D", "type": "Scene3D::Base3DBehavior" @@ -3525,21 +3498,6 @@ { "name": "Tween", "type": "Tween::TweenBehavior" - }, - { - "name": "NavMeshCharacter", - "type": "NavMeshPathfinding::NavMeshCharacterBehavior", - "cellSize": 10, - "cellDepth": 2, - "slopeMaxAngle": 40, - "stairHeightMax": 20, - "walkableRadius": -1, - "speedScaleY": 1, - "walkableDepth": 150 - }, - { - "name": "NavMeshObstacle", - "type": "NavMeshPathfinding::NavMeshObstacleBehavior" } ] } @@ -3638,8 +3596,8 @@ ] }, { - "name": "A shot enemy is pushed away by the impact", "description": "Puts an enemy in front of the player and shoots it: the impulse at the impact throws it back, tumbling.", + "name": "A shot enemy is pushed away by the impact", "type": "gameplay", "source": [ "await harness.goToScene('Game Scene');", @@ -3683,8 +3641,8 @@ ] }, { - "name": "A patrolling enemy walks toward the player", "description": "Puts an enemy far from the player, out of sight: it patrols toward the player on the navmesh, staying on the ground.", + "name": "A patrolling enemy walks toward the player", "type": "gameplay", "source": [ "await harness.goToScene('Game Scene');", @@ -3712,8 +3670,8 @@ ] }, { - "name": "An enemy that sees the player chases and catches them", "description": "Puts an enemy in front of the player: it sees the player, runs to them, and catching the player restarts the scene.", + "name": "An enemy that sees the player chases and catches them", "type": "gameplay", "source": [ "await harness.goToScene('Game Scene');", @@ -8698,7 +8656,6 @@ "name": "PlatformerMultitouchMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -8952,7 +8909,6 @@ "name": "Platformer3DMultitouchMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -9127,7 +9083,6 @@ "name": "Shooter3DMultitouchMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -9291,7 +9246,6 @@ "name": "FirstPersonMultitouchMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -10496,7 +10450,6 @@ "name": "PhysicsCar3DMultitouchMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -10709,7 +10662,6 @@ "name": "TopDownMultitouchMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -12744,13 +12696,15 @@ "propertyName": "ShouldBeHiddenWhenReleased" } ] - } + }, + "variants": [] } ] }, { "author": "PANDAKO-GitHub", "category": "Input", + "dimension": "", "extensionNamespace": "", "fullName": "Mouse Pointer Lock", "gdevelopVersion": ">=5.5.222", @@ -13528,12 +13482,68 @@ "objectGroups": [] } ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onFirstSceneLoaded" + }, + { + "functionName": "onScenePostEvents" + }, + { + "functionName": "RequestPointerLock" + }, + { + "functionName": "ExitPointerLock" + }, + { + "functionName": "isPointerLocked" + }, + { + "functionName": "isPointerActuallyLocked" + }, + { + "functionName": "isEmulatingPointerLock" + }, + { + "functionName": "IsMoving" + }, + { + "functionName": "MovementX" + }, + { + "functionName": "SetMovementX" + }, + { + "functionName": "MovementY" + }, + { + "functionName": "SetMovementY" + }, + { + "functionName": "TouchX" + }, + { + "functionName": "TouchY" + }, + { + "functionName": "TouchSpeedFactor" + }, + { + "functionName": "SetTouchSpeedFactor" + } + ] + }, "eventsBasedBehaviors": [ { "description": "Control camera rotations with a mouse.", "fullName": "First person camera mouse mapper", + "helpPath": "", + "iconUrl": "", "name": "FirstPersonPointerMapper", "objectType": "", + "previewIconUrl": "", "eventsFunctions": [ { "fullName": "", @@ -13808,6 +13818,7 @@ "fullName": "", "functionType": "ActionWithOperator", "getterName": "HorizontalSpeed", + "group": "First person camera mouse mapper configuration", "name": "SetHorizontalSpeed", "sentence": "", "events": [ @@ -13888,6 +13899,7 @@ "fullName": "", "functionType": "ActionWithOperator", "getterName": "VerticalSpeed", + "group": "First person camera mouse mapper configuration", "name": "SetVerticalSpeed", "sentence": "", "events": [ @@ -13968,6 +13980,7 @@ "fullName": "", "functionType": "ActionWithOperator", "getterName": "VerticalAngleMin", + "group": "First person camera mouse mapper configuration", "name": "SetVerticalAngleMin", "sentence": "", "events": [ @@ -14048,6 +14061,7 @@ "fullName": "", "functionType": "ActionWithOperator", "getterName": "VerticalAngleMax", + "group": "First person camera mouse mapper configuration", "name": "SetVerticalAngleMax", "sentence": "", "events": [ @@ -14128,6 +14142,7 @@ "fullName": "", "functionType": "ActionWithOperator", "getterName": "OffsetZ", + "group": "First person camera mouse mapper configuration", "name": "SetOffsetZ", "sentence": "", "events": [ @@ -14164,6 +14179,57 @@ "objectGroups": [] } ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "doStepPreEvents" + }, + { + "folderName": "Layers and cameras", + "children": [ + { + "functionName": "LookFromObjectEyes" + } + ] + }, + { + "folderName": "First person camera mouse mapper configuration", + "children": [ + { + "functionName": "HorizontalSpeed" + }, + { + "functionName": "SetHorizontalSpeed" + }, + { + "functionName": "VerticalSpeed" + }, + { + "functionName": "SetVerticalSpeed" + }, + { + "functionName": "VerticalAngleMin" + }, + { + "functionName": "SetVerticalAngleMin" + }, + { + "functionName": "VerticalAngleMax" + }, + { + "functionName": "SetVerticalAngleMax" + }, + { + "functionName": "OffsetZ" + }, + { + "functionName": "SetOffsetZ" + } + ] + } + ] + }, "propertyDescriptors": [ { "value": "", @@ -14217,7 +14283,32 @@ "name": "ShouldLock" } ], - "sharedPropertyDescriptors": [] + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "propertyName": "Object3D" + }, + { + "propertyName": "HorizontalSpeed" + }, + { + "propertyName": "VerticalSpeed" + }, + { + "propertyName": "VerticalAngleMin" + }, + { + "propertyName": "VerticalAngleMax" + }, + { + "propertyName": "OffsetZ" + }, + { + "propertyName": "ShouldLock" + } + ] + } } ], "eventsBasedObjects": [] @@ -40454,9 +40545,9 @@ "type": "Number", "unit": "PixelSpeed", "label": "Start min speed", + "description": "Initial speed of the particles, in the emission direction (the local Z axis of the emitter).", "group": "Speed", - "name": "StartSpeedMin", - "description": "Initial speed of the particles, in the emission direction (the local Z axis of the emitter)." + "name": "StartSpeedMin" }, { "value": "100", @@ -40485,25 +40576,25 @@ "type": "Number", "unit": "Second", "label": "Emission duration", - "name": "Duration", - "description": "0 emits forever." + "description": "0 emits forever.", + "name": "Duration" }, { "value": "", "type": "Boolean", "label": "Particles move with the emitter", + "description": "When checked, the particles already emitted follow the emitter when it moves or rotates. Leave it unchecked for rain, snow or smoke that must stay where it was emitted while the emitter follows the camera or a character.", "group": "Speed", - "name": "AreParticlesRelative", - "description": "When checked, the particles already emitted follow the emitter when it moves or rotates. Leave it unchecked for rain, snow or smoke that must stay where it was emitted while the emitter follows the camera or a character." + "name": "AreParticlesRelative" }, { "value": "30", "type": "Number", "unit": "DegreeAngle", "label": "Spray cone angle", + "description": "Spread of the particles around the emission direction, which is the local Z axis of the emitter (upward by default, rotate the instance to aim it). 0 or more than 180 emits in every direction, unless an emission zone radius is set: then 0 emits straight.", "group": "Position", - "name": "SpayConeAngle", - "description": "Spread of the particles around the emission direction, which is the local Z axis of the emitter (upward by default, rotate the instance to aim it). 0 or more than 180 emits in every direction, unless an emission zone radius is set: then 0 emits straight." + "name": "SpayConeAngle" }, { "value": "0", @@ -40518,6 +40609,7 @@ "value": "Additive", "type": "Choice", "label": "Blending", + "description": "Additive makes particles glow (fire, magic, energy). Use Normal for rain, snow, smoke or dust. Only applied when the object is created.", "group": "Color", "choices": [ { @@ -40541,13 +40633,13 @@ "value": "None" } ], - "name": "Blending", - "description": "Additive makes particles glow (fire, magic, energy). Use Normal for rain, snow, smoke or dust. Only applied when the object is created." + "name": "Blending" }, { "value": "Y-", "type": "Choice", "label": "Gravity top", + "description": "Z+: gravity pulls particles down along the Z axis (the usual choice in 3D games). Y-: gravity pulls particles toward Y+.", "group": "Speed", "choices": [ { @@ -40559,24 +40651,23 @@ "value": "Z+" } ], - "name": "GravityTop", - "description": "Z+: gravity pulls particles down along the Z axis (the usual choice in 3D games). Y-: gravity pulls particles toward Y+." + "name": "GravityTop" }, { "value": "0", "type": "Number", "unit": "PixelAcceleration", "label": "Gravity", + "description": "Acceleration pulling the particles in the direction given by the gravity top. 0 disables gravity.", "group": "Speed", - "name": "Gravity", - "description": "Acceleration pulling the particles in the direction given by the gravity top. 0 disables gravity." + "name": "Gravity" }, { "value": "true", "type": "Boolean", "label": "Delete when emission ends", - "name": "ShouldAutodestruct", - "description": "Delete the object once the emission duration is over and every particle disappeared. Uncheck it and set the emission duration to 0 for a continuous effect like fire, rain or snow." + "description": "Delete the object once the emission duration is over and every particle disappeared. Uncheck it and set the emission duration to 0 for a continuous effect like fire, rain or snow.", + "name": "ShouldAutodestruct" }, { "value": "Center-center", @@ -40619,25 +40710,26 @@ "type": "Number", "unit": "Dimensionless", "label": "Start min length", + "description": "Number of past positions kept to draw the trail, one per frame: the visible length is this number multiplied by the distance traveled by the particle each frame.", "group": "Trail", "advanced": true, - "name": "TrailStartLengthMin", - "description": "Number of past positions kept to draw the trail, one per frame: the visible length is this number multiplied by the distance traveled by the particle each frame." + "name": "TrailStartLengthMin" }, { "value": "120", "type": "Number", "unit": "Dimensionless", "label": "Start max length", + "description": "Number of past positions kept to draw the trail, one per frame: the visible length is this number multiplied by the distance traveled by the particle each frame.", "group": "Trail", "advanced": true, - "name": "TrailStartLengthMax", - "description": "Number of past positions kept to draw the trail, one per frame: the visible length is this number multiplied by the distance traveled by the particle each frame." + "name": "TrailStartLengthMax" }, { "value": "Billboard", "type": "Choice", "label": "Render mode", + "description": "\"Image facing the camera\" draws each particle as an image. \"Image following a trail\" stretches the image along the path of each particle (sparks, rain streaks).", "choices": [ { "label": "Image facing the camera", @@ -40649,8 +40741,7 @@ } ], "advanced": true, - "name": "RenderMode", - "description": "\"Image facing the camera\" draws each particle as an image. \"Image following a trail\" stretches the image along the path of each particle (sparks, rain streaks)." + "name": "RenderMode" }, { "value": "", @@ -40786,7 +40877,7804 @@ "propertyName": "ParentOrigin" } ] - } + }, + "variants": [] + } + ] + }, + { + "author": "", + "category": "General", + "dimension": "3D", + "extensionNamespace": "", + "fullName": "3D terrain", + "gdevelopVersion": ">=5.5.222", + "helpPath": "", + "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLXRlcnJhaW4iIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTQsNkwxMC4yNSwxMUwxMy4xLDE0LjhMMTEuNSwxNkM5LjgxLDEzLjc1IDcsMTAgNywxMEwxLDE4SDIzTDE0LDZaIiAvPjwvc3ZnPg==", + "name": "Terrain3D", + "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", + "shortDescription": "A ground with hills and valleys that can be sculpted and painted in the editor, with collisions.", + "version": "0.1.0", + "description": [ + "A 3D terrain object for open worlds, adventure or racing games: hills, mountains, islands or valleys, with painted layers of grass, dirt, rock or sand.", + "", + "- **Sculpt and paint in the scene editor**: select a terrain to show the tools (raise, lower, smooth, flatten and paint 4 layers).", + "- **Start from a relief** (hills, mountains, island) or **from a heightmap image**.", + "- **Steep slopes automatically show rock** (or any layer).", + "- **Natural borders between layers**: stones and tufts of a texture cover the gaps of the other where layers meet (height blending), and the repetition of textures is made less visible (texture variation).", + "- **Physics**: add the **3D physics** behavior to the terrain. Characters and objects collide with the ground exactly. The terrain is always static.", + "- **Pathfinding**: add the **floor/obstacle for pathfinding (navmesh based)** behavior to the terrain, so that characters with the navmesh pathfinding behavior walk on its hills and around its cliffs (in recent GDevelop versions).", + "- **Shape it with the Edits property**: a list of raise, flatten, smooth and paint strokes written in JSON, shown in the scene editor. It's readable and can be changed by hand or by an AI agent, like the relief, the heightmap and the other properties. Actions only change the terrain during the game.", + "- **Change it during the game** with actions (raise, lower, flatten, smooth, paint, along a line for paths and rivers) and read the ground height, slope or layer with expressions.", + "- **Grass**: add a **terrain grass** object over the terrain. Thousands of blades sway in the wind, grow where a layer (like grass) is painted and where the ground is not too steep, and are pushed aside by characters. Sculpt or paint the terrain and the grass follows.", + "- **Forests, rocks and bushes**: add a **scattered models** object over the terrain. It places hundreds or thousands of copies of a 3D model (trees, rocks, bushes, flowers...) on the ground and draws them all at once, which is much faster than an object for each. Copies grow where a layer is painted, away from steep slopes, and follow the ground when it's sculpted. Add or erase them with the brushes of the scene editor or with the Edits property, give trees trunk collisions, and cut them down with actions. Ready-made forests, rocks and bushes are in the asset store.", + "- **Tunnels and caves**: make holes in the ground (and fill them again) with the brushes, the Edits property or actions, and put a 3D model of the tunnel or the cave (with a mesh collision shape) under the ground. Nothing is drawn in holes, and characters, cars and rays go through them.", + "- **Made for large worlds**: the terrain is split in chunks that are hidden when off-screen and simplified when far away, and collisions are updated only where the ground changes." + ], + "origin": { + "identifier": "Terrain3D", + "name": "gdevelop-extension-store" + }, + "tags": [ + "3d", + "terrain", + "ground", + "heightmap", + "landscape", + "open world", + "grass", + "foliage", + "forest", + "trees", + "tunnel", + "cave" + ], + "authorIds": [], + "dependencies": [], + "globalVariables": [], + "sceneVariables": [], + "eventsFunctions": [ + { + "description": "Define helper classes JavaScript code.", + "fullName": "Define helper classes", + "functionType": "Action", + "name": "DefineHelperClasses", + "private": true, + "sentence": "Define helper classes JavaScript code", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "if (gdjs.__terrain3DExtension) {", + " return;", + "}", + "", + "// Tested by the gameplay tests of the \"3D island terrain\" example (GDevelop-examples repository).", + "", + "const groundsByScene = new WeakMap();", + "/**", + " * The grounds of a scene (the terrains), followed by the terrain grass.", + " *", + " * A ground has: `getVersion()` (changing when the ground changes),", + " * `containsPoint(x, y)`, `getHeightAt(x, y)` (Z position of the ground),", + " * `getSlopeAt(x, y)` (in degrees), `getLayerWeightAt(x, y, layer)` (how", + " * much a painted layer, from 1 to 4, is there, from 0 to 1) and optionally", + " * `getTintAt(x, y)` (the painted tint, as red, green and blue from 0 to 255).", + " * @param {gdjs.RuntimeScene} runtimeScene", + " * @returns {Set}", + " */", + "const getGrounds = (runtimeScene) => {", + " let grounds = groundsByScene.get(runtimeScene);", + " if (!grounds) {", + " grounds = new Set();", + " groundsByScene.set(runtimeScene, grounds);", + " }", + " return grounds;", + "};", + "", + "/**", + " * @param {number} seed", + " * @returns {() => number} Random numbers between 0 (included) and 1 (excluded), the same for the same seed.", + " */", + "const createRandom = (seed) => {", + " let state = seed >>> 0 || 1;", + " return () => {", + " state ^= state << 13;", + " state ^= state >>> 17;", + " state ^= state << 5;", + " return (state >>> 0) / 4294967296;", + " };", + "};", + "", + "/**", + " * Find the highest ground at a position: grounds can overlap (like an island over a sea floor).", + " * @param {Iterable} grounds", + " * @param {{ground: object | null, z: number}} result Set to the ground (null if there is none) and its Z position.", + " */", + "const findHighestGround = (grounds, x, y, result) => {", + " result.ground = null;", + " for (const ground of grounds) {", + " if (!ground.containsPoint(x, y)) continue;", + " const z = ground.getHeightAt(x, y);", + " if (!result.ground || z > result.z) {", + " result.ground = ground;", + " result.z = z;", + " }", + " }", + " return result;", + "};", + "", + "// The grounds or the objects placed on them can change every frame (while", + "// sculpting or moving them in the editor): things are placed again at most this often.", + "const PLACEMENT_DELAY = 0.3;", + "", + "/**", + " * Tells when things placed on the grounds of a scene (grass blades, scattered", + " * models) must be placed again: after the grounds or the given properties", + " * changed, at most every `PLACEMENT_DELAY` seconds.", + " */", + "class GroundPlacement {", + " /** @param {gdjs.RuntimeScene} runtimeScene */", + " constructor(runtimeScene) {", + " this.grounds = getGrounds(runtimeScene);", + " this.placedFrom = '';", + " this.timeSincePlacement = 0;", + " }", + "", + " /**", + " * @param {number} elapsedSeconds", + " * @param {string} propertiesKey Changes when the things must be placed again.", + " */", + " shouldPlaceAgain(elapsedSeconds, propertiesKey) {", + " this.timeSincePlacement += elapsedSeconds;", + " if (this.placedFrom && this.timeSincePlacement < PLACEMENT_DELAY) return false;", + " let placementKey = propertiesKey;", + " for (const ground of this.grounds) placementKey += '|' + ground.getVersion();", + " if (placementKey === this.placedFrom) return false;", + " this.placedFrom = placementKey;", + " this.timeSincePlacement = 0;", + " return true;", + " }", + "", + " placeAgain() {", + " this.placedFrom = '';", + " }", + "}", + "", + "/**", + " * Replace parts of the shader code of a Three.js material, with an error if", + " * a part is missing (for example after an update of Three.js).", + " * @param {string} shaderCode", + " * @param {Array<[string, string]>} replacements The searched code and its replacement.", + " * @param {string} materialName", + " */", + "const patchShaderCode = (shaderCode, replacements, materialName) => {", + " for (const [searchedCode, newCode] of replacements) {", + " if (!shaderCode.includes(searchedCode)) {", + " console.error(`${materialName}: \"${searchedCode}\" was not found in the shader, which won't look as expected.`);", + " }", + " shaderCode = shaderCode.replace(searchedCode, newCode);", + " }", + " return shaderCode;", + "};", + "", + "// Size of the object inner area. Instances are scaled from it, so these only", + "// define the default size of a new terrain.", + "const AREA_SIZE = 4096;", + "const AREA_DEPTH = 600;", + "", + "// Terrains have at most 8 x 8 chunks (so at most 64 draw calls), of at least", + "// 32 x 32 cells.", + "const MIN_CHUNK_CELLS = 32;", + "const MAX_CHUNKS_PER_SIDE = 8;", + "/** @param {number} resolution */", + "const getChunkCells = (resolution) => Math.max(MIN_CHUNK_CELLS, resolution / MAX_CHUNKS_PER_SIDE);", + "// Grid steps of each level of detail, and the distance (in chunk sizes)", + "// from which each one is used.", + "const LOD_STEPS = [1, 2, 4];", + "const LOD_DISTANCES = [0, 3, 6];", + "", + "const LAYER_COUNT = 4;", + "const RESOLUTIONS = [64, 128, 256, 512, 1024];", + "const DEFAULT_RESOLUTION = 256;", + "// Each undo step is a copy of the data: fewer are kept for big terrains.", + "const UNDO_STEPS_SAMPLES = 20 * 257 * 257;", + "", + "const clamp01 = (value) => (value < 0 ? 0 : value > 1 ? 1 : value);", + "const clampInteger = (value, min, max) =>", + " value < min ? min : value > max ? max : Math.floor(value);", + "", + "/**", + " * A rectangle of grid samples, inclusive. Empty when `minI > maxI`.", + " */", + "class SampleRectangle {", + " constructor() {", + " this.clear();", + " }", + "", + " clear() {", + " this.minI = Number.MAX_SAFE_INTEGER;", + " this.minJ = Number.MAX_SAFE_INTEGER;", + " this.maxI = -1;", + " this.maxJ = -1;", + " }", + "", + " isEmpty() {", + " return this.minI > this.maxI;", + " }", + "", + " /** @param {SampleRectangle} other */", + " add(other) {", + " if (other.isEmpty()) return;", + " this.minI = Math.min(this.minI, other.minI);", + " this.minJ = Math.min(this.minJ, other.minJ);", + " this.maxI = Math.max(this.maxI, other.maxI);", + " this.maxJ = Math.max(this.maxJ, other.maxJ);", + " }", + "", + " set(minI, minJ, maxI, maxJ) {", + " this.minI = minI;", + " this.minJ = minJ;", + " this.maxI = maxI;", + " this.maxJ = maxJ;", + " return this;", + " }", + "}", + "", + "/**", + " * The heights (between 0 and 1) and the painted layers weights of a square", + " * grid of `size * size` samples.", + " */", + "class TerrainData {", + " /** @param {number} resolution The number of cells on each side. */", + " constructor(resolution) {", + " this.resolution = resolution;", + " this.size = resolution + 1;", + " this.heights = new Float32Array(this.size * this.size);", + " /** 4 weights (0-255) per sample, one for each layer. */", + " this.splat = new Uint8Array(this.size * this.size * LAYER_COUNT);", + " this.fillLayer(0);", + " /**", + " * 4 bytes per sample: a tint multiplying the layers (red, green and", + " * blue, white for none) and the wetness (0 for dry).", + " */", + " this.colors = new Uint8Array(this.size * this.size * 4);", + " this.clearColors();", + " }", + "", + " clearColors() {", + " this.colors.fill(255);", + " for (let index = 3; index < this.colors.length; index += 4) {", + " this.colors[index] = 0;", + " }", + " }", + "", + " /** @param {number} layer */", + " fillLayer(layer) {", + " this.splat.fill(0);", + " for (let index = layer; index < this.splat.length; index += LAYER_COUNT) {", + " this.splat[index] = 255;", + " }", + " }", + "", + " getHeight(i, j) {", + " const last = this.resolution;", + " const clampedI = i < 0 ? 0 : i > last ? last : i;", + " const clampedJ = j < 0 ? 0 : j > last ? last : j;", + " return this.heights[clampedJ * this.size + clampedI];", + " }", + "", + " /**", + " * @param {number} gridX", + " * @param {number} gridY", + " * @returns {number} The height interpolated on the triangles that are drawn.", + " */", + " sampleHeight(gridX, gridY) {", + " const x = Math.min(Math.max(gridX, 0), this.resolution);", + " const y = Math.min(Math.max(gridY, 0), this.resolution);", + " const i = Math.min(Math.floor(x), this.resolution - 1);", + " const j = Math.min(Math.floor(y), this.resolution - 1);", + " const fx = x - i;", + " const fy = y - j;", + " const topRight = this.getHeight(i + 1, j);", + " const bottomLeft = this.getHeight(i, j + 1);", + " if (fx + fy <= 1) {", + " const topLeft = this.getHeight(i, j);", + " return topLeft + fx * (topRight - topLeft) + fy * (bottomLeft - topLeft);", + " }", + " const bottomRight = this.getHeight(i + 1, j + 1);", + " return (", + " bottomRight +", + " (1 - fx) * (bottomLeft - bottomRight) +", + " (1 - fy) * (topRight - bottomRight)", + " );", + " }", + "", + " /** @returns {number} The weight (0-1) of a layer, interpolated between samples. */", + " sampleLayerWeight(gridX, gridY, layer) {", + " const x = Math.min(Math.max(gridX, 0), this.resolution);", + " const y = Math.min(Math.max(gridY, 0), this.resolution);", + " const i = Math.min(Math.floor(x), this.resolution - 1);", + " const j = Math.min(Math.floor(y), this.resolution - 1);", + " const fx = x - i;", + " const fy = y - j;", + " const weight = (i, j) => this.splat[(j * this.size + i) * LAYER_COUNT + layer];", + " const top = weight(i, j) + fx * (weight(i + 1, j) - weight(i, j));", + " const bottom = weight(i, j + 1) + fx * (weight(i + 1, j + 1) - weight(i, j + 1));", + " return (top + fy * (bottom - top)) / 255;", + " }", + "", + " /**", + " * @param {number} channel 0, 1 and 2 for the tint, 3 for the wetness.", + " * @returns {number} The channel (0-1), interpolated between samples.", + " */", + " sampleColor(gridX, gridY, channel) {", + " const x = Math.min(Math.max(gridX, 0), this.resolution);", + " const y = Math.min(Math.max(gridY, 0), this.resolution);", + " const i = Math.min(Math.floor(x), this.resolution - 1);", + " const j = Math.min(Math.floor(y), this.resolution - 1);", + " const fx = x - i;", + " const fy = y - j;", + " const value = (i, j) => this.colors[(j * this.size + i) * 4 + channel];", + " const top = value(i, j) + fx * (value(i + 1, j) - value(i, j));", + " const bottom = value(i, j + 1) + fx * (value(i + 1, j + 1) - value(i, j + 1));", + " return (top + fy * (bottom - top)) / 255;", + " }", + "", + " /**", + " * @param {number} index The index of a sample.", + " * @returns {boolean} true if no layer is painted on the sample: it's a hole.", + " */", + " isHole(index) {", + " const offset = index * LAYER_COUNT;", + " return (this.splat[offset] | this.splat[offset + 1] | this.splat[offset + 2] | this.splat[offset + 3]) === 0;", + " }", + "", + " /**", + " * @returns {number} The sum of the layer weights (0-1), interpolated between samples: there is a", + " * hole where it's under 0.5, like what is drawn.", + " */", + " sampleSolidity(gridX, gridY) {", + " let solidity = 0;", + " for (let layer = 0; layer < LAYER_COUNT; layer++) solidity += this.sampleLayerWeight(gridX, gridY, layer);", + " return solidity;", + " }", + "", + " /** @returns {number} The index (from 0) of the layer painted the most. */", + " getDominantLayer(gridX, gridY) {", + " const i = clampInteger(Math.round(gridX), 0, this.resolution);", + " const j = clampInteger(Math.round(gridY), 0, this.resolution);", + " const offset = (j * this.size + i) * LAYER_COUNT;", + " let dominantLayer = 0;", + " for (let layer = 1; layer < LAYER_COUNT; layer++) {", + " if (this.splat[offset + layer] > this.splat[offset + dominantLayer]) {", + " dominantLayer = layer;", + " }", + " }", + " return dominantLayer;", + " }", + "", + " /** @param {TerrainData} other */", + " copyFrom(other) {", + " this.heights.set(other.heights);", + " this.splat.set(other.splat);", + " this.colors.set(other.colors);", + " }", + "", + " /**", + " * @param {number} resolution", + " * @returns {TerrainData} A copy of this data with another resolution.", + " */", + " resampled(resolution) {", + " const data = new TerrainData(resolution);", + " const ratio = this.resolution / resolution;", + " for (let j = 0; j < data.size; j++) {", + " for (let i = 0; i < data.size; i++) {", + " const index = j * data.size + i;", + " data.heights[index] = this.sampleHeight(i * ratio, j * ratio);", + " const sourceOffset =", + " (Math.round(j * ratio) * this.size + Math.round(i * ratio)) * LAYER_COUNT;", + " for (let layer = 0; layer < LAYER_COUNT; layer++) {", + " data.splat[index * LAYER_COUNT + layer] = this.splat[sourceOffset + layer];", + " }", + " for (let channel = 0; channel < 4; channel++) {", + " data.colors[index * 4 + channel] = this.colors[sourceOffset + channel];", + " }", + " }", + " }", + " return data;", + " }", + "}", + "", + "/**", + " * Brushes change the samples around a segment, with an effect going smoothly", + " * from full on the segment to none at `radius`. A point is a segment of", + " * length 0. Positions are in scene units relative to the terrain origin.", + " */", + "const brushes = {", + " /**", + " * @param {TerrainData} data", + " * @param {{cellWidth: number, cellHeight: number, ax: number, ay: number, bx: number, by: number, radius: number}} stroke", + " * @param {(sampleIndex: number, weight: number, t: number) => void} apply Called with the weight of", + " * the brush and the position (from 0 to 1) of the nearest point of the segment.", + " * @param {SampleRectangle} changedSamples", + " */", + " forEachSample(data, stroke, apply, changedSamples) {", + " const { cellWidth, cellHeight, ax, ay, bx, by, radius } = stroke;", + " const last = data.resolution;", + " const minI = clampInteger(Math.floor((Math.min(ax, bx) - radius) / cellWidth), 0, last);", + " const maxI = clampInteger(Math.ceil((Math.max(ax, bx) + radius) / cellWidth), 0, last);", + " const minJ = clampInteger(Math.floor((Math.min(ay, by) - radius) / cellHeight), 0, last);", + " const maxJ = clampInteger(Math.ceil((Math.max(ay, by) + radius) / cellHeight), 0, last);", + " changedSamples.set(minI, minJ, maxI, maxJ);", + " if (radius <= 0) return;", + "", + " const abX = bx - ax;", + " const abY = by - ay;", + " const abLengthSquared = abX * abX + abY * abY;", + " const radiusSquared = radius * radius;", + " for (let j = minJ; j <= maxJ; j++) {", + " const y = j * cellHeight;", + " for (let i = minI; i <= maxI; i++) {", + " const x = i * cellWidth;", + " const t =", + " abLengthSquared > 0", + " ? clamp01(((x - ax) * abX + (y - ay) * abY) / abLengthSquared)", + " : 0;", + " const dx = x - (ax + t * abX);", + " const dy = y - (ay + t * abY);", + " const distanceSquared = dx * dx + dy * dy;", + " if (distanceSquared >= radiusSquared) continue;", + " const falloff = 1 - distanceSquared / radiusSquared;", + " apply(j * data.size + i, falloff * falloff, t);", + " }", + " }", + " },", + "", + " raise(data, stroke, amount, changedSamples) {", + " const { heights } = data;", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index, weight) => {", + " heights[index] = clamp01(heights[index] + amount * weight);", + " },", + " changedSamples", + " );", + " },", + "", + " /**", + " * @param {number} targetHeight", + " * @param {number} [endTargetHeight] The target at the end of the segment, for a slope (like a road", + " * going up). The same target as at its start by default.", + " */", + " flatten(data, stroke, targetHeight, strength, changedSamples, endTargetHeight = targetHeight) {", + " const { heights } = data;", + " const startTarget = clamp01(targetHeight);", + " const endTarget = clamp01(endTargetHeight);", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index, weight, t) => {", + " const target = startTarget + (endTarget - startTarget) * t;", + " heights[index] += (target - heights[index]) * clamp01(weight * strength);", + " },", + " changedSamples", + " );", + " },", + "", + " smooth(data, stroke, strength, changedSamples) {", + " const { heights, size } = data;", + " const changes = [];", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index, weight) => {", + " const i = index % size;", + " const j = (index - i) / size;", + " let sum = 0;", + " for (let dj = -1; dj <= 1; dj++) {", + " for (let di = -1; di <= 1; di++) {", + " sum += data.getHeight(i + di, j + dj);", + " }", + " }", + " changes.push(index, heights[index] + (sum / 9 - heights[index]) * clamp01(weight * strength));", + " },", + " changedSamples", + " );", + " // Written after all averages are computed, so that the result", + " // doesn't depend on the order in which samples are visited.", + " for (let index = 0; index < changes.length; index += 2) {", + " heights[changes[index]] = changes[index + 1];", + " }", + " },", + "", + " paint(data, stroke, layer, strength, changedSamples) {", + " const { splat } = data;", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index, weight) => {", + " const amount = clamp01(weight * strength);", + " const offset = index * LAYER_COUNT;", + " for (let otherLayer = 0; otherLayer < LAYER_COUNT; otherLayer++) {", + " const target = otherLayer === layer ? 255 : 0;", + " const value = splat[offset + otherLayer];", + " // Rounded towards the target, so that light strokes still paint.", + " const change = (target - value) * amount;", + " splat[offset + otherLayer] = value + (change > 0 ? Math.ceil(change) : Math.floor(change));", + " }", + " },", + " changedSamples", + " );", + " },", + "", + " /** Remove all the layers, making a hole (`fill` or painting a layer fills it again). */", + " hole(data, stroke, changedSamples) {", + " const { splat } = data;", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index) => {", + " splat.fill(0, index * LAYER_COUNT, (index + 1) * LAYER_COUNT);", + " },", + " changedSamples", + " );", + " },", + "", + " /**", + " * Fill the holes in the radius with the layers of the ground around them:", + " * each pass fills the holes next to the ground with the average of their", + " * neighbors, from the edges of the holes inward.", + " */", + " fill(data, stroke, changedSamples) {", + " const { splat, size, resolution } = data;", + " /** @type {number[]} */", + " let holes = [];", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index) => {", + " if (data.isHole(index)) holes.push(index);", + " },", + " changedSamples", + " );", + " const filledWeights = new Float32Array(holes.length * LAYER_COUNT);", + " while (holes.length > 0) {", + " /** @type {number[]} */", + " const remainingHoles = [];", + " /** @type {number[]} */", + " const filledHoles = [];", + " for (const index of holes) {", + " const i = index % size;", + " const j = (index - i) / size;", + " const weights = filledWeights.subarray(filledHoles.length * LAYER_COUNT, (filledHoles.length + 1) * LAYER_COUNT);", + " weights.fill(0);", + " for (let neighborJ = Math.max(j - 1, 0); neighborJ <= Math.min(j + 1, resolution); neighborJ++) {", + " for (let neighborI = Math.max(i - 1, 0); neighborI <= Math.min(i + 1, resolution); neighborI++) {", + " const neighbor = neighborJ * size + neighborI;", + " if (data.isHole(neighbor)) continue;", + " for (let layer = 0; layer < LAYER_COUNT; layer++) weights[layer] += splat[neighbor * LAYER_COUNT + layer];", + " }", + " }", + " if (weights[0] + weights[1] + weights[2] + weights[3] > 0) filledHoles.push(index);", + " else remainingHoles.push(index);", + " }", + " if (filledHoles.length === 0) {", + " // No ground around (a hole bigger than the brush): the first layer is used.", + " for (const index of remainingHoles) splat[index * LAYER_COUNT] = 255;", + " break;", + " }", + " // Filled after the pass, so that the holes are filled from the edges inward.", + " filledHoles.forEach((index, filledIndex) => {", + " const weights = filledWeights.subarray(filledIndex * LAYER_COUNT, (filledIndex + 1) * LAYER_COUNT);", + " const total = weights[0] + weights[1] + weights[2] + weights[3];", + " for (let layer = 0; layer < LAYER_COUNT; layer++) {", + " splat[index * LAYER_COUNT + layer] = Math.round((weights[layer] / total) * 255);", + " }", + " });", + " holes = remainingHoles;", + " }", + " },", + "", + " /** @param {number[]} color The red, green and blue (0-255) of the tint: white removes it. */", + " tint(data, stroke, color, strength, changedSamples) {", + " brushes.paintColors(data, stroke, 0, color, strength, changedSamples);", + " },", + "", + " /** @param {number} wetness From 0 (dry) to 1. */", + " wet(data, stroke, wetness, strength, changedSamples) {", + " brushes.paintColors(data, stroke, 3, [Math.round(clamp01(wetness) * 255)], strength, changedSamples);", + " },", + "", + " /**", + " * Bring channels of the colors (see `TerrainData.colors`) towards targets.", + " * @param {number} firstChannel", + " * @param {number[]} targets From 0 to 255, for the channels from the first one.", + " */", + " paintColors(data, stroke, firstChannel, targets, strength, changedSamples) {", + " const { colors } = data;", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index, weight) => {", + " const amount = clamp01(weight * strength);", + " for (let channel = 0; channel < targets.length; channel++) {", + " const offset = index * 4 + firstChannel + channel;", + " // Rounded towards the target, so that light strokes still paint.", + " const change = (targets[channel] - colors[offset]) * amount;", + " colors[offset] += change > 0 ? Math.ceil(change) : Math.floor(change);", + " }", + " },", + " changedSamples", + " );", + " },", + "};", + "", + "const relief = (() => {", + " const hash = (x, y, seed) => {", + " let h = Math.imul(x, 374761393) + Math.imul(y, 668265263) + Math.imul(seed, 982451653);", + " h = Math.imul(h ^ (h >>> 13), 1274126177);", + " return ((h ^ (h >>> 16)) >>> 0) / 4294967296;", + " };", + " const valueNoise = (x, y, seed) => {", + " const x0 = Math.floor(x);", + " const y0 = Math.floor(y);", + " const fx = x - x0;", + " const fy = y - y0;", + " const sx = fx * fx * (3 - 2 * fx);", + " const sy = fy * fy * (3 - 2 * fy);", + " const top = hash(x0, y0, seed) + sx * (hash(x0 + 1, y0, seed) - hash(x0, y0, seed));", + " const bottom =", + " hash(x0, y0 + 1, seed) + sx * (hash(x0 + 1, y0 + 1, seed) - hash(x0, y0 + 1, seed));", + " return top + sy * (bottom - top);", + " };", + " /** Fractal noise between 0 and 1. */", + " const fractalNoise = (x, y, seed) => {", + " let value = 0;", + " let amplitude = 0.5;", + " let frequency = 1;", + " for (let octave = 0; octave < 5; octave++) {", + " value += amplitude * valueNoise(x * frequency, y * frequency, seed + octave);", + " amplitude *= 0.5;", + " frequency *= 2;", + " }", + " return value / 0.96875;", + " };", + " const smoothstep = (edge0, edge1, x) => {", + " const t = clamp01((x - edge0) / (edge1 - edge0));", + " return t * t * (3 - 2 * t);", + " };", + " const reliefHeights = {", + " Flat: () => 0,", + " Hills: (u, v, seed) => 0.05 + 0.5 * Math.pow(fractalNoise(u * 3, v * 3, seed), 2),", + " Mountains: (u, v, seed) => {", + " const ridge = 1 - Math.abs(2 * fractalNoise(u * 2.5, v * 2.5, seed) - 1);", + " return 0.9 * Math.pow(ridge, 2.5);", + " },", + " Island: (u, v, seed) => {", + " const distanceToCenter = 2 * Math.hypot(u - 0.5, v - 0.5);", + " const coast = 0.2 * (fractalNoise(u * 4, v * 4, seed + 7) - 0.5);", + " const land = smoothstep(0.95, 0.55, distanceToCenter + coast);", + " return land * (0.12 + 0.6 * Math.pow(fractalNoise(u * 3, v * 3, seed), 1.5));", + " },", + " };", + " const beachLayer = 3;", + "", + " return {", + " /**", + " * @param {TerrainData} data", + " * @param {string} reliefName Flat, Hills, Mountains or Island.", + " * @param {number} seed", + " */", + " generate(data, reliefName, seed) {", + " const getHeight = reliefHeights[reliefName] || reliefHeights.Flat;", + " const integerSeed = Math.floor(seed) | 0;", + " data.fillLayer(0);", + " for (let j = 0; j < data.size; j++) {", + " for (let i = 0; i < data.size; i++) {", + " const index = j * data.size + i;", + " const height = clamp01(", + " getHeight(i / data.resolution, j / data.resolution, integerSeed)", + " );", + " data.heights[index] = height;", + " if (reliefName === 'Island') {", + " const sand = Math.round(255 * smoothstep(0.16, 0.1, height));", + " const offset = index * LAYER_COUNT;", + " data.splat[offset] = 255 - sand;", + " data.splat[offset + beachLayer] = sand;", + " }", + " }", + " }", + " },", + " };", + "})();", + "", + "/**", + " * Edits written in the Edits property: a JSON list of brush strokes, like", + " * `[{\"tool\": \"raise\", \"x\": 0.5, \"y\": 0.5, \"radius\": 0.2, \"height\": 0.3}]`,", + " * applied on the relief or the heightmap. Positions and radius are fractions", + " * of the terrain size, heights and Z positions fractions of its depth: they", + " * are the same for all instances, whatever their size. They are made to be", + " * read and written by people and by AI agents.", + " */", + "const edits = {", + " /**", + " * @param {TerrainData} data", + " * @param {string} text", + " */", + " apply(data, text) {", + " if (!text.trim()) return;", + " let editList;", + " try {", + " editList = JSON.parse(text);", + " } catch (error) {", + " console.warn('The edits of a terrain are not valid JSON: ' + error.message);", + " return;", + " }", + " if (!Array.isArray(editList)) {", + " console.warn('The edits of a terrain must be a JSON list (like [{\"tool\": \"raise\", ...}]).');", + " return;", + " }", + " const numberOr = (value, defaultValue) => (typeof value === 'number' && isFinite(value) ? value : defaultValue);", + " const changedSamples = new SampleRectangle();", + " for (const edit of editList) {", + " if (!edit || typeof edit !== 'object') continue;", + " const x = numberOr(edit.x, 0.5);", + " const y = numberOr(edit.y, 0.5);", + " const stroke = {", + " cellWidth: 1 / data.resolution,", + " cellHeight: 1 / data.resolution,", + " ax: x,", + " ay: y,", + " bx: numberOr(edit.toX, x),", + " by: numberOr(edit.toY, y),", + " radius: numberOr(edit.radius, 0.1),", + " };", + " const strength = clamp01(numberOr(edit.strength, 1));", + " if (edit.tool === 'raise') {", + " brushes.raise(data, stroke, numberOr(edit.height, 0.1), changedSamples);", + " } else if (edit.tool === 'flatten') {", + " const z = numberOr(edit.z, 0);", + " brushes.flatten(data, stroke, z, strength, changedSamples, numberOr(edit.toZ, z));", + " } else if (edit.tool === 'smooth') {", + " // Smoothing the same fraction of the terrain at any resolution.", + " const passes = Math.max(Math.round(((1 + 9 * strength) * data.resolution) / 256), 1);", + " for (let pass = 0; pass < passes; pass++) brushes.smooth(data, stroke, 1, changedSamples);", + " } else if (edit.tool === 'paint') {", + " const layer = clampInteger(numberOr(edit.layer, 1) - 1, 0, LAYER_COUNT - 1);", + " brushes.paint(data, stroke, layer, strength, changedSamples);", + " } else if (edit.tool === 'tint') {", + " const color = gdjs.rgbOrHexToRGBColor(typeof edit.color === 'string' ? edit.color : '255;255;255');", + " brushes.tint(data, stroke, color, strength, changedSamples);", + " } else if (edit.tool === 'wetness') {", + " brushes.wet(data, stroke, numberOr(edit.amount, 1), strength, changedSamples);", + " } else if (edit.tool === 'hole') {", + " brushes.hole(data, stroke, changedSamples);", + " } else if (edit.tool === 'fill') {", + " brushes.fill(data, stroke, changedSamples);", + " } else {", + " console.warn('Unknown tool in the edits of a terrain: \"' + edit.tool + '\" (use raise, flatten, smooth, paint, tint, wetness, hole or fill).');", + " }", + " }", + " },", + "};", + "", + "/**", + " * @param {gdjs.RuntimeGame} game", + " * @param {string} imageResourceName", + " * @returns {CanvasImageSource | null} The image, or null if it's not loaded (yet).", + " */", + "const getLoadedImage = (game, imageResourceName) => {", + " const imageManager = game.getImageManager();", + " if (typeof imageManager.getImageSource === 'function') return imageManager.getImageSource(imageResourceName);", + " // Older GDevelop versions: a 192 x 192 placeholder is given for images that are not loaded.", + " const texture = imageManager.getPIXITexture(imageResourceName);", + " return texture && texture !== imageManager.getInvalidPIXITexture() ? texture.baseTexture.getDrawableSource() : null;", + "};", + "", + "/**", + " * Loads heights from an image: black is the lowest, white the highest.", + " * Heights have 16 bits of precision when the red channel holds the most", + " * significant byte and the green one the least significant (a gray image", + " * works as expected too).", + " * @param {TerrainData} data", + " * @param {gdjs.RuntimeGame} game", + " * @param {string} imageResourceName", + " * @returns {boolean} true if the image could be read.", + " */", + "const loadHeightsFromImage = (data, game, imageResourceName) => {", + " const source = getLoadedImage(game, imageResourceName);", + " if (!source) return false;", + "", + " const canvas = document.createElement('canvas');", + " canvas.width = source.width;", + " canvas.height = source.height;", + " const context = canvas.getContext('2d', { willReadFrequently: true });", + " if (!context) return false;", + " context.drawImage(source, 0, 0);", + " const pixels = context.getImageData(0, 0, source.width, source.height).data;", + " // Pixels are decoded before being interpolated, to keep the 16 bits.", + " const pixelHeight = (x, y) => {", + " const offset = (y * source.width + x) * 4;", + " return (256 * pixels[offset] + pixels[offset + 1]) / 65535;", + " };", + " for (let j = 0; j < data.size; j++) {", + " const y = (j / data.resolution) * (source.height - 1);", + " const y0 = Math.floor(y);", + " const y1 = Math.min(y0 + 1, source.height - 1);", + " for (let i = 0; i < data.size; i++) {", + " const x = (i / data.resolution) * (source.width - 1);", + " const x0 = Math.floor(x);", + " const x1 = Math.min(x0 + 1, source.width - 1);", + " const top = pixelHeight(x0, y0) + (x - x0) * (pixelHeight(x1, y0) - pixelHeight(x0, y0));", + " const bottom = pixelHeight(x0, y1) + (x - x0) * (pixelHeight(x1, y1) - pixelHeight(x0, y1));", + " data.heights[j * data.size + i] = top + (y - y0) * (bottom - top);", + " }", + " }", + " return true;", + "};", + "", + "/**", + " * Stores the data in a string. Only the differences with the base data (from", + " * the relief or the heightmap) are stored: heights on 16 bits, layer weights", + " * on 8 bits, then (since format 2) the tint and wetness on 8 bits. Each value is predicted from its neighbors and the", + " * prediction errors are written as variable length integers, with runs of", + " * zeros collapsed. Untouched or smoothly changed areas take almost no space.", + " */", + "const codec = (() => {", + " const formatVersion = '2';", + " // Format 1 has no tint and wetness.", + " const readableFormatVersions = ['1', '2'];", + "", + " class ByteWriter {", + " constructor() {", + " this.bytes = new Uint8Array(1024);", + " this.length = 0;", + " }", + " writeVarUint(value) {", + " if (this.length + 5 > this.bytes.length) {", + " const bytes = new Uint8Array(this.bytes.length * 2);", + " bytes.set(this.bytes);", + " this.bytes = bytes;", + " }", + " while (value >= 0x80) {", + " this.bytes[this.length++] = (value & 0x7f) | 0x80;", + " value >>>= 7;", + " }", + " this.bytes[this.length++] = value;", + " }", + " }", + "", + " class ByteReader {", + " /** @param {Uint8Array} bytes */", + " constructor(bytes) {", + " this.bytes = bytes;", + " this.position = 0;", + " }", + " readVarUint() {", + " let value = 0;", + " let shift = 0;", + " let byte;", + " do {", + " if (this.position >= this.bytes.length) throw new Error('Truncated terrain data.');", + " byte = this.bytes[this.position++];", + " value += (byte & 0x7f) * Math.pow(2, shift);", + " shift += 7;", + " } while (byte & 0x80);", + " return value;", + " }", + " }", + "", + " /** Predicts a value from the left, top and top-left ones. */", + " const predict = (values, size, index) => {", + " const i = index % size;", + " if (index < size) return i === 0 ? 0 : values[index - 1];", + " if (i === 0) return values[index - size];", + " return values[index - 1] + values[index - size] - values[index - size - 1];", + " };", + "", + " /**", + " * @param {ByteWriter} writer", + " * @param {Int32Array} values A grid of `size * size` values.", + " */", + " const writeGrid = (writer, values, size) => {", + " let zeroRun = 0;", + " for (let index = 0; index < values.length; index++) {", + " const error = values[index] - predict(values, size, index);", + " if (error === 0) {", + " zeroRun++;", + " continue;", + " }", + " if (zeroRun > 0) {", + " writer.writeVarUint(0);", + " writer.writeVarUint(zeroRun);", + " zeroRun = 0;", + " }", + " // Zigzag: small negative and positive numbers get small codes.", + " writer.writeVarUint(error > 0 ? error * 2 : -error * 2 - 1);", + " }", + " if (zeroRun > 0) {", + " writer.writeVarUint(0);", + " writer.writeVarUint(zeroRun);", + " }", + " };", + "", + " /** @param {ByteReader} reader */", + " const readGrid = (reader, size) => {", + " const values = new Int32Array(size * size);", + " let zeroRun = 0;", + " for (let index = 0; index < values.length; index++) {", + " let error = 0;", + " if (zeroRun > 0) {", + " zeroRun--;", + " } else {", + " const code = reader.readVarUint();", + " if (code === 0) {", + " zeroRun = reader.readVarUint() - 1;", + " } else {", + " error = code % 2 === 0 ? code / 2 : -(code + 1) / 2;", + " }", + " }", + " values[index] = predict(values, size, index) + error;", + " }", + " return values;", + " };", + "", + " const toBase64 = (bytes, length) => {", + " let binary = '';", + " for (let offset = 0; offset < length; offset += 0x8000) {", + " binary += String.fromCharCode.apply(null, bytes.subarray(offset, Math.min(offset + 0x8000, length)));", + " }", + " return btoa(binary);", + " };", + "", + " const fromBase64 = (base64) => {", + " const binary = atob(base64);", + " const bytes = new Uint8Array(binary.length);", + " for (let index = 0; index < binary.length; index++) {", + " bytes[index] = binary.charCodeAt(index);", + " }", + " return bytes;", + " };", + "", + " return {", + " /**", + " * @param {TerrainData} data", + " * @param {TerrainData} baseData The data without changes, with the same resolution.", + " */", + " encode(data, baseData) {", + " const writer = new ByteWriter();", + " const values = new Int32Array(data.heights.length);", + " for (let index = 0; index < values.length; index++) {", + " values[index] = Math.round((data.heights[index] - baseData.heights[index]) * 65535);", + " }", + " writeGrid(writer, values, data.size);", + " for (let layer = 0; layer < LAYER_COUNT; layer++) {", + " for (let index = 0; index < values.length; index++) {", + " const offset = index * LAYER_COUNT + layer;", + " values[index] = data.splat[offset] - baseData.splat[offset];", + " }", + " writeGrid(writer, values, data.size);", + " }", + " // Not painted, these grids are zeros: a few bytes each.", + " for (let channel = 0; channel < 4; channel++) {", + " for (let index = 0; index < values.length; index++) {", + " const offset = index * 4 + channel;", + " values[index] = data.colors[offset] - baseData.colors[offset];", + " }", + " writeGrid(writer, values, data.size);", + " }", + " return formatVersion + ';' + data.resolution + ';' + toBase64(writer.bytes, writer.length);", + " },", + "", + " /**", + " * Store grids of integers in a string, like the terrain data. Used by other objects (the scattered models).", + " * @param {Int32Array[]} grids Grids of `size * size` values.", + " * @param {number} size", + " */", + " encodeGrids(grids, size) {", + " const writer = new ByteWriter();", + " for (const grid of grids) writeGrid(writer, grid, size);", + " return toBase64(writer.bytes, writer.length);", + " },", + "", + " /**", + " * @param {string} base64 A string from `encodeGrids`.", + " * @param {number} size", + " * @param {number} count The number of grids.", + " * @returns {Int32Array[]} The grids (an error is thrown if the string is not valid).", + " */", + " decodeGrids(base64, size, count) {", + " const reader = new ByteReader(fromBase64(base64));", + " return Array.from({ length: count }, () => readGrid(reader, size));", + " },", + "", + " /**", + " * @param {string} text", + " * @param {(resolution: number) => TerrainData} createBaseData", + " * @returns {TerrainData | null} null if the text is empty or not valid.", + " */", + " decode(text, createBaseData) {", + " if (!text) return null;", + " const parts = text.split(';');", + " const resolution = parseInt(parts[1], 10);", + " if (readableFormatVersions.indexOf(parts[0]) === -1 || RESOLUTIONS.indexOf(resolution) === -1) {", + " return null;", + " }", + " try {", + " const reader = new ByteReader(fromBase64(parts[2]));", + " const data = createBaseData(resolution);", + " const heightChanges = readGrid(reader, data.size);", + " for (let index = 0; index < heightChanges.length; index++) {", + " data.heights[index] = clamp01(data.heights[index] + heightChanges[index] / 65535);", + " }", + " for (let layer = 0; layer < LAYER_COUNT; layer++) {", + " const weightChanges = readGrid(reader, data.size);", + " for (let index = 0; index < weightChanges.length; index++) {", + " const offset = index * LAYER_COUNT + layer;", + " // The base may have changed since the data was saved.", + " data.splat[offset] = Math.min(Math.max(data.splat[offset] + weightChanges[index], 0), 255);", + " }", + " }", + " if (parts[0] !== '1') {", + " for (let channel = 0; channel < 4; channel++) {", + " const colorChanges = readGrid(reader, data.size);", + " for (let index = 0; index < colorChanges.length; index++) {", + " const offset = index * 4 + channel;", + " data.colors[offset] = Math.min(Math.max(data.colors[offset] + colorChanges[index], 0), 255);", + " }", + " }", + " }", + " return data;", + " } catch (error) {", + " console.error('Terrain data could not be read:', error);", + " return null;", + " }", + " },", + " };", + "})();", + "", + "const lodIndices = new Map();", + "/**", + " * The triangles of a chunk, shared by all chunks of the same size. Chunk", + " * borders have a skirt going down, hiding the gaps between chunks drawn", + " * with different levels of detail.", + " * @param {number} chunkCells", + " * @param {number} step", + " */", + "const getLodIndex = (chunkCells, step) => {", + " const key = chunkCells + ';' + step;", + " const existingIndex = lodIndices.get(key);", + " if (existingIndex) return existingIndex;", + "", + " const sideVertices = chunkCells + 1;", + " const gridVertices = sideVertices * sideVertices;", + " const indices = [];", + " const grid = (i, j) => j * sideVertices + i;", + " for (let j = 0; j < chunkCells; j += step) {", + " for (let i = 0; i < chunkCells; i += step) {", + " const topLeft = grid(i, j);", + " const topRight = grid(i + step, j);", + " const bottomLeft = grid(i, j + step);", + " const bottomRight = grid(i + step, j + step);", + " indices.push(topLeft, topRight, bottomLeft, topRight, bottomRight, bottomLeft);", + " }", + " }", + " const borders = [", + " (k) => grid(k, 0),", + " (k) => grid(k, chunkCells),", + " (k) => grid(0, k),", + " (k) => grid(chunkCells, k),", + " ];", + " borders.forEach((borderVertex, border) => {", + " const skirtVertex = (k) => gridVertices + border * sideVertices + k;", + " for (let k = 0; k < chunkCells; k += step) {", + " const a = borderVertex(k);", + " const b = borderVertex(k + step);", + " const skirtA = skirtVertex(k);", + " const skirtB = skirtVertex(k + step);", + " // Both faces, as a skirt can be seen from either side.", + " indices.push(a, b, skirtB, a, skirtB, skirtA, a, skirtB, b, a, skirtA, skirtB);", + " }", + " });", + " const index = new THREE.BufferAttribute(new Uint16Array(indices), 1);", + " lodIndices.set(key, index);", + " return index;", + "};", + "", + "const vertexShaderDeclarations = `", + "varying vec2 vTerrainLocalXY;", + "varying float vTerrainLocalZ;", + "uniform float terrainWorldScale;", + "varying vec2 vTerrainWorldXY;", + "varying float vTerrainUp;", + "`;", + "const vertexShaderCode = `", + "vTerrainLocalXY = position.xy;", + "vTerrainLocalZ = position.z;", + "vTerrainWorldXY = (modelMatrix * vec4(transformed, 1.0)).xy * terrainWorldScale;", + "vTerrainUp = normalize(transpose(inverse(mat3(modelMatrix))) * objectNormal).z;", + "`;", + "const fragmentShaderDeclarations = `", + "uniform sampler2D terrainSplatMap;", + "uniform vec2 terrainSplatUvScaleAndOffset;", + "uniform vec3 terrainLayerColors[4];", + "uniform sampler2D terrainLayerMap0;", + "uniform sampler2D terrainLayerMap1;", + "uniform sampler2D terrainLayerMap2;", + "uniform sampler2D terrainLayerMap3;", + "uniform sampler2D terrainLayerNormalMap0;", + "uniform sampler2D terrainLayerNormalMap1;", + "uniform sampler2D terrainLayerNormalMap2;", + "uniform sampler2D terrainLayerNormalMap3;", + "uniform float terrainHasNormalMaps;", + "uniform float terrainNormalMapStrength;", + "uniform float terrainTextureSize;", + "uniform float terrainHeightBlending;", + "uniform float terrainTextureVariation;", + "uniform vec4 terrainCliffLayer;", + "uniform vec2 terrainCliffCosines;", + "uniform vec4 terrainBrush;", + "uniform sampler2D terrainColorMap;", + "uniform vec3 terrainWetShore;", + "varying vec2 vTerrainLocalXY;", + "varying float vTerrainLocalZ;", + "varying vec2 vTerrainWorldXY;", + "varying float vTerrainUp;", + "", + "float terrainHash(vec2 p) {", + " return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);", + "}", + "// The rotation of the copies of textures used to make repetition less visible.", + "const mat2 terrainCopyRotation = mat2(0.8, -0.6, 0.6, 0.8);", + "/** A bump of a normal map (from -1 to 1), mixed with its rotated copy in patches. */", + "vec3 terrainSampleNormal(sampler2D normalMap, vec2 uv, vec2 uv2, float patches) {", + " vec3 bump = texture2D(normalMap, uv).xyz * 2.0 - 1.0;", + " if (patches <= 0.0) return bump;", + " vec3 copyBump = texture2D(normalMap, uv2).xyz * 2.0 - 1.0;", + " // The bumps of the copy are rotated back with the inverse rotation.", + " copyBump.xy = transpose(terrainCopyRotation) * copyBump.xy;", + " return mix(bump, copyBump, patches);", + "}", + "float terrainNoise(vec2 p) {", + " vec2 i = floor(p);", + " vec2 f = fract(p);", + " vec2 u = f * f * (3.0 - 2.0 * f);", + " return mix(", + " mix(terrainHash(i), terrainHash(i + vec2(1.0, 0.0)), u.x),", + " mix(terrainHash(i + vec2(0.0, 1.0)), terrainHash(i + vec2(1.0, 1.0)), u.x),", + " u.y", + " );", + "}", + "`;", + "// Holes are samples without any painted layer: the interpolated sum of the", + "// weights gives smooth edges. Only compiled for terrains having holes, as", + "// discard makes drawing slower on some GPUs.", + "const holeShaderCode = `", + "#ifdef TERRAIN_HOLES", + "if (dot(texture2D(terrainSplatMap, vTerrainLocalXY * terrainSplatUvScaleAndOffset.x + terrainSplatUvScaleAndOffset.y), vec4(1.0)) < 0.5) discard;", + "#endif", + "`;", + "// For the shadows: the depth of holes is not drawn.", + "const holeDepthShaderDeclarations = `", + "uniform sampler2D terrainSplatMap;", + "uniform vec2 terrainSplatUvScaleAndOffset;", + "varying vec2 vTerrainLocalXY;", + "`;", + "", + "const fragmentShaderCode = holeShaderCode + `", + "// Borders between layers are made irregular and sharper, to look painted.", + "vec2 terrainNoisePosition = vTerrainWorldXY / 24.0;", + "vec4 terrainJitter = vec4(", + " terrainNoise(terrainNoisePosition),", + " terrainNoise(terrainNoisePosition + 19.1),", + " terrainNoise(terrainNoisePosition + 37.7),", + " terrainNoise(terrainNoisePosition + 53.3)", + ");", + "// Layer weights are in a texture, so that painted details stay the same", + "// whatever the level of detail of the ground.", + "vec2 terrainSplatUv = vTerrainLocalXY * terrainSplatUvScaleAndOffset.x + terrainSplatUvScaleAndOffset.y;", + "vec4 terrainWeights = texture2D(terrainSplatMap, terrainSplatUv) * (0.7 + 0.6 * terrainJitter);", + "terrainWeights = terrainWeights * terrainWeights;", + "terrainWeights /= max(dot(terrainWeights, vec4(1.0)), 0.00001);", + "float terrainCliff = 1.0 - smoothstep(terrainCliffCosines.x, terrainCliffCosines.y, vTerrainUp);", + "terrainWeights = mix(terrainWeights, terrainCliffLayer, terrainCliff * dot(terrainCliffLayer, vec4(1.0)));", + "", + "vec2 terrainUv = vTerrainWorldXY / terrainTextureSize;", + "vec3 terrainTexture0 = texture2D(terrainLayerMap0, terrainUv).rgb;", + "vec3 terrainTexture1 = texture2D(terrainLayerMap1, terrainUv).rgb;", + "vec3 terrainTexture2 = texture2D(terrainLayerMap2, terrainUv).rgb;", + "vec3 terrainTexture3 = texture2D(terrainLayerMap3, terrainUv).rgb;", + "", + "// Less visible repetition: in irregular patches, textures are replaced by a", + "// rotated and shifted copy of themselves, and colors slowly vary.", + "float terrainPatches = 0.0;", + "vec2 terrainUv2 = terrainUv;", + "float terrainColorVariation = 1.0;", + "if (terrainTextureVariation > 0.0) {", + " // Up to half of the ground is covered by the copies, each patch showing", + " // only one of them (mixing them would make them blurry).", + " float terrainPatchThreshold = 1.0 - 0.5 * terrainTextureVariation;", + " terrainPatches = smoothstep(terrainPatchThreshold - 0.05, terrainPatchThreshold + 0.05, terrainNoise(terrainUv * 0.23 + 7.3));", + " terrainUv2 = terrainCopyRotation * terrainUv + vec2(0.31, 0.73);", + " terrainTexture0 = mix(terrainTexture0, texture2D(terrainLayerMap0, terrainUv2).rgb, terrainPatches);", + " terrainTexture1 = mix(terrainTexture1, texture2D(terrainLayerMap1, terrainUv2).rgb, terrainPatches);", + " terrainTexture2 = mix(terrainTexture2, texture2D(terrainLayerMap2, terrainUv2).rgb, terrainPatches);", + " terrainTexture3 = mix(terrainTexture3, texture2D(terrainLayerMap3, terrainUv2).rgb, terrainPatches);", + " terrainColorVariation += terrainTextureVariation * 0.3 *", + " (terrainNoise(terrainUv * 0.11) + 0.5 * terrainNoise(terrainUv * 0.37 + 3.1) - 0.75);", + "}", + "", + "// Height-based blending: the brightness of a texture, compared to its", + "// average, is used as its height. Where layers meet, the high parts (stones,", + "// tufts) cover the low parts (sand, gaps) instead of fading into each other.", + "if (terrainHeightBlending > 0.0) {", + " const vec3 terrainLuminance = vec3(0.299, 0.587, 0.114);", + " vec4 terrainHeights = vec4(", + " dot(terrainTexture0 - textureLod(terrainLayerMap0, vec2(0.5), 16.0).rgb, terrainLuminance),", + " dot(terrainTexture1 - textureLod(terrainLayerMap1, vec2(0.5), 16.0).rgb, terrainLuminance),", + " dot(terrainTexture2 - textureLod(terrainLayerMap2, vec2(0.5), 16.0).rgb, terrainLuminance),", + " dot(terrainTexture3 - textureLod(terrainLayerMap3, vec2(0.5), 16.0).rgb, terrainLuminance)", + " );", + " vec4 terrainHeightWeights = terrainWeights + terrainHeights;", + " float terrainTop = max(max(terrainHeightWeights.x, terrainHeightWeights.y), max(terrainHeightWeights.z, terrainHeightWeights.w));", + " // Only painted layers (and the cliff layer) can show.", + " vec4 terrainHeightBlend = max(terrainHeightWeights - terrainTop + 0.1, 0.0) * step(0.004, terrainWeights);", + " terrainHeightBlend /= max(dot(terrainHeightBlend, vec4(1.0)), 0.00001);", + " terrainWeights = mix(terrainWeights, terrainHeightBlend, terrainHeightBlending);", + "}", + "", + "vec3 terrainColor = terrainColorVariation * (", + " terrainWeights.x * terrainLayerColors[0] * terrainTexture0 +", + " terrainWeights.y * terrainLayerColors[1] * terrainTexture1 +", + " terrainWeights.z * terrainLayerColors[2] * terrainTexture2 +", + " terrainWeights.w * terrainLayerColors[3] * terrainTexture3);", + "", + "// The painted tint multiplies all layers. Wet ground is darker (and", + "// shinier, see the roughness).", + "vec4 terrainTintAndWetness = texture2D(terrainColorMap, terrainSplatUv);", + "float terrainWetness = max(", + " terrainTintAndWetness.a,", + " terrainWetShore.z * (1.0 - smoothstep(terrainWetShore.x, terrainWetShore.x + terrainWetShore.y, vTerrainLocalZ))", + ");", + "terrainColor *= terrainTintAndWetness.rgb * (1.0 - 0.4 * terrainWetness);", + "diffuseColor.rgb *= terrainColor;", + "", + "if (terrainBrush.w > 0.5) {", + " float brushDistance = distance(vTerrainWorldXY, terrainBrush.xy);", + " float ringWidth = max(terrainBrush.z * 0.04, 1.5);", + " float ring = 1.0 - smoothstep(0.0, ringWidth, abs(brushDistance - terrainBrush.z));", + " float disc = brushDistance < terrainBrush.z ? 0.12 : 0.0;", + " diffuseColor.rgb = mix(diffuseColor.rgb, vec3(1.0), max(ring * 0.85, disc));", + "}", + "`;", + "", + "const fragmentShaderRoughnessCode = `", + "roughnessFactor = mix(roughnessFactor, 0.3, terrainWetness);", + "`;", + "", + "const fragmentShaderNormalCode = `", + "if (terrainHasNormalMaps > 0.5) {", + " vec3 terrainMapNormal =", + " terrainWeights.x * terrainSampleNormal(terrainLayerNormalMap0, terrainUv, terrainUv2, terrainPatches) +", + " terrainWeights.y * terrainSampleNormal(terrainLayerNormalMap1, terrainUv, terrainUv2, terrainPatches) +", + " terrainWeights.z * terrainSampleNormal(terrainLayerNormalMap2, terrainUv, terrainUv2, terrainPatches) +", + " terrainWeights.w * terrainSampleNormal(terrainLayerNormalMap3, terrainUv, terrainUv2, terrainPatches);", + " terrainMapNormal.xy *= terrainNormalMapStrength;", + " // The X and Y of textures follow the X and Y of the 3D world, projected on", + " // the ground (the camera space of the normal is used).", + " vec3 terrainTangent = (viewMatrix * vec4(1.0, 0.0, 0.0, 0.0)).xyz;", + " terrainTangent -= normal * dot(normal, terrainTangent);", + " // A ground facing the X axis (a rotated terrain) uses the Y axis instead.", + " if (dot(terrainTangent, terrainTangent) < 0.000001) {", + " terrainTangent = (viewMatrix * vec4(0.0, 1.0, 0.0, 0.0)).xyz;", + " terrainTangent -= normal * dot(normal, terrainTangent);", + " }", + " terrainTangent = normalize(terrainTangent);", + " vec3 terrainBitangent = cross(normal, terrainTangent);", + " normal = normalize(mat3(terrainTangent, terrainBitangent, normal) * terrainMapNormal);", + "}", + "`;", + "", + "let whiteTexture = null;", + "const getWhiteTexture = () => {", + " if (!whiteTexture) {", + " whiteTexture = new THREE.DataTexture(new Uint8Array([255, 255, 255, 255]), 1, 1);", + " whiteTexture.needsUpdate = true;", + " }", + " return whiteTexture;", + "};", + "", + "let flatNormalTexture = null;", + "const getFlatNormalTexture = () => {", + " if (!flatNormalTexture) {", + " flatNormalTexture = new THREE.DataTexture(new Uint8Array([128, 128, 255, 255]), 1, 1);", + " flatNormalTexture.needsUpdate = true;", + " }", + " return flatNormalTexture;", + "};", + "", + "const layerTextures = new WeakMap();", + "const layerNormalMaps = new WeakMap();", + "/**", + " * Textures of the image manager have no mipmaps: they would shimmer when", + " * repeated in the distance.", + " * @param {gdjs.RuntimeGame} game", + " * @param {string} resourceName", + " * @param {boolean} isNormalMap Normal maps are directions, not colors.", + " */", + "const getLayerTexture = (game, resourceName, isNormalMap) => {", + " if (!resourceName) return isNormalMap ? getFlatNormalTexture() : getWhiteTexture();", + " const imageTexture = game.getImageManager().getThreeTexture(resourceName);", + " const textures = isNormalMap ? layerNormalMaps : layerTextures;", + " let texture = textures.get(imageTexture);", + " if (!texture) {", + " texture = imageTexture.clone();", + " if (isNormalMap) texture.colorSpace = THREE.NoColorSpace;", + " texture.wrapS = THREE.RepeatWrapping;", + " texture.wrapT = THREE.RepeatWrapping;", + " texture.minFilter = THREE.LinearMipmapLinearFilter;", + " texture.generateMipmaps = true;", + " texture.anisotropy = 4;", + " texture.needsUpdate = true;", + " textures.set(imageTexture, texture);", + " }", + " return texture;", + "};", + "", + "/**", + " * A texture of 4 bytes per sample of a grid of `size * size` samples (layer", + " * weights, tint and wetness...), interpolated between samples.", + " * @param {Uint8Array} bytes", + " * @param {number} size", + " */", + "const createGridTexture = (bytes, size) => {", + " const texture = new THREE.DataTexture(bytes, size, size);", + " texture.magFilter = THREE.LinearFilter;", + " texture.minFilter = THREE.LinearFilter;", + " texture.userData.isUploaded = false;", + " texture.onUpdate = () => {", + " texture.userData.isUploaded = true;", + " };", + " return texture;", + "};", + "", + "/**", + " * Send the rows of the changed samples to the GPU, or the whole texture", + " * before its first upload (and in older GDevelop versions).", + " * @param {THREE.DataTexture} texture", + " * @param {SampleRectangle} samples", + " */", + "const uploadGridTextureRows = (texture, samples) => {", + " if (texture.userData.isUploaded && typeof texture.addUpdateRange === 'function') {", + " const size = texture.image.width;", + " for (let j = samples.minJ; j <= samples.maxJ; j++) {", + " texture.addUpdateRange((j * size + samples.minI) * 4, (samples.maxI - samples.minI + 1) * 4);", + " }", + " }", + " texture.needsUpdate = true;", + "};", + "", + "class TerrainRenderer {", + " /** @param {Terrain} terrain */", + " constructor(terrain) {", + " this.terrain = terrain;", + " this.group = new THREE.Group();", + " terrain.object.get3DRendererObject().add(this.group);", + " /** @type {Array<{mesh: THREE.Mesh, geometry: THREE.BufferGeometry, firstI: number, firstJ: number, center: THREE.Vector3, lod: number}>} */", + " this.chunks = [];", + " this.chunksPerSide = 0;", + " this.chunkCells = 0;", + " this.chunkSideVertices = 0;", + " this.chunkGridVertices = 0;", + " this.chunkVertices = 0;", + " /** @type {THREE.DataTexture | null} */", + " this.splatTexture = null;", + " /** @type {THREE.DataTexture | null} */", + " this.colorTexture = null;", + " this.uniforms = {", + " terrainSplatMap: { value: getWhiteTexture() },", + " terrainSplatUvScaleAndOffset: { value: new THREE.Vector2() },", + " terrainLayerColors: { value: [new THREE.Color(), new THREE.Color(), new THREE.Color(), new THREE.Color()] },", + " terrainLayerMap0: { value: getWhiteTexture() },", + " terrainLayerMap1: { value: getWhiteTexture() },", + " terrainLayerMap2: { value: getWhiteTexture() },", + " terrainLayerMap3: { value: getWhiteTexture() },", + " terrainLayerNormalMap0: { value: getFlatNormalTexture() },", + " terrainLayerNormalMap1: { value: getFlatNormalTexture() },", + " terrainLayerNormalMap2: { value: getFlatNormalTexture() },", + " terrainLayerNormalMap3: { value: getFlatNormalTexture() },", + " terrainHasNormalMaps: { value: 0 },", + " terrainNormalMapStrength: { value: 1 },", + " terrainTextureSize: { value: 256 },", + " terrainHeightBlending: { value: 0 },", + " terrainTextureVariation: { value: 0 },", + " terrainCliffLayer: { value: new THREE.Vector4() },", + " terrainCliffCosines: { value: new THREE.Vector2() },", + " terrainBrush: { value: new THREE.Vector4() },", + " terrainColorMap: { value: getWhiteTexture() },", + " // Z (in the terrain group) under which the ground is wet, fade height, and 1 if it's used.", + " terrainWetShore: { value: new THREE.Vector3() },", + " // Scene units by 3D world unit.", + " terrainWorldScale: { value: 1 },", + " };", + " this.material = new THREE.MeshStandardMaterial({ roughness: 1, metalness: 0 });", + " this.material.onBeforeCompile = (shader) => {", + " Object.assign(shader.uniforms, this.uniforms);", + " shader.vertexShader = patchShaderCode(shader.vertexShader, [", + " ['#include ', '#include \\n' + vertexShaderDeclarations],", + " ['#include ', '#include \\n' + vertexShaderCode],", + " ], 'Terrain3D');", + " shader.fragmentShader = patchShaderCode(shader.fragmentShader, [", + " ['#include ', '#include \\n' + fragmentShaderDeclarations],", + " ['#include ', fragmentShaderCode],", + " ['#include ', '#include \\n' + fragmentShaderRoughnessCode],", + " ['#include ', '#include \\n' + fragmentShaderNormalCode],", + " ], 'Terrain3D');", + " };", + " this.material.customProgramCacheKey = () => (this.hasHoles ? 'Terrain3DWithHoles' : 'Terrain3D');", + " this.hasHoles = false;", + " // Without them, holes would cast shadows.", + " this.depthMaterial = this._createHoleDepthMaterial(new THREE.MeshDepthMaterial());", + " this.distanceMaterial = this._createHoleDepthMaterial(new THREE.MeshDistanceMaterial());", + " this._cameraPosition = new THREE.Vector3();", + " this._chunkPosition = new THREE.Vector3();", + " this._scale = new THREE.Vector3();", + " }", + "", + " /**", + " * @param {THREE.MeshDepthMaterial | THREE.MeshDistanceMaterial} material", + " * @returns The material, drawing nothing in holes.", + " */", + " _createHoleDepthMaterial(material) {", + " material.defines = { TERRAIN_HOLES: '' };", + " material.onBeforeCompile = (shader) => {", + " Object.assign(shader.uniforms, this.uniforms);", + " shader.vertexShader = patchShaderCode(shader.vertexShader, [", + " ['#include ', '#include \\nvarying vec2 vTerrainLocalXY;'],", + " ['#include ', '#include \\nvTerrainLocalXY = position.xy;'],", + " ], 'Terrain3D shadows');", + " shader.fragmentShader = patchShaderCode(shader.fragmentShader, [", + " ['#include ', '#include \\n' + holeDepthShaderDeclarations],", + " ['#include ', holeShaderCode + '#include '],", + " ], 'Terrain3D shadows');", + " };", + " material.customProgramCacheKey = () => 'Terrain3DHoles';", + " return material;", + " }", + "", + " /**", + " * Use the shaders drawing holes, or the faster ones without them.", + " * @param {boolean} hasHoles", + " */", + " setHasHoles(hasHoles) {", + " if (hasHoles === this.hasHoles) return;", + " this.hasHoles = hasHoles;", + " // The material has its own defines (STANDARD).", + " if (hasHoles) this.material.defines.TERRAIN_HOLES = '';", + " else delete this.material.defines.TERRAIN_HOLES;", + " this.material.needsUpdate = true;", + " for (const chunk of this.chunks) this._setChunkShadowMaterials(chunk.mesh);", + " }", + "", + " /** @param {THREE.Mesh} mesh */", + " _setChunkShadowMaterials(mesh) {", + " mesh.customDepthMaterial = this.hasHoles ? this.depthMaterial : undefined;", + " mesh.customDistanceMaterial = this.hasHoles ? this.distanceMaterial : undefined;", + " }", + "", + " /** Recreate all the chunks, for example after the resolution changed. */", + " rebuild() {", + " this.disposeChunks();", + " const { data } = this.terrain;", + " this.chunkCells = getChunkCells(data.resolution);", + " this.chunkSideVertices = this.chunkCells + 1;", + " this.chunkGridVertices = this.chunkSideVertices * this.chunkSideVertices;", + " this.chunkVertices = this.chunkGridVertices + 4 * this.chunkSideVertices;", + " this.chunksPerSide = data.resolution / this.chunkCells;", + " const cellSize = AREA_SIZE / data.resolution;", + " // Sample centers are at the texel centers.", + " this.splatTexture = createGridTexture(data.splat, data.size);", + " this.uniforms.terrainSplatMap.value = this.splatTexture;", + " this.colorTexture = createGridTexture(data.colors, data.size);", + " // Tints are colors chosen like the layer colors.", + " this.colorTexture.colorSpace = THREE.SRGBColorSpace;", + " this.uniforms.terrainColorMap.value = this.colorTexture;", + " this.uniforms.terrainSplatUvScaleAndOffset.value.set(1 / (cellSize * data.size), 0.5 / data.size);", + " for (let chunkJ = 0; chunkJ < this.chunksPerSide; chunkJ++) {", + " for (let chunkI = 0; chunkI < this.chunksPerSide; chunkI++) {", + " const firstI = chunkI * this.chunkCells;", + " const firstJ = chunkJ * this.chunkCells;", + " const positions = new Float32Array(this.chunkVertices * 3);", + " for (let j = 0; j < this.chunkSideVertices; j++) {", + " for (let i = 0; i < this.chunkSideVertices; i++) {", + " const vertex = j * this.chunkSideVertices + i;", + " positions[vertex * 3] = (firstI + i) * cellSize;", + " positions[vertex * 3 + 1] = (firstJ + j) * cellSize;", + " }", + " }", + " // Skirt vertices are below border vertices.", + " this._forEachSkirtVertex((skirtVertex, borderVertex) => {", + " positions[skirtVertex * 3] = positions[borderVertex * 3];", + " positions[skirtVertex * 3 + 1] = positions[borderVertex * 3 + 1];", + " });", + " const geometry = new THREE.BufferGeometry();", + " geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));", + " geometry.setAttribute('normal', new THREE.BufferAttribute(new Float32Array(this.chunkVertices * 3), 3));", + " geometry.setIndex(getLodIndex(this.chunkCells, LOD_STEPS[0]));", + " geometry.boundingBox = new THREE.Box3();", + " geometry.boundingSphere = new THREE.Sphere();", + " const mesh = new THREE.Mesh(geometry, this.material);", + " mesh.matrixAutoUpdate = false;", + " // Rays hit the ground at its full resolution, whatever the level of", + " // detail drawn, and go through holes.", + " const chunkCells = this.chunkCells;", + " const terrainData = () => this.terrain.data;", + " const localPoint = new THREE.Vector3();", + " mesh.raycast = function (raycaster, intersects) {", + " const drawnIndex = geometry.index;", + " const firstIntersection = intersects.length;", + " geometry.setIndex(getLodIndex(chunkCells, LOD_STEPS[0]));", + " THREE.Mesh.prototype.raycast.call(this, raycaster, intersects);", + " geometry.setIndex(drawnIndex);", + " const data = terrainData();", + " const cellSize = AREA_SIZE / data.resolution;", + " for (let index = intersects.length - 1; index >= firstIntersection; index--) {", + " this.worldToLocal(localPoint.copy(intersects[index].point));", + " if (data.sampleSolidity(localPoint.x / cellSize, localPoint.y / cellSize) < 0.5) {", + " intersects.splice(index, 1);", + " }", + " }", + " };", + " this._setChunkShadowMaterials(mesh);", + " this.group.add(mesh);", + " this.chunks.push({", + " mesh,", + " geometry,", + " firstI,", + " firstJ,", + " center: new THREE.Vector3(", + " (firstI + this.chunkCells / 2) * cellSize,", + " (firstJ + this.chunkCells / 2) * cellSize,", + " 0", + " ),", + " lod: 0,", + " });", + " }", + " }", + " this.updateSamples(new SampleRectangle().set(0, 0, data.resolution, data.resolution), true);", + " this.updateMaterial();", + " }", + "", + " _forEachSkirtVertex(callback) {", + " for (let k = 0; k < this.chunkSideVertices; k++) {", + " callback(this.chunkGridVertices + k, k);", + " callback(this.chunkGridVertices + this.chunkSideVertices + k, this.chunkCells * this.chunkSideVertices + k);", + " callback(this.chunkGridVertices + 2 * this.chunkSideVertices + k, k * this.chunkSideVertices);", + " callback(", + " this.chunkGridVertices + 3 * this.chunkSideVertices + k,", + " k * this.chunkSideVertices + this.chunkCells", + " );", + " }", + " }", + "", + " /**", + " * Skirts go down only to the lowest point a coarser neighbor chunk can", + " * draw along the border, so that they can't be seen through holes.", + " * @param {Float32Array} positions", + " */", + " _updateSkirtHeights(positions) {", + " const coarsestStep = LOD_STEPS[LOD_STEPS.length - 1];", + " const borderVertices = [", + " (k) => k,", + " (k) => this.chunkCells * this.chunkSideVertices + k,", + " (k) => k * this.chunkSideVertices,", + " (k) => k * this.chunkSideVertices + this.chunkCells,", + " ];", + " borderVertices.forEach((borderVertex, border) => {", + " for (let k = 0; k < this.chunkSideVertices; k++) {", + " let minZ = Infinity;", + " const from = Math.max(k - coarsestStep, 0);", + " const to = Math.min(k + coarsestStep, this.chunkCells);", + " for (let other = from; other <= to; other++) {", + " minZ = Math.min(minZ, positions[borderVertex(other) * 3 + 2]);", + " }", + " const skirtVertex = this.chunkGridVertices + border * this.chunkSideVertices + k;", + " positions[skirtVertex * 3 + 2] = minZ;", + " }", + " });", + " }", + "", + " /**", + " * Update the vertices of the chunks sharing the given samples.", + " * @param {SampleRectangle} samples", + " * @param {boolean} hasPaintChanged", + " */", + " updateSamples(samples, hasPaintChanged) {", + " if (samples.isEmpty()) return;", + " const { data } = this.terrain;", + " const cellSize = AREA_SIZE / data.resolution;", + " const slopeFactor = AREA_DEPTH / cellSize;", + " // Normals of samples next to changed ones change too.", + " const minI = Math.max(samples.minI - 1, 0);", + " const minJ = Math.max(samples.minJ - 1, 0);", + " const maxI = Math.min(samples.maxI + 1, data.resolution);", + " const maxJ = Math.min(samples.maxJ + 1, data.resolution);", + " const firstChunkI = Math.max(Math.ceil(minI / this.chunkCells) - 1, 0);", + " const firstChunkJ = Math.max(Math.ceil(minJ / this.chunkCells) - 1, 0);", + " const lastChunkI = Math.min(Math.floor(maxI / this.chunkCells), this.chunksPerSide - 1);", + " const lastChunkJ = Math.min(Math.floor(maxJ / this.chunkCells), this.chunksPerSide - 1);", + "", + " for (let chunkJ = firstChunkJ; chunkJ <= lastChunkJ; chunkJ++) {", + " for (let chunkI = firstChunkI; chunkI <= lastChunkI; chunkI++) {", + " const chunk = this.chunks[chunkJ * this.chunksPerSide + chunkI];", + " const positions = chunk.geometry.attributes.position.array;", + " const normals = chunk.geometry.attributes.normal.array;", + " const fromI = Math.max(minI - chunk.firstI, 0);", + " const toI = Math.min(maxI - chunk.firstI, this.chunkCells);", + " const fromJ = Math.max(minJ - chunk.firstJ, 0);", + " const toJ = Math.min(maxJ - chunk.firstJ, this.chunkCells);", + " for (let j = fromJ; j <= toJ; j++) {", + " const gridJ = chunk.firstJ + j;", + " for (let i = fromI; i <= toI; i++) {", + " const gridI = chunk.firstI + i;", + " const vertex = j * this.chunkSideVertices + i;", + " const sample = gridJ * data.size + gridI;", + " positions[vertex * 3 + 2] = data.heights[sample] * AREA_DEPTH;", + "", + " const leftI = Math.max(gridI - 1, 0);", + " const rightI = Math.min(gridI + 1, data.resolution);", + " const upJ = Math.max(gridJ - 1, 0);", + " const downJ = Math.min(gridJ + 1, data.resolution);", + " const nx =", + " ((data.getHeight(leftI, gridJ) - data.getHeight(rightI, gridJ)) * slopeFactor) /", + " (rightI - leftI);", + " const ny =", + " ((data.getHeight(gridI, upJ) - data.getHeight(gridI, downJ)) * slopeFactor) /", + " (downJ - upJ);", + " const inverseLength = 1 / Math.sqrt(nx * nx + ny * ny + 1);", + " normals[vertex * 3] = nx * inverseLength;", + " normals[vertex * 3 + 1] = ny * inverseLength;", + " normals[vertex * 3 + 2] = inverseLength;", + " }", + " }", + " this._forEachSkirtVertex((skirtVertex, borderVertex) => {", + " for (let axis = 0; axis < 3; axis++) {", + " normals[skirtVertex * 3 + axis] = normals[borderVertex * 3 + axis];", + " }", + " });", + " this._updateSkirtHeights(positions);", + " let maxZ = 0;", + " for (let vertex = 0; vertex < this.chunkGridVertices; vertex++) {", + " maxZ = Math.max(maxZ, positions[vertex * 3 + 2]);", + " }", + " const chunkSize = this.chunkCells * cellSize;", + " chunk.geometry.boundingBox.min.set(chunk.firstI * cellSize, chunk.firstJ * cellSize, 0);", + " chunk.geometry.boundingBox.max.set(", + " chunk.firstI * cellSize + chunkSize,", + " chunk.firstJ * cellSize + chunkSize,", + " maxZ", + " );", + " chunk.geometry.boundingBox.getBoundingSphere(chunk.geometry.boundingSphere);", + " chunk.center.z = maxZ / 2;", + " chunk.geometry.attributes.position.needsUpdate = true;", + " chunk.geometry.attributes.normal.needsUpdate = true;", + " }", + " }", + " if (hasPaintChanged) {", + " // The data may have been replaced by one with the same resolution.", + " this.splatTexture.image.data = data.splat;", + " uploadGridTextureRows(this.splatTexture, samples);", + " this.colorTexture.image.data = data.colors;", + " uploadGridTextureRows(this.colorTexture, samples);", + " }", + " }", + "", + " updateMaterial() {", + " const { object } = this.terrain;", + " const game = object.getRuntimeScene().getGame();", + " const colors = [object._getLayer1Color(), object._getLayer2Color(), object._getLayer3Color(), object._getLayer4Color()];", + " const textures = [", + " object._getLayer1Texture(),", + " object._getLayer2Texture(),", + " object._getLayer3Texture(),", + " object._getLayer4Texture(),", + " ];", + " const normalMaps = [", + " object._getLayer1NormalMap(),", + " object._getLayer2NormalMap(),", + " object._getLayer3NormalMap(),", + " object._getLayer4NormalMap(),", + " ];", + " for (let layer = 0; layer < LAYER_COUNT; layer++) {", + " this.uniforms.terrainLayerColors.value[layer].set(gdjs.rgbOrHexStringToNumber(colors[layer]));", + " this.uniforms['terrainLayerMap' + layer].value = getLayerTexture(game, textures[layer], false);", + " this.uniforms['terrainLayerNormalMap' + layer].value = getLayerTexture(game, normalMaps[layer], true);", + " }", + " this.uniforms.terrainHasNormalMaps.value = normalMaps.some((normalMap) => !!normalMap) ? 1 : 0;", + " this.uniforms.terrainNormalMapStrength.value = Math.max(object._getNormalMapStrength(), 0);", + " this.uniforms.terrainTextureSize.value = Math.max(object._getTextureSize(), 1);", + " this.uniforms.terrainHeightBlending.value = clamp01(object._getHeightBlending());", + " this.uniforms.terrainTextureVariation.value = clamp01(object._getTextureVariation());", + "", + " const cliffLayer = parseInt(object._getCliffLayer(), 10);", + " const cliffAngle = gdjs.toRad(object._getCliffAngle());", + " const cliffBlendAngle = gdjs.toRad(4);", + " this.uniforms.terrainCliffLayer.value.set(", + " cliffLayer === 1 ? 1 : 0,", + " cliffLayer === 2 ? 1 : 0,", + " cliffLayer === 3 ? 1 : 0,", + " cliffLayer === 4 ? 1 : 0", + " );", + " this.uniforms.terrainCliffCosines.value.set(", + " Math.cos(cliffAngle + cliffBlendAngle),", + " Math.cos(cliffAngle - cliffBlendAngle)", + " );", + "", + " const castShadow = object._getCastShadow();", + " const receiveShadow = object._getReceiveShadow();", + " for (const chunk of this.chunks) {", + " chunk.mesh.castShadow = castShadow;", + " chunk.mesh.receiveShadow = receiveShadow;", + " }", + " }", + "", + " /**", + " * Show the brush circle of the editor.", + " * @param {THREE.Vector3 | null} localPosition", + " * @param {number} radius In scene units.", + " */", + " setBrush(localPosition, radius) {", + " const brush = this.uniforms.terrainBrush.value;", + " if (!localPosition) {", + " brush.w = 0;", + " return;", + " }", + " this.group.updateWorldMatrix(true, false);", + " const worldPosition = this._chunkPosition.copy(localPosition).applyMatrix4(this.group.matrixWorld);", + " const worldScale = this.uniforms.terrainWorldScale.value;", + " brush.set(worldPosition.x * worldScale, worldPosition.y * worldScale, radius, 1);", + " }", + "", + " /** @param {THREE.Camera | null} camera */", + " updateLevelsOfDetail(camera) {", + " if (!camera || !this.group.visible) return;", + " const matrixWorld = this.group.matrixWorld;", + " this._cameraPosition.setFromMatrixPosition(camera.matrixWorld);", + " this._scale.setFromMatrixScale(matrixWorld);", + " const cellSize = AREA_SIZE / this.terrain.data.resolution;", + " const chunkWorldSize = this.chunkCells * cellSize * Math.max(this._scale.x, this._scale.y);", + " for (const chunk of this.chunks) {", + " const distance =", + " this._chunkPosition.copy(chunk.center).applyMatrix4(matrixWorld).distanceTo(this._cameraPosition) /", + " chunkWorldSize;", + " let lod = LOD_DISTANCES.length - 1;", + " while (lod > 0 && distance < LOD_DISTANCES[lod]) lod--;", + " if (lod !== chunk.lod) {", + " chunk.lod = lod;", + " chunk.geometry.setIndex(getLodIndex(this.chunkCells, LOD_STEPS[lod]));", + " }", + " }", + " }", + "", + " disposeChunks() {", + " for (const chunk of this.chunks) {", + " chunk.mesh.removeFromParent();", + " // The index is shared by all chunks: it must stay on the GPU.", + " chunk.geometry.setIndex(null);", + " chunk.geometry.dispose();", + " }", + " this.chunks.length = 0;", + " if (this.splatTexture) this.splatTexture.dispose();", + " this.splatTexture = null;", + " if (this.colorTexture) this.colorTexture.dispose();", + " this.colorTexture = null;", + " }", + "", + " dispose() {", + " this.disposeChunks();", + " this.material.dispose();", + " this.depthMaterial.dispose();", + " this.distanceMaterial.dispose();", + " this.group.removeFromParent();", + " }", + "}", + "", + "/**", + " * Makes a Physics3D behavior use a height field shape following the terrain.", + " * Changes of heights are applied to the shape without recreating it.", + " * @implements {gdjs.Physics3DRuntimeBehavior.BodyUpdater}", + " */", + "class TerrainBodyUpdater {", + " /**", + " * @param {Terrain} terrain", + " * @param {gdjs.Physics3DRuntimeBehavior} physics", + " */", + " constructor(terrain, physics) {", + " this.terrain = terrain;", + " this.physics = physics;", + " this.defaultBodyUpdater = physics.bodyUpdater;", + " this.changedSamples = new SampleRectangle();", + " /** @type {Jolt.HeightFieldShape | null} */", + " this.heightField = null;", + " this.heightScale = 1;", + " this.heightsBufferPointer = 0;", + " this.heightsBufferLength = 0;", + " this.bodyX = 0;", + " this.bodyY = 0;", + " this.bodyZ = 0;", + " this.bodyHeight = 0;", + " physics.bodyType = 'Static';", + " physics.bodyUpdater = this;", + " }", + "", + " createShape() {", + " const { data, object } = this.terrain;", + " const worldInvScale = this.physics._sharedData.worldInvScale;", + " const { size } = data;", + " this.heightScale = object.getDepth() * worldInvScale;", + "", + " // Changes of heights (SetHeights) break collisions unless the sample", + " // count is a power of 2: extra samples have no collision.", + " const sampleCount = Math.pow(2, Math.ceil(Math.log2(size)));", + " const settings = new Jolt.HeightFieldShapeSettings();", + " settings.mScale.Set(", + " (object.getWidth() / data.resolution) * worldInvScale,", + " 1,", + " (object.getHeight() / data.resolution) * worldInvScale", + " );", + " settings.mSampleCount = sampleCount;", + " settings.mBlockSize = 4;", + " // Allows later changes of heights on the whole depth of the object.", + " settings.mMinHeightValue = 0;", + " settings.mMaxHeightValue = this.heightScale;", + " settings.mHeightSamples.resize(sampleCount * sampleCount);", + " const samples = new Float32Array(", + " Jolt.HEAPF32.buffer,", + " Jolt.getPointer(settings.mHeightSamples.data()),", + " sampleCount * sampleCount", + " );", + " const noCollision = Jolt.HeightFieldShapeConstantValues.prototype.cNoCollisionValue;", + " samples.fill(noCollision);", + " // Height fields go along X and Z: rows are flipped so that, once", + " // rotated to have heights along Z, rows go along Y.", + " for (let j = 0; j < size; j++) {", + " const row = (size - 1 - j) * sampleCount;", + " for (let i = 0; i < size; i++) {", + " samples[row + i] = this._getSampleHeight(j * size + i, noCollision);", + " }", + " }", + " const shape = settings.Create().Get();", + " Jolt.destroy(settings);", + " this.heightField = Jolt.castObject(shape, Jolt.HeightFieldShape);", + " this.changedSamples.clear();", + " return shape;", + " }", + "", + " /** @returns {number} The height of a sample in the shape, or no collision in a hole. */", + " _getSampleHeight(index, noCollision) {", + " const height = this.terrain.surfaceHeights[index];", + " return Number.isNaN(height) ? noCollision : height * this.heightScale;", + " }", + "", + " _getBodyPosition() {", + " const { object } = this.terrain;", + " const worldInvScale = this.physics._sharedData.worldInvScale;", + " this.bodyX = object.getX();", + " this.bodyY = object.getY();", + " this.bodyZ = object.getZ();", + " // The origin of the height field is at the bottom of the terrain (see createShape).", + " this.bodyHeight = object.getHeight();", + " return this.physics._sharedData.getRVec3(", + " object.getX() * worldInvScale,", + " (object.getY() + object.getHeight()) * worldInvScale,", + " object.getZ() * worldInvScale", + " );", + " }", + "", + " _getBodyRotation() {", + " // A quarter turn around X: heights go along Z.", + " return this.physics._sharedData.getQuat(Math.SQRT1_2, 0, 0, Math.SQRT1_2);", + " }", + "", + " createAndAddBody() {", + " const { physics } = this;", + " const { bodyInterface } = physics._sharedData;", + " const shape = this.createShape();", + " const settings = new Jolt.BodyCreationSettings(", + " shape,", + " this._getBodyPosition(),", + " this._getBodyRotation(),", + " Jolt.EMotionType_Static,", + " physics.getBodyLayer()", + " );", + " settings.mFriction = physics.friction;", + " settings.mRestitution = physics.restitution;", + " const body = bodyInterface.CreateBody(settings);", + " Jolt.destroy(settings);", + " bodyInterface.AddBody(body.GetID(), Jolt.EActivation_DontActivate);", + " return body;", + " }", + "", + " updateObjectFromBody() {", + " // The terrain is static: the body never moves it.", + " }", + "", + " updateBodyFromObject() {", + " const { physics } = this;", + " const body = physics._body;", + " if (!body) return;", + " const { object } = this.terrain;", + " const { bodyInterface } = physics._sharedData;", + " if (", + " object.getX() !== this.bodyX ||", + " object.getY() !== this.bodyY ||", + " object.getZ() !== this.bodyZ ||", + " object.getHeight() !== this.bodyHeight", + " ) {", + " bodyInterface.SetPositionAndRotation(", + " body.GetID(),", + " this._getBodyPosition(),", + " this._getBodyRotation(),", + " Jolt.EActivation_DontActivate", + " );", + " }", + " if (!this.changedSamples.isEmpty()) {", + " this._applyChangedHeights(body);", + " }", + " }", + "", + " _applyChangedHeights(body) {", + " const { data } = this.terrain;", + " const { size } = data;", + " const { minI, minJ, maxI, maxJ } = this.changedSamples;", + " // Jolt only changes blocks of samples. Rows are flipped and extra", + " // samples have no collision (see createShape).", + " const blockSize = this.heightField.GetBlockSize();", + " const sampleCount = this.heightField.GetSampleCount();", + " const alignDown = (value) => Math.floor(value / blockSize) * blockSize;", + " const alignUp = (value) => Math.min(Math.ceil(value / blockSize) * blockSize, sampleCount);", + " const firstX = alignDown(minI);", + " const firstRow = alignDown(size - 1 - maxJ);", + " const width = alignUp(maxI + 1) - firstX;", + " const height = alignUp(size - minJ) - firstRow;", + " if (width * height > this.heightsBufferLength) {", + " if (this.heightsBufferPointer) Jolt._webidl_free(this.heightsBufferPointer);", + " this.heightsBufferLength = width * height;", + " this.heightsBufferPointer = Jolt._webidl_malloc(this.heightsBufferLength * 4);", + " }", + " const heights = new Float32Array(Jolt.HEAPF32.buffer, this.heightsBufferPointer, width * height);", + " const noCollision = Jolt.HeightFieldShapeConstantValues.prototype.cNoCollisionValue;", + " for (let row = 0; row < height; row++) {", + " const j = size - 1 - (firstRow + row);", + " for (let column = 0; column < width; column++) {", + " const i = firstX + column;", + " heights[row * width + column] =", + " i < size && j >= 0 ? this._getSampleHeight(j * size + i, noCollision) : noCollision;", + " }", + " }", + " const sharedData = this.physics._sharedData;", + " this.heightField.SetHeights(", + " firstX,", + " firstRow,", + " width,", + " height,", + " this.heightsBufferPointer,", + " width,", + " sharedData.jolt.GetTempAllocator()", + " );", + " sharedData.bodyInterface.NotifyShapeChanged(", + " body.GetID(),", + " sharedData.getVec3(0, 0, 0),", + " false,", + " Jolt.EActivation_DontActivate", + " );", + " this._wakeUpBodiesOnChangedSamples();", + " this.changedSamples.clear();", + " }", + "", + " /** Bodies sleeping on the changed area would otherwise float or sink. */", + " _wakeUpBodiesOnChangedSamples() {", + " const { object, data } = this.terrain;", + " const worldInvScale = this.physics._sharedData.worldInvScale;", + " const cellWidth = object.getWidth() / data.resolution;", + " const cellHeight = object.getHeight() / data.resolution;", + " const { minI, minJ, maxI, maxJ } = this.changedSamples;", + " const min = new Jolt.Vec3(", + " (object.getX() + (minI - 1) * cellWidth) * worldInvScale,", + " (object.getY() + (minJ - 1) * cellHeight) * worldInvScale,", + " (object.getZ() - 1) * worldInvScale", + " );", + " const max = new Jolt.Vec3(", + " (object.getX() + (maxI + 1) * cellWidth) * worldInvScale,", + " (object.getY() + (maxJ + 1) * cellHeight) * worldInvScale,", + " (object.getZ() + object.getDepth() + 1) * worldInvScale", + " );", + " const box = new Jolt.AABox(min, max);", + " const broadPhaseLayerFilter = new Jolt.BroadPhaseLayerFilter();", + " const objectLayerFilter = new Jolt.ObjectLayerFilter();", + " this.physics._sharedData.bodyInterface.ActivateBodiesInAABox(box, broadPhaseLayerFilter, objectLayerFilter);", + " Jolt.destroy(min);", + " Jolt.destroy(max);", + " Jolt.destroy(box);", + " Jolt.destroy(broadPhaseLayerFilter);", + " Jolt.destroy(objectLayerFilter);", + " }", + "", + " recreateShape() {", + " const body = this.physics._body;", + " if (!body) return;", + " this.physics._sharedData.bodyInterface.SetShape(", + " body.GetID(),", + " this.createShape(),", + " false,", + " Jolt.EActivation_DontActivate", + " );", + " const { resolution } = this.terrain.data;", + " this.changedSamples.set(0, 0, resolution, resolution);", + " this._wakeUpBodiesOnChangedSamples();", + " this.changedSamples.clear();", + " }", + "", + " destroyBody() {", + " this.defaultBodyUpdater.destroyBody();", + " this.heightField = null;", + " }", + "", + " dispose() {", + " if (this.heightsBufferPointer) Jolt._webidl_free(this.heightsBufferPointer);", + " this.heightsBufferPointer = 0;", + " this.heightsBufferLength = 0;", + " }", + "}", + "", + "/** @param {TerrainData} data */", + "const copyTerrainData = (data) => {", + " const copy = new TerrainData(data.resolution);", + " copy.copyFrom(data);", + " return copy;", + "};", + "", + "// Generating a relief or reading an image is long for big terrains, and done", + "// for every instance and every hot-reload: the results are kept.", + "const baseDataCache = new Map();", + "const MAX_CACHED_BASE_DATA = 8;", + "// Changes of the shape of a terrain kept for consumers of its surface that", + "// missed some versions: older changes update the whole surface.", + "const MAX_SURFACE_CHANGES = 64;", + "// Unique among all terrains: a terrain replaced by another one never has its version.", + "let lastGroundVersion = 0;", + "", + "/**", + " * The terrain of a Terrain3D object: its data, how it's drawn and its", + " * surface (for collisions and navigation meshes). Positions are in the scene.", + " */", + "class Terrain {", + " /** @param {gdjs.CustomRuntimeObject3D} object */", + " constructor(object) {", + " this.object = object;", + " /** @type {TerrainData} */", + " this.data = new TerrainData(DEFAULT_RESOLUTION);", + " /** The data before any sculpting or painting, from the relief or heightmap. */", + " this.baseData = this.data;", + " this.renderer = new TerrainRenderer(this);", + " /** @type {TerrainBodyUpdater[]} */", + " this.bodyUpdaters = [];", + " this.changedSamples = new SampleRectangle();", + " this.hasPaintChanged = false;", + " /** Increased each time the ground changes, for objects following it (like grass). */", + " this.version = 0;", + " /** Increased each time the shape of the ground changes (not its paint or position). */", + " this.surfaceVersion = 0;", + " /**", + " * The last changes of the shape of the ground, for `getChangedArea`.", + " * @type {Array<{version: number, samples: SampleRectangle}>}", + " */", + " this.surfaceChanges = [];", + " /** The changes since this version are all in `surfaceChanges`. */", + " this.surfaceChangesStartVersion = 0;", + " this._stroke = { cellWidth: 1, cellHeight: 1, ax: 0, ay: 0, bx: 0, by: 0, radius: 0 };", + " this._strokeChangedSamples = new SampleRectangle();", + " /** The properties the data was loaded from, to reload it only if they change. */", + " this.loadedFrom = '';", + " /** The heightmap image being loaded: the relief is used until it is. */", + " this.loadingHeightmapImage = '';", + " this.isDisposed = false;", + " /** The last value of the sculpt data property read or saved. */", + " this.sculptData = '';", + " /**", + " * Values saved by the editor tools: the editor sends them back with a", + " * hot-reload, possibly after newer ones were saved.", + " * @type {string[]}", + " */", + " this.savedSculptData = [];", + " /** The heights of the surface: the heights of the data, and NaN in holes. */", + " this.surfaceHeights = new Float32Array(0);", + " this.loadFromProperties();", + " this._transform = this._getTransform();", + " getGrounds(object.getRuntimeScene()).add(this);", + " /**", + " * The shape of the ground, for the physics engine and navigation meshes.", + " * @type {gdjs.Surface}", + " */", + " this.surface = {", + " getVersion: () => this.surfaceVersion,", + " getChangedArea: (sinceVersion) => this.getChangedArea(sinceVersion),", + " getTriangles: () => null,", + " getHeightField: () => ({ columns: this.data.size, rows: this.data.size, heights: this.surfaceHeights }),", + " };", + " if (typeof object.setSurface === 'function') {", + " object.setSurface(this.surface);", + " } else if (gdjs.Physics3DRuntimeBehavior) {", + " // Older GDevelop versions don't use surfaces.", + " for (const behavior of object._behaviors) {", + " if (behavior instanceof gdjs.Physics3DRuntimeBehavior) {", + " this.bodyUpdaters.push(new TerrainBodyUpdater(this, behavior));", + " }", + " }", + " }", + " }", + "", + " _getSourceProperties() {", + " const { object } = this;", + " return [", + " object._getResolution(),", + " object._getRelief(),", + " object._getSeed(),", + " object._getHeightmapImage(),", + " object._getEdits(),", + " ].join('|');", + " }", + "", + " loadFromProperties() {", + " const { object } = this;", + " const resolution = parseInt(object._getResolution(), 10);", + " const validResolution = RESOLUTIONS.indexOf(resolution) !== -1 ? resolution : DEFAULT_RESOLUTION;", + " this.sculptData = object._getSculptData();", + " this.loadedFrom = this._getSourceProperties();", + "", + " this.baseData = this._getBaseData(validResolution);", + " let data = codec.decode(this.sculptData, (resolution) => copyTerrainData(this._getBaseData(resolution)));", + " if (data && data.resolution !== validResolution) {", + " data = data.resampled(validResolution);", + " }", + " this.setData(data || copyTerrainData(this.baseData));", + " }", + "", + " /**", + " * @param {number} resolution", + " * @returns {TerrainData} The data from the relief or the heightmap, shared: it must not be changed.", + " */", + " _getBaseData(resolution) {", + " const { object } = this;", + " const key = [resolution, object._getRelief(), object._getSeed(), object._getHeightmapImage(), object._getEdits()].join('|');", + " const cachedData = baseDataCache.get(key);", + " if (cachedData) return cachedData;", + "", + " const data = new TerrainData(resolution);", + " const heightmapImage = object._getHeightmapImage();", + " const isHeightmapLoaded =", + " !!heightmapImage && loadHeightsFromImage(data, object.getRuntimeScene().getGame(), heightmapImage);", + " if (!isHeightmapLoaded) {", + " relief.generate(data, object._getRelief(), object._getSeed());", + " if (heightmapImage) this._reloadOnceLoaded(heightmapImage);", + " }", + " edits.apply(data, object._getEdits());", + " // Not kept if the image is missing: it may be loaded later.", + " if (isHeightmapLoaded || !heightmapImage) baseDataCache.set(key, data);", + " if (baseDataCache.size > MAX_CACHED_BASE_DATA) {", + " baseDataCache.delete(baseDataCache.keys().next().value);", + " }", + " return data;", + " }", + "", + " /** @param {TerrainData} data */", + " setData(data) {", + " this.version = ++lastGroundVersion;", + " this.surfaceVersion = this.version;", + " this.surfaceChanges.length = 0;", + " this.surfaceChangesStartVersion = this.version;", + " const hasSameResolution = data.resolution === this.data.resolution && this.renderer.chunks.length > 0;", + " this.data = data;", + " const allSamples = new SampleRectangle().set(0, 0, data.resolution, data.resolution);", + " this.surfaceHeights = new Float32Array(data.heights.length);", + " this.renderer.setHasHoles(false);", + " this._updateSurfaceHeights(allSamples);", + " if (hasSameResolution) {", + " this.renderer.updateSamples(allSamples, true);", + " } else {", + " this.renderer.rebuild();", + " }", + " for (const bodyUpdater of this.bodyUpdaters) {", + " bodyUpdater.recreateShape();", + " }", + " }", + "", + " onHotReloading() {", + " const sculptData = this.object._getSculptData();", + " const savedIndex = this.savedSculptData.indexOf(sculptData);", + " if (savedIndex !== -1 && this._getSourceProperties() === this.loadedFrom) {", + " // The terrain already has this data, or newer changes.", + " this.savedSculptData.splice(0, savedIndex + 1);", + " } else if (sculptData !== this.sculptData || this._getSourceProperties() !== this.loadedFrom) {", + " this.savedSculptData.length = 0;", + " this.loadFromProperties();", + " }", + " this.renderer.updateMaterial();", + " }", + "", + " /** @returns {string} The data to save in the sculpt data property. */", + " save() {", + " this.sculptData = codec.encode(this.data, this.baseData);", + " this.savedSculptData.push(this.sculptData);", + " return this.sculptData;", + " }", + "", + " getCellWidth() {", + " return this.object.getWidth() / this.data.resolution;", + " }", + "", + " getCellHeight() {", + " return this.object.getHeight() / this.data.resolution;", + " }", + "", + " _toGridX(x) {", + " return (x - this.object.getX()) / this.getCellWidth();", + " }", + "", + " _toGridY(y) {", + " return (y - this.object.getY()) / this.getCellHeight();", + " }", + "", + " /** @returns {number} The Z position of the ground. */", + " getHeightAt(x, y) {", + " return this.object.getZ() + this.data.sampleHeight(this._toGridX(x), this._toGridY(y)) * this.object.getDepth();", + " }", + "", + " /** @returns {number} The angle of the ground with the horizontal, in degrees. */", + " getSlopeAt(x, y) {", + " const gridX = this._toGridX(x);", + " const gridY = this._toGridY(y);", + " const { data } = this;", + " const depth = this.object.getDepth();", + " const slopeX = ((data.sampleHeight(gridX + 0.5, gridY) - data.sampleHeight(gridX - 0.5, gridY)) * depth) / this.getCellWidth();", + " const slopeY = ((data.sampleHeight(gridX, gridY + 0.5) - data.sampleHeight(gridX, gridY - 0.5)) * depth) / this.getCellHeight();", + " return gdjs.toDegrees(Math.atan(Math.hypot(slopeX, slopeY)));", + " }", + "", + " /** @returns {number} The layer painted the most, from 1 to 4. */", + " getLayerAt(x, y) {", + " return this.data.getDominantLayer(this._toGridX(x), this._toGridY(y)) + 1;", + " }", + "", + " /**", + " * @param {number} layer From 1 to 4.", + " * @returns {number} How much the layer is painted, from 0 to 1.", + " */", + " getLayerWeightAt(x, y, layer) {", + " const layerIndex = clampInteger(layer - 1, 0, LAYER_COUNT - 1);", + " return this.data.sampleLayerWeight(this._toGridX(x), this._toGridY(y), layerIndex);", + " }", + "", + " /** @returns {number[]} The painted tint: red, green and blue, from 0 to 255 (white when there is none). */", + " getTintAt(x, y) {", + " const gridX = this._toGridX(x);", + " const gridY = this._toGridY(y);", + " return [0, 1, 2].map((channel) => Math.round(this.data.sampleColor(gridX, gridY, channel) * 255));", + " }", + "", + " /** @returns {number} How wet the ground is, from 0 (dry) to 1, painted or near the water (see the wet shore). */", + " getWetnessAt(x, y) {", + " const paintedWetness = this.data.sampleColor(this._toGridX(x), this._toGridY(y), 3);", + " const { object } = this;", + " if (!object._getWetShore()) return paintedWetness;", + " const fade = Math.max(object._getWetShoreFade(), 0.001);", + " const shoreWetness = clamp01((object._getWetShoreZ() + fade - this.getHeightAt(x, y)) / fade);", + " // Like the smoothstep of the shader.", + " return Math.max(paintedWetness, shoreWetness * shoreWetness * (3 - 2 * shoreWetness));", + " }", + "", + " _isInArea(x, y) {", + " const { object } = this;", + " return (", + " x >= object.getX() &&", + " x <= object.getX() + object.getWidth() &&", + " y >= object.getY() &&", + " y <= object.getY() + object.getHeight()", + " );", + " }", + "", + " /** @returns {boolean} true if the position is above or under the terrain, and not in a hole. */", + " containsPoint(x, y) {", + " return this._isInArea(x, y) && this.data.sampleSolidity(this._toGridX(x), this._toGridY(y)) >= 0.5;", + " }", + "", + " /** @returns {boolean} true if there is a hole in the terrain at the position, like what is drawn. */", + " isHoleAt(x, y) {", + " return this._isInArea(x, y) && this.data.sampleSolidity(this._toGridX(x), this._toGridY(y)) < 0.5;", + " }", + "", + " _getStroke(x1, y1, x2, y2, radius) {", + " const stroke = this._stroke;", + " stroke.cellWidth = this.getCellWidth();", + " stroke.cellHeight = this.getCellHeight();", + " stroke.ax = x1 - this.object.getX();", + " stroke.ay = y1 - this.object.getY();", + " stroke.bx = x2 - this.object.getX();", + " stroke.by = y2 - this.object.getY();", + " stroke.radius = Math.max(radius, 0);", + " return stroke;", + " }", + "", + " /**", + " * @param {SampleRectangle} samples", + " * @param {boolean} haveHeightsChanged false when only the paint changed.", + " */", + " _onSamplesChanged(samples, haveHeightsChanged) {", + " if (samples.isEmpty()) return;", + " this.version = ++lastGroundVersion;", + " this.changedSamples.add(samples);", + " // Painting changes the surface when it makes holes or fills them.", + " const hasSurfaceChanged = this._updateSurfaceHeights(samples);", + " if (!haveHeightsChanged && !hasSurfaceChanged) return;", + " this.surfaceVersion = this.version;", + " this.surfaceChanges.push({ version: this.version, samples: new SampleRectangle().set(samples.minI, samples.minJ, samples.maxI, samples.maxJ) });", + " if (this.surfaceChanges.length > MAX_SURFACE_CHANGES) {", + " this.surfaceChangesStartVersion = this.surfaceChanges.shift().version;", + " }", + " for (const bodyUpdater of this.bodyUpdaters) {", + " bodyUpdater.changedSamples.add(samples);", + " }", + " }", + "", + " /**", + " * @param {SampleRectangle} samples", + " * @returns {boolean} true if heights of the surface changed, or holes were made or filled.", + " */", + " _updateSurfaceHeights(samples) {", + " const { data, surfaceHeights } = this;", + " let hasChanged = false;", + " let hasHoles = false;", + " for (let j = samples.minJ; j <= samples.maxJ; j++) {", + " for (let i = samples.minI; i <= samples.maxI; i++) {", + " const index = j * data.size + i;", + " const isHole = data.isHole(index);", + " const height = isHole ? NaN : data.heights[index];", + " const previousHeight = surfaceHeights[index];", + " if (isHole) {", + " hasHoles = true;", + " if (Number.isNaN(previousHeight)) continue;", + " } else if (height === previousHeight) {", + " continue;", + " }", + " surfaceHeights[index] = height;", + " hasChanged = true;", + " }", + " }", + " // Filled holes keep the shaders drawing holes until the data is loaded again.", + " if (hasHoles) this.renderer.setHasHoles(true);", + " return hasChanged;", + " }", + "", + " /** @param {number} height In scene units, negative to lower the ground. */", + " raise(x1, y1, x2, y2, radius, height) {", + " const samples = this._strokeChangedSamples;", + " brushes.raise(this.data, this._getStroke(x1, y1, x2, y2, radius), height / this.object.getDepth(), samples);", + " this._onSamplesChanged(samples, true);", + " }", + "", + " /** @param {number} z The Z position to flatten the ground to. */", + " /** @param {number} [toZ] The Z position at the end of the segment, for a slope. The same as z by default. */", + " flatten(x1, y1, x2, y2, radius, z, strength, toZ = z) {", + " const samples = this._strokeChangedSamples;", + " const toHeight = (value) => (value - this.object.getZ()) / this.object.getDepth();", + " brushes.flatten(this.data, this._getStroke(x1, y1, x2, y2, radius), toHeight(z), strength, samples, toHeight(toZ));", + " this._onSamplesChanged(samples, true);", + " }", + "", + " smooth(x1, y1, x2, y2, radius, strength) {", + " const samples = this._strokeChangedSamples;", + " brushes.smooth(this.data, this._getStroke(x1, y1, x2, y2, radius), strength, samples);", + " this._onSamplesChanged(samples, true);", + " }", + "", + " /** @param {number} layer From 1 to 4. */", + " paint(x1, y1, x2, y2, radius, layer, strength) {", + " const layerIndex = clampInteger(layer - 1, 0, LAYER_COUNT - 1);", + " const samples = this._strokeChangedSamples;", + " brushes.paint(this.data, this._getStroke(x1, y1, x2, y2, radius), layerIndex, strength, samples);", + " this.hasPaintChanged = true;", + " this._onSamplesChanged(samples, false);", + " }", + "", + " /** @param {number[]} color The red, green and blue (0-255) of the tint: white removes it. */", + " tint(x1, y1, x2, y2, radius, color, strength) {", + " const samples = this._strokeChangedSamples;", + " brushes.tint(this.data, this._getStroke(x1, y1, x2, y2, radius), color, strength, samples);", + " this.hasPaintChanged = true;", + " this._onSamplesChanged(samples, false);", + " }", + "", + " /** @param {number} wetness From 0 (dry) to 1. */", + " wet(x1, y1, x2, y2, radius, wetness, strength) {", + " const samples = this._strokeChangedSamples;", + " brushes.wet(this.data, this._getStroke(x1, y1, x2, y2, radius), wetness, strength, samples);", + " this.hasPaintChanged = true;", + " this._onSamplesChanged(samples, false);", + " }", + "", + " /** Make a hole around a segment (`fillHoles` fills it again). */", + " makeHole(x1, y1, x2, y2, radius) {", + " const samples = this._strokeChangedSamples;", + " brushes.hole(this.data, this._getStroke(x1, y1, x2, y2, radius), samples);", + " this.hasPaintChanged = true;", + " this._onSamplesChanged(samples, false);", + " }", + "", + " /** Fill the holes around a segment with the layers of the ground around them. */", + " fillHoles(x1, y1, x2, y2, radius) {", + " const samples = this._strokeChangedSamples;", + " brushes.fill(this.data, this._getStroke(x1, y1, x2, y2, radius), samples);", + " this.hasPaintChanged = true;", + " this._onSamplesChanged(samples, false);", + " }", + "", + " /**", + " * Make the entrance of a tunnel or a cave: a hole, with the ground in", + " * front of it flattened to the floor of the tunnel.", + " * @param {number} angle The direction going into the ground, in degrees.", + " * @param {number} floorZ", + " * @param {number} length The length of the flat ground in front of the entrance.", + " */", + " carveEntrance(x, y, radius, angle, floorZ, length) {", + " const directionX = Math.cos(gdjs.toRad(angle));", + " const directionY = Math.sin(gdjs.toRad(angle));", + " this.flatten(x - directionX * length, y - directionY * length, x, y, radius * 1.5, floorZ, 1);", + " this.makeHole(x, y, x + directionX * radius, y + directionY * radius, radius);", + " }", + "", + " generate(reliefName, seed) {", + " this.baseData = new TerrainData(this.data.resolution);", + " relief.generate(this.baseData, reliefName, seed);", + " this.setData(copyTerrainData(this.baseData));", + " }", + "", + " loadHeightmap(imageResourceName) {", + " const baseData = new TerrainData(this.data.resolution);", + " if (loadHeightsFromImage(baseData, this.object.getRuntimeScene().getGame(), imageResourceName)) {", + " baseData.splat.set(this.data.splat);", + " baseData.colors.set(this.data.colors);", + " this.baseData = baseData;", + " this.setData(copyTerrainData(baseData));", + " }", + " }", + "", + " /** @param {gdjs.RuntimeObject[]} objects */", + " placeOnGround(objects) {", + " for (const object of objects) {", + " if (object === this.object || !gdjs.Base3DHandler.is3D(object)) continue;", + " const groundZ = this.getHeightAt(object.getCenterXInScene(), object.getCenterYInScene());", + " // The origin of the object is not always at its bottom.", + " object.setZ(groundZ + object.getZ() - object.getUnrotatedAABBMinZ());", + " }", + " }", + "", + " /**", + " * @returns {THREE.Vector3 | null} The point of the ground under the cursor,", + " * in the terrain group coordinates. Holes are hit too, so that brushes can fill them.", + " */", + " raycastFromCursor(game, raycaster, ndc) {", + " const layer = this.object.getInstanceContainer().getLayer(this.object.getLayer());", + " const camera = layer.getRenderer().getThreeCamera();", + " if (!camera) return null;", + " const inputManager = game.getInputManager();", + " ndc.set(", + " (inputManager.getCursorX() / game.getGameResolutionWidth()) * 2 - 1,", + " -(inputManager.getCursorY() / game.getGameResolutionHeight()) * 2 + 1", + " );", + " raycaster.setFromCamera(ndc, camera);", + " this.renderer.group.updateWorldMatrix(true, false);", + " const ray = raycaster.ray.clone().applyMatrix4(this.renderer.group.matrixWorld.clone().invert());", + " ray.direction.normalize();", + "", + " const box = new THREE.Box3(new THREE.Vector3(0, 0, 0), new THREE.Vector3(AREA_SIZE, AREA_SIZE, AREA_DEPTH));", + " const entry = box.containsPoint(ray.origin) ? ray.origin.clone() : ray.intersectBox(box, new THREE.Vector3());", + " if (!entry) return null;", + " const cellSize = AREA_SIZE / this.data.resolution;", + " const groundZAt = (point) => this.data.sampleHeight(point.x / cellSize, point.y / cellSize) * AREA_DEPTH;", + " const step = cellSize / 2;", + " const point = entry.clone();", + " const previousPoint = entry.clone();", + " for (let distance = 0; distance < AREA_SIZE * 2; distance += step) {", + " if (point.z <= groundZAt(point)) {", + " // Refine between the last point above the ground and this one.", + " for (let iteration = 0; iteration < 8; iteration++) {", + " const middle = previousPoint.clone().lerp(point, 0.5);", + " if (middle.z <= groundZAt(middle)) point.copy(middle);", + " else previousPoint.copy(middle);", + " }", + " return point;", + " }", + " if (!box.containsPoint(point)) return null;", + " previousPoint.copy(point);", + " point.addScaledVector(ray.direction, step);", + " }", + " return null;", + " }", + "", + " /**", + " * @param {THREE.Vector3} localPosition In the terrain group coordinates (like from `raycastFromCursor`).", + " * @returns {{x: number, y: number, z: number}} The position in the scene.", + " */", + " toScenePosition(localPosition) {", + " const { object } = this;", + " return {", + " x: object.getX() + (localPosition.x / AREA_SIZE) * object.getWidth(),", + " y: object.getY() + (localPosition.y / AREA_SIZE) * object.getHeight(),", + " z: object.getZ() + (localPosition.z / AREA_DEPTH) * object.getDepth(),", + " };", + " }", + "", + " /** Draw the changes of the ground (collisions are updated by the physics engine). */", + " applyChanges() {", + " this.renderer.updateSamples(this.changedSamples, this.hasPaintChanged);", + " this.changedSamples.clear();", + " this.hasPaintChanged = false;", + " }", + "", + " /** @returns {number} A number changing each time the ground changes (see `getGrounds`). */", + " getVersion() {", + " return this.version;", + " }", + "", + " /**", + " * @param {number} sinceVersion", + " * @returns {gdjs.SurfaceArea | null} The part of the ground whose shape changed since the version (see `gdjs.Surface`).", + " */", + " getChangedArea(sinceVersion) {", + " if (sinceVersion < this.surfaceChangesStartVersion) return null;", + " const samples = new SampleRectangle();", + " for (const change of this.surfaceChanges) {", + " if (change.version > sinceVersion) samples.add(change.samples);", + " }", + " if (samples.isEmpty()) return { minX: 0, minY: 0, maxX: 0, maxY: 0 };", + " const { resolution } = this.data;", + " return {", + " minX: samples.minI / resolution,", + " minY: samples.minJ / resolution,", + " maxX: samples.maxI / resolution,", + " maxY: samples.maxJ / resolution,", + " };", + " }", + "", + " _getTransform() {", + " const { object } = this;", + " return [object.getX(), object.getY(), object.getZ(), object.getWidth(), object.getHeight(), object.getDepth()].join(',');", + " }", + "", + " /** @param {string} imageResourceName A heightmap image that is not loaded yet. */", + " _reloadOnceLoaded(imageResourceName) {", + " if (this.loadingHeightmapImage === imageResourceName) return;", + " this.loadingHeightmapImage = imageResourceName;", + " const game = this.object.getRuntimeScene().getGame();", + " game.getImageManager()", + " .loadResource(imageResourceName)", + " .then(() => {", + " if (this.isDisposed || this.loadingHeightmapImage !== imageResourceName) return;", + " this.loadingHeightmapImage = '';", + " if (getLoadedImage(game, imageResourceName)) this.loadFromProperties();", + " })", + " .catch(() => {});", + " }", + "", + " update() {", + " const transform = this._getTransform();", + " if (transform !== this._transform) {", + " this._transform = transform;", + " this.version = ++lastGroundVersion;", + " }", + " this.applyChanges();", + " this.renderer.uniforms.terrainWorldScale.value = getWorldScale(this.object);", + " this._updateWetShore();", + " const layer = this.object.getInstanceContainer().getLayer(this.object.getLayer());", + " this.renderer.updateLevelsOfDetail(layer.getRenderer().getThreeCamera());", + " }", + "", + " _updateWetShore() {", + " const { object } = this;", + " // In the coordinates of the terrain group, like the vertices.", + " const depthScale = AREA_DEPTH / Math.max(object.getDepth(), 0.0001);", + " this.renderer.uniforms.terrainWetShore.value.set(", + " (object._getWetShoreZ() - object.getZ()) * depthScale,", + " Math.max(object._getWetShoreFade(), 0.001) * depthScale,", + " object._getWetShore() ? 1 : 0", + " );", + " }", + "", + " dispose() {", + " this.isDisposed = true;", + " getGrounds(this.object.getRuntimeScene()).delete(this);", + " if (typeof this.object.setSurface === 'function' && this.object.getSurface() === this.surface) {", + " this.object.setSurface(null);", + " }", + " this.renderer.dispose();", + " for (const bodyUpdater of this.bodyUpdaters) {", + " bodyUpdater.dispose();", + " }", + " }", + "}", + "", + "/**", + " * @param {gdjs.RuntimeObject} object", + " * @returns {number} The number of scene units by 3D world unit (1 in GDevelop versions before the world scale).", + " */", + "const getWorldScale = (object) => {", + " const scene = object.getRuntimeScene();", + " return typeof scene.getRenderer3DWorldScale === 'function' ? scene.getRenderer3DWorldScale() : 1;", + "};", + "", + "const averageColors = new WeakMap();", + "let white = null;", + "/**", + " * @param {THREE.Texture} texture", + " * @returns {THREE.Color} The average color of the texture (white when it's not loaded).", + " */", + "const getAverageColor = (texture) => {", + " const cachedColor = averageColors.get(texture);", + " if (cachedColor) return cachedColor;", + " const image = texture.image;", + " const isDrawable =", + " typeof document !== 'undefined' &&", + " ((image instanceof HTMLImageElement && image.complete && image.naturalWidth > 0) ||", + " image instanceof HTMLCanvasElement ||", + " (typeof ImageBitmap !== 'undefined' && image instanceof ImageBitmap));", + " if (!isDrawable) return white || (white = new THREE.Color(1, 1, 1));", + " const size = 8;", + " const canvas = document.createElement('canvas');", + " canvas.width = size;", + " canvas.height = size;", + " const context = canvas.getContext('2d');", + " context.drawImage(image, 0, 0, size, size);", + " const pixels = context.getImageData(0, 0, size, size).data;", + " let red = 0;", + " let green = 0;", + " let blue = 0;", + " for (let index = 0; index < pixels.length; index += 4) {", + " red += pixels[index];", + " green += pixels[index + 1];", + " blue += pixels[index + 2];", + " }", + " const count = size * size * 255;", + " const color = new THREE.Color().setRGB(red / count, green / count, blue / count, THREE.SRGBColorSpace);", + " averageColors.set(texture, color);", + " return color;", + "};", + "", + "const svgIcon = (content) =>", + " 'data:image/svg+xml,' +", + " encodeURIComponent('' + content + '');", + "const strokes = 'fill=\"none\" stroke=\"#000\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"';", + "const toolIcons = {", + " Raise: svgIcon(''),", + " Lower: svgIcon(''),", + " Smooth: svgIcon(''),", + " Flatten: svgIcon(''),", + " Size: svgIcon(''),", + " Strength: svgIcon(''),", + " Undo: svgIcon(''),", + " Hole: svgIcon(''),", + " FillHoles: svgIcon(''),", + " Wetness: svgIcon(''),", + "};", + "// Tints for the color brush when the editor has no color picker (older GDevelop versions).", + "const tintPresets = ['#e2c879', '#a5c45a', '#8a6a48', '#c98a62', '#9a9a92', '#ffffff'];", + "", + "/**", + " * The sculpt and paint tools shown in the scene editor when a terrain is", + " * selected. Without the needed editor functions (older GDevelop versions),", + " * nothing is shown: the terrain can still be changed with events.", + " */", + "class TerrainEditorTools {", + " constructor() {", + " /** '' when no brush is used. */", + " this.toolName = '';", + " this.sizePercent = 30;", + " this.strengthPercent = 50;", + " /** @type {Terrain | null} */", + " this.terrain = null;", + " /** @type {gdjs.InGameEditor | null} */", + " this.editor = null;", + " this.isStroking = false;", + " this.flattenHeight = 0;", + " /** The color of the \"Tint\" brush, as \"#rrggbb\". */", + " this.tintColor = tintPresets[0];", + " this.wetnessPercent = 100;", + " this.lastStrokeTime = 0;", + " /** @type {{data: TerrainData, baseData: TerrainData}[]} */", + " this.undoSteps = [];", + " this.raycaster = new THREE.Raycaster();", + " this.ndc = new THREE.Vector2();", + " this._swatchColor = new THREE.Color();", + " }", + "", + " /** @param {gdjs.InGameEditor} editor */", + " update(editor) {", + " if (typeof editor.showToolbar !== 'function') {", + " // Older GDevelop versions: the terrain can only be changed with events.", + " return;", + " }", + " this.editor = editor;", + " const selectedObjects = editor.getSelectedObjects();", + " const terrain =", + " selectedObjects.length === 1 && selectedObjects[0].__terrain3D ? selectedObjects[0].__terrain3D : null;", + " if (terrain !== this.terrain) {", + " this._endStroke();", + " if (this.terrain) this.terrain.renderer.setBrush(null, 0);", + " this.terrain = terrain;", + " this.undoSteps.length = 0;", + " }", + " if (terrain) editor.showToolbar('Terrain3D', this._getToolbarItems(terrain));", + " const game = editor.getRuntimeGame();", + " if (!terrain || !this.toolName) {", + " if (terrain) terrain.renderer.setBrush(null, 0);", + " return;", + " }", + "", + " const inputManager = game.getInputManager();", + " if (inputManager.wasKeyJustPressed(gdjs.evtTools.input.keysNameToCode.Escape)) {", + " this._endStroke();", + " this.toolName = '';", + " terrain.renderer.setBrush(null, 0);", + " return;", + " }", + " editor.captureLeftMouseButton();", + " const hit = terrain.raycastFromCursor(game, this.raycaster, this.ndc);", + " const radius = this._getRadius();", + " terrain.renderer.setBrush(hit, radius);", + "", + " // Older GDevelop versions don't have it.", + " const isPressedForTool =", + " typeof editor.isCapturedLeftMouseButtonPressed === 'function'", + " ? editor.isCapturedLeftMouseButtonPressed()", + " : inputManager.isMouseButtonPressed(0);", + " // The button is released outside the canvas when the mouse is on a toolbar.", + " const isPressed = isPressedForTool && inputManager.isMouseInsideCanvas();", + " if (isPressed && hit && !this.isStroking) {", + " this._startStroke(terrain, hit);", + " }", + " if (this.isStroking && isPressed && hit) {", + " this._applyBrush(terrain, hit, radius);", + " }", + " if (!isPressed) {", + " this._endStroke();", + " }", + " }", + "", + " _getRadius() {", + " const { object } = this.terrain;", + " const maxRadius = Math.max(object.getWidth(), object.getHeight()) / 4;", + " return Math.max(maxRadius * Math.pow(this.sizePercent / 100, 2), this.terrain.getCellWidth());", + " }", + "", + " _startStroke(terrain, hit) {", + " this.isStroking = true;", + " this.lastStrokeTime = performance.now();", + " this.flattenHeight = terrain.toScenePosition(hit).z;", + " this.undoSteps.push({ data: copyTerrainData(terrain.data), baseData: terrain.baseData });", + " const maxUndoSteps = Math.max(Math.floor(UNDO_STEPS_SAMPLES / terrain.data.heights.length), 2);", + " while (this.undoSteps.length > maxUndoSteps) this.undoSteps.shift();", + " }", + "", + " _endStroke() {", + " if (!this.isStroking) return;", + " this.isStroking = false;", + " this._save();", + " }", + "", + " _applyBrush(terrain, hit, radius) {", + " const now = performance.now();", + " const elapsedSeconds = Math.min((now - this.lastStrokeTime) / 1000, 0.1);", + " this.lastStrokeTime = now;", + " const { x, y } = terrain.toScenePosition(hit);", + " const amount = (this.strengthPercent / 100) * elapsedSeconds;", + " const depth = terrain.object.getDepth();", + " if (this.toolName === 'Raise') terrain.raise(x, y, x, y, radius, amount * depth * 0.5);", + " else if (this.toolName === 'Lower') terrain.raise(x, y, x, y, radius, -amount * depth * 0.5);", + " else if (this.toolName === 'Smooth') terrain.smooth(x, y, x, y, radius, amount * 10);", + " else if (this.toolName === 'Flatten') terrain.flatten(x, y, x, y, radius, this.flattenHeight, amount * 10);", + " else if (this.toolName.startsWith('Paint')) {", + " terrain.paint(x, y, x, y, radius, parseInt(this.toolName.slice(5), 10), amount * 8);", + " } else if (this.toolName === 'Hole') {", + " terrain.makeHole(x, y, x, y, radius);", + " } else if (this.toolName === 'FillHoles') {", + " terrain.fillHoles(x, y, x, y, radius);", + " } else if (this.toolName === 'Tint') {", + " terrain.tint(x, y, x, y, radius, gdjs.rgbOrHexToRGBColor(this.tintColor), amount * 8);", + " } else if (this.toolName === 'Wetness') {", + " terrain.wet(x, y, x, y, radius, this.wetnessPercent / 100, amount * 8);", + " }", + " // Drawn now, as the terrain was updated earlier in this frame.", + " terrain.applyChanges();", + " }", + "", + " _save() {", + " const { terrain, editor } = this;", + " if (!terrain || !editor) return;", + " editor.updateObjectProperties(terrain.object.getName(), { SculptData: terrain.save() });", + " }", + "", + " _undo() {", + " const undoStep = this.undoSteps.pop();", + " // The relief, the edits or the resolution may have changed since the stroke.", + " if (!this.terrain || !undoStep || undoStep.baseData !== this.terrain.baseData) {", + " this.undoSteps.length = 0;", + " return;", + " }", + " this.terrain.setData(undoStep.data);", + " this._save();", + " }", + "", + " /** @returns {Array} The settings of the tint or wetness brush, when it's used. */", + " _getColorToolItems() {", + " const { editor } = this;", + " if (this.toolName === 'Tint') {", + " if (editor && typeof editor.isToolbarItemTypeSupported === 'function' && editor.isToolbarItemTypeSupported('color')) {", + " return [", + " {", + " type: 'color',", + " id: 'TintColor',", + " tooltip: 'Color of the tint',", + " value: this.tintColor,", + " onChange: (color) => (this.tintColor = color),", + " },", + " ];", + " }", + " return tintPresets.map((color) => ({", + " type: 'button',", + " id: 'TintPreset' + color,", + " tooltip: color === '#ffffff' ? 'No tint' : 'Tint with this color',", + " color,", + " isActive: this.tintColor === color,", + " onClick: () => (this.tintColor = color),", + " }));", + " }", + " if (this.toolName === 'Wetness') {", + " return [", + " {", + " type: 'slider',", + " id: 'WetnessAmount',", + " tooltip: 'Wetness',", + " iconUrl: toolIcons.Wetness,", + " min: 0,", + " max: 100,", + " value: this.wetnessPercent,", + " onChange: (value) => (this.wetnessPercent = value),", + " },", + " ];", + " }", + " return [];", + " }", + "", + " /**", + " * @param {Terrain} terrain", + " * @returns {Array} The toolbar for the current state (the editor only", + " * updates what changed).", + " */", + " _getToolbarItems(terrain) {", + " const { object } = terrain;", + " const toolButton = (toolName, tooltip, color) => ({", + " type: 'button',", + " id: toolName,", + " tooltip,", + " iconUrl: toolIcons[toolName],", + " color,", + " isActive: this.toolName === toolName,", + " onClick: () => (this.toolName = this.toolName === toolName ? '' : toolName),", + " });", + " const layerColors = [object._getLayer1Color(), object._getLayer2Color(), object._getLayer3Color(), object._getLayer4Color()];", + " // Layers are shown with their color, tinting their texture.", + " const swatchColors = layerColors.map((color, index) =>", + " this._swatchColor", + " .set(gdjs.rgbOrHexStringToNumber(color))", + " .multiply(getAverageColor(terrain.renderer.uniforms['terrainLayerMap' + index].value))", + " .getHexString()", + " );", + " return [", + " toolButton('Raise', 'Raise the ground (drag on the terrain)'),", + " toolButton('Lower', 'Lower the ground'),", + " toolButton('Smooth', 'Smooth the ground'),", + " toolButton('Flatten', 'Flatten the ground to the height where the drag starts'),", + " { type: 'divider', id: 'PaintDivider' },", + " ...swatchColors.map((swatchColor, index) =>", + " toolButton('Paint' + (index + 1), 'Paint layer ' + (index + 1), '#' + swatchColor)", + " ),", + " toolButton('Hole', 'Make a hole, for the entrance of a tunnel or a cave'),", + " toolButton('FillHoles', 'Fill holes, with the layers of the ground around them'),", + " { type: 'divider', id: 'ColorDivider' },", + " toolButton('Tint', 'Tint the ground (white removes the tint)', this.tintColor),", + " toolButton('Wetness', 'Make the ground wet, or dry with a wetness of 0'),", + " ...this._getColorToolItems(),", + " { type: 'divider', id: 'BrushDivider' },", + " {", + " type: 'slider',", + " id: 'Size',", + " tooltip: 'Brush size',", + " iconUrl: toolIcons.Size,", + " min: 1,", + " max: 100,", + " value: this.sizePercent,", + " onChange: (value) => (this.sizePercent = value),", + " },", + " {", + " type: 'slider',", + " id: 'Strength',", + " tooltip: 'Brush strength',", + " iconUrl: toolIcons.Strength,", + " min: 5,", + " max: 100,", + " value: this.strengthPercent,", + " onChange: (value) => (this.strengthPercent = value),", + " },", + " { type: 'button', id: 'Undo', tooltip: 'Undo the last stroke on this terrain', iconUrl: toolIcons.Undo, onClick: () => this._undo() },", + " ];", + " }", + "}", + "", + "const editorTools = new TerrainEditorTools();", + "if (gdjs.registerInGameEditorPostStepCallback) {", + " gdjs.registerInGameEditorPostStepCallback((editor) => editorTools.update(editor));", + "}", + "", + "gdjs.__terrain3DExtension = {", + " Terrain,", + " // Used by the terrain grass and the scattered models.", + " getWorldScale,", + " getGrounds,", + " patchShaderCode,", + " createRandom,", + " findHighestGround,", + " GroundPlacement,", + " svgIcon,", + " toolIcons,", + " // Exposed for tests.", + " TerrainBodyUpdater,", + " TerrainData,", + " SampleRectangle,", + " brushes,", + " edits,", + " codec,", + " relief,", + "};" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [], + "objectGroups": [] + }, + { + "description": "Define grass helper classes JavaScript code.", + "fullName": "Define grass helper classes", + "functionType": "Action", + "name": "DefineGrassHelperClasses", + "private": true, + "sentence": "Define grass helper classes JavaScript code", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "if (gdjs.__terrainGrassExtension) {", + " return;", + "}", + "// Blades grow on the grounds of the scene: the 3D terrains (see `getGrounds`", + "// in the terrain helper).", + "", + "// Size of the object inner area: instances are scaled from it.", + "const AREA_SIZE = 1000;", + "const AREA_DEPTH = 40;", + "const CHUNK_SIZE = 500;", + "const MAX_BENDING_OBJECTS = 8;", + "", + "/**", + " * A blade: a thin, tapered strip. Its width and height are 1: blades are", + " * scaled by the shader.", + " */", + "const createBladeGeometry = () => {", + " const halfWidths = [0.5, 0.42, 0.28, 0];", + " const heights = [0, 0.35, 0.7, 1];", + " const positions = [];", + " halfWidths.forEach((halfWidth, level) => {", + " positions.push(-halfWidth, 0, heights[level]);", + " if (halfWidth > 0) positions.push(halfWidth, 0, heights[level]);", + " });", + " const indices = [0, 1, 2, 1, 3, 2, 2, 3, 4, 3, 5, 4, 4, 5, 6];", + " const geometry = new THREE.InstancedBufferGeometry();", + " geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));", + " geometry.setAttribute('normal', new THREE.Float32BufferAttribute(new Array(positions.length).fill(0), 3));", + " geometry.setIndex(indices);", + " return geometry;", + "};", + "", + "const vertexShaderDeclarations = `", + "attribute vec4 grassRoot;", + "attribute vec2 grassShape;", + "// The tint of the ground at the root (linear, from 0 to 1).", + "attribute vec3 grassGroundTint;", + "uniform mat3 grassWorldToLocal;", + "uniform float grassWorldScale;", + "uniform float grassBladeWidth;", + "uniform float grassTime;", + "uniform float grassWindStrength;", + "uniform float grassWindSpeed;", + "uniform vec4 grassBenders[${MAX_BENDING_OBJECTS}];", + "uniform vec3 grassBaseColor;", + "uniform vec3 grassTipColor;", + "varying vec3 vGrassColor;", + "`;", + "const vertexShaderPosition = `", + "// Blades are shaped in scene units (with Y flipped like the 3D world), then", + "// moved back in the (scaled) object space.", + "float bladeHeight = position.z;", + "float grassAngle = grassRoot.w;", + "float bladeWidth = position.x * grassBladeWidth;", + "vec3 blade = vec3(", + " bladeWidth * cos(grassAngle),", + " bladeWidth * sin(grassAngle),", + " bladeHeight * grassShape.x", + ");", + "vec3 rootWorld = (modelMatrix * vec4(grassRoot.xyz, 1.0)).xyz * grassWorldScale;", + "// Tips bend more than the base.", + "float bendFactor = bladeHeight * bladeHeight * grassShape.x;", + "float gust = sin(grassTime * grassWindSpeed + rootWorld.x * 0.013 + rootWorld.y * 0.011);", + "float flutter = sin(grassTime * grassWindSpeed * 2.7 + grassShape.y * 40.0) * 0.3;", + "blade.xy += vec2(0.8, 0.5) * (gust + flutter + 0.6) * grassWindStrength * 0.35 * bendFactor;", + "for (int i = 0; i < ${MAX_BENDING_OBJECTS}; i++) {", + " vec4 bender = grassBenders[i];", + " if (bender.z <= 0.0) continue;", + " vec2 away = rootWorld.xy - bender.xy;", + " float distanceToBender = length(away);", + " float push = 1.0 - smoothstep(bender.z * 0.3, bender.z, distanceToBender);", + " if (push > 0.0) {", + " blade.xy += normalize(away + vec2(0.001)) * push * bendFactor * 0.9;", + " blade.z *= 1.0 - push * 0.5;", + " }", + "}", + "vec3 transformed = grassRoot.xyz + grassWorldToLocal * (blade / grassWorldScale);", + "vGrassColor = mix(grassBaseColor, grassTipColor, bladeHeight) * (0.85 + 0.3 * grassShape.y) * grassGroundTint;", + "`;", + "const vertexShaderNormal = `", + "// Blades are lit like the ground below them.", + "transformedNormal = normalize((viewMatrix * vec4(0.0, 0.0, 1.0, 0.0)).xyz);", + "`;", + "const fragmentShaderDeclarations = `", + "varying vec3 vGrassColor;", + "`;", + "const fragmentShaderColor = `", + "diffuseColor.rgb *= vGrassColor;", + "`;", + "// Both faces of blades are lit like the ground (not flipped for back faces).", + "const fragmentShaderNormal = `", + "#include ", + "normal = normalize(vNormal);", + "nonPerturbedNormal = normal;", + "`;", + "", + "const { getWorldScale, patchShaderCode, createRandom, findHighestGround, GroundPlacement } = gdjs.__terrain3DExtension;", + "", + "/**", + " * The grass of a terrain grass object: blades drawn with GPU instancing, in chunks", + " * that are hidden when off-screen and thinned out with the distance. They grow", + " * on the grounds below, like 3D terrains (only where a given layer is painted).", + " */", + "class Grass {", + " /** @param {gdjs.CustomRuntimeObject3D} object */", + " constructor(object) {", + " this.object = object;", + " this.group = new THREE.Group();", + " object.get3DRendererObject().add(this.group);", + " this.bladeGeometry = createBladeGeometry();", + " /** @type {Array<{mesh: THREE.Mesh, blades: number, center: THREE.Vector3}>} */", + " this.chunks = [];", + " this.placement = new GroundPlacement(object.getRuntimeScene());", + " this._groundAtBlade = { ground: null, z: 0 };", + " this.uniforms = {", + " grassWorldToLocal: { value: new THREE.Matrix3() },", + " // Scene units by 3D world unit.", + " grassWorldScale: { value: 1 },", + " grassBladeWidth: { value: 1 },", + " grassTime: { value: 0 },", + " grassWindStrength: { value: 1 },", + " grassWindSpeed: { value: 1 },", + " grassBenders: { value: Array.from({ length: MAX_BENDING_OBJECTS }, () => new THREE.Vector4()) },", + " grassBaseColor: { value: new THREE.Color() },", + " grassTipColor: { value: new THREE.Color() },", + " };", + " this.material = new THREE.MeshStandardMaterial({ roughness: 1, metalness: 0, side: THREE.DoubleSide });", + " this.material.onBeforeCompile = (shader) => {", + " Object.assign(shader.uniforms, this.uniforms);", + " shader.vertexShader = patchShaderCode(shader.vertexShader, [", + " ['#include ', '#include \\n' + vertexShaderDeclarations],", + " ['#include ', vertexShaderPosition],", + " ['#include ', '#include \\n' + vertexShaderNormal],", + " ], 'TerrainGrass');", + " shader.fragmentShader = patchShaderCode(shader.fragmentShader, [", + " ['#include ', '#include \\n' + fragmentShaderDeclarations],", + " ['#include ', '#include \\n' + fragmentShaderColor],", + " ['#include ', fragmentShaderNormal],", + " ], 'TerrainGrass');", + " };", + " this.material.customProgramCacheKey = () => 'TerrainGrass';", + "", + " this._matrix = new THREE.Matrix4();", + " this._cameraPosition = new THREE.Vector3();", + " this._chunkPosition = new THREE.Vector3();", + " this._scale = new THREE.Vector3();", + " this.updateFromProperties();", + " }", + "", + " updateFromProperties() {", + " const { object } = this;", + " this.uniforms.grassBaseColor.value.set(gdjs.rgbOrHexStringToNumber(object._getBaseColor()));", + " this.uniforms.grassTipColor.value.set(gdjs.rgbOrHexStringToNumber(object._getTipColor()));", + " this.uniforms.grassWindStrength.value = object._getWindStrength();", + " this.uniforms.grassWindSpeed.value = object._getWindSpeed();", + " this.uniforms.grassBladeWidth.value = Math.max(object._getBladeWidth(), 0);", + " this.material.needsUpdate = true;", + " this.placement.placeAgain();", + " }", + "", + " _getPlacementKey() {", + " const { object } = this;", + " return [", + " object.getX(), object.getY(), object.getZ(), object.getWidth(), object.getHeight(), object.getDepth(),", + " object._getDensity(), object._getBladeHeight(), object._getGroundLayer(), object._getMaxSlope(),", + " object._getSeed(),", + " ].join('|');", + " }", + "", + " /** Place the blades again, for example after the ground or the object changed. */", + " rebuild() {", + " const grounds = Array.from(this.placement.grounds);", + " for (const chunk of this.chunks) {", + " chunk.mesh.removeFromParent();", + " chunk.mesh.geometry.dispose();", + " }", + " this.chunks.length = 0;", + "", + " const { object } = this;", + " const width = object.getWidth();", + " const height = object.getHeight();", + " const scaleX = width / AREA_SIZE;", + " const scaleY = height / AREA_SIZE;", + " const scaleZ = Math.max(object.getDepth() / AREA_DEPTH, 0.0001);", + " const bladesPerChunk = Math.round((Math.max(object._getDensity(), 0) * CHUNK_SIZE * CHUNK_SIZE) / 10000);", + " const groundLayer = parseInt(object._getGroundLayer(), 10) || 0;", + " const maxSlope = object._getMaxSlope();", + " const chunksX = Math.max(Math.ceil(width / CHUNK_SIZE), 1);", + " const chunksY = Math.max(Math.ceil(height / CHUNK_SIZE), 1);", + " const nextRandom = createRandom(object._getSeed() * 7919 + 1);", + " const bladeHeight = Math.max(object._getBladeHeight(), 0);", + "", + " for (let chunkY = 0; chunkY < chunksY; chunkY++) {", + " for (let chunkX = 0; chunkX < chunksX; chunkX++) {", + " const roots = new Float32Array(bladesPerChunk * 4);", + " const shapes = new Float32Array(bladesPerChunk * 2);", + " const groundTints = new Uint8Array(bladesPerChunk * 3).fill(255);", + " let blades = 0;", + " let minZ = Infinity;", + " let maxZ = -Infinity;", + " for (let attempt = 0; attempt < bladesPerChunk; attempt++) {", + " // In scene units, relative to the object.", + " const x = (chunkX + nextRandom()) * CHUNK_SIZE;", + " const y = (chunkY + nextRandom()) * CHUNK_SIZE;", + " const angle = nextRandom() * Math.PI * 2;", + " const heightFactor = 0.6 + 0.8 * nextRandom();", + " const tint = nextRandom();", + " const growth = nextRandom();", + " if (x > width || y > height) continue;", + " const sceneX = object.getX() + x;", + " const sceneY = object.getY() + y;", + " const { ground, z } = findHighestGround(grounds, sceneX, sceneY, this._groundAtBlade);", + " const groundZ = ground ? z : object.getZ();", + " if (ground) {", + " if (groundLayer > 0 && growth > ground.getLayerWeightAt(sceneX, sceneY, groundLayer)) continue;", + " if (ground.getSlopeAt(sceneX, sceneY) > maxSlope) continue;", + " if (typeof ground.getTintAt === 'function') {", + " const tint = ground.getTintAt(sceneX, sceneY);", + " for (let channel = 0; channel < 3; channel++) {", + " groundTints[blades * 3 + channel] = Math.round(255 * Math.pow(tint[channel] / 255, 2.2));", + " }", + " }", + " }", + " const localZ = (groundZ - object.getZ()) / scaleZ;", + " roots[blades * 4] = x / scaleX;", + " roots[blades * 4 + 1] = y / scaleY;", + " roots[blades * 4 + 2] = localZ;", + " roots[blades * 4 + 3] = angle;", + " shapes[blades * 2] = heightFactor * bladeHeight;", + " shapes[blades * 2 + 1] = tint;", + " minZ = Math.min(minZ, localZ);", + " maxZ = Math.max(maxZ, localZ);", + " blades++;", + " }", + " if (blades === 0) continue;", + "", + " const geometry = new THREE.InstancedBufferGeometry();", + " geometry.index = this.bladeGeometry.index;", + " geometry.setAttribute('position', this.bladeGeometry.attributes.position);", + " geometry.setAttribute('normal', this.bladeGeometry.attributes.normal);", + " geometry.setAttribute('grassRoot', new THREE.InstancedBufferAttribute(roots.subarray(0, blades * 4), 4));", + " geometry.setAttribute('grassShape', new THREE.InstancedBufferAttribute(shapes.subarray(0, blades * 2), 2));", + " geometry.setAttribute('grassGroundTint', new THREE.InstancedBufferAttribute(groundTints.subarray(0, blades * 3), 3, true));", + " geometry.instanceCount = blades;", + " // The furthest tips can go with the wind and the bending objects (see the shader).", + " const maxBladeHeight = bladeHeight * 1.4;", + " const lean =", + " (0.35 * 1.9 * 0.943 * Math.max(object._getWindStrength(), 0) + 0.9) * maxBladeHeight +", + " Math.max(object._getBladeWidth(), 0) / 2;", + " const leanX = lean / scaleX;", + " const leanY = lean / scaleY;", + " geometry.boundingBox = new THREE.Box3(", + " new THREE.Vector3((chunkX * CHUNK_SIZE) / scaleX - leanX, (chunkY * CHUNK_SIZE) / scaleY - leanY, minZ),", + " new THREE.Vector3(", + " Math.min((chunkX + 1) * CHUNK_SIZE, width) / scaleX + leanX,", + " Math.min((chunkY + 1) * CHUNK_SIZE, height) / scaleY + leanY,", + " maxZ + (bladeHeight * 1.4) / scaleZ", + " )", + " );", + " geometry.boundingSphere = geometry.boundingBox.getBoundingSphere(new THREE.Sphere());", + " const mesh = new THREE.Mesh(geometry, this.material);", + " mesh.receiveShadow = true;", + " // Raycasts would only test one blade: the selection plane is used instead.", + " mesh.raycast = () => {};", + " this.group.add(mesh);", + " this.chunks.push({ mesh, blades, center: geometry.boundingSphere.center.clone() });", + " }", + " }", + " }", + "", + " _updateBendingObjects() {", + " const benders = this.uniforms.grassBenders.value;", + " const objectName = this.object._getBendingObject();", + " const bendingObjects = objectName ? this.object.getInstanceContainer().getObjects(objectName) || [] : [];", + " for (let index = 0; index < MAX_BENDING_OBJECTS; index++) {", + " const bendingObject = bendingObjects[index];", + " if (!bendingObject) {", + " benders[index].set(0, 0, 0, 0);", + " continue;", + " }", + " // Blades are pushed in the 3D scene, which is flipped on Y.", + " const radius = Math.max(bendingObject.getWidth(), bendingObject.getHeight()) * 1.2;", + " benders[index].set(bendingObject.getCenterXInScene(), -bendingObject.getCenterYInScene(), radius, 0);", + " }", + " }", + "", + " /**", + " * @param {number} elapsedSeconds", + " * @param {number} time In seconds, the same for all grass objects.", + " */", + " update(elapsedSeconds, time, camera) {", + " if (this.placement.shouldPlaceAgain(elapsedSeconds, this._getPlacementKey())) {", + " this.rebuild();", + " }", + "", + " this.uniforms.grassTime.value = time;", + " this.uniforms.grassWorldScale.value = getWorldScale(this.object);", + " // The object transformation is applied just before rendering, it's needed now.", + " this.object.getRenderer().ensureUpToDate();", + " this.group.updateWorldMatrix(true, false);", + " this.uniforms.grassWorldToLocal.value.setFromMatrix4(this._matrix.copy(this.group.matrixWorld).invert());", + " this._updateBendingObjects();", + " if (!camera) return;", + "", + " // Far chunks are hidden, and thinned out before (in 3D world units).", + " const fadeDistance = Math.max(this.object._getFadeDistance(), 1) / getWorldScale(this.object);", + " this._cameraPosition.setFromMatrixPosition(camera.matrixWorld);", + " for (const chunk of this.chunks) {", + " const distance = this._chunkPosition", + " .copy(chunk.center)", + " .applyMatrix4(this.group.matrixWorld)", + " .distanceTo(this._cameraPosition);", + " const density = gdjs.evtTools.common.clamp((fadeDistance - distance) / (fadeDistance * 0.5), 0, 1);", + " chunk.mesh.visible = density > 0;", + " chunk.mesh.geometry.instanceCount = Math.ceil(chunk.blades * density);", + " }", + " }", + "", + " dispose() {", + " for (const chunk of this.chunks) {", + " chunk.mesh.geometry.dispose();", + " }", + " this.bladeGeometry.dispose();", + " this.material.dispose();", + " this.group.removeFromParent();", + " }", + "}", + "", + "gdjs.__terrainGrassExtension = { Grass };" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [], + "objectGroups": [] + }, + { + "description": "Define scattered models helper classes JavaScript code.", + "fullName": "Define scattered models helper classes", + "functionType": "Action", + "name": "DefineScatterHelperClasses", + "private": true, + "sentence": "Define scattered models helper classes JavaScript code", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "if (gdjs.__terrainScatterExtension) {", + " return;", + "}", + "// Copies grow on the grounds of the scene, like the terrain grass (see", + "// `getGrounds` in the terrain helper).", + "", + "const {", + " getWorldScale,", + " getGrounds,", + " patchShaderCode,", + " createRandom,", + " findHighestGround,", + " GroundPlacement,", + " SampleRectangle,", + " brushes,", + " codec,", + " svgIcon,", + " toolIcons,", + "} = gdjs.__terrain3DExtension;", + "", + "// Size of the object inner area: instances are scaled from it.", + "const AREA_SIZE = 2000;", + "const AREA_DEPTH = 200;", + "// The painted density is stored in a grid of this many cells on each side,", + "// whatever the size of the object.", + "const MASK_RESOLUTION = 256;", + "const MASK_FORMAT_VERSION = '1';", + "// Copies are hidden in chunks: big chunks mean fewer draw calls, small ones", + "// hide more copies out of the screen.", + "const MIN_CHUNK_SIZE = 500;", + "const MAX_CHUNK_SIZE = 2000;", + "// Copies shrink to nothing over this part of the draw distance, instead of popping out.", + "const FADE_PART = 0.15;", + "const MAX_COPIES = 200000;", + "// Prisms of the trunk collisions.", + "const TRUNK_SIDES = 6;", + "", + "const clamp01 = (value) => (value < 0 ? 0 : value > 1 ? 1 : value);", + "", + "/**", + " * @returns {number} A number for a cell of the copies grid, changing with the seed.", + " */", + "const hashCell = (seed, i, j) => {", + " let h = Math.imul(i, 374761393) + Math.imul(j, 668265263) + Math.imul(seed, 982451653);", + " h = Math.imul(h ^ (h >>> 13), 1274126177);", + " return (h ^ (h >>> 16)) >>> 0;", + "};", + "", + "/**", + " * How dense copies are, from 0 to 1, in a grid covering the object: painted", + " * with the editor brushes, the edits or events.", + " */", + "class DensityMask {", + " /** @param {number} fillValue From 0 to 255. */", + " constructor(fillValue) {", + " // `resolution` and `size` let terrain brushes paint it.", + " this.resolution = MASK_RESOLUTION;", + " this.size = MASK_RESOLUTION + 1;", + " this.values = new Uint8Array(this.size * this.size).fill(fillValue);", + " }", + "", + " /**", + " * @param {number} u From 0 to 1, along the object width.", + " * @param {number} v From 0 to 1, along the object height.", + " * @returns {number} The density, from 0 to 1, interpolated between samples.", + " */", + " sample(u, v) {", + " const x = clamp01(u) * this.resolution;", + " const y = clamp01(v) * this.resolution;", + " const i = Math.min(Math.floor(x), this.resolution - 1);", + " const j = Math.min(Math.floor(y), this.resolution - 1);", + " const fx = x - i;", + " const fy = y - j;", + " const value = (i, j) => this.values[j * this.size + i];", + " const top = value(i, j) + fx * (value(i + 1, j) - value(i, j));", + " const bottom = value(i, j + 1) + fx * (value(i + 1, j + 1) - value(i, j + 1));", + " return (top + fy * (bottom - top)) / 255;", + " }", + "", + " /**", + " * Bring the density towards a target around a segment: fully up to 70%", + " * of the radius, then fading out (forests have dense centers and soft edges).", + " * @param {{cellWidth: number, cellHeight: number, ax: number, ay: number, bx: number, by: number, radius: number}} stroke", + " * @param {number} density The target, from 0 to 1.", + " * @param {number} strength From 0 to 1.", + " * @param {SampleRectangle} changedSamples", + " */", + " paint(stroke, density, strength, changedSamples) {", + " const { values } = this;", + " const target = Math.round(clamp01(density) * 255);", + " brushes.forEachSample(", + " this,", + " stroke,", + " (index, falloff) => {", + " // The falloff of terrain brushes is (1 - distance² / radius²)².", + " const weight = clamp01(2 * Math.sqrt(falloff));", + " // Rounded towards the target, so that light strokes still paint.", + " const change = (target - values[index]) * clamp01(weight * strength);", + " values[index] += change > 0 ? Math.ceil(change) : Math.floor(change);", + " },", + " changedSamples", + " );", + " }", + "", + " /** @returns {DensityMask} */", + " clone() {", + " const mask = new DensityMask(0);", + " mask.values.set(this.values);", + " return mask;", + " }", + "}", + "", + "/**", + " * Edits written in the Edits property: a JSON list of brush strokes, like", + " * `[{\"tool\": \"add\", \"x\": 0.5, \"y\": 0.5, \"radius\": 0.2}]`. Positions and", + " * radius are fractions of the object size, like the terrain edits.", + " */", + "const applyEdits = (mask, text) => {", + " if (!text.trim()) return;", + " let editList;", + " try {", + " editList = JSON.parse(text);", + " } catch (error) {", + " console.warn('The edits of scattered models are not valid JSON: ' + error.message);", + " return;", + " }", + " if (!Array.isArray(editList)) {", + " console.warn('The edits of scattered models must be a JSON list (like [{\"tool\": \"add\", ...}]).');", + " return;", + " }", + " const numberOr = (value, defaultValue) => (typeof value === 'number' && isFinite(value) ? value : defaultValue);", + " const changedSamples = new SampleRectangle();", + " for (const edit of editList) {", + " if (!edit || typeof edit !== 'object') continue;", + " const x = numberOr(edit.x, 0.5);", + " const y = numberOr(edit.y, 0.5);", + " const stroke = {", + " cellWidth: 1 / mask.resolution,", + " cellHeight: 1 / mask.resolution,", + " ax: x,", + " ay: y,", + " bx: numberOr(edit.toX, x),", + " by: numberOr(edit.toY, y),", + " radius: numberOr(edit.radius, 0.1),", + " };", + " const strength = clamp01(numberOr(edit.strength, 1));", + " if (edit.tool === 'add') {", + " mask.paint(stroke, numberOr(edit.density, 1), strength, changedSamples);", + " } else if (edit.tool === 'erase') {", + " mask.paint(stroke, 0, strength, changedSamples);", + " } else {", + " console.warn('Unknown tool in the edits of scattered models: \"' + edit.tool + '\" (use add or erase).');", + " }", + " }", + "};", + "", + "/**", + " * The density painted in the editor, stored as its difference with the", + " * density from the properties (the coverage and the edits), like the terrain", + " * sculpt data.", + " */", + "const maskCodec = {", + " /**", + " * @param {DensityMask} mask", + " * @param {DensityMask} baseMask", + " */", + " encode(mask, baseMask) {", + " const changes = new Int32Array(mask.values.length);", + " let hasChanges = false;", + " for (let index = 0; index < changes.length; index++) {", + " changes[index] = mask.values[index] - baseMask.values[index];", + " if (changes[index] !== 0) hasChanges = true;", + " }", + " if (!hasChanges) return '';", + " return MASK_FORMAT_VERSION + ';' + MASK_RESOLUTION + ';' + codec.encodeGrids([changes], mask.size);", + " },", + "", + " /**", + " * @param {string} text", + " * @param {DensityMask} baseMask", + " * @returns {DensityMask | null} null if the text is empty or not valid.", + " */", + " decode(text, baseMask) {", + " if (!text) return null;", + " const parts = text.split(';');", + " if (parts[0] !== MASK_FORMAT_VERSION || parseInt(parts[1], 10) !== MASK_RESOLUTION) return null;", + " try {", + " const mask = baseMask.clone();", + " const [changes] = codec.decodeGrids(parts[2], mask.size, 1);", + " for (let index = 0; index < changes.length; index++) {", + " // The base may have changed since the data was saved.", + " mask.values[index] = Math.min(Math.max(mask.values[index] + changes[index], 0), 255);", + " }", + " return mask;", + " } catch (error) {", + " console.error('The painted data of scattered models could not be read:', error);", + " return null;", + " }", + " },", + "};", + "", + "const fadeShaderDeclarations = `", + "uniform float scatterDrawDistance;", + "`;", + "// Instance matrices are applied after this: copies shrink towards their base.", + "const fadeShaderCode = `", + "#ifdef USE_INSTANCING", + "vec3 scatterCopyPosition = (modelMatrix * instanceMatrix * vec4(0.0, 0.0, 0.0, 1.0)).xyz;", + "transformed *= clamp(", + " (scatterDrawDistance - distance(scatterCopyPosition, cameraPosition)) / (scatterDrawDistance * ${FADE_PART}),", + " 0.0,", + " 1.0", + ");", + "#endif", + "`;", + "", + "const flipTriangles = (geometry) => {", + " const index = geometry.index;", + " for (let offset = 0; offset + 2 < index.count; offset += 3) {", + " const second = index.getX(offset + 1);", + " index.setX(offset + 1, index.getX(offset + 2));", + " index.setX(offset + 2, second);", + " }", + "};", + "", + "/**", + " * The parts of a model, ready to be drawn with instancing: each glTF", + " * primitive with its material. Geometries are moved so that the model base", + " * is at the origin, its height is 1 and it stands along Z (like 3D model", + " * objects with a 90° rotation around X).", + " * @param {THREE.Object3D} modelScene", + " * @param {object} uniforms Uniforms added to the materials.", + " * @returns {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}}", + " */", + "const createModelParts = (modelScene, uniforms) => {", + " modelScene.updateMatrixWorld(true);", + " const rootInverse = modelScene.matrixWorld.clone().invert();", + " const standUp = new THREE.Matrix4().makeRotationX(Math.PI / 2);", + " /** @type {Array<{mesh: THREE.Mesh, matrix: THREE.Matrix4}>} */", + " const meshes = [];", + " const box = new THREE.Box3();", + " modelScene.traverse((node) => {", + " if (!node.isMesh || !node.geometry || !node.geometry.attributes.position) return;", + " const matrix = standUp.clone().multiply(rootInverse).multiply(node.matrixWorld);", + " if (!node.geometry.boundingBox) node.geometry.computeBoundingBox();", + " box.union(node.geometry.boundingBox.clone().applyMatrix4(matrix));", + " meshes.push({ mesh: node, matrix });", + " });", + " if (box.isEmpty()) return { parts: [], width: 0, depth: 0 };", + "", + " const height = Math.max(box.max.z - box.min.z, 0.000001);", + " // The Y axis of the scene goes down: the model is flipped, like 3D model objects.", + " const normalization = new THREE.Matrix4()", + " .makeScale(1 / height, -1 / height, 1 / height)", + " .multiply(", + " new THREE.Matrix4().makeTranslation(", + " -(box.min.x + box.max.x) / 2,", + " -(box.min.y + box.max.y) / 2,", + " -box.min.z", + " )", + " );", + " const parts = meshes.map(({ mesh, matrix }) => {", + " let geometry = mesh.geometry.clone();", + " if (!geometry.index) {", + " const indices = Array.from({ length: geometry.attributes.position.count }, (_, index) => index);", + " geometry.setIndex(indices);", + " }", + " geometry.applyMatrix4(normalization.clone().multiply(matrix));", + " // The flip turned the faces inside out.", + " flipTriangles(geometry);", + " geometry.computeBoundingSphere();", + " geometry.computeBoundingBox();", + " const patchMaterial = (originalMaterial) => {", + " const material = originalMaterial.clone();", + " // Like 3D model objects (\"Standard without metalness\"), which look better without an environment map.", + " if (material.metalness) material.metalness = 0;", + " material.onBeforeCompile = (shader) => {", + " Object.assign(shader.uniforms, uniforms);", + " shader.vertexShader = patchShaderCode(shader.vertexShader, [", + " ['#include ', '#include \\n' + fadeShaderDeclarations],", + " ['#include ', '#include \\n' + fadeShaderCode],", + " ], 'ScatteredModels');", + " };", + " material.customProgramCacheKey = () => 'ScatteredModels';", + " return material;", + " };", + " const material = Array.isArray(mesh.material) ? mesh.material.map(patchMaterial) : patchMaterial(mesh.material);", + " return { geometry, material };", + " });", + " return {", + " parts,", + " width: (box.max.x - box.min.x) / height,", + " depth: (box.max.y - box.min.y) / height,", + " };", + "};", + "", + "/**", + " * A copy of the model: its base position in the scene, its scale (the", + " * height of the copy) and the key of its cell in the grid of copies.", + " * @typedef {{x: number, y: number, z: number, angle: number, height: number, key: number}} Copy", + " */", + "", + "/**", + " * The scattered models of an object: copies of a 3D model placed on the", + " * grounds of the scene, drawn with GPU instancing in chunks that are hidden", + " * when far or off-screen.", + " */", + "class Scatter {", + " /** @param {gdjs.CustomRuntimeObject3D} object */", + " constructor(object) {", + " this.object = object;", + " this.group = new THREE.Group();", + " object.get3DRendererObject().add(this.group);", + " /** @type {Array<{firstCellI: number, firstCellJ: number, meshes: THREE.InstancedMesh[], copies: Copy[], center: THREE.Vector3, radius: number}>} */", + " this.chunks = [];", + " /**", + " * The grid of copies: one copy at most in each cell, cells grouped in chunks.", + " * @type {{spacing: number, cellsPerChunk: number, cellsX: number, cellsY: number} | null}", + " */", + " this.grid = null;", + " this.placement = new GroundPlacement(object.getRuntimeScene());", + " this.uniforms = {", + " // In 3D world units.", + " scatterDrawDistance: { value: 1 },", + " };", + " /** @type {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}} */", + " this.model = { parts: [], width: 0, depth: 0 };", + " this.modelResourceName = '';", + " this.loadingModelResourceName = '';", + " /**", + " * Copies removed by events (for example, cut trees), by key.", + " * @type {Map}", + " */", + " this.removedCopies = new Map();", + " /** Increased each time copies change, for the collisions. */", + " this.version = 0;", + " this.isDisposed = false;", + " /** The last value of the painted data property read or saved. */", + " this.paintData = '';", + " /**", + " * Values saved by the editor tools: the editor sends them back with a", + " * hot-reload, possibly after newer ones were saved.", + " * @type {string[]}", + " */", + " this.savedPaintData = [];", + " this.maskSource = '';", + " /** @type {DensityMask} */", + " this.baseMask = new DensityMask(255);", + " /** @type {DensityMask} */", + " this.mask = this.baseMask;", + " this.changedSamples = new SampleRectangle();", + " this._stroke = { cellWidth: 1, cellHeight: 1, ax: 0, ay: 0, bx: 0, by: 0, radius: 0 };", + " this._groundAtCopy = { ground: null, z: 0 };", + " this._matrix = new THREE.Matrix4();", + " this._raycastSphere = new THREE.Sphere();", + " this._raycastRay = new THREE.Ray();", + " this._raycastBox = new THREE.Box3();", + " this._raycastInverseMatrix = new THREE.Matrix4();", + " this._raycastScale = new THREE.Vector3();", + " /** Placed on a copy to test the triangles of the model. */", + " this._raycastMesh = new THREE.Mesh();", + " /** @type {THREE.Intersection[]} */", + " this._raycastHits = [];", + " this._color = new THREE.Color();", + " this._tint = new THREE.Color();", + " this._cameraPosition = new THREE.Vector3();", + " this._chunkPosition = new THREE.Vector3();", + " /**", + " * Trunks, for the 3D physics behavior and navigation meshes. Always given,", + " * empty without trunk collisions: otherwise these behaviors would use the", + " * object box, covering the whole area.", + " * @type {gdjs.Surface}", + " */", + " this.surface = {", + " getVersion: () => this.version,", + " getChangedArea: () => null,", + " getTriangles: () => this._getTrunkTriangles(),", + " getHeightField: () => null,", + " };", + " /** The trunk collision properties, changing the surface. */", + " this.trunkKey = '';", + " this.loadMaskFromProperties();", + " this.updateFromProperties();", + " // Before the behaviors create their shapes.", + " this._updateSurface();", + " }", + "", + " _getMaskSource() {", + " return this.object._getCoverage() + '|' + this.object._getEdits();", + " }", + "", + " loadMaskFromProperties() {", + " const { object } = this;", + " this.maskSource = this._getMaskSource();", + " this.paintData = object._getPaintData();", + " this.baseMask = new DensityMask(object._getCoverage() === 'Painted' ? 0 : 255);", + " applyEdits(this.baseMask, object._getEdits());", + " this.mask = maskCodec.decode(this.paintData, this.baseMask) || this.baseMask.clone();", + " this.placement.placeAgain();", + " }", + "", + " onHotReloading() {", + " const paintData = this.object._getPaintData();", + " const savedIndex = this.savedPaintData.indexOf(paintData);", + " if (savedIndex !== -1 && this._getMaskSource() === this.maskSource) {", + " // The object already has this data, or newer changes.", + " this.savedPaintData.splice(0, savedIndex + 1);", + " } else if (paintData !== this.paintData || this._getMaskSource() !== this.maskSource) {", + " this.savedPaintData.length = 0;", + " this.loadMaskFromProperties();", + " }", + " this.updateFromProperties();", + " }", + "", + " /** @returns {string} The data to save in the painted data property. */", + " save() {", + " this.paintData = maskCodec.encode(this.mask, this.baseMask);", + " this.savedPaintData.push(this.paintData);", + " return this.paintData;", + " }", + "", + " updateFromProperties() {", + " const { object } = this;", + " const modelResourceName = object._getModel();", + " if (modelResourceName !== this.modelResourceName) {", + " this.modelResourceName = modelResourceName;", + " this._loadModel();", + " }", + " this.placement.placeAgain();", + " }", + "", + " _disposeModel() {", + " for (const part of this.model.parts) {", + " part.geometry.dispose();", + " for (const material of Array.isArray(part.material) ? part.material : [part.material]) {", + " material.dispose();", + " }", + " }", + " this.model = { parts: [], width: 0, depth: 0 };", + " }", + "", + " _loadModel() {", + " this._disposeChunks(this.chunks);", + " this._disposeModel();", + " const resourceName = this.modelResourceName;", + " if (!resourceName) return;", + " const game = this.object.getRuntimeScene().getGame();", + " const modelManager = game.getModel3DManager();", + " const gltf = modelManager.getModel(resourceName);", + " if (gltf === modelManager._invalidModel) {", + " // Not loaded yet, for example a model just chosen in the editor.", + " if (this.loadingModelResourceName === resourceName) return;", + " this.loadingModelResourceName = resourceName;", + " game.getResourceLoader()", + " .loadResource(resourceName)", + " .then(() => {", + " if (this.isDisposed || this.modelResourceName !== resourceName) return;", + " this.loadingModelResourceName = '';", + " if (modelManager.getModel(resourceName) !== modelManager._invalidModel) {", + " this._loadModel();", + " this.placement.placeAgain();", + " }", + " })", + " .catch(() => {});", + " return;", + " }", + " this.model = createModelParts(gltf.scene, this.uniforms);", + " }", + "", + " _getPlacementKey() {", + " const { object } = this;", + " return [", + " object.getX(), object.getY(), object.getZ(), object.getWidth(), object.getHeight(), object.getDepth(),", + " object._getDensity(), object._getSize(), object._getMinScale(), object._getMaxScale(),", + " object._getRandomRotation(), object._getRandomTint(), object._getGroundLayer(), object._getMaxSlope(),", + " object._getMinGroundZ(), object._getMaxGroundZ(), object._getSeed(), object._getDrawDistance(),", + " object._getCastShadow(), object._getReceiveShadow(), this.model.parts.length,", + " ].join('|');", + " }", + "", + " _getChunkSize() {", + " return Math.min(Math.max(this.object._getDrawDistance() / 4, MIN_CHUNK_SIZE), MAX_CHUNK_SIZE);", + " }", + "", + " /** @returns {number} The distance between copies (one copy per cell of this size at most). */", + " _getSpacing() {", + " const { object } = this;", + " const density = Math.max(object._getDensity(), 0);", + " if (density <= 0) return 0;", + " const spacing = 100 / Math.sqrt(density);", + " // Too many cells would freeze the game: the density is lowered.", + " const cellCount = (object.getWidth() / spacing) * (object.getHeight() / spacing);", + " return cellCount > MAX_COPIES ? spacing * Math.sqrt(cellCount / MAX_COPIES) : spacing;", + " }", + "", + " /**", + " * Find the copy of a cell of the grid, if there is one.", + " * @returns {Copy | null}", + " */", + " _placeCopy(cellI, cellJ, spacing, grounds) {", + " const { object } = this;", + " const width = object.getWidth();", + " const height = object.getHeight();", + " const nextRandom = createRandom(hashCell(Math.floor(object._getSeed()), cellI, cellJ));", + " // Always drawn in the same order, so that a copy stays the same when properties change.", + " const x = (cellI + nextRandom()) * spacing;", + " const y = (cellJ + nextRandom()) * spacing;", + " const presence = nextRandom();", + " const growth = nextRandom();", + " const scaleFactor = nextRandom();", + " const angle = nextRandom() * Math.PI * 2;", + " if (x > width || y > height) return null;", + " if (presence >= this.mask.sample(x / width, y / height)) return null;", + " const key = cellJ * 65536 + cellI;", + " if (this.removedCopies.has(key)) return null;", + "", + " const sceneX = object.getX() + x;", + " const sceneY = object.getY() + y;", + " const { ground, z } = findHighestGround(grounds, sceneX, sceneY, this._groundAtCopy);", + " const groundZ = ground ? z : object.getZ();", + " if (groundZ < object._getMinGroundZ() || groundZ > object._getMaxGroundZ()) return null;", + " const copyHeight =", + " Math.max(object._getSize(), 0) *", + " (object._getMinScale() + (object._getMaxScale() - object._getMinScale()) * scaleFactor);", + " if (ground) {", + " if (ground.getSlopeAt(sceneX, sceneY) > object._getMaxSlope()) return null;", + " const groundLayer = parseInt(object._getGroundLayer(), 10) || 0;", + " if (groundLayer > 0) {", + " if (growth > ground.getLayerWeightAt(sceneX, sceneY, groundLayer)) return null;", + " // Copies don't stand at the border of their layer, so that trees don't cover roads.", + " const margin = (copyHeight * Math.max(this.model.width, this.model.depth)) / 4;", + " for (const [offsetX, offsetY] of [[margin, 0], [-margin, 0], [0, margin], [0, -margin]]) {", + " if (ground.getLayerWeightAt(sceneX + offsetX, sceneY + offsetY, groundLayer) < 0.5) return null;", + " }", + " }", + " }", + " return {", + " x: sceneX,", + " y: sceneY,", + " z: groundZ,", + " angle: object._getRandomRotation() ? angle : 0,", + " height: copyHeight,", + " key,", + " };", + " }", + "", + " _disposeChunks(chunks) {", + " for (const chunk of chunks) {", + " for (const mesh of chunk.meshes) {", + " mesh.removeFromParent();", + " // Geometries and materials are the ones of the model.", + " mesh.dispose();", + " }", + " }", + " if (chunks === this.chunks) this.chunks = [];", + " }", + "", + " /** Place the copies again, for example after the ground or the object changed. */", + " rebuild() {", + " this._disposeChunks(this.chunks);", + " this.version++;", + " const { object } = this;", + " const spacing = this._getSpacing();", + " if (spacing <= 0 || this.model.parts.length === 0) {", + " this.grid = null;", + " return;", + " }", + " const cellsPerChunk = Math.max(Math.round(this._getChunkSize() / spacing), 1);", + " const grid = {", + " spacing,", + " cellsPerChunk,", + " cellsX: Math.ceil(object.getWidth() / spacing),", + " cellsY: Math.ceil(object.getHeight() / spacing),", + " };", + " this.grid = grid;", + " for (let firstCellJ = 0; firstCellJ < grid.cellsY; firstCellJ += cellsPerChunk) {", + " for (let firstCellI = 0; firstCellI < grid.cellsX; firstCellI += cellsPerChunk) {", + " this._createChunk(firstCellI, firstCellJ);", + " }", + " }", + " }", + "", + " /**", + " * Place again the copies of the chunks touching a rectangle of the scene,", + " * for example after painting.", + " */", + " _rebuildArea(minX, minY, maxX, maxY) {", + " const { grid, object } = this;", + " if (!grid) return;", + " const chunkSize = grid.spacing * grid.cellsPerChunk;", + " const firstChunkI = Math.max(Math.floor((minX - object.getX()) / chunkSize), 0);", + " const firstChunkJ = Math.max(Math.floor((minY - object.getY()) / chunkSize), 0);", + " const lastChunkI = Math.min(Math.floor((maxX - object.getX()) / chunkSize), Math.ceil(grid.cellsX / grid.cellsPerChunk) - 1);", + " const lastChunkJ = Math.min(Math.floor((maxY - object.getY()) / chunkSize), Math.ceil(grid.cellsY / grid.cellsPerChunk) - 1);", + " if (firstChunkI > lastChunkI || firstChunkJ > lastChunkJ) return;", + " const isInArea = (chunk) => {", + " const chunkI = chunk.firstCellI / grid.cellsPerChunk;", + " const chunkJ = chunk.firstCellJ / grid.cellsPerChunk;", + " return chunkI >= firstChunkI && chunkI <= lastChunkI && chunkJ >= firstChunkJ && chunkJ <= lastChunkJ;", + " };", + " this._disposeChunks(this.chunks.filter(isInArea));", + " this.chunks = this.chunks.filter((chunk) => !isInArea(chunk));", + " for (let chunkJ = firstChunkJ; chunkJ <= lastChunkJ; chunkJ++) {", + " for (let chunkI = firstChunkI; chunkI <= lastChunkI; chunkI++) {", + " this._createChunk(chunkI * grid.cellsPerChunk, chunkJ * grid.cellsPerChunk);", + " }", + " }", + " this.version++;", + " }", + "", + " _createChunk(firstCellI, firstCellJ) {", + " const { grid } = this;", + " const grounds = Array.from(this.placement.grounds);", + " /** @type {Copy[]} */", + " const copies = [];", + " for (let cellJ = firstCellJ; cellJ < Math.min(firstCellJ + grid.cellsPerChunk, grid.cellsY); cellJ++) {", + " for (let cellI = firstCellI; cellI < Math.min(firstCellI + grid.cellsPerChunk, grid.cellsX); cellI++) {", + " const copy = this._placeCopy(cellI, cellJ, grid.spacing, grounds);", + " if (copy) copies.push(copy);", + " }", + " }", + " if (copies.length === 0) return;", + "", + " const { object } = this;", + " // Copies are placed in scene units, and the object group is scaled", + " // from its inner area: they are scaled back to keep their proportions.", + " const scaleX = Math.max(object.getWidth() / AREA_SIZE, 0.0001);", + " const scaleY = Math.max(object.getHeight() / AREA_SIZE, 0.0001);", + " const scaleZ = Math.max(object.getDepth() / AREA_DEPTH, 0.0001);", + " const matrices = new Float32Array(copies.length * 16);", + " const tintColor = gdjs.rgbOrHexStringToNumber(object._getRandomTint());", + " const hasTint = tintColor !== 0xffffff;", + " const colors = hasTint ? new Float32Array(copies.length * 3) : null;", + " this._tint.setHex(tintColor);", + " const min = new THREE.Vector3(Infinity, Infinity, Infinity);", + " const max = new THREE.Vector3(-Infinity, -Infinity, -Infinity);", + " copies.forEach((copy, index) => {", + " const cos = Math.cos(copy.angle) * copy.height;", + " const sin = Math.sin(copy.angle) * copy.height;", + " const localX = (copy.x - object.getX()) / scaleX;", + " const localY = (copy.y - object.getY()) / scaleY;", + " const localZ = (copy.z - object.getZ()) / scaleZ;", + " this._matrix.set(", + " cos / scaleX, -sin / scaleX, 0, localX,", + " sin / scaleY, cos / scaleY, 0, localY,", + " 0, 0, copy.height / scaleZ, localZ,", + " 0, 0, 0, 1", + " );", + " this._matrix.toArray(matrices, index * 16);", + " if (colors) {", + " // The tint of each copy is between white and the tint color.", + " this._color.setRGB(1, 1, 1).lerp(this._tint, createRandom(copy.key + 1)());", + " this._color.toArray(colors, index * 3);", + " }", + " const reach = (copy.height * Math.max(this.model.width, this.model.depth)) / 2;", + " min.min(this._chunkPosition.set(copy.x - reach, copy.y - reach, copy.z));", + " max.max(this._chunkPosition.set(copy.x + reach, copy.y + reach, copy.z + copy.height));", + " });", + " const instanceMatrix = new THREE.InstancedBufferAttribute(matrices, 16);", + " const instanceColor = colors ? new THREE.InstancedBufferAttribute(colors, 3) : null;", + " const castShadow = object._getCastShadow();", + " const receiveShadow = object._getReceiveShadow();", + " const meshes = this.model.parts.map((part) => {", + " const mesh = new THREE.InstancedMesh(part.geometry, part.material, 0);", + " // All the parts of the model share the positions of the copies.", + " mesh.instanceMatrix = instanceMatrix;", + " mesh.instanceColor = instanceColor;", + " mesh.count = copies.length;", + " mesh.computeBoundingSphere();", + " mesh.castShadow = castShadow;", + " mesh.receiveShadow = receiveShadow;", + " // The first mesh tests all the parts of the copies.", + " mesh.raycast = () => {};", + " this.group.add(mesh);", + " return mesh;", + " });", + " const center = new THREE.Vector3(", + " ((min.x + max.x) / 2 - object.getX()) / scaleX,", + " ((min.y + max.y) / 2 - object.getY()) / scaleY,", + " ((min.z + max.z) / 2 - object.getZ()) / scaleZ", + " );", + " const chunk = { firstCellI, firstCellJ, meshes, copies, center, radius: min.distanceTo(max) / 2 };", + " // Used by the scene editor to select the object when a copy is clicked.", + " meshes[0].raycast = (raycaster, intersects) => this._raycastChunk(chunk, raycaster, intersects);", + " this.chunks.push(chunk);", + " }", + "", + " /**", + " * Add the hits of a ray on the copies of a chunk, like they are drawn. There", + " * can be thousands of copies: only the ones whose box is crossed by the ray", + " * are tested with the triangles of the model.", + " * @param {{meshes: THREE.InstancedMesh[], copies: Copy[], center: THREE.Vector3, radius: number}} chunk", + " * @param {THREE.Raycaster} raycaster", + " * @param {THREE.Intersection[]} intersects", + " */", + " _raycastChunk(chunk, raycaster, intersects) {", + " const { object, group } = this;", + " this._raycastSphere.center.copy(chunk.center).applyMatrix4(group.matrixWorld);", + " this._raycastSphere.radius = chunk.radius / getWorldScale(object);", + " if (!raycaster.ray.intersectsSphere(this._raycastSphere)) return;", + "", + " // The ray in the scene coordinates, like the copies.", + " const scale = this._raycastScale.set(", + " Math.max(object.getWidth() / AREA_SIZE, 0.0001),", + " Math.max(object.getHeight() / AREA_SIZE, 0.0001),", + " Math.max(object.getDepth() / AREA_DEPTH, 0.0001)", + " );", + " const ray = this._raycastRay.copy(raycaster.ray).applyMatrix4(this._raycastInverseMatrix.copy(group.matrixWorld).invert());", + " ray.origin.multiply(scale).add(this._chunkPosition.set(object.getX(), object.getY(), object.getZ()));", + " ray.direction.multiply(scale);", + "", + " const reachPerHeight = Math.max(this.model.width, this.model.depth) / 2;", + " const instanceMatrices = chunk.meshes[0].instanceMatrix.array;", + " chunk.copies.forEach((copy, index) => {", + " const reach = copy.height * reachPerHeight;", + " this._raycastBox.min.set(copy.x - reach, copy.y - reach, copy.z);", + " this._raycastBox.max.set(copy.x + reach, copy.y + reach, copy.z + copy.height);", + " if (!ray.intersectsBox(this._raycastBox)) return;", + " this._raycastMesh.matrixWorld.fromArray(instanceMatrices, index * 16).premultiply(group.matrixWorld);", + " for (const mesh of chunk.meshes) {", + " this._raycastMesh.geometry = mesh.geometry;", + " this._raycastMesh.material = mesh.material;", + " this._raycastHits.length = 0;", + " this._raycastMesh.raycast(raycaster, this._raycastHits);", + " for (const hit of this._raycastHits) {", + " hit.object = mesh;", + " hit.instanceId = index;", + " intersects.push(hit);", + " }", + " }", + " });", + " }", + "", + " /** @param {THREE.Camera | null} camera */", + " _hideFarChunks(camera) {", + " if (!camera) return;", + " const worldScale = getWorldScale(this.object);", + " const drawDistance = Math.max(this.object._getDrawDistance(), 1) / worldScale;", + " this.uniforms.scatterDrawDistance.value = drawDistance;", + " this._cameraPosition.setFromMatrixPosition(camera.matrixWorld);", + " for (const chunk of this.chunks) {", + " const distance = this._chunkPosition", + " .copy(chunk.center)", + " .applyMatrix4(this.group.matrixWorld)", + " .distanceTo(this._cameraPosition);", + " const isVisible = distance - chunk.radius / worldScale < drawDistance;", + " for (const mesh of chunk.meshes) mesh.visible = isVisible;", + " }", + " }", + "", + " /**", + " * @param {number} elapsedSeconds", + " * @param {THREE.Camera | null} camera", + " */", + " update(elapsedSeconds, camera) {", + " if (this.placement.shouldPlaceAgain(elapsedSeconds, this._getPlacementKey())) {", + " this.rebuild();", + " }", + " if (!this.changedSamples.isEmpty()) {", + " const { object } = this;", + " const sampleWidth = object.getWidth() / MASK_RESOLUTION;", + " const sampleHeight = object.getHeight() / MASK_RESOLUTION;", + " const { minI, minJ, maxI, maxJ } = this.changedSamples;", + " // Copies between samples use the density of the samples around them.", + " this._rebuildArea(", + " object.getX() + (minI - 1) * sampleWidth,", + " object.getY() + (minJ - 1) * sampleHeight,", + " object.getX() + (maxI + 1) * sampleWidth,", + " object.getY() + (maxJ + 1) * sampleHeight", + " );", + " this.changedSamples.clear();", + " }", + " // The object transformation is applied just before rendering, it's needed now.", + " this.object.getRenderer().ensureUpToDate();", + " this.group.updateWorldMatrix(true, false);", + " this._hideFarChunks(camera);", + " this._updateSurface();", + " }", + "", + " _updateSurface() {", + " const { object } = this;", + " if (typeof object.setSurface !== 'function') return;", + " const trunkKey = object._getTrunkCollisions() + '|' + object._getTrunkDiameter();", + " if (trunkKey !== this.trunkKey) {", + " this.trunkKey = trunkKey;", + " this.version++;", + " }", + " if (object.getSurface() !== this.surface) object.setSurface(this.surface);", + " }", + "", + " /** @returns {Copy[]} */", + " getCopies() {", + " const copies = [];", + " for (const chunk of this.chunks) copies.push(...chunk.copies);", + " return copies;", + " }", + "", + " /**", + " * @returns {Copy | null} The copy whose base is the nearest to a position (on X and Y), if any.", + " */", + " findNearestCopy(x, y) {", + " let nearestCopy = null;", + " let nearestDistanceSquared = Infinity;", + " for (const chunk of this.chunks) {", + " for (const copy of chunk.copies) {", + " const distanceSquared = (copy.x - x) * (copy.x - x) + (copy.y - y) * (copy.y - y);", + " if (distanceSquared < nearestDistanceSquared) {", + " nearestDistanceSquared = distanceSquared;", + " nearestCopy = copy;", + " }", + " }", + " }", + " return nearestCopy;", + " }", + "", + " getCopyCount() {", + " let count = 0;", + " for (const chunk of this.chunks) count += chunk.copies.length;", + " return count;", + " }", + "", + " /** Remove the copies whose base is in a circle (they come back when the scene is restarted). */", + " removeCopies(x, y, radius) {", + " let hasRemovedCopies = false;", + " for (const chunk of this.chunks) {", + " for (const copy of chunk.copies) {", + " if ((copy.x - x) * (copy.x - x) + (copy.y - y) * (copy.y - y) <= radius * radius) {", + " this.removedCopies.set(copy.key, copy);", + " hasRemovedCopies = true;", + " }", + " }", + " }", + " if (hasRemovedCopies) this._rebuildArea(x - radius, y - radius, x + radius, y + radius);", + " }", + "", + " /** Paint the full density around a position, and bring back the copies removed there. */", + " addCopies(x, y, radius) {", + " for (const [key, copy] of this.removedCopies) {", + " if ((copy.x - x) * (copy.x - x) + (copy.y - y) * (copy.y - y) <= radius * radius) {", + " this.removedCopies.delete(key);", + " }", + " }", + " this.paint(x, y, x, y, radius, 1, 1);", + " }", + "", + " _getStroke(x1, y1, x2, y2, radius) {", + " const { object } = this;", + " const stroke = this._stroke;", + " stroke.cellWidth = object.getWidth() / MASK_RESOLUTION;", + " stroke.cellHeight = object.getHeight() / MASK_RESOLUTION;", + " stroke.ax = x1 - object.getX();", + " stroke.ay = y1 - object.getY();", + " stroke.bx = x2 - object.getX();", + " stroke.by = y2 - object.getY();", + " stroke.radius = Math.max(radius, 0);", + " return stroke;", + " }", + "", + " /**", + " * Paint the density around a segment (copies are placed again at the next frame).", + " * @param {number} density From 0 (erase) to 1 (as dense as the Density property).", + " * @param {number} strength From 0 to 1.", + " */", + " paint(x1, y1, x2, y2, radius, density, strength) {", + " const changedSamples = new SampleRectangle();", + " this.mask.paint(this._getStroke(x1, y1, x2, y2, radius), density, strength, changedSamples);", + " this.changedSamples.add(changedSamples);", + " }", + "", + " /** @param {DensityMask} mask */", + " setMask(mask) {", + " this.mask = mask;", + " this.rebuild();", + " }", + "", + " /**", + " * Prisms around the trunks, from 0 to 1 in the object box (see `gdjs.Surface`).", + " * @returns {gdjs.SurfaceTriangles}", + " */", + " _getTrunkTriangles() {", + " const { object } = this;", + " const copies = object._getTrunkCollisions() ? this.getCopies() : [];", + " const verticesPerCopy = TRUNK_SIDES * 2 + 1;", + " const positions = new Float32Array(copies.length * verticesPerCopy * 3);", + " const indices = new Uint32Array(copies.length * TRUNK_SIDES * 3 * 3);", + " const width = object.getWidth() || 1;", + " const height = object.getHeight() || 1;", + " const depth = object.getDepth() || 1;", + " let indexOffset = 0;", + " copies.forEach((copy, copyIndex) => {", + " const radius = (Math.max(object._getTrunkDiameter(), 0) / 2) * (copy.height / Math.max(object._getSize(), 1));", + " const top = copy.z + copy.height * 0.8;", + " const firstVertex = copyIndex * verticesPerCopy;", + " const setVertex = (vertex, x, y, z) => {", + " positions[vertex * 3] = (x - object.getX()) / width;", + " positions[vertex * 3 + 1] = (y - object.getY()) / height;", + " positions[vertex * 3 + 2] = (z - object.getZ()) / depth;", + " };", + " for (let side = 0; side < TRUNK_SIDES; side++) {", + " const angle = ((side + 0.5) / TRUNK_SIDES) * Math.PI * 2;", + " const x = copy.x + Math.cos(angle) * radius;", + " const y = copy.y + Math.sin(angle) * radius;", + " // Starting a bit under the ground, which can be sloped.", + " setVertex(firstVertex + side * 2, x, y, copy.z - radius);", + " setVertex(firstVertex + side * 2 + 1, x, y, top);", + " }", + " const center = firstVertex + TRUNK_SIDES * 2;", + " setVertex(center, copy.x, copy.y, top);", + " for (let side = 0; side < TRUNK_SIDES; side++) {", + " const bottom = firstVertex + side * 2;", + " const nextBottom = firstVertex + ((side + 1) % TRUNK_SIDES) * 2;", + " // Triangles face out of the prism (vertices at odd indices are at the top).", + " indices.set([bottom, nextBottom, bottom + 1, nextBottom, nextBottom + 1, bottom + 1], indexOffset);", + " indices.set([center, bottom + 1, nextBottom + 1], indexOffset + 6);", + " indexOffset += 9;", + " }", + " });", + " if (copies.length === 0) {", + " // Without triangles, the physics engine and navigation meshes would use", + " // the object box: a tiny triangle under the object is used instead.", + " return { positions: new Float32Array([0, 0, -1, 0.0001, 0, -1, 0, 0.0001, -1]), indices: new Uint32Array([0, 2, 1]) };", + " }", + " return { positions, indices };", + " }", + "", + " dispose() {", + " this.isDisposed = true;", + " if (typeof this.object.setSurface === 'function' && this.object.getSurface() === this.surface) {", + " this.object.setSurface(null);", + " }", + " this._disposeChunks(this.chunks);", + " this._disposeModel();", + " this.group.removeFromParent();", + " }", + "}", + "", + "const scatterToolIcons = {", + " Add: svgIcon(", + " ''", + " ),", + " Erase: svgIcon(", + " ''", + " ),", + "};", + "", + "/**", + " * The brushes shown in the scene editor when scattered models are selected,", + " * to add or erase copies. Without the needed editor functions (older GDevelop", + " * versions), nothing is shown: copies can still be painted with events.", + " */", + "class ScatterEditorTools {", + " constructor() {", + " /** '' when no brush is used. */", + " this.toolName = '';", + " this.sizePercent = 30;", + " this.strengthPercent = 50;", + " /** @type {Scatter | null} */", + " this.scatter = null;", + " /** @type {gdjs.InGameEditor | null} */", + " this.editor = null;", + " this.isStroking = false;", + " this.lastStrokeTime = 0;", + " /** @type {Array<{mask: DensityMask, baseMask: DensityMask}>} */", + " this.undoSteps = [];", + " this.raycaster = new THREE.Raycaster();", + " this.ndc = new THREE.Vector2();", + " this._groundAtCursor = { ground: null, z: 0 };", + " /** A circle following the ground under the cursor. */", + " this.brushRing = new THREE.LineLoop(", + " new THREE.BufferGeometry().setAttribute('position', new THREE.BufferAttribute(new Float32Array(64 * 3), 3)),", + " new THREE.LineBasicMaterial({ color: 0xffffff, depthTest: false, transparent: true })", + " );", + " this.brushRing.renderOrder = 1000;", + " this.brushRing.frustumCulled = false;", + " this.brushRing.raycast = () => {};", + " }", + "", + " /** @param {gdjs.InGameEditor} editor */", + " update(editor) {", + " if (typeof editor.showToolbar !== 'function') return;", + " this.editor = editor;", + " const selectedObjects = editor.getSelectedObjects();", + " const scatter =", + " selectedObjects.length === 1 && selectedObjects[0].__terrainScatter ? selectedObjects[0].__terrainScatter : null;", + " if (scatter !== this.scatter) {", + " this._endStroke();", + " this.scatter = scatter;", + " this.undoSteps.length = 0;", + " }", + " if (scatter) editor.showToolbar('ScatteredModels', this._getToolbarItems());", + " const game = editor.getRuntimeGame();", + " if (!scatter || !this.toolName) {", + " this.brushRing.removeFromParent();", + " return;", + " }", + " const inputManager = game.getInputManager();", + " if (inputManager.wasKeyJustPressed(gdjs.evtTools.input.keysNameToCode.Escape)) {", + " this._endStroke();", + " this.toolName = '';", + " this.brushRing.removeFromParent();", + " return;", + " }", + " editor.captureLeftMouseButton();", + " const hit = this._getPositionUnderCursor(scatter, game);", + " const radius = this._getRadius();", + " this._showBrushRing(scatter, hit, radius);", + "", + " const isPressedForTool =", + " typeof editor.isCapturedLeftMouseButtonPressed === 'function'", + " ? editor.isCapturedLeftMouseButtonPressed()", + " : inputManager.isMouseButtonPressed(0);", + " // The button is released outside the canvas when the mouse is on a toolbar.", + " const isPressed = isPressedForTool && inputManager.isMouseInsideCanvas();", + " if (isPressed && hit && !this.isStroking) {", + " this._startStroke(scatter);", + " }", + " if (this.isStroking && isPressed && hit) {", + " this._applyBrush(scatter, hit, radius);", + " }", + " if (!isPressed) {", + " this._endStroke();", + " }", + " }", + "", + " _getRadius() {", + " const { object } = this.scatter;", + " const maxRadius = Math.max(object.getWidth(), object.getHeight()) / 4;", + " return Math.max(maxRadius * Math.pow(this.sizePercent / 100, 2), 10);", + " }", + "", + " /**", + " * @returns {{x: number, y: number} | null} The position of the scene under the cursor: on the", + " * highest ground hit, or on the plane at the object Z.", + " */", + " _getPositionUnderCursor(scatter, game) {", + " let hit = null;", + " for (const ground of getGrounds(scatter.object.getRuntimeScene())) {", + " if (typeof ground.raycastFromCursor !== 'function') continue;", + " const localHit = ground.raycastFromCursor(game, this.raycaster, this.ndc);", + " if (!localHit) continue;", + " const sceneHit = ground.toScenePosition(localHit);", + " if (!hit || sceneHit.z > hit.z) hit = sceneHit;", + " }", + " if (hit) return hit;", + "", + " const { object } = scatter;", + " const layer = object.getInstanceContainer().getLayer(object.getLayer());", + " const camera = layer.getRenderer().getThreeCamera();", + " if (!camera) return null;", + " const inputManager = game.getInputManager();", + " this.ndc.set(", + " (inputManager.getCursorX() / game.getGameResolutionWidth()) * 2 - 1,", + " -(inputManager.getCursorY() / game.getGameResolutionHeight()) * 2 + 1", + " );", + " this.raycaster.setFromCamera(this.ndc, camera);", + " scatter.group.updateWorldMatrix(true, false);", + " const ray = this.raycaster.ray.clone().applyMatrix4(scatter.group.matrixWorld.clone().invert());", + " const point = ray.intersectPlane(new THREE.Plane(new THREE.Vector3(0, 0, 1), 0), new THREE.Vector3());", + " if (!point) return null;", + " return {", + " x: object.getX() + (point.x / AREA_SIZE) * object.getWidth(),", + " y: object.getY() + (point.y / AREA_SIZE) * object.getHeight(),", + " };", + " }", + "", + " _showBrushRing(scatter, hit, radius) {", + " if (!hit) {", + " this.brushRing.removeFromParent();", + " return;", + " }", + " const { object } = scatter;", + " if (this.brushRing.parent !== scatter.group) scatter.group.add(this.brushRing);", + " const grounds = getGrounds(object.getRuntimeScene());", + " const positions = this.brushRing.geometry.attributes.position;", + " for (let index = 0; index < positions.count; index++) {", + " const angle = (index / positions.count) * Math.PI * 2;", + " const x = hit.x + Math.cos(angle) * radius;", + " const y = hit.y + Math.sin(angle) * radius;", + " const { ground, z } = findHighestGround(grounds, x, y, this._groundAtCursor);", + " positions.setXYZ(", + " index,", + " ((x - object.getX()) / object.getWidth()) * AREA_SIZE,", + " ((y - object.getY()) / object.getHeight()) * AREA_SIZE,", + " (((ground ? z : object.getZ()) + 2 - object.getZ()) / Math.max(object.getDepth(), 0.0001)) * AREA_DEPTH", + " );", + " }", + " positions.needsUpdate = true;", + " }", + "", + " _startStroke(scatter) {", + " this.isStroking = true;", + " this.lastStrokeTime = performance.now();", + " this.undoSteps.push({ mask: scatter.mask.clone(), baseMask: scatter.baseMask });", + " while (this.undoSteps.length > 30) this.undoSteps.shift();", + " }", + "", + " _endStroke() {", + " if (!this.isStroking) return;", + " this.isStroking = false;", + " this._save();", + " }", + "", + " _applyBrush(scatter, hit, radius) {", + " const now = performance.now();", + " const elapsedSeconds = Math.min((now - this.lastStrokeTime) / 1000, 0.1);", + " this.lastStrokeTime = now;", + " const strength = (this.strengthPercent / 100) * elapsedSeconds * 8;", + " scatter.paint(hit.x, hit.y, hit.x, hit.y, radius, this.toolName === 'Add' ? 1 : 0, strength);", + " }", + "", + " _save() {", + " const { scatter, editor } = this;", + " if (!scatter || !editor) return;", + " editor.updateObjectProperties(scatter.object.getName(), { PaintData: scatter.save() });", + " }", + "", + " _undo() {", + " const undoStep = this.undoSteps.pop();", + " // The coverage or the edits may have changed since the stroke.", + " if (!this.scatter || !undoStep || undoStep.baseMask !== this.scatter.baseMask) {", + " this.undoSteps.length = 0;", + " return;", + " }", + " this.scatter.setMask(undoStep.mask);", + " this._save();", + " }", + "", + " /** @returns {Array} */", + " _getToolbarItems() {", + " const toolButton = (toolName, tooltip) => ({", + " type: 'button',", + " id: toolName,", + " tooltip,", + " iconUrl: scatterToolIcons[toolName],", + " isActive: this.toolName === toolName,", + " onClick: () => (this.toolName = this.toolName === toolName ? '' : toolName),", + " });", + " return [", + " toolButton('Add', 'Add copies (drag on the ground)'),", + " toolButton('Erase', 'Erase copies'),", + " { type: 'divider', id: 'BrushDivider' },", + " {", + " type: 'slider',", + " id: 'Size',", + " tooltip: 'Brush size',", + " iconUrl: toolIcons.Size,", + " min: 1,", + " max: 100,", + " value: this.sizePercent,", + " onChange: (value) => (this.sizePercent = value),", + " },", + " {", + " type: 'slider',", + " id: 'Strength',", + " tooltip: 'Brush strength',", + " iconUrl: toolIcons.Strength,", + " min: 5,", + " max: 100,", + " value: this.strengthPercent,", + " onChange: (value) => (this.strengthPercent = value),", + " },", + " {", + " type: 'button',", + " id: 'Undo',", + " tooltip: 'Undo the last stroke on these scattered models',", + " iconUrl: toolIcons.Undo,", + " onClick: () => this._undo(),", + " },", + " ];", + " }", + "}", + "", + "const editorTools = new ScatterEditorTools();", + "if (gdjs.registerInGameEditorPostStepCallback) {", + " gdjs.registerInGameEditorPostStepCallback((editor) => editorTools.update(editor));", + "}", + "", + "gdjs.__terrainScatterExtension = {", + " Scatter,", + " // Exposed for tests.", + " DensityMask,", + " maskCodec,", + " applyEdits,", + "};", + "" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [], + "objectGroups": [] + } + ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "DefineHelperClasses" + }, + { + "functionName": "DefineGrassHelperClasses" + }, + { + "functionName": "DefineScatterHelperClasses" + } + ] + }, + "eventsBasedBehaviors": [], + "eventsBasedObjects": [ + { + "areaMaxX": 4096, + "areaMaxY": 4096, + "areaMaxZ": 600, + "areaMinX": 0, + "areaMinY": 0, + "areaMinZ": 0, + "assetStoreTag": "3d terrains", + "defaultName": "Terrain", + "description": "A ground with hills, mountains or valleys that can be sculpted and painted in the scene editor, or changed with events. Add the 3D physics behavior to it to make objects walk or roll on it.", + "fullName": "3D terrain", + "helpPath": "", + "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLXRlcnJhaW4iIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTQsNkwxMC4yNSwxMUwxMy4xLDE0LjhMMTEuNSwxNkM5LjgxLDEzLjc1IDcsMTAgNywxMEwxLDE4SDIzTDE0LDZaIiAvPjwvc3ZnPg==", + "is3D": true, + "isUsingLegacyInstancesRenderer": true, + "name": "Terrain3D", + "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", + "objects": [ + { + "assetStoreId": "", + "name": "AreaPlaceholder", + "type": "Scene3D::Cube3DObject", + "variables": [], + "effects": [], + "behaviors": [], + "content": { + "width": 4096, + "height": 4096, + "depth": 600, + "enableTextureTransparency": false, + "facesOrientation": "Y", + "backFaceUpThroughWhichAxisRotation": "X", + "materialType": "Basic", + "tint": "255;255;255", + "frontFaceResourceName": "", + "frontFaceVisible": false, + "frontFaceResourceRepeat": false, + "backFaceResourceName": "", + "backFaceVisible": false, + "backFaceResourceRepeat": false, + "leftFaceResourceName": "", + "leftFaceVisible": false, + "leftFaceResourceRepeat": false, + "rightFaceResourceName": "", + "rightFaceVisible": false, + "rightFaceResourceRepeat": false, + "topFaceResourceName": "", + "topFaceVisible": false, + "topFaceResourceRepeat": false, + "bottomFaceResourceName": "", + "bottomFaceVisible": false, + "bottomFaceResourceRepeat": false + } + } + ], + "objectsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "objectName": "AreaPlaceholder" + } + ] + }, + "objectsGroups": [], + "layers": [ + { + "ambientLightColorB": 200, + "ambientLightColorG": 200, + "ambientLightColorR": 200, + "camera2DPlaneMaxDrawingDistance": 5000, + "camera3DFarPlaneDistance": 10000, + "camera3DFieldOfView": 45, + "camera3DNearPlaneDistance": 3, + "cameraType": "", + "followBaseLayerCamera": false, + "isLightingLayer": false, + "isLocked": false, + "name": "", + "renderingType": "", + "visibility": true, + "cameras": [ + { + "defaultSize": true, + "defaultViewport": true, + "height": 0, + "viewportBottom": 1, + "viewportLeft": 0, + "viewportRight": 1, + "viewportTop": 0, + "width": 0 + } + ], + "effects": [] + } + ], + "instances": [ + { + "angle": 0, + "customSize": true, + "depth": 600, + "height": 4096, + "keepRatio": true, + "layer": "", + "name": "AreaPlaceholder", + "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-004096004096", + "width": 4096, + "x": 0, + "y": 0, + "zOrder": 1, + "numberProperties": [], + "stringProperties": [], + "initialVariables": [] + } + ], + "editionSettings": {}, + "eventsFunctions": [ + { + "fullName": "", + "functionType": "Action", + "name": "onCreated", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Terrain3D::DefineHelperClasses" + }, + "parameters": [ + "", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "for (const child of object.getChildrenContainer().getAdhocListOfAllInstances()) {", + " child.deleteFromScene();", + "}", + "if (object.__terrain3D) object.__terrain3D.dispose();", + "object.__terrain3D = new gdjs.__terrain3DExtension.Terrain(object);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "onDestroy", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrain3D.dispose();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "doStepPostEvents", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrain3D.update();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "onHotReloading", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrain3D.onHotReloading();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "description": "Raise the ground around a position (or lower it with a negative height). It's raised the most at the center and less and less up to the radius.", + "fullName": "Raise or lower the ground", + "functionType": "Action", + "group": "Sculpt", + "name": "RaiseGround", + "sentence": "Raise the ground of _PARAM0_ by _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const height = eventsFunctionContext.getArgument(\"Height\");", + "object.__terrain3D.raise(x, y, x, y, radius, height);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Height to add (negative to lower the ground)", + "name": "Height", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Raise the ground along a line, for example for a ridge, or lower it with a negative height to dig a river bed or a valley.", + "fullName": "Raise or lower the ground along a line", + "functionType": "Action", + "group": "Sculpt", + "name": "RaiseGroundAlongLine", + "sentence": "Raise the ground of _PARAM0_ by _PARAM6_ from _PARAM1_; _PARAM2_ to _PARAM3_; _PARAM4_ with a radius of _PARAM5_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x1 = eventsFunctionContext.getArgument(\"X1\");", + "const y1 = eventsFunctionContext.getArgument(\"Y1\");", + "const x2 = eventsFunctionContext.getArgument(\"X2\");", + "const y2 = eventsFunctionContext.getArgument(\"Y2\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const height = eventsFunctionContext.getArgument(\"Height\");", + "object.__terrain3D.raise(x1, y1, x2, y2, radius, height);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the start", + "name": "X1", + "type": "expression" + }, + { + "description": "Y position of the start", + "name": "Y1", + "type": "expression" + }, + { + "description": "X position of the end", + "name": "X2", + "type": "expression" + }, + { + "description": "Y position of the end", + "name": "Y2", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance from the line.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Height to add (negative to lower the ground)", + "name": "Height", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Flatten the ground around a position to a given height, for example to place a building.", + "fullName": "Flatten the ground", + "functionType": "Action", + "group": "Sculpt", + "name": "FlattenGround", + "sentence": "Flatten the ground of _PARAM0_ to a Z of _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const z = eventsFunctionContext.getArgument(\"Z\");", + "object.__terrain3D.flatten(x, y, x, y, radius, z, 1);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Z position of the flat ground", + "name": "Z", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Smooth the ground around a position, softening bumps and cliffs.", + "fullName": "Smooth the ground", + "functionType": "Action", + "group": "Sculpt", + "name": "SmoothGround", + "sentence": "Smooth the ground of _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_ (strength: _PARAM4_)", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const strength = eventsFunctionContext.getArgument(\"Strength\");", + "object.__terrain3D.smooth(x, y, x, y, radius, strength);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Strength (between 0 and 1)", + "name": "Strength", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Paint a layer (like grass, dirt, rock or sand) on the ground around a position.", + "fullName": "Paint a layer", + "functionType": "Action", + "group": "Paint", + "name": "PaintLayer", + "sentence": "Paint layer _PARAM4_ on _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const layer = eventsFunctionContext.getArgument(\"Layer\");", + "object.__terrain3D.paint(x, y, x, y, radius, layer, 1);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Layer (1 to 4)", + "name": "Layer", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Paint a layer on the ground along a line, for example for a path or a road.", + "fullName": "Paint a layer along a line", + "functionType": "Action", + "group": "Paint", + "name": "PaintLayerAlongLine", + "sentence": "Paint layer _PARAM6_ on _PARAM0_ from _PARAM1_; _PARAM2_ to _PARAM3_; _PARAM4_ with a radius of _PARAM5_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x1 = eventsFunctionContext.getArgument(\"X1\");", + "const y1 = eventsFunctionContext.getArgument(\"Y1\");", + "const x2 = eventsFunctionContext.getArgument(\"X2\");", + "const y2 = eventsFunctionContext.getArgument(\"Y2\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const layer = eventsFunctionContext.getArgument(\"Layer\");", + "object.__terrain3D.paint(x1, y1, x2, y2, radius, layer, 1);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the start", + "name": "X1", + "type": "expression" + }, + { + "description": "Y position of the start", + "name": "Y1", + "type": "expression" + }, + { + "description": "X position of the end", + "name": "X2", + "type": "expression" + }, + { + "description": "Y position of the end", + "name": "Y2", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance from the line.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Layer (1 to 4)", + "name": "Layer", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Tint the ground around a position with a color multiplying its layers, like dry grass or darker mud. White removes the tint.", + "fullName": "Tint the ground", + "functionType": "Action", + "group": "Paint", + "name": "TintGround", + "sentence": "Tint the ground of _PARAM0_ with _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const color = gdjs.rgbOrHexToRGBColor(eventsFunctionContext.getArgument(\"Color\"));", + "object.__terrain3D.tint(x, y, x, y, radius, color, 1);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Color", + "name": "Color", + "type": "color" + } + ], + "objectGroups": [] + }, + { + "description": "Make the ground around a position wet (darker and shiny, like after rain or near water) or dry.", + "fullName": "Change the wetness of the ground", + "functionType": "Action", + "group": "Paint", + "name": "WetGround", + "sentence": "Change the wetness of the ground of _PARAM0_ to _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const wetness = eventsFunctionContext.getArgument(\"Wetness\");", + "object.__terrain3D.wet(x, y, x, y, radius, wetness, 1);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Wetness (from 0 for dry to 1)", + "name": "Wetness", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "the wetness of the ground at a position, from 0 (dry) to 1, painted or near the water (wet shore).", + "fullName": "Ground wetness", + "functionType": "ExpressionAndCondition", + "group": "Paint", + "name": "GroundWetness", + "sentence": "the wetness of the ground at _PARAM1_; _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "eventsFunctionContext.returnValue = object.__terrain3D.getWetnessAt(x, y);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "The tint painted on the ground at a position, like \"255;255;255\" (white when there is none).", + "fullName": "Ground tint", + "functionType": "StringExpression", + "group": "Paint", + "name": "GroundTint", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "eventsFunctionContext.returnValue = object.__terrain3D.getTintAt(x, y).join(\";\");" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "string" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Remove the ground in a circle, for the entrance of a tunnel, a cave or a well: there is nothing drawn and no collision there. Fill it again with the \"Fill holes\" action. A height field can't go under itself: put a 3D model (with a collision shape) for the tunnel or the cave under the ground. The physics engine removes the triangles touching a hole, so the hole in the collisions can be up to a cell larger than the one drawn: make the model go a bit further than the hole, or put rocks around the entrance.", + "fullName": "Make a hole", + "functionType": "Action", + "group": "Holes", + "name": "MakeHole", + "sentence": "Make a hole in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "object.__terrain3D.makeHole(x, y, x, y, radius);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "name": "Radius", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Fill the holes in a circle with the layers of the ground around them: the ground is drawn and has collisions again.", + "fullName": "Fill holes", + "functionType": "Action", + "group": "Holes", + "name": "FillHoles", + "sentence": "Fill the holes in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "object.__terrain3D.fillHoles(x, y, x, y, radius);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "name": "Radius", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Make the entrance of a tunnel or a cave on a slope: a hole, with the ground in front of it flattened to the floor of the tunnel.", + "fullName": "Carve an entrance", + "functionType": "Action", + "group": "Holes", + "name": "CarveEntrance", + "sentence": "Carve an entrance in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_, going in at _PARAM4_° with its floor at Z _PARAM5_ and _PARAM6_ pixels of flat ground in front", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const angle = eventsFunctionContext.getArgument(\"Angle\");", + "const floorZ = eventsFunctionContext.getArgument(\"FloorZ\");", + "const length = eventsFunctionContext.getArgument(\"Length\");", + "object.__terrain3D.carveEntrance(x, y, radius, angle, floorZ, length);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "name": "Radius", + "type": "expression" + }, + { + "description": "Direction going into the ground (in degrees)", + "name": "Angle", + "type": "expression" + }, + { + "description": "Z position of the floor", + "name": "FloorZ", + "type": "expression" + }, + { + "description": "Length of the flat ground in front of the entrance (in pixels)", + "name": "Length", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Check if there is a hole in the ground at a position.", + "fullName": "Hole", + "functionType": "Condition", + "group": "Holes", + "name": "IsHole", + "sentence": "There is a hole in _PARAM0_ at _PARAM1_; _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "eventsFunctionContext.returnValue = object.__terrain3D.isHoleAt(x, y);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "The Z position of the ground at a position, or the given value where there is a hole.", + "fullName": "Ground Z position, with holes", + "functionType": "Expression", + "group": "Holes", + "name": "GroundZWithHoles", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const terrain = object.__terrain3D;", + "eventsFunctionContext.returnValue = terrain.isHoleAt(x, y) ? eventsFunctionContext.getArgument(\"ZInHoles\") : terrain.getHeightAt(x, y);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + }, + { + "description": "Value in holes", + "longDescription": "For example, a Z position far under the ground.", + "name": "ZInHoles", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Replace the ground with a generated relief. Painted layers are reset.", + "fullName": "Generate a relief", + "functionType": "Action", + "group": "Generation", + "name": "GenerateRelief", + "sentence": "Generate a relief of _PARAM1_ on _PARAM0_ with seed _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const relief = eventsFunctionContext.getArgument(\"Relief\");", + "const seed = eventsFunctionContext.getArgument(\"Seed\");", + "object.__terrain3D.generate(relief, seed);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "Relief", + "name": "Relief", + "supplementaryInformation": "[\"Flat\", \"Hills\", \"Mountains\", \"Island\"]", + "type": "stringWithSelector" + }, + { + "description": "Seed", + "longDescription": "Each seed gives a different terrain with the same relief.", + "name": "Seed", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Replace the ground with the heights of an image. Painted layers are kept.", + "fullName": "Load a heightmap", + "functionType": "Action", + "group": "Generation", + "name": "LoadHeightmap", + "sentence": "Load the heights of _PARAM0_ from _PARAM1_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const image = eventsFunctionContext.getArgument(\"Image\");", + "object.__terrain3D.loadHeightmap(image);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "Heightmap image", + "longDescription": "Black is the lowest, white the highest. For more precision, store the height on 16 bits in the red (most significant byte) and green channels.", + "name": "Image", + "type": "imageResource" + } + ], + "objectGroups": [] + }, + { + "description": "Move objects up or down so that their bottom is on the ground, under their center. Useful to place trees, rocks or characters on a terrain.", + "fullName": "Put objects on the ground", + "functionType": "Action", + "group": "Position", + "name": "PutOnGround", + "sentence": "Put _PARAM1_ on the ground of _PARAM0_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrain3D.placeOnGround(eventsFunctionContext.getObjects(\"Objects\"));" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "Objects to put on the ground", + "name": "Objects", + "type": "objectList" + } + ], + "objectGroups": [] + }, + { + "description": "the Z position of the ground at a position of the scene.", + "fullName": "Ground Z position", + "functionType": "ExpressionAndCondition", + "group": "Position", + "name": "GroundZ", + "sentence": "the ground Z position at _PARAM1_; _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "eventsFunctionContext.returnValue = object.__terrain3D.getHeightAt(x, y);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "the slope of the ground (angle with the horizontal, in degrees) at a position of the scene.", + "fullName": "Ground slope", + "functionType": "ExpressionAndCondition", + "group": "Position", + "name": "GroundSlope", + "sentence": "the ground slope at _PARAM1_; _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "eventsFunctionContext.returnValue = object.__terrain3D.getSlopeAt(x, y);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "the layer (from 1 to 4) painted the most at a position of the scene. Useful to play footstep sounds or to slow down characters in water or mud.", + "fullName": "Ground layer", + "functionType": "ExpressionAndCondition", + "group": "Paint", + "name": "GroundLayer", + "sentence": "the ground layer at _PARAM1_; _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "eventsFunctionContext.returnValue = object.__terrain3D.getLayerAt(x, y);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + } + ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + }, + { + "folderName": "Sculpt", + "children": [ + { + "functionName": "RaiseGround" + }, + { + "functionName": "RaiseGroundAlongLine" + }, + { + "functionName": "FlattenGround" + }, + { + "functionName": "SmoothGround" + } + ] + }, + { + "folderName": "Paint", + "children": [ + { + "functionName": "PaintLayer" + }, + { + "functionName": "PaintLayerAlongLine" + }, + { + "functionName": "TintGround" + }, + { + "functionName": "WetGround" + }, + { + "functionName": "GroundWetness" + }, + { + "functionName": "GroundTint" + }, + { + "functionName": "GroundLayer" + } + ] + }, + { + "folderName": "Holes", + "children": [ + { + "functionName": "MakeHole" + }, + { + "functionName": "FillHoles" + }, + { + "functionName": "CarveEntrance" + }, + { + "functionName": "IsHole" + }, + { + "functionName": "GroundZWithHoles" + } + ] + }, + { + "folderName": "Generation", + "children": [ + { + "functionName": "GenerateRelief" + }, + { + "functionName": "LoadHeightmap" + } + ] + }, + { + "folderName": "Position", + "children": [ + { + "functionName": "PutOnGround" + }, + { + "functionName": "GroundZ" + }, + { + "functionName": "GroundSlope" + } + ] + } + ] + }, + "propertyDescriptors": [ + { + "value": "Hills", + "type": "Choice", + "label": "Relief", + "description": "The initial shape, before sculpting.", + "group": "Shape", + "choices": [ + { + "label": "Flat", + "value": "Flat" + }, + { + "label": "Hills", + "value": "Hills" + }, + { + "label": "Mountains", + "value": "Mountains" + }, + { + "label": "Island", + "value": "Island" + } + ], + "name": "Relief" + }, + { + "value": "1", + "type": "Number", + "label": "Seed", + "description": "Each seed gives a different terrain with the same relief.", + "group": "Shape", + "name": "Seed" + }, + { + "value": "", + "type": "Resource", + "label": "Heightmap image", + "description": "Optional. Replaces the relief: black is the lowest, white the highest (the object depth).", + "group": "Shape", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "HeightmapImage" + }, + { + "value": "", + "type": "MultilineString", + "label": "Edits", + "description": "Changes of the relief or heightmap, applied in order, as a JSON list, for example [{\"tool\": \"raise\", \"x\": 0.3, \"y\": 0.4, \"radius\": 0.15, \"height\": 0.4}, {\"tool\": \"paint\", \"x\": 0.1, \"y\": 0.9, \"toX\": 0.8, \"toY\": 0.2, \"radius\": 0.02, \"layer\": 2}]. Tools: raise (height added to the ground, negative to lower), flatten (z: the ground is brought to this height, fully at the center, cutting what is above it and filling what is below; with toX and toY, toZ gives the height at the end of the line, for a road going up), smooth (strength), paint (layer, from 1 to 4, and strength), tint (color, like \"120;140;60\", multiplying the layers, white removing the tint, and strength), wetness (amount, from 0 for dry to 1 for wet, and strength), hole (no ground in the radius, for the entrances of tunnels and caves) and fill (the holes in the radius are filled with the layers of the ground around them). x, y and radius are fractions (0 to 1) of the terrain size (x is (scene X - terrain X) / terrain width). height and z are fractions of its depth: the ground at z is at Z = terrain Z + z * terrain depth. Add toX and toY to change the ground along a line (paths, rivers, ridges). Shown in the scene editor.", + "group": "Shape", + "advanced": true, + "name": "Edits" + }, + { + "value": "256", + "type": "Choice", + "label": "Resolution", + "description": "The number of cells on each side of the terrain. Higher is more detailed but slower.", + "group": "Shape", + "choices": [ + { + "label": "64 x 64", + "value": "64" + }, + { + "label": "128 x 128", + "value": "128" + }, + { + "label": "256 x 256", + "value": "256" + }, + { + "label": "512 x 512", + "value": "512" + }, + { + "label": "1024 x 1024", + "value": "1024" + } + ], + "name": "Resolution" + }, + { + "value": "96;160;64", + "type": "Color", + "label": "Layer 1 color (grass)", + "group": "Layers", + "name": "Layer1Color" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 1 texture", + "description": "Optional. Tinted by the layer color.", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer1Texture" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 1 normal map", + "description": "Optional. The bumps of the texture, lit by the lights (an OpenGL normal map, like the textures of ambientCG).", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer1NormalMap" + }, + { + "value": "150;112;72", + "type": "Color", + "label": "Layer 2 color (dirt)", + "group": "Layers", + "name": "Layer2Color" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 2 texture", + "description": "Optional. Tinted by the layer color.", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer2Texture" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 2 normal map", + "description": "Optional. The bumps of the texture, lit by the lights (an OpenGL normal map, like the textures of ambientCG).", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer2NormalMap" + }, + { + "value": "124;118;112", + "type": "Color", + "label": "Layer 3 color (rock)", + "group": "Layers", + "name": "Layer3Color" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 3 texture", + "description": "Optional. Tinted by the layer color.", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer3Texture" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 3 normal map", + "description": "Optional. The bumps of the texture, lit by the lights (an OpenGL normal map, like the textures of ambientCG).", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer3NormalMap" + }, + { + "value": "224;204;148", + "type": "Color", + "label": "Layer 4 color (sand)", + "group": "Layers", + "name": "Layer4Color" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 4 texture", + "description": "Optional. Tinted by the layer color.", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer4Texture" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 4 normal map", + "description": "Optional. The bumps of the texture, lit by the lights (an OpenGL normal map, like the textures of ambientCG).", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer4NormalMap" + }, + { + "value": "1", + "type": "Number", + "label": "Normal map strength", + "description": "How much the normal maps bump the ground: 0 for none, 1 as they are, more to exaggerate them.", + "group": "Layers", + "name": "NormalMapStrength" + }, + { + "value": "256", + "type": "Number", + "unit": "Pixel", + "label": "Texture size", + "description": "The size, in the scene, of one repetition of the layer textures.", + "group": "Layers", + "name": "TextureSize" + }, + { + "value": "0.5", + "type": "Number", + "label": "Height blending", + "description": "Where layers meet, the bright parts of textures (stones, tufts) cover the dark ones (gaps, sand) instead of fading into each other: 0 for none, 1 for the most.", + "group": "Layers", + "name": "HeightBlending" + }, + { + "value": "0.5", + "type": "Number", + "label": "Texture variation", + "description": "Makes the repetition of textures less visible, with patches of bigger and rotated textures and colors slowly varying: 0 for none, 1 for the most.", + "group": "Layers", + "name": "TextureVariation" + }, + { + "value": "false", + "type": "Boolean", + "label": "Wet shore", + "description": "Make the ground wet (darker and shiny) under a Z position, like the sand of a beach near the water.", + "group": "Wetness", + "name": "WetShore" + }, + { + "value": "0", + "type": "Number", + "unit": "Pixel", + "label": "Wet below Z", + "description": "The ground is wet under this Z position (for example, the Z of a 3D water object).", + "group": "Wetness", + "name": "WetShoreZ" + }, + { + "value": "60", + "type": "Number", + "unit": "Pixel", + "label": "Wet shore fade", + "description": "The ground becomes dry over this height above \"Wet below Z\".", + "group": "Wetness", + "name": "WetShoreFade" + }, + { + "value": "3", + "type": "Choice", + "label": "Layer on steep slopes", + "description": "This layer is shown where the ground is steep, whatever is painted.", + "group": "Layers", + "choices": [ + { + "label": "None", + "value": "0" + }, + { + "label": "Layer 1", + "value": "1" + }, + { + "label": "Layer 2", + "value": "2" + }, + { + "label": "Layer 3", + "value": "3" + }, + { + "label": "Layer 4", + "value": "4" + } + ], + "name": "CliffLayer" + }, + { + "value": "45", + "type": "Number", + "unit": "DegreeAngle", + "label": "Steep slope angle", + "group": "Layers", + "name": "CliffAngle" + }, + { + "value": "true", + "type": "Boolean", + "label": "Cast shadows", + "group": "Rendering", + "name": "CastShadow" + }, + { + "value": "true", + "type": "Boolean", + "label": "Receive shadows", + "group": "Rendering", + "name": "ReceiveShadow" + }, + { + "value": "", + "type": "String", + "label": "Sculpted and painted data", + "description": "Written by the sculpt and paint tools of the scene editor, in a compact format not meant to be read or written by hand: use Edits instead. Clear it to go back to the relief, heightmap and edits.", + "advanced": true, + "name": "SculptData" + } + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Shape", + "children": [ + { + "propertyName": "Relief" + }, + { + "propertyName": "Seed" + }, + { + "propertyName": "HeightmapImage" + }, + { + "propertyName": "Edits" + }, + { + "propertyName": "Resolution" + } + ] + }, + { + "folderName": "Layers", + "children": [ + { + "propertyName": "Layer1Color" + }, + { + "propertyName": "Layer1Texture" + }, + { + "propertyName": "Layer1NormalMap" + }, + { + "propertyName": "Layer2Color" + }, + { + "propertyName": "Layer2Texture" + }, + { + "propertyName": "Layer2NormalMap" + }, + { + "propertyName": "Layer3Color" + }, + { + "propertyName": "Layer3Texture" + }, + { + "propertyName": "Layer3NormalMap" + }, + { + "propertyName": "Layer4Color" + }, + { + "propertyName": "Layer4Texture" + }, + { + "propertyName": "Layer4NormalMap" + }, + { + "propertyName": "NormalMapStrength" + }, + { + "propertyName": "TextureSize" + }, + { + "propertyName": "HeightBlending" + }, + { + "propertyName": "TextureVariation" + }, + { + "propertyName": "CliffLayer" + }, + { + "propertyName": "CliffAngle" + } + ] + }, + { + "folderName": "Wetness", + "children": [ + { + "propertyName": "WetShore" + }, + { + "propertyName": "WetShoreZ" + }, + { + "propertyName": "WetShoreFade" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "CastShadow" + }, + { + "propertyName": "ReceiveShadow" + } + ] + }, + { + "propertyName": "SculptData" + } + ] + }, + "variants": [] + }, + { + "areaMaxX": 1000, + "areaMaxY": 1000, + "areaMaxZ": 40, + "areaMinX": 0, + "areaMinY": 0, + "areaMinZ": 0, + "defaultName": "Grass", + "description": "A field of grass blades swaying in the wind and pushed aside by characters. They grow in the area of the object, on the highest 3D terrain at each point (one field can cover several terrains), or at the Z position of the object where there is no terrain.", + "fullName": "Terrain grass", + "helpPath": "", + "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGQ9Ik0zIDIxYzEtNSAxLTkgMC0xMyAyIDMgMyA3IDMgMTN6bTQgMGMwLTYgMS0xMSA0LTE2LTEgNS0xIDEwIDAgMTZ6bTUgMGMwLTUgMi05IDUtMTItMiA0LTIgOC0xIDEyem01IDBjMS00IDItNiA0LTgtMSAzLTEgNS0xIDh6Ii8+PC9zdmc+", + "is3D": true, + "isUsingLegacyInstancesRenderer": true, + "name": "TerrainGrass", + "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/732ef90b7fcf5dd9171fe95d0cf262e09159b487304915a4693baa893d9ce16c_grass.svg", + "objects": [ + { + "assetStoreId": "", + "name": "AreaPlaceholder", + "type": "Scene3D::Cube3DObject", + "variables": [], + "effects": [], + "behaviors": [], + "content": { + "width": 1000, + "height": 1000, + "depth": 40, + "enableTextureTransparency": false, + "facesOrientation": "Y", + "backFaceUpThroughWhichAxisRotation": "X", + "materialType": "Basic", + "tint": "255;255;255", + "frontFaceResourceName": "", + "frontFaceVisible": false, + "frontFaceResourceRepeat": false, + "backFaceResourceName": "", + "backFaceVisible": false, + "backFaceResourceRepeat": false, + "leftFaceResourceName": "", + "leftFaceVisible": false, + "leftFaceResourceRepeat": false, + "rightFaceResourceName": "", + "rightFaceVisible": false, + "rightFaceResourceRepeat": false, + "topFaceResourceName": "", + "topFaceVisible": false, + "topFaceResourceRepeat": false, + "bottomFaceResourceName": "", + "bottomFaceVisible": false, + "bottomFaceResourceRepeat": false + } + } + ], + "objectsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "objectName": "AreaPlaceholder" + } + ] + }, + "objectsGroups": [], + "layers": [ + { + "ambientLightColorB": 200, + "ambientLightColorG": 200, + "ambientLightColorR": 200, + "camera2DPlaneMaxDrawingDistance": 5000, + "camera3DFarPlaneDistance": 10000, + "camera3DFieldOfView": 45, + "camera3DNearPlaneDistance": 3, + "cameraType": "", + "followBaseLayerCamera": false, + "isLightingLayer": false, + "isLocked": false, + "name": "", + "renderingType": "", + "visibility": true, + "cameras": [ + { + "defaultSize": true, + "defaultViewport": true, + "height": 0, + "viewportBottom": 1, + "viewportLeft": 0, + "viewportRight": 1, + "viewportTop": 0, + "width": 0 + } + ], + "effects": [] + } + ], + "instances": [ + { + "angle": 0, + "customSize": true, + "depth": 40, + "height": 1000, + "keepRatio": true, + "layer": "", + "name": "AreaPlaceholder", + "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-001000001000", + "width": 1000, + "x": 0, + "y": 0, + "zOrder": 1, + "numberProperties": [], + "stringProperties": [], + "initialVariables": [] + } + ], + "editionSettings": {}, + "eventsFunctions": [ + { + "fullName": "", + "functionType": "Action", + "name": "onCreated", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Terrain3D::DefineHelperClasses" + }, + "parameters": [ + "", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Terrain3D::DefineGrassHelperClasses" + }, + "parameters": [ + "", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "for (const child of object.getChildrenContainer().getAdhocListOfAllInstances()) {", + " child.deleteFromScene();", + "}", + "if (object.__terrainGrass) object.__terrainGrass.dispose();", + "object.__terrainGrass = new gdjs.__terrainGrassExtension.Grass(object);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "onDestroy", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrainGrass.dispose();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "doStepPostEvents", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const game = runtimeScene.getGame();", + "// Time doesn't pass in the scene editor: real time is used to show the wind.", + "const isInGameEdition = game.isInGameEdition && game.isInGameEdition();", + "const elapsedTime = isInGameEdition ? 1 / 60 : object.getElapsedTime() / 1000;", + "const time = isInGameEdition", + " ? performance.now() / 1000", + " : object.getRuntimeScene().getTimeManager().getTimeFromStart() / 1000;", + "const layer = object.getInstanceContainer().getLayer(object.getLayer());", + "object.__terrainGrass.update(elapsedTime, time, layer.getRenderer().getThreeCamera());" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "onHotReloading", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrainGrass.updateFromProperties();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" + } + ], + "objectGroups": [] + } + ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + } + ] + }, + "propertyDescriptors": [ + { + "value": "60", + "type": "Number", + "label": "Density", + "description": "The number of blades in a square of 100 x 100 pixels.", + "group": "Blades", + "name": "Density" + }, + { + "value": "36", + "type": "Number", + "unit": "Pixel", + "label": "Blade height", + "description": "The average height of blades (each blade is between 60% and 140% of it).", + "group": "Blades", + "name": "BladeHeight" + }, + { + "value": "5", + "type": "Number", + "unit": "Pixel", + "label": "Blade width", + "group": "Blades", + "name": "BladeWidth" + }, + { + "value": "52;104;36", + "type": "Color", + "label": "Base color", + "group": "Blades", + "name": "BaseColor" + }, + { + "value": "150;196;82", + "type": "Color", + "label": "Tip color", + "group": "Blades", + "name": "TipColor" + }, + { + "value": "1", + "type": "Number", + "label": "Seed", + "description": "Each seed places blades differently.", + "group": "Blades", + "name": "Seed" + }, + { + "value": "1", + "type": "Choice", + "label": "Grows on terrain layer", + "description": "On a 3D terrain, blades only grow where this layer is painted. A new terrain has layer 1 painted everywhere.", + "group": "Ground", + "choices": [ + { + "label": "Any", + "value": "0" + }, + { + "label": "Layer 1", + "value": "1" + }, + { + "label": "Layer 2", + "value": "2" + }, + { + "label": "Layer 3", + "value": "3" + }, + { + "label": "Layer 4", + "value": "4" + } + ], + "name": "GroundLayer" + }, + { + "value": "40", + "type": "Number", + "unit": "DegreeAngle", + "label": "Maximum slope", + "description": "No blades grow on steeper ground.", + "group": "Ground", + "name": "MaxSlope" + }, + { + "value": "1", + "type": "Number", + "label": "Wind strength", + "description": "0 for no wind.", + "group": "Wind", + "name": "WindStrength" + }, + { + "value": "2", + "type": "Number", + "label": "Wind speed", + "group": "Wind", + "name": "WindSpeed" + }, + { + "value": "", + "type": "String", + "label": "Object bending the grass", + "description": "The name of an object pushing blades aside when walking through the grass, like the player. The 8 first instances are used.", + "group": "Wind", + "name": "BendingObject" + }, + { + "value": "2500", + "type": "Number", + "unit": "Pixel", + "label": "Fade distance", + "description": "Blades are less and less dense up to this distance from the camera, and hidden after.", + "group": "Rendering", + "name": "FadeDistance" + } + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Blades", + "children": [ + { + "propertyName": "Density" + }, + { + "propertyName": "BladeHeight" + }, + { + "propertyName": "BladeWidth" + }, + { + "propertyName": "BaseColor" + }, + { + "propertyName": "TipColor" + }, + { + "propertyName": "Seed" + } + ] + }, + { + "folderName": "Ground", + "children": [ + { + "propertyName": "GroundLayer" + }, + { + "propertyName": "MaxSlope" + } + ] + }, + { + "folderName": "Wind", + "children": [ + { + "propertyName": "WindStrength" + }, + { + "propertyName": "WindSpeed" + }, + { + "propertyName": "BendingObject" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "FadeDistance" + } + ] + } + ] + }, + "variants": [] + }, + { + "areaMaxX": 2000, + "areaMaxY": 2000, + "areaMaxZ": 200, + "areaMinX": 0, + "areaMinY": 0, + "areaMinZ": 0, + "assetStoreTag": "3d scattered models", + "defaultName": "ScatteredModels", + "description": "Many copies of a 3D model (trees, rocks, bushes, flowers...) placed in the area of the object and drawn all at once, which is much faster than an object for each one. They stand on the highest 3D terrain at each point, or at the Z position of the object where there is no terrain. Paint where they grow with the brushes of the scene editor.", + "fullName": "Scattered models (forests, rocks...)", + "helpPath": "", + "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGQ9Ik0xMCAyMXYtM0gzbDUtNUg1bDUtNUg3bDUtNSA1IDVoLTNsNSA1aC0zbDUgNWgtN3YzeiIvPjwvc3ZnPg==", + "is3D": true, + "isUsingLegacyInstancesRenderer": true, + "name": "ScatteredModels", + "previewIconUrl": "", + "objects": [ + { + "assetStoreId": "", + "name": "AreaPlaceholder", + "type": "Scene3D::Cube3DObject", + "variables": [], + "effects": [], + "behaviors": [], + "content": { + "width": 2000, + "height": 2000, + "depth": 200, + "enableTextureTransparency": false, + "facesOrientation": "Y", + "backFaceUpThroughWhichAxisRotation": "X", + "materialType": "Basic", + "tint": "255;255;255", + "frontFaceResourceName": "", + "frontFaceVisible": false, + "frontFaceResourceRepeat": false, + "backFaceResourceName": "", + "backFaceVisible": false, + "backFaceResourceRepeat": false, + "leftFaceResourceName": "", + "leftFaceVisible": false, + "leftFaceResourceRepeat": false, + "rightFaceResourceName": "", + "rightFaceVisible": false, + "rightFaceResourceRepeat": false, + "topFaceResourceName": "", + "topFaceVisible": false, + "topFaceResourceRepeat": false, + "bottomFaceResourceName": "", + "bottomFaceVisible": false, + "bottomFaceResourceRepeat": false + } + } + ], + "objectsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "objectName": "AreaPlaceholder" + } + ] + }, + "objectsGroups": [], + "layers": [ + { + "ambientLightColorB": 200, + "ambientLightColorG": 200, + "ambientLightColorR": 200, + "camera2DPlaneMaxDrawingDistance": 5000, + "camera3DFarPlaneDistance": 10000, + "camera3DFieldOfView": 45, + "camera3DNearPlaneDistance": 3, + "cameraType": "", + "followBaseLayerCamera": false, + "isLightingLayer": false, + "isLocked": false, + "name": "", + "renderingType": "", + "visibility": true, + "cameras": [ + { + "defaultSize": true, + "defaultViewport": true, + "height": 0, + "viewportBottom": 1, + "viewportLeft": 0, + "viewportRight": 1, + "viewportTop": 0, + "width": 0 + } + ], + "effects": [] + } + ], + "instances": [ + { + "angle": 0, + "customSize": true, + "depth": 200, + "height": 2000, + "keepRatio": true, + "layer": "", + "name": "AreaPlaceholder", + "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-002000002000", + "width": 2000, + "x": 0, + "y": 0, + "zOrder": 1, + "numberProperties": [], + "stringProperties": [], + "initialVariables": [] + } + ], + "editionSettings": {}, + "eventsFunctions": [ + { + "fullName": "", + "functionType": "Action", + "name": "onCreated", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Terrain3D::DefineHelperClasses" + }, + "parameters": [ + "", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Terrain3D::DefineScatterHelperClasses" + }, + "parameters": [ + "", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "for (const child of object.getChildrenContainer().getAdhocListOfAllInstances()) {", + " child.deleteFromScene();", + "}", + "if (object.__terrainScatter) object.__terrainScatter.dispose();", + "object.__terrainScatter = new gdjs.__terrainScatterExtension.Scatter(object);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "onDestroy", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrainScatter.dispose();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "doStepPostEvents", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const game = runtimeScene.getGame();", + "const isInGameEdition = game.isInGameEdition && game.isInGameEdition();", + "const elapsedTime = isInGameEdition ? 1 / 60 : object.getElapsedTime() / 1000;", + "const layer = object.getInstanceContainer().getLayer(object.getLayer());", + "object.__terrainScatter.update(elapsedTime, layer.getRenderer().getThreeCamera());" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "onHotReloading", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrainScatter.onHotReloading();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "description": "Paint the density of copies around a position, up to the density of the object (copies removed with \"Erase copies\" come back). The effect fades out up to the radius.", + "fullName": "Add copies", + "functionType": "Action", + "group": "Paint", + "name": "AddCopies", + "sentence": "Add copies of _PARAM0_ around _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "object.__terrainScatter.addCopies(x, y, radius);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "name": "Radius", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Remove the copies standing in a circle, for example a tree cut by the player (use the position of the nearest copy and a small radius) or a forest cleared for a building. They come back when the scene is restarted.", + "fullName": "Erase copies", + "functionType": "Action", + "group": "Paint", + "name": "EraseCopies", + "sentence": "Erase the copies of _PARAM0_ in a circle at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "object.__terrainScatter.removeCopies(x, y, radius);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "name": "Radius", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "the number of copies placed in the area.", + "fullName": "Number of copies", + "functionType": "ExpressionAndCondition", + "group": "Copies", + "name": "CopyCount", + "sentence": "the number of copies", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "eventsFunctionContext.returnValue = object.__terrainScatter.getCopyCount();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "description": "The X position of the base of the copy nearest to a position (the position itself if there is no copy).", + "fullName": "X position of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyX", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? copy.x : x;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "The Y position of the base of the copy nearest to a position (the position itself if there is no copy).", + "fullName": "Y position of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyY", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? copy.y : y;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "The Z position of the base of the copy nearest to a position (on the ground).", + "fullName": "Z position of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyZ", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? copy.z : object.getZ();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "the distance (on X and Y) from a position to the base of the nearest copy. It's a very big number when there is no copy.", + "fullName": "Distance to the nearest copy", + "functionType": "ExpressionAndCondition", + "group": "Copies", + "name": "NearestCopyDistance", + "sentence": "the distance from _PARAM1_; _PARAM2_ to the nearest copy", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? Math.hypot(copy.x - x, copy.y - y) : Number.MAX_VALUE;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + } + ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + }, + { + "folderName": "Paint", + "children": [ + { + "functionName": "AddCopies" + }, + { + "functionName": "EraseCopies" + } + ] + }, + { + "folderName": "Copies", + "children": [ + { + "functionName": "CopyCount" + }, + { + "functionName": "NearestCopyX" + }, + { + "functionName": "NearestCopyY" + }, + { + "functionName": "NearestCopyZ" + }, + { + "functionName": "NearestCopyDistance" + } + ] + } + ] + }, + "propertyDescriptors": [ + { + "value": "", + "type": "Resource", + "label": "3D model", + "description": "The model copied everywhere (a .glb file). Models standing on the Y axis, like most models of the asset store, are stood up along Z.", + "group": "Model", + "extraInformation": [ + "model3D" + ], + "choices": [], + "name": "Model" + }, + { + "value": "300", + "type": "Number", + "unit": "Pixel", + "label": "Height of a copy", + "description": "Copies keep the proportions of the model.", + "group": "Model", + "name": "Size" + }, + { + "value": "0.8", + "type": "Number", + "label": "Smallest random scale", + "description": "Each copy is scaled at random between the smallest and the largest scale.", + "group": "Model", + "name": "MinScale" + }, + { + "value": "1.2", + "type": "Number", + "label": "Largest random scale", + "group": "Model", + "name": "MaxScale" + }, + { + "value": "true", + "type": "Boolean", + "label": "Random rotation", + "description": "Turn each copy around the Z axis at random.", + "group": "Model", + "name": "RandomRotation" + }, + { + "value": "255;255;255", + "type": "Color", + "label": "Random tint", + "description": "Each copy is tinted between white and this color, at random. White for no tint.", + "group": "Model", + "name": "RandomTint" + }, + { + "value": "0.3", + "type": "Number", + "label": "Density", + "description": "The number of copies in a square of 100 x 100 pixels where the density is fully painted (0.3 is about one copy every 180 pixels). Copies never overlap their neighbors much: they are placed at random in a grid.", + "group": "Placement", + "name": "Density" + }, + { + "value": "1", + "type": "Number", + "label": "Seed", + "description": "Each seed places copies differently.", + "group": "Placement", + "name": "Seed" + }, + { + "value": "Everywhere", + "type": "Choice", + "label": "Copies grow", + "description": "Everywhere in the area, or only where they are painted with the \"Add copies\" brush (or the edits).", + "group": "Placement", + "choices": [ + { + "label": "Everywhere (erase them with the brush)", + "value": "Everywhere" + }, + { + "label": "Only where painted", + "value": "Painted" + } + ], + "name": "Coverage" + }, + { + "value": "1", + "type": "Choice", + "label": "Grows on terrain layer", + "description": "On a 3D terrain, copies only grow where this layer is painted, away from its borders (so that trees don't stand on roads painted with another layer). A new terrain has layer 1 painted everywhere.", + "group": "Ground", + "choices": [ + { + "label": "Any", + "value": "0" + }, + { + "label": "Layer 1", + "value": "1" + }, + { + "label": "Layer 2", + "value": "2" + }, + { + "label": "Layer 3", + "value": "3" + }, + { + "label": "Layer 4", + "value": "4" + } + ], + "name": "GroundLayer" + }, + { + "value": "30", + "type": "Number", + "unit": "DegreeAngle", + "label": "Maximum slope", + "description": "No copies grow on steeper ground.", + "group": "Ground", + "name": "MaxSlope" + }, + { + "value": "-100000", + "type": "Number", + "unit": "Pixel", + "label": "Minimum ground Z", + "description": "No copies grow where the ground is lower (for example, under the water level).", + "group": "Ground", + "name": "MinGroundZ" + }, + { + "value": "100000", + "type": "Number", + "unit": "Pixel", + "label": "Maximum ground Z", + "description": "No copies grow where the ground is higher (for example, above the tree line).", + "group": "Ground", + "name": "MaxGroundZ" + }, + { + "value": "false", + "type": "Boolean", + "label": "Trunk collisions", + "description": "Give each copy a static collision shape around its trunk, for the 3D physics behavior (add it to this object, with the \"Static\" type) and navigation meshes. Without them, these behaviors have no effect on this object.", + "group": "Collisions", + "name": "TrunkCollisions" + }, + { + "value": "30", + "type": "Number", + "unit": "Pixel", + "label": "Trunk diameter", + "description": "The diameter of the collision shape of a copy at the height given in \"Height of a copy\" (it is scaled with the copy).", + "group": "Collisions", + "name": "TrunkDiameter" + }, + { + "value": "8000", + "type": "Number", + "unit": "Pixel", + "label": "Draw distance", + "description": "Copies shrink and are hidden at this distance from the camera.", + "group": "Rendering", + "name": "DrawDistance" + }, + { + "value": "true", + "type": "Boolean", + "label": "Cast shadows", + "group": "Rendering", + "name": "CastShadow" + }, + { + "value": "true", + "type": "Boolean", + "label": "Receive shadows", + "group": "Rendering", + "name": "ReceiveShadow" + }, + { + "value": "", + "type": "MultilineString", + "label": "Edits", + "description": "Where copies are added or erased, applied in order, as a JSON list, for example [{\"tool\": \"add\", \"x\": 0.3, \"y\": 0.4, \"radius\": 0.15}, {\"tool\": \"erase\", \"x\": 0.1, \"y\": 0.9, \"toX\": 0.8, \"toY\": 0.2, \"radius\": 0.02}]. Tools: add (density: from 0 to 1, 1 by default, how dense copies are compared to the Density property) and erase. strength (from 0 to 1) makes a stroke lighter. x, y and radius are fractions (0 to 1) of the object size (x is (scene X - object X) / object width). Add toX and toY to paint along a line (a row of trees, a path through a forest). With \"Copies grow: only where painted\", only the added areas have copies. Shown in the scene editor.", + "group": "Placement", + "advanced": true, + "name": "Edits" + }, + { + "value": "", + "type": "String", + "label": "Painted data", + "description": "Written by the brushes of the scene editor, in a compact format not meant to be read or written by hand: use Edits instead. Clear it to go back to the coverage and the edits.", + "advanced": true, + "name": "PaintData" + } + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Model", + "children": [ + { + "propertyName": "Model" + }, + { + "propertyName": "Size" + }, + { + "propertyName": "MinScale" + }, + { + "propertyName": "MaxScale" + }, + { + "propertyName": "RandomRotation" + }, + { + "propertyName": "RandomTint" + } + ] + }, + { + "folderName": "Placement", + "children": [ + { + "propertyName": "Density" + }, + { + "propertyName": "Seed" + }, + { + "propertyName": "Coverage" + }, + { + "propertyName": "Edits" + } + ] + }, + { + "folderName": "Ground", + "children": [ + { + "propertyName": "GroundLayer" + }, + { + "propertyName": "MaxSlope" + }, + { + "propertyName": "MinGroundZ" + }, + { + "propertyName": "MaxGroundZ" + } + ] + }, + { + "folderName": "Collisions", + "children": [ + { + "propertyName": "TrunkCollisions" + }, + { + "propertyName": "TrunkDiameter" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "DrawDistance" + }, + { + "propertyName": "CastShadow" + }, + { + "propertyName": "ReceiveShadow" + } + ] + }, + { + "propertyName": "PaintData" + } + ] + }, + "variants": [] } ] } diff --git a/examples/starting-first-person-shooter-horror/assets/StartingWall.png b/examples/starting-first-person-shooter-horror/assets/StartingWall.png index b3a097887..06278d6e5 100644 Binary files a/examples/starting-first-person-shooter-horror/assets/StartingWall.png and b/examples/starting-first-person-shooter-horror/assets/StartingWall.png differ diff --git a/examples/starting-first-person-shooter-horror/starting-first-person-shooter-horror.json b/examples/starting-first-person-shooter-horror/starting-first-person-shooter-horror.json index 8bd90ff9c..d7fc13677 100644 --- a/examples/starting-first-person-shooter-horror/starting-first-person-shooter-horror.json +++ b/examples/starting-first-person-shooter-horror/starting-first-person-shooter-horror.json @@ -1,7 +1,7 @@ { "firstLayout": "", "gdVersion": { - "build": 283, + "build": 284, "major": 5, "minor": 6, "revision": 0 @@ -270,6 +270,14 @@ "name": "assets/DarkSkybox_Right.png", "smoothed": true, "userAdded": false + }, + { + "file": "assets/StartingGround.png", + "kind": "image", + "metadata": "", + "name": "assets/StartingGround.png", + "smoothed": true, + "userAdded": false } ] }, @@ -286,12 +294,13 @@ "mangledName": "Game_32Scene", "name": "Game Scene", "r": 255, + "renderer3DWorldScale": 100, "standardSortMethod": true, "stopSoundsOnStartup": true, "title": "", "v": 255, "uiSettings": { - "grid": false, + "grid": true, "gridType": "rectangular", "gridWidth": 32, "gridHeight": 32, @@ -301,7 +310,7 @@ "gridOffsetZ": 0, "gridColor": 10401023, "gridAlpha": 0.8, - "snap": false, + "snap": true, "zoomFactor": 0.24672586957407638, "windowMask": false, "selectedLayer": "", @@ -319,6 +328,9 @@ }, { "name": "Player" + }, + { + "name": "Terrain" } ] }, @@ -332,10 +344,10 @@ "name": "PushableBox" }, { - "name": "Ground" + "name": "Monster" }, { - "name": "Monster" + "name": "Terrain" } ] } @@ -349,24 +361,6 @@ } ], "instances": [ - { - "angle": 0, - "customSize": true, - "depth": 30, - "height": 2240, - "layer": "", - "locked": true, - "name": "Ground", - "persistentUuid": "6a14e5ab-5c31-4692-8cd5-c50937fd60c6", - "width": 2432, - "x": -576, - "y": -832, - "z": -30, - "zOrder": 1, - "numberProperties": [], - "stringProperties": [], - "initialVariables": [] - }, { "angle": 0, "customSize": true, @@ -967,77 +961,25 @@ "numberProperties": [], "stringProperties": [], "initialVariables": [] + }, + { + "angle": 0, + "customSize": true, + "height": 2240, + "layer": "", + "locked": true, + "name": "Terrain", + "persistentUuid": "ca2e30df-d0db-4f6e-9096-ad4d66ce9f86", + "width": 2432, + "x": -576, + "y": -832, + "zOrder": 0, + "numberProperties": [], + "stringProperties": [], + "initialVariables": [] } ], "objects": [ - { - "assetStoreId": "", - "name": "Ground", - "persistentUuid": "f46cabd7-51f2-4d49-9571-59176d6eec16", - "type": "Scene3D::Cube3DObject", - "variables": [], - "effects": [], - "behaviors": [ - { - "name": "Physics3D", - "type": "Physics3D::Physics3DBehavior", - "object3D": "Object3D", - "bodyType": "Static", - "bullet": false, - "fixedRotation": false, - "shape": "Box", - "shapeOrientation": "Z", - "shapeDimensionA": 0, - "shapeDimensionB": 0, - "shapeDimensionC": 0, - "density": 1, - "friction": 0.3, - "restitution": 0.1, - "linearDamping": 0.1, - "angularDamping": 0.1, - "gravityScale": 1, - "layers": 17, - "masks": 17, - "shapeOffsetX": 0, - "shapeOffsetY": 0, - "shapeOffsetZ": 0, - "massCenterOffsetX": 0, - "massCenterOffsetY": 0, - "massCenterOffsetZ": 0, - "massOverride": 0 - } - ], - "content": { - "width": 1000, - "height": 1000, - "depth": 40, - "enableTextureTransparency": false, - "facesOrientation": "Y", - "frontFaceResourceName": "assets\\StartingGround.png", - "backFaceResourceName": "", - "backFaceUpThroughWhichAxisRotation": "X", - "leftFaceResourceName": "assets\\StartingWall.png", - "rightFaceResourceName": "assets\\StartingWall.png", - "topFaceResourceName": "assets\\StartingWall.png", - "bottomFaceResourceName": "assets\\StartingWall.png", - "frontFaceVisible": true, - "backFaceVisible": false, - "leftFaceVisible": true, - "rightFaceVisible": true, - "topFaceVisible": true, - "bottomFaceVisible": true, - "frontFaceResourceRepeat": true, - "backFaceResourceRepeat": false, - "leftFaceResourceRepeat": false, - "rightFaceResourceRepeat": false, - "topFaceResourceRepeat": false, - "bottomFaceResourceRepeat": false, - "materialType": "StandardWithoutMetalness", - "isCastingShadow": true, - "isReceivingShadow": true, - "tileScale": 0.2 - } - }, { "assetStoreId": "", "name": "Obstacle", @@ -1522,6 +1464,7 @@ { "assetStoreId": "", "name": "ControlsToggle", + "persistentUuid": "7a2516d4-e9f8-4b0c-aebe-c9f61ddcffd9", "type": "SpriteToggleSwitch::SpriteToggleSwitch", "variant": "Controls toggle", "variables": [], @@ -1932,6 +1875,59 @@ "Smoothing": 0.15, "ShadowBias": 0.0001 } + }, + { + "assetStoreId": "", + "name": "Terrain", + "persistentUuid": "9d0fa5ab-2a16-44a7-8e7b-bc72fc5845b0", + "type": "Terrain3D::Terrain3D", + "variant": "", + "variables": [], + "effects": [], + "behaviors": [ + { + "name": "Physics3D", + "type": "Physics3D::Physics3DBehavior", + "object3D": "Object3D", + "bodyType": "Static", + "bullet": false, + "fixedRotation": false, + "shape": "Box", + "meshShapeResourceName": "", + "shapeOrientation": "Z", + "shapeDimensionA": 0, + "shapeDimensionB": 0, + "shapeDimensionC": 0, + "shapeOffsetX": 0, + "shapeOffsetY": 0, + "shapeOffsetZ": 0, + "massCenterOffsetX": 0, + "massCenterOffsetY": 0, + "massCenterOffsetZ": 0, + "massOverride": 0, + "density": 1, + "friction": 0.3, + "restitution": 0.1, + "linearDamping": 0.1, + "angularDamping": 0.1, + "gravityScale": 1, + "layers": 17, + "masks": 17 + } + ], + "content": { + "SculptData": "2;256;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AQABAQIBAgIAAQEAAQEAAQIBAAICAQEEAQEAVQIAAQIAAQIBAgIDAgIAAQIAARAAAQEAAQECAQABAQIAAQECAQEFBAACAgABAQADAQABAQACAQAFAgAEAQIAAQICAgEDAgIAAQECBgABAwABCAMAAQMCBAMGAwACAQEMAQMHAAICAAEBGhkEAwIABAECAQYDAQAEAQIABAEGAwABAgMCAAECAwIAAgEAAgQFAgQAAQEDAgAEAgMEAAEDAgIAAQUCAggHAAIBAgEAAgQBAQEAVwICAAEBAgIAAgQFAgIAAQ4AAQEAAwEAAQQDAAIFAQICAAECAAEBAQQDAQEAFgIEAQADAgEGAgcCCAEBAAEBBgECAAECBQACDAEJAAEDAgACAR4ZAgUCAgEBAAEBAgADAgECAQQAAQUEAQAEAQABBAMCAAEBAAMBBAEBBgMAAgIAAQMEAQEEAwIBAgABAgMAAQQDAgAFAwIAAgEAXAQAAwEGAwQBAAEEDgECAQABAwQCBQQHAQIGAwIBAQADAwAdAgIAAgEGBgcAAQYBAAEBAAECAgABAQABBAUAAQoDBQEAAwIAAQEEAQABAwIAAQIBAAMBAgACAgMAAQIBAgABAQEEAAQBAgMEAQIAAgEAAwECAgABAQACAQABBAMAAQIAAQMCAgEABgIAAQEBAF8EAgACAQABAgACAgwAAgECAgMAAQEFAgIAAwEAAgEBACICAgABAgQFAgYFBAEAAgIAAQIDAAIEAgMBAwABAgACHBsBAgACAgMEAAIDAAECAAEBAgACAgMAAQICAQACAwIBBgMEAwQFAgACAQABAgEEAQABAQABBgMBAgIDAAECAAEBAgECAAEBAAMBAGICAAIEAgEBAgACDgABBQQBAQQHAQQAAQIAAQMCAQEBACgECAUAAQYDAAEDCAMAAQQDCAUDDAEFAAEDAQIBAhgVAwQBAgEBAAICAAEBAgEAAQIBAAICAQIBAAICAwIBBAMAAQIAAgECAAECAgEDAgEEAwABAgEBBAEABwQFAGcCAgECAAECAAEBAgoBBAABAwIFAAECAAIBAAIDAC0GAQABCAUGBwQCAQYJBAEAAQwCDRAFBQUCBAEBBAwCBAkJAQQCBgcBAgMCAw4PAgECAQICAAIDBgABAQMCAQABAgABAQABAgECAgMCAAEBAgEAAQQDBgMCBQIBAGkCAAECAQQBAAEEAQgEAQABAQMAAQIMDQIAAQMBAC8EAgICAgYJBBQbCAABDg8IFAEkPygBKzoHKwMOHQEEJBcGAw02AysIEhECAgMCEBIjHAgFHx4GHxgBFRgbHBUOERYVEA0KDxQDDQwJCA0KAQMAAQQFAGkEAQIAAgIAAQIMAwIFAQQAAgUIAwMAMgIBBgUIAwABBQoUGQUKBgkDAgQWChkAARliRwUAAQoFEBEFEAsRDhYGIQwLNCMFEgcOCwcGHCMBAAEGAgMAAQIFAQACAQIBBAIKDwIKCwACBAMAAQQAAQMAawIAAQIBAAMIAgUBBAABAQABAwA3AgYHEgABBxQRHgMIDwABFDE+OQNAOzw3QhMaDQgpKAMREhsHDgsOCAUrJgQjCxIRAAICAQABIB8AARoZFhUWERATEA8MCwIBAAEEAwgHAgEAAQIBAGwEAQECAgECCgUBBAQFAgEDAPIBBAACAQIAAQQBBAIFAgABAQEA8gECAgIAAQEAAQIEAgMAAgMA9QECAAICAAEGAAEDAgABAwD2AQIAAQECAgYBAAIDAPgBBAEAAgICAgUA+gEEAAEDBAEBAPoBAgACAgQAAQMA+wECAAICAQD8AQIBAgIBAPwBAgIBAP8BAgCBAgIA/wECAQD/AQIAgQICAIACAgCBBAIAjBoBAIIGAQCABAECAP8BAgCCBAEA/wEBAgD/AQIDAIACAQD/AQMEAP8BAQD/AQEAggICAP0BAQEEAQCAAgEA/gECAQUA/gEBAwgA/AEBAAEDBgD9AQIDAgEBAPwBAQICAwD9AQMEAwYA/AEBBAMCAAECAPsBAgMCAgABBwD7AQECAgIHBgD7AQICAwUEAQD8AQEDBgEBAP0BBAkEBAD7AQEAAQEIAwD8AQMAAgICAgD6AQUEAAEEAwABAQD6AQIBAAEDAgEBAPoBAgIDAAEDAAEEAPoBAwABBAABAQYDAPoBAgMDAggBAwD6AQMAAQYCBwEGAPoBAgQDBwYBAPsBBAUCAgECAwD6AQECAQYCBQEA+gEDBAQDBwgGAPoBBAEFAAEIAQD8AQMEAgEBAPsBAwYAAQEAAgMA/gEBAAEEAPoBBAEDAAECBAEA+gEDAAEEAgIFAQD9AQEBAgD+AQEBAgQDAgIA+AEBAwIEAAIHAgYA9wEBAAEEAAMCAAEBAAECAPYBAQQAAgIBAQECAwQA9gEEAAEBAgEAAgECAQMGAPUBAgECAAEBBAMAAQECAAEDBAD0AQIAAQIDBAMEAwIAAQIBAAECAPQBAgECAQIAAQQDAQIAAQEAAQIA8gECAAIBAgEAAQIBAAEBAgD4AQIDAgEAAQECAwYBAPQBAgEBAgAEAwQAAQMEAAEBAgDzAQIAAQEAAwQDAAEBAgD2AQIDBAABAgABAgEAAQIDAgD0AQEAAQIAAQICAgABAQIBAPcBAQICAQEA+wECAAIDAgIBAgIAAQECAQD1AQIAAQIBAgACAgEEAQEA9AEEBQICAgMAAQIBAgABAQIAAQEA8gEBBgABAQECAAECAAIBAAEBAgDzAQICAwABAgEAAQMAAQYBAAECAQD1AQICAQECAAIFBAIBAPYBAgMCAgIAAwEBAgD3AQIAAgIBAgABAQIBAPYBAgADBAEAAQEA+wECAAICAQABAgEA+AECAQQCAAEBAgEA+QECAAEBAAICAQD7AQIDAgABAQD9AQIAgAIBAIACAgCAAgIAgAICAP8BAgMAgAICAIEEAgCDCAIA5hoDAAICAQABAgIA+QEBAAMCAgEBAAECAAEDBAD0AQQAAgMBAwACAgABAgIBAAICAPUBAgABAQABAwEBAgABBgABAQIA8AEBAgECAQQCAQACAQACBwABAgQAAQIBAgDtAQMCAgIFAAECBAMCAAECAgEDAwABBAQBAOwBAwABBAUEBgEAAQEEAAEBAQABAgABAwABBQABCADrAQcCBAEEAQUEAAICAQYDAgECAgABAQMFAgYCAOgBAwIBAQABAQYCAAIBAgMEAwQCAQACAgMBAwIGAOgBAgEAAwEBAAEBAgQAAQMCAAEBBAEEAAECAQMDAQYCAOYBBAACAQACAQABAgEFAAEEAAEBAAICAAECAAECAAEBAQMGAAEBAgDjAQECAAEEAQAFAgEBAAQBAQABAgABAgIDAQMCBgECAOYBAgABAQAEAgEABAIBAQACAgABAgEDAgMGAQIA4QEBAAECAwICAAYCAQACAQICAQAFAgEBAgcIAQIA4gEBBAEAAQIABAECAgABAQABAQABBAMABAICAwEGCQgBAOABAgACAwADAgABAgECAQACAgEAAgECAAMCAQADAwQHCADfAQIAAQEAAQIBAAIDAgQEAQIAAgICAAECAAMBAAEBAAICAAIBAAEHAN8BCgABAgACAgEAAQIBBQECAQACAQACAQDinQEBAIACAQD/AQUA/wEDAAEBAP0BAQMAAQMA/AEBAwACAgECAPkBAwABAwQAAgECAPkBAwECAQABAQMEAPcBAQMCAQIBAwQDAQYA9QEBAQEAAQIDBAEBAwABBAD1AQUAAgIDBAMBAAEDAAECAgDyAQEFBAACAQABAwMEBQUEAQQA8QEDAQQAAQEBAAEHAgADAwEBAgYA7wEDAgABAwEDAAIEAQIBAAEJBAICAgDtAQEBAAEBAAEBAQIAAQUGAQEAAQIFAAQGAOwBAQMBAAEBAQQBAgIHAgIBAwcCAAEDBgICAOgBAQEDAAEBAgABAQYDAgcCBAEBAQEJAAEEAgMCBAIA5gEBAwEAAgIAAQEEAwEDBgEAAgUAAQoA6aECBAACAQIAAQEBAPkBAgADAQECAgABAQABBAMA9AEDAAIEAgQAAgEAAQEBAgACAQD1AQEAAQIAAQQCAgEAAQUAAQIBAPABAgECAQIDAQIAAgIAAggAAQEDAAEBAgEA7QEEAQEBBgABAQMEAQABAQECBAQAAQMDAgDsAQQAAQMGAwUCAAECAwABAgIAAQEAAQQAAQYAAQcA6wEIAQMCAwIGAwACAQIFBAECAQEAAQIEBgEFAQDoAQQBAgIAAQIFAQACAgEEAwQDAQIAAgEEAgQBBQDoAQECAAMCAgABAgEDAAEEAQABAgMCAwABAQIEBAIFAQDmAQMAAgIAAgIAAQECBgABAwABAgACAQABAQABAQABAgIEBQABAgEA4wECAQABAwIABQECAgAEAgIAAQEAAQEBBAIEAQUCAQDmAQEAAQIABAECAAQBAgIAAgEAAQECBAEEBQIBAOEBAgABAQQBAQAGAQIAAgIBAQIABQECAgEIBwIBAOIBAgMCAAEBAAQCAQEAAQIAAQIAAQMEAAQBAQQCBQoHAgDgAQEAAgQAAwEAAQECAQIAAgECAAICAQADAQIAAwQDCAcA3wEBAAECAAEBAgACBAEDAwIBAAIBAQABAQADAgABAgACAQACAgABCADfAQkAAQEAAgECAAEBAgYCAQIAAgIAAgIA9L8BqgERBw8JCQkHCQcJAwUFBQUDBQACAQEBAQEBAQABAQABAQEABAIAAQIAAgIBAgABAgEAAQQEAQIGBAYCAgoECAIGAgoKBAgGAgIODgABBAMDAQEBIAUCAAIHAQcCAQUBBQABAQUBAAIDAAEDAwEDAQEAAQMDAgMAAQMAAQEBAwIFAgUCAwMCAwABBQACAwABAQABBQABAQMBAAEBAwABAgEHAQACAwEBAgABAwIDAQBVAgIEBAYGCggICggKDhATBgUGAQIAAwICAQABAgACBAQBAQACAQADAgECAAICAAYBAAIDBAECAAEEAAEDAwQAAQUAAgQCCQYBBAUEAQUGAQABBAABCwABDAIBAgABAgIlIAECAAECAQQDAgEBBgIDAQQAAQEAAQEEAAEBAAECAAQBAgECAgMCAgMCAQIBAgIDBAACAwIEBQIAAQIBAgABAgECAQEAAQIAAQIAAQECAAIBAgIDAgIAVQEAAQEAAQECAQEEAQEAAQEAAQ8AAQIAAQIBAgABAgEAAQIBAgIGAwACAQABAgADAgABAgACAgAFAQAEAgEAAQEBAQIEAQEAAQIBBQABBAABBwQAAQQBAwQFBAACAgILAgQIAAIBAAECGRoDBAEABAIBAgUEAgAEAgEABAIFBAABAQQBAAEBBAEAAgIAAgMGAQMAAQIEAQAEAQQDAAEEAQEAAQYBAQcIAAICAQIAAgMCAgIAVwEBAAECAQEAAgMGAQEAAQ0AAQIAAwIAAQMEAAIGAgEBAAEBAAECAgMEAgIAFgEDAgADAQIFAQgBBwICAAECBQIBAAEBBgACCwIKAAEEAQACAh0aAQYBAQICAAECAQADAQIBAgMAAQYDAgAEAgABAwQBAAECAAMCAwICBQQAAgEAAQQDAgIDBAECAQABAQQAAQMEAQAFBAEAAgIAXAMAAwIFBAMCAAEDDQIBAgABBAMBBgMIAgEFBAECAgADBAAdAQEAAgIFBQgAAQUCAAECAAEBAQABAgABAwYAAQkEBgIAAwEAAQIDAgABBAEAAQECAAMCAQACAQQAAQECAQABAgIDAAQCAQQDAgEAAgIAAwIBAQABAgACAgABAwQAAQEAAQQBAQIABgEAAQICAF8DAQACAgABAQACAQsAAgIBAQQAAQIGAQEAAwIAAgICACIBAQABAQMGAQUGAwIAAgEAAQEEAAIDAQQCBAABAQACGxwCAQACAQQDAAIEAAEBAAECAQACAQQAAQEBAgACBAECBQQDBAMGAQACAgABAQIDAgABAgABBQQCAQEEAAEBAAECAQIBAAECAAMCAGIBAAIDAQICAQACDQABBgMCAgMIAgMAAQEAAQQBAgICACgDBwYAAQUEAAEEBwQAAQMEBwYECwIGAAEEAgECARcWBAMCAQICAAIBAAECAQIAAQECAAIBAgECAAIBBAECAwQAAQEAAgIBAAEBAQIEAQIDBAABAQICAwIABwMGAGcBAQIBAAEBAAECAQkCAwABBAEGAAEBAAICAAIEAC0FAgABBwYFCAMBAgUKAwIAAQsBDg8GBgYBAwICAwsBAwoKAgMBBQgCAQQBBA0QAQIBAgEBAAIEBQABAgQBAgABAQABAgABAQIBAQQBAAECAQIAAQMEBQQBBgECAGkBAAEBAgMCAAEDAgcDAgABAgQAAQELDgEAAQQCAC8DAQEBAQUKAxMcBwABDRAHEwIjQCcCLDkILAQNHgIDIxgFBA41BCwHERIBAQQBDxEkGwcGIB0FIBcCFhccGxYNEhUWDw4JEBMEDgsKBw4JAgQAAQMGAGkDAgEAAgEAAQELBAEGAgMAAgYHBAQAMgECBQYHBAABBgkTGgYJBQoEAQMVCRoAARphSAYAAQkGDxIGDwwSDRUFIgsMMyQGEQgNDAgFGyQCAAEFAQQAAQEGAgACAgECAwEJEAEJDAACAwQAAQMAAQQAawEAAQECAAMHAQYCAwABAgABBAA3AQUIEQABCBMSHQQHEAABEzI9OgQ/PDs4QRQZDgcqJwQSERwIDQwNBwYsJQMkDBESAAIBAgABHyAAARkaFRYVEg8UDxALDAECAAEDBAcIAQIAAQECAGwDAgIBAQIBCQYCAwMGAQIEAPIBAwACAgEAAQMCAwEGAQABAgIA8gEBAQEAAQIAAQEDAQQAAgQA9QEBAAIBAAEFAAEEAQABBAD2AQEAAQIBAQUCAAIEAPgBAwIAAgEBAQYA+gEDAAEEAwICAPoBAQACAQMAAQQA+wEBAAIBAgD8AQECAQECAPwBAQECAP8BAQCBAgEA/wEBAgD/AQEAgQIBAIACAQCBBAEAjBoCAIIGAgCABAIBAP8BAQCCBAIA/wECAQD/AQEEAIACAgD/AQQDAP8BAgD/AQIAggIBAP0BAgIDAgCAAgIA/gEBAgYA/gECBAcA/AECAAEEBQD9AQEEAQICAPwBAgEBBAD9AQQDBAUA/AECAwQBAAEBAPsBAQQBAQABCAD7AQIBAQEIBQD7AQEBBAYDAgD8AQIEBQICAP0BAwoDAwD7AQIAAQIHBAD8AQQAAgEBAQD6AQYDAAEDBAABAgD6AQECAAEEAQICAPoBAQEEAAEEAAEDAPoBBAABAwABAgUEAPoBAQQEAQcCBAD6AQQAAQUBCAIFAPoBAQMECAUCAPsBAwYBAQIBBAD6AQIBAgUBBgIA+gEEAwMECAcFAPoBAwIGAAEHAgD8AQQDAQICAPsBBAUAAQIAAgQA/gECAAEDAPoBAwIEAAEBAwIA+gEEAAEDAQEGAgD9AQICAQD+AQICAQMEAQEA+AECBAEDAAIIAQUA9wECAAEDAAMBAAECAAEBAPYBAgMAAgECAgIBBAMA9gEDAAECAQIAAgIBAgQFAPUBAQIBAAECAwQAAQIBAAEEAwD0AQEAAQEEAwQDBAEAAQECAAEBAPQBAQIBAgEAAQMEAgEAAQIAAQEA8gEBAAICAQIAAQECAAECAQD4AQEEAQIAAQIBBAUCAPQBAQICAQAEBAMAAQQDAAECAQDzAQEAAQIAAwMEAAECAQD2AQEEAwABAQABAQIAAQEEAQD0AQIAAQEAAQEBAQABAgECAPcBAgEBAgIA+wEBAAIEAQECAQEAAQIBAgD1AQEAAQECAQACAQIDAgIA9AEDBgEBAQQAAQECAQABAgEAAQIA8gECBQABAgIBAAEBAAICAAECAQDzAQEBBAABAQIAAQQAAQUCAAEBAgD1AQEBAgIBAAIGAwECAPYBAQQBAQEAAwICAQD3AQEAAgECAQABAgECAPYBAQADAwIAAQIA+wEBAAIBAgABAQIA+AEBAgMBAAECAQIA+QEBAAECAAIBAgD7AQEEAQABAgD9AQEAgAICAIACAQCAAgEAgAIBAP8BAQQAgAIBAIEEAQCDCAEA7NoBAgCAAgIA/wEGAP8BBAABAgD9AQIEAAEEAPwBAgQAAgECAQD5AQQAAQQDAAICAQD5AQQCAQIAAQIEAwD3AQIEAQIBAgQDBAIFAPUBAgICAAEBBAMCAgQAAQMA9QEGAAIBBAMEAgABBAABAQEA8gECBgMAAgIAAQQEAwYGAwIDAPEBBAIDAAECAgABCAEAAwQCAgEFAO8BBAEAAQQCBAACAwIBAgABCgMBAQEA7QECAgABAgABAgIBAAEGBQICAAEBBgAEBQDsAQIEAgABAgIDAgEBCAEBAgQIAQABBAUBAQDoAQICBAABAgEAAQIFBAEIAQMCAgICCgABAwEEAQMBAOYBAgQCAAIBAAECAwQCBAUCAAIGAAEJAIGEBACBhAQAgYQEAIGEBACBhAQ=", + "Layer1Texture": "assets/StartingGround.png", + "Layer1Color": "255;255;255", + "HeightBlending": 0.5, + "TextureVariation": 0, + "TextureSize": 128, + "CliffLayer": "3", + "Layer2Texture": "", + "Layer3Texture": "", + "CliffAngle": 45, + "CastShadow": false + } } ], "objectsFolderStructure": { @@ -1956,7 +1952,7 @@ "objectName": "Obstacle" }, { - "objectName": "Ground" + "objectName": "Terrain" }, { "objectName": "Flashlight" @@ -2526,8 +2522,8 @@ "Monster", "Pathfinding", "", - "RandomInRange(Ground.BoundingBoxLeft(), Ground.BoundingBoxRight())", - "RandomInRange(Ground.BoundingBoxTop(), Ground.BoundingBoxBottom())" + "RandomInRange(Terrain.BoundingBoxLeft(), Terrain.BoundingBoxRight())", + "RandomInRange(Terrain.BoundingBoxTop(), Terrain.BoundingBoxBottom())" ] } ] @@ -2826,7 +2822,7 @@ "parameters": [ "SpawnXPosition", "=", - "RandomInRange(Ground.BoundingBoxLeft(), Ground.BoundingBoxRight())" + "RandomInRange(Terrain.BoundingBoxLeft(), Terrain.BoundingBoxRight())" ] }, { @@ -2836,7 +2832,7 @@ "parameters": [ "SpawnYPosition", "=", - "RandomInRange(Ground.BoundingBoxTop(), Ground.BoundingBoxBottom())" + "RandomInRange(Terrain.BoundingBoxTop(), Terrain.BoundingBoxBottom())" ] } ], @@ -8696,7 +8692,6 @@ "name": "PlatformerMultitouchMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -8950,7 +8945,6 @@ "name": "Platformer3DMultitouchMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -9125,7 +9119,6 @@ "name": "Shooter3DMultitouchMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -9289,7 +9282,6 @@ "name": "FirstPersonMultitouchMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -10494,7 +10486,6 @@ "name": "PhysicsCar3DMultitouchMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -10707,7 +10698,6 @@ "name": "TopDownMultitouchMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -45807,6 +45797,7802 @@ "variants": [] } ] + }, + { + "author": "", + "category": "General", + "dimension": "3D", + "extensionNamespace": "", + "fullName": "3D terrain", + "gdevelopVersion": ">=5.5.222", + "helpPath": "", + "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLXRlcnJhaW4iIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTQsNkwxMC4yNSwxMUwxMy4xLDE0LjhMMTEuNSwxNkM5LjgxLDEzLjc1IDcsMTAgNywxMEwxLDE4SDIzTDE0LDZaIiAvPjwvc3ZnPg==", + "name": "Terrain3D", + "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", + "shortDescription": "A ground with hills and valleys that can be sculpted and painted in the editor, with collisions.", + "version": "0.1.0", + "description": [ + "A 3D terrain object for open worlds, adventure or racing games: hills, mountains, islands or valleys, with painted layers of grass, dirt, rock or sand.", + "", + "- **Sculpt and paint in the scene editor**: select a terrain to show the tools (raise, lower, smooth, flatten and paint 4 layers).", + "- **Start from a relief** (hills, mountains, island) or **from a heightmap image**.", + "- **Steep slopes automatically show rock** (or any layer).", + "- **Natural borders between layers**: stones and tufts of a texture cover the gaps of the other where layers meet (height blending), and the repetition of textures is made less visible (texture variation).", + "- **Physics**: add the **3D physics** behavior to the terrain. Characters and objects collide with the ground exactly. The terrain is always static.", + "- **Pathfinding**: add the **floor/obstacle for pathfinding (navmesh based)** behavior to the terrain, so that characters with the navmesh pathfinding behavior walk on its hills and around its cliffs (in recent GDevelop versions).", + "- **Shape it with the Edits property**: a list of raise, flatten, smooth and paint strokes written in JSON, shown in the scene editor. It's readable and can be changed by hand or by an AI agent, like the relief, the heightmap and the other properties. Actions only change the terrain during the game.", + "- **Change it during the game** with actions (raise, lower, flatten, smooth, paint, along a line for paths and rivers) and read the ground height, slope or layer with expressions.", + "- **Grass**: add a **terrain grass** object over the terrain. Thousands of blades sway in the wind, grow where a layer (like grass) is painted and where the ground is not too steep, and are pushed aside by characters. Sculpt or paint the terrain and the grass follows.", + "- **Forests, rocks and bushes**: add a **scattered models** object over the terrain. It places hundreds or thousands of copies of a 3D model (trees, rocks, bushes, flowers...) on the ground and draws them all at once, which is much faster than an object for each. Copies grow where a layer is painted, away from steep slopes, and follow the ground when it's sculpted. Add or erase them with the brushes of the scene editor or with the Edits property, give trees trunk collisions, and cut them down with actions. Ready-made forests, rocks and bushes are in the asset store.", + "- **Tunnels and caves**: make holes in the ground (and fill them again) with the brushes, the Edits property or actions, and put a 3D model of the tunnel or the cave (with a mesh collision shape) under the ground. Nothing is drawn in holes, and characters, cars and rays go through them.", + "- **Made for large worlds**: the terrain is split in chunks that are hidden when off-screen and simplified when far away, and collisions are updated only where the ground changes." + ], + "origin": { + "identifier": "Terrain3D", + "name": "gdevelop-extension-store" + }, + "tags": [ + "3d", + "terrain", + "ground", + "heightmap", + "landscape", + "open world", + "grass", + "foliage", + "forest", + "trees", + "tunnel", + "cave" + ], + "authorIds": [], + "dependencies": [], + "globalVariables": [], + "sceneVariables": [], + "eventsFunctions": [ + { + "description": "Define helper classes JavaScript code.", + "fullName": "Define helper classes", + "functionType": "Action", + "name": "DefineHelperClasses", + "private": true, + "sentence": "Define helper classes JavaScript code", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "if (gdjs.__terrain3DExtension) {", + " return;", + "}", + "", + "// Tested by the gameplay tests of the \"3D island terrain\" example (GDevelop-examples repository).", + "", + "const groundsByScene = new WeakMap();", + "/**", + " * The grounds of a scene (the terrains), followed by the terrain grass.", + " *", + " * A ground has: `getVersion()` (changing when the ground changes),", + " * `containsPoint(x, y)`, `getHeightAt(x, y)` (Z position of the ground),", + " * `getSlopeAt(x, y)` (in degrees), `getLayerWeightAt(x, y, layer)` (how", + " * much a painted layer, from 1 to 4, is there, from 0 to 1) and optionally", + " * `getTintAt(x, y)` (the painted tint, as red, green and blue from 0 to 255).", + " * @param {gdjs.RuntimeScene} runtimeScene", + " * @returns {Set}", + " */", + "const getGrounds = (runtimeScene) => {", + " let grounds = groundsByScene.get(runtimeScene);", + " if (!grounds) {", + " grounds = new Set();", + " groundsByScene.set(runtimeScene, grounds);", + " }", + " return grounds;", + "};", + "", + "/**", + " * @param {number} seed", + " * @returns {() => number} Random numbers between 0 (included) and 1 (excluded), the same for the same seed.", + " */", + "const createRandom = (seed) => {", + " let state = seed >>> 0 || 1;", + " return () => {", + " state ^= state << 13;", + " state ^= state >>> 17;", + " state ^= state << 5;", + " return (state >>> 0) / 4294967296;", + " };", + "};", + "", + "/**", + " * Find the highest ground at a position: grounds can overlap (like an island over a sea floor).", + " * @param {Iterable} grounds", + " * @param {{ground: object | null, z: number}} result Set to the ground (null if there is none) and its Z position.", + " */", + "const findHighestGround = (grounds, x, y, result) => {", + " result.ground = null;", + " for (const ground of grounds) {", + " if (!ground.containsPoint(x, y)) continue;", + " const z = ground.getHeightAt(x, y);", + " if (!result.ground || z > result.z) {", + " result.ground = ground;", + " result.z = z;", + " }", + " }", + " return result;", + "};", + "", + "// The grounds or the objects placed on them can change every frame (while", + "// sculpting or moving them in the editor): things are placed again at most this often.", + "const PLACEMENT_DELAY = 0.3;", + "", + "/**", + " * Tells when things placed on the grounds of a scene (grass blades, scattered", + " * models) must be placed again: after the grounds or the given properties", + " * changed, at most every `PLACEMENT_DELAY` seconds.", + " */", + "class GroundPlacement {", + " /** @param {gdjs.RuntimeScene} runtimeScene */", + " constructor(runtimeScene) {", + " this.grounds = getGrounds(runtimeScene);", + " this.placedFrom = '';", + " this.timeSincePlacement = 0;", + " }", + "", + " /**", + " * @param {number} elapsedSeconds", + " * @param {string} propertiesKey Changes when the things must be placed again.", + " */", + " shouldPlaceAgain(elapsedSeconds, propertiesKey) {", + " this.timeSincePlacement += elapsedSeconds;", + " if (this.placedFrom && this.timeSincePlacement < PLACEMENT_DELAY) return false;", + " let placementKey = propertiesKey;", + " for (const ground of this.grounds) placementKey += '|' + ground.getVersion();", + " if (placementKey === this.placedFrom) return false;", + " this.placedFrom = placementKey;", + " this.timeSincePlacement = 0;", + " return true;", + " }", + "", + " placeAgain() {", + " this.placedFrom = '';", + " }", + "}", + "", + "/**", + " * Replace parts of the shader code of a Three.js material, with an error if", + " * a part is missing (for example after an update of Three.js).", + " * @param {string} shaderCode", + " * @param {Array<[string, string]>} replacements The searched code and its replacement.", + " * @param {string} materialName", + " */", + "const patchShaderCode = (shaderCode, replacements, materialName) => {", + " for (const [searchedCode, newCode] of replacements) {", + " if (!shaderCode.includes(searchedCode)) {", + " console.error(`${materialName}: \"${searchedCode}\" was not found in the shader, which won't look as expected.`);", + " }", + " shaderCode = shaderCode.replace(searchedCode, newCode);", + " }", + " return shaderCode;", + "};", + "", + "// Size of the object inner area. Instances are scaled from it, so these only", + "// define the default size of a new terrain.", + "const AREA_SIZE = 4096;", + "const AREA_DEPTH = 600;", + "", + "// Terrains have at most 8 x 8 chunks (so at most 64 draw calls), of at least", + "// 32 x 32 cells.", + "const MIN_CHUNK_CELLS = 32;", + "const MAX_CHUNKS_PER_SIDE = 8;", + "/** @param {number} resolution */", + "const getChunkCells = (resolution) => Math.max(MIN_CHUNK_CELLS, resolution / MAX_CHUNKS_PER_SIDE);", + "// Grid steps of each level of detail, and the distance (in chunk sizes)", + "// from which each one is used.", + "const LOD_STEPS = [1, 2, 4];", + "const LOD_DISTANCES = [0, 3, 6];", + "", + "const LAYER_COUNT = 4;", + "const RESOLUTIONS = [64, 128, 256, 512, 1024];", + "const DEFAULT_RESOLUTION = 256;", + "// Each undo step is a copy of the data: fewer are kept for big terrains.", + "const UNDO_STEPS_SAMPLES = 20 * 257 * 257;", + "", + "const clamp01 = (value) => (value < 0 ? 0 : value > 1 ? 1 : value);", + "const clampInteger = (value, min, max) =>", + " value < min ? min : value > max ? max : Math.floor(value);", + "", + "/**", + " * A rectangle of grid samples, inclusive. Empty when `minI > maxI`.", + " */", + "class SampleRectangle {", + " constructor() {", + " this.clear();", + " }", + "", + " clear() {", + " this.minI = Number.MAX_SAFE_INTEGER;", + " this.minJ = Number.MAX_SAFE_INTEGER;", + " this.maxI = -1;", + " this.maxJ = -1;", + " }", + "", + " isEmpty() {", + " return this.minI > this.maxI;", + " }", + "", + " /** @param {SampleRectangle} other */", + " add(other) {", + " if (other.isEmpty()) return;", + " this.minI = Math.min(this.minI, other.minI);", + " this.minJ = Math.min(this.minJ, other.minJ);", + " this.maxI = Math.max(this.maxI, other.maxI);", + " this.maxJ = Math.max(this.maxJ, other.maxJ);", + " }", + "", + " set(minI, minJ, maxI, maxJ) {", + " this.minI = minI;", + " this.minJ = minJ;", + " this.maxI = maxI;", + " this.maxJ = maxJ;", + " return this;", + " }", + "}", + "", + "/**", + " * The heights (between 0 and 1) and the painted layers weights of a square", + " * grid of `size * size` samples.", + " */", + "class TerrainData {", + " /** @param {number} resolution The number of cells on each side. */", + " constructor(resolution) {", + " this.resolution = resolution;", + " this.size = resolution + 1;", + " this.heights = new Float32Array(this.size * this.size);", + " /** 4 weights (0-255) per sample, one for each layer. */", + " this.splat = new Uint8Array(this.size * this.size * LAYER_COUNT);", + " this.fillLayer(0);", + " /**", + " * 4 bytes per sample: a tint multiplying the layers (red, green and", + " * blue, white for none) and the wetness (0 for dry).", + " */", + " this.colors = new Uint8Array(this.size * this.size * 4);", + " this.clearColors();", + " }", + "", + " clearColors() {", + " this.colors.fill(255);", + " for (let index = 3; index < this.colors.length; index += 4) {", + " this.colors[index] = 0;", + " }", + " }", + "", + " /** @param {number} layer */", + " fillLayer(layer) {", + " this.splat.fill(0);", + " for (let index = layer; index < this.splat.length; index += LAYER_COUNT) {", + " this.splat[index] = 255;", + " }", + " }", + "", + " getHeight(i, j) {", + " const last = this.resolution;", + " const clampedI = i < 0 ? 0 : i > last ? last : i;", + " const clampedJ = j < 0 ? 0 : j > last ? last : j;", + " return this.heights[clampedJ * this.size + clampedI];", + " }", + "", + " /**", + " * @param {number} gridX", + " * @param {number} gridY", + " * @returns {number} The height interpolated on the triangles that are drawn.", + " */", + " sampleHeight(gridX, gridY) {", + " const x = Math.min(Math.max(gridX, 0), this.resolution);", + " const y = Math.min(Math.max(gridY, 0), this.resolution);", + " const i = Math.min(Math.floor(x), this.resolution - 1);", + " const j = Math.min(Math.floor(y), this.resolution - 1);", + " const fx = x - i;", + " const fy = y - j;", + " const topRight = this.getHeight(i + 1, j);", + " const bottomLeft = this.getHeight(i, j + 1);", + " if (fx + fy <= 1) {", + " const topLeft = this.getHeight(i, j);", + " return topLeft + fx * (topRight - topLeft) + fy * (bottomLeft - topLeft);", + " }", + " const bottomRight = this.getHeight(i + 1, j + 1);", + " return (", + " bottomRight +", + " (1 - fx) * (bottomLeft - bottomRight) +", + " (1 - fy) * (topRight - bottomRight)", + " );", + " }", + "", + " /** @returns {number} The weight (0-1) of a layer, interpolated between samples. */", + " sampleLayerWeight(gridX, gridY, layer) {", + " const x = Math.min(Math.max(gridX, 0), this.resolution);", + " const y = Math.min(Math.max(gridY, 0), this.resolution);", + " const i = Math.min(Math.floor(x), this.resolution - 1);", + " const j = Math.min(Math.floor(y), this.resolution - 1);", + " const fx = x - i;", + " const fy = y - j;", + " const weight = (i, j) => this.splat[(j * this.size + i) * LAYER_COUNT + layer];", + " const top = weight(i, j) + fx * (weight(i + 1, j) - weight(i, j));", + " const bottom = weight(i, j + 1) + fx * (weight(i + 1, j + 1) - weight(i, j + 1));", + " return (top + fy * (bottom - top)) / 255;", + " }", + "", + " /**", + " * @param {number} channel 0, 1 and 2 for the tint, 3 for the wetness.", + " * @returns {number} The channel (0-1), interpolated between samples.", + " */", + " sampleColor(gridX, gridY, channel) {", + " const x = Math.min(Math.max(gridX, 0), this.resolution);", + " const y = Math.min(Math.max(gridY, 0), this.resolution);", + " const i = Math.min(Math.floor(x), this.resolution - 1);", + " const j = Math.min(Math.floor(y), this.resolution - 1);", + " const fx = x - i;", + " const fy = y - j;", + " const value = (i, j) => this.colors[(j * this.size + i) * 4 + channel];", + " const top = value(i, j) + fx * (value(i + 1, j) - value(i, j));", + " const bottom = value(i, j + 1) + fx * (value(i + 1, j + 1) - value(i, j + 1));", + " return (top + fy * (bottom - top)) / 255;", + " }", + "", + " /**", + " * @param {number} index The index of a sample.", + " * @returns {boolean} true if no layer is painted on the sample: it's a hole.", + " */", + " isHole(index) {", + " const offset = index * LAYER_COUNT;", + " return (this.splat[offset] | this.splat[offset + 1] | this.splat[offset + 2] | this.splat[offset + 3]) === 0;", + " }", + "", + " /**", + " * @returns {number} The sum of the layer weights (0-1), interpolated between samples: there is a", + " * hole where it's under 0.5, like what is drawn.", + " */", + " sampleSolidity(gridX, gridY) {", + " let solidity = 0;", + " for (let layer = 0; layer < LAYER_COUNT; layer++) solidity += this.sampleLayerWeight(gridX, gridY, layer);", + " return solidity;", + " }", + "", + " /** @returns {number} The index (from 0) of the layer painted the most. */", + " getDominantLayer(gridX, gridY) {", + " const i = clampInteger(Math.round(gridX), 0, this.resolution);", + " const j = clampInteger(Math.round(gridY), 0, this.resolution);", + " const offset = (j * this.size + i) * LAYER_COUNT;", + " let dominantLayer = 0;", + " for (let layer = 1; layer < LAYER_COUNT; layer++) {", + " if (this.splat[offset + layer] > this.splat[offset + dominantLayer]) {", + " dominantLayer = layer;", + " }", + " }", + " return dominantLayer;", + " }", + "", + " /** @param {TerrainData} other */", + " copyFrom(other) {", + " this.heights.set(other.heights);", + " this.splat.set(other.splat);", + " this.colors.set(other.colors);", + " }", + "", + " /**", + " * @param {number} resolution", + " * @returns {TerrainData} A copy of this data with another resolution.", + " */", + " resampled(resolution) {", + " const data = new TerrainData(resolution);", + " const ratio = this.resolution / resolution;", + " for (let j = 0; j < data.size; j++) {", + " for (let i = 0; i < data.size; i++) {", + " const index = j * data.size + i;", + " data.heights[index] = this.sampleHeight(i * ratio, j * ratio);", + " const sourceOffset =", + " (Math.round(j * ratio) * this.size + Math.round(i * ratio)) * LAYER_COUNT;", + " for (let layer = 0; layer < LAYER_COUNT; layer++) {", + " data.splat[index * LAYER_COUNT + layer] = this.splat[sourceOffset + layer];", + " }", + " for (let channel = 0; channel < 4; channel++) {", + " data.colors[index * 4 + channel] = this.colors[sourceOffset + channel];", + " }", + " }", + " }", + " return data;", + " }", + "}", + "", + "/**", + " * Brushes change the samples around a segment, with an effect going smoothly", + " * from full on the segment to none at `radius`. A point is a segment of", + " * length 0. Positions are in scene units relative to the terrain origin.", + " */", + "const brushes = {", + " /**", + " * @param {TerrainData} data", + " * @param {{cellWidth: number, cellHeight: number, ax: number, ay: number, bx: number, by: number, radius: number}} stroke", + " * @param {(sampleIndex: number, weight: number, t: number) => void} apply Called with the weight of", + " * the brush and the position (from 0 to 1) of the nearest point of the segment.", + " * @param {SampleRectangle} changedSamples", + " */", + " forEachSample(data, stroke, apply, changedSamples) {", + " const { cellWidth, cellHeight, ax, ay, bx, by, radius } = stroke;", + " const last = data.resolution;", + " const minI = clampInteger(Math.floor((Math.min(ax, bx) - radius) / cellWidth), 0, last);", + " const maxI = clampInteger(Math.ceil((Math.max(ax, bx) + radius) / cellWidth), 0, last);", + " const minJ = clampInteger(Math.floor((Math.min(ay, by) - radius) / cellHeight), 0, last);", + " const maxJ = clampInteger(Math.ceil((Math.max(ay, by) + radius) / cellHeight), 0, last);", + " changedSamples.set(minI, minJ, maxI, maxJ);", + " if (radius <= 0) return;", + "", + " const abX = bx - ax;", + " const abY = by - ay;", + " const abLengthSquared = abX * abX + abY * abY;", + " const radiusSquared = radius * radius;", + " for (let j = minJ; j <= maxJ; j++) {", + " const y = j * cellHeight;", + " for (let i = minI; i <= maxI; i++) {", + " const x = i * cellWidth;", + " const t =", + " abLengthSquared > 0", + " ? clamp01(((x - ax) * abX + (y - ay) * abY) / abLengthSquared)", + " : 0;", + " const dx = x - (ax + t * abX);", + " const dy = y - (ay + t * abY);", + " const distanceSquared = dx * dx + dy * dy;", + " if (distanceSquared >= radiusSquared) continue;", + " const falloff = 1 - distanceSquared / radiusSquared;", + " apply(j * data.size + i, falloff * falloff, t);", + " }", + " }", + " },", + "", + " raise(data, stroke, amount, changedSamples) {", + " const { heights } = data;", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index, weight) => {", + " heights[index] = clamp01(heights[index] + amount * weight);", + " },", + " changedSamples", + " );", + " },", + "", + " /**", + " * @param {number} targetHeight", + " * @param {number} [endTargetHeight] The target at the end of the segment, for a slope (like a road", + " * going up). The same target as at its start by default.", + " */", + " flatten(data, stroke, targetHeight, strength, changedSamples, endTargetHeight = targetHeight) {", + " const { heights } = data;", + " const startTarget = clamp01(targetHeight);", + " const endTarget = clamp01(endTargetHeight);", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index, weight, t) => {", + " const target = startTarget + (endTarget - startTarget) * t;", + " heights[index] += (target - heights[index]) * clamp01(weight * strength);", + " },", + " changedSamples", + " );", + " },", + "", + " smooth(data, stroke, strength, changedSamples) {", + " const { heights, size } = data;", + " const changes = [];", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index, weight) => {", + " const i = index % size;", + " const j = (index - i) / size;", + " let sum = 0;", + " for (let dj = -1; dj <= 1; dj++) {", + " for (let di = -1; di <= 1; di++) {", + " sum += data.getHeight(i + di, j + dj);", + " }", + " }", + " changes.push(index, heights[index] + (sum / 9 - heights[index]) * clamp01(weight * strength));", + " },", + " changedSamples", + " );", + " // Written after all averages are computed, so that the result", + " // doesn't depend on the order in which samples are visited.", + " for (let index = 0; index < changes.length; index += 2) {", + " heights[changes[index]] = changes[index + 1];", + " }", + " },", + "", + " paint(data, stroke, layer, strength, changedSamples) {", + " const { splat } = data;", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index, weight) => {", + " const amount = clamp01(weight * strength);", + " const offset = index * LAYER_COUNT;", + " for (let otherLayer = 0; otherLayer < LAYER_COUNT; otherLayer++) {", + " const target = otherLayer === layer ? 255 : 0;", + " const value = splat[offset + otherLayer];", + " // Rounded towards the target, so that light strokes still paint.", + " const change = (target - value) * amount;", + " splat[offset + otherLayer] = value + (change > 0 ? Math.ceil(change) : Math.floor(change));", + " }", + " },", + " changedSamples", + " );", + " },", + "", + " /** Remove all the layers, making a hole (`fill` or painting a layer fills it again). */", + " hole(data, stroke, changedSamples) {", + " const { splat } = data;", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index) => {", + " splat.fill(0, index * LAYER_COUNT, (index + 1) * LAYER_COUNT);", + " },", + " changedSamples", + " );", + " },", + "", + " /**", + " * Fill the holes in the radius with the layers of the ground around them:", + " * each pass fills the holes next to the ground with the average of their", + " * neighbors, from the edges of the holes inward.", + " */", + " fill(data, stroke, changedSamples) {", + " const { splat, size, resolution } = data;", + " /** @type {number[]} */", + " let holes = [];", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index) => {", + " if (data.isHole(index)) holes.push(index);", + " },", + " changedSamples", + " );", + " const filledWeights = new Float32Array(holes.length * LAYER_COUNT);", + " while (holes.length > 0) {", + " /** @type {number[]} */", + " const remainingHoles = [];", + " /** @type {number[]} */", + " const filledHoles = [];", + " for (const index of holes) {", + " const i = index % size;", + " const j = (index - i) / size;", + " const weights = filledWeights.subarray(filledHoles.length * LAYER_COUNT, (filledHoles.length + 1) * LAYER_COUNT);", + " weights.fill(0);", + " for (let neighborJ = Math.max(j - 1, 0); neighborJ <= Math.min(j + 1, resolution); neighborJ++) {", + " for (let neighborI = Math.max(i - 1, 0); neighborI <= Math.min(i + 1, resolution); neighborI++) {", + " const neighbor = neighborJ * size + neighborI;", + " if (data.isHole(neighbor)) continue;", + " for (let layer = 0; layer < LAYER_COUNT; layer++) weights[layer] += splat[neighbor * LAYER_COUNT + layer];", + " }", + " }", + " if (weights[0] + weights[1] + weights[2] + weights[3] > 0) filledHoles.push(index);", + " else remainingHoles.push(index);", + " }", + " if (filledHoles.length === 0) {", + " // No ground around (a hole bigger than the brush): the first layer is used.", + " for (const index of remainingHoles) splat[index * LAYER_COUNT] = 255;", + " break;", + " }", + " // Filled after the pass, so that the holes are filled from the edges inward.", + " filledHoles.forEach((index, filledIndex) => {", + " const weights = filledWeights.subarray(filledIndex * LAYER_COUNT, (filledIndex + 1) * LAYER_COUNT);", + " const total = weights[0] + weights[1] + weights[2] + weights[3];", + " for (let layer = 0; layer < LAYER_COUNT; layer++) {", + " splat[index * LAYER_COUNT + layer] = Math.round((weights[layer] / total) * 255);", + " }", + " });", + " holes = remainingHoles;", + " }", + " },", + "", + " /** @param {number[]} color The red, green and blue (0-255) of the tint: white removes it. */", + " tint(data, stroke, color, strength, changedSamples) {", + " brushes.paintColors(data, stroke, 0, color, strength, changedSamples);", + " },", + "", + " /** @param {number} wetness From 0 (dry) to 1. */", + " wet(data, stroke, wetness, strength, changedSamples) {", + " brushes.paintColors(data, stroke, 3, [Math.round(clamp01(wetness) * 255)], strength, changedSamples);", + " },", + "", + " /**", + " * Bring channels of the colors (see `TerrainData.colors`) towards targets.", + " * @param {number} firstChannel", + " * @param {number[]} targets From 0 to 255, for the channels from the first one.", + " */", + " paintColors(data, stroke, firstChannel, targets, strength, changedSamples) {", + " const { colors } = data;", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index, weight) => {", + " const amount = clamp01(weight * strength);", + " for (let channel = 0; channel < targets.length; channel++) {", + " const offset = index * 4 + firstChannel + channel;", + " // Rounded towards the target, so that light strokes still paint.", + " const change = (targets[channel] - colors[offset]) * amount;", + " colors[offset] += change > 0 ? Math.ceil(change) : Math.floor(change);", + " }", + " },", + " changedSamples", + " );", + " },", + "};", + "", + "const relief = (() => {", + " const hash = (x, y, seed) => {", + " let h = Math.imul(x, 374761393) + Math.imul(y, 668265263) + Math.imul(seed, 982451653);", + " h = Math.imul(h ^ (h >>> 13), 1274126177);", + " return ((h ^ (h >>> 16)) >>> 0) / 4294967296;", + " };", + " const valueNoise = (x, y, seed) => {", + " const x0 = Math.floor(x);", + " const y0 = Math.floor(y);", + " const fx = x - x0;", + " const fy = y - y0;", + " const sx = fx * fx * (3 - 2 * fx);", + " const sy = fy * fy * (3 - 2 * fy);", + " const top = hash(x0, y0, seed) + sx * (hash(x0 + 1, y0, seed) - hash(x0, y0, seed));", + " const bottom =", + " hash(x0, y0 + 1, seed) + sx * (hash(x0 + 1, y0 + 1, seed) - hash(x0, y0 + 1, seed));", + " return top + sy * (bottom - top);", + " };", + " /** Fractal noise between 0 and 1. */", + " const fractalNoise = (x, y, seed) => {", + " let value = 0;", + " let amplitude = 0.5;", + " let frequency = 1;", + " for (let octave = 0; octave < 5; octave++) {", + " value += amplitude * valueNoise(x * frequency, y * frequency, seed + octave);", + " amplitude *= 0.5;", + " frequency *= 2;", + " }", + " return value / 0.96875;", + " };", + " const smoothstep = (edge0, edge1, x) => {", + " const t = clamp01((x - edge0) / (edge1 - edge0));", + " return t * t * (3 - 2 * t);", + " };", + " const reliefHeights = {", + " Flat: () => 0,", + " Hills: (u, v, seed) => 0.05 + 0.5 * Math.pow(fractalNoise(u * 3, v * 3, seed), 2),", + " Mountains: (u, v, seed) => {", + " const ridge = 1 - Math.abs(2 * fractalNoise(u * 2.5, v * 2.5, seed) - 1);", + " return 0.9 * Math.pow(ridge, 2.5);", + " },", + " Island: (u, v, seed) => {", + " const distanceToCenter = 2 * Math.hypot(u - 0.5, v - 0.5);", + " const coast = 0.2 * (fractalNoise(u * 4, v * 4, seed + 7) - 0.5);", + " const land = smoothstep(0.95, 0.55, distanceToCenter + coast);", + " return land * (0.12 + 0.6 * Math.pow(fractalNoise(u * 3, v * 3, seed), 1.5));", + " },", + " };", + " const beachLayer = 3;", + "", + " return {", + " /**", + " * @param {TerrainData} data", + " * @param {string} reliefName Flat, Hills, Mountains or Island.", + " * @param {number} seed", + " */", + " generate(data, reliefName, seed) {", + " const getHeight = reliefHeights[reliefName] || reliefHeights.Flat;", + " const integerSeed = Math.floor(seed) | 0;", + " data.fillLayer(0);", + " for (let j = 0; j < data.size; j++) {", + " for (let i = 0; i < data.size; i++) {", + " const index = j * data.size + i;", + " const height = clamp01(", + " getHeight(i / data.resolution, j / data.resolution, integerSeed)", + " );", + " data.heights[index] = height;", + " if (reliefName === 'Island') {", + " const sand = Math.round(255 * smoothstep(0.16, 0.1, height));", + " const offset = index * LAYER_COUNT;", + " data.splat[offset] = 255 - sand;", + " data.splat[offset + beachLayer] = sand;", + " }", + " }", + " }", + " },", + " };", + "})();", + "", + "/**", + " * Edits written in the Edits property: a JSON list of brush strokes, like", + " * `[{\"tool\": \"raise\", \"x\": 0.5, \"y\": 0.5, \"radius\": 0.2, \"height\": 0.3}]`,", + " * applied on the relief or the heightmap. Positions and radius are fractions", + " * of the terrain size, heights and Z positions fractions of its depth: they", + " * are the same for all instances, whatever their size. They are made to be", + " * read and written by people and by AI agents.", + " */", + "const edits = {", + " /**", + " * @param {TerrainData} data", + " * @param {string} text", + " */", + " apply(data, text) {", + " if (!text.trim()) return;", + " let editList;", + " try {", + " editList = JSON.parse(text);", + " } catch (error) {", + " console.warn('The edits of a terrain are not valid JSON: ' + error.message);", + " return;", + " }", + " if (!Array.isArray(editList)) {", + " console.warn('The edits of a terrain must be a JSON list (like [{\"tool\": \"raise\", ...}]).');", + " return;", + " }", + " const numberOr = (value, defaultValue) => (typeof value === 'number' && isFinite(value) ? value : defaultValue);", + " const changedSamples = new SampleRectangle();", + " for (const edit of editList) {", + " if (!edit || typeof edit !== 'object') continue;", + " const x = numberOr(edit.x, 0.5);", + " const y = numberOr(edit.y, 0.5);", + " const stroke = {", + " cellWidth: 1 / data.resolution,", + " cellHeight: 1 / data.resolution,", + " ax: x,", + " ay: y,", + " bx: numberOr(edit.toX, x),", + " by: numberOr(edit.toY, y),", + " radius: numberOr(edit.radius, 0.1),", + " };", + " const strength = clamp01(numberOr(edit.strength, 1));", + " if (edit.tool === 'raise') {", + " brushes.raise(data, stroke, numberOr(edit.height, 0.1), changedSamples);", + " } else if (edit.tool === 'flatten') {", + " const z = numberOr(edit.z, 0);", + " brushes.flatten(data, stroke, z, strength, changedSamples, numberOr(edit.toZ, z));", + " } else if (edit.tool === 'smooth') {", + " // Smoothing the same fraction of the terrain at any resolution.", + " const passes = Math.max(Math.round(((1 + 9 * strength) * data.resolution) / 256), 1);", + " for (let pass = 0; pass < passes; pass++) brushes.smooth(data, stroke, 1, changedSamples);", + " } else if (edit.tool === 'paint') {", + " const layer = clampInteger(numberOr(edit.layer, 1) - 1, 0, LAYER_COUNT - 1);", + " brushes.paint(data, stroke, layer, strength, changedSamples);", + " } else if (edit.tool === 'tint') {", + " const color = gdjs.rgbOrHexToRGBColor(typeof edit.color === 'string' ? edit.color : '255;255;255');", + " brushes.tint(data, stroke, color, strength, changedSamples);", + " } else if (edit.tool === 'wetness') {", + " brushes.wet(data, stroke, numberOr(edit.amount, 1), strength, changedSamples);", + " } else if (edit.tool === 'hole') {", + " brushes.hole(data, stroke, changedSamples);", + " } else if (edit.tool === 'fill') {", + " brushes.fill(data, stroke, changedSamples);", + " } else {", + " console.warn('Unknown tool in the edits of a terrain: \"' + edit.tool + '\" (use raise, flatten, smooth, paint, tint, wetness, hole or fill).');", + " }", + " }", + " },", + "};", + "", + "/**", + " * @param {gdjs.RuntimeGame} game", + " * @param {string} imageResourceName", + " * @returns {CanvasImageSource | null} The image, or null if it's not loaded (yet).", + " */", + "const getLoadedImage = (game, imageResourceName) => {", + " const imageManager = game.getImageManager();", + " if (typeof imageManager.getImageSource === 'function') return imageManager.getImageSource(imageResourceName);", + " // Older GDevelop versions: a 192 x 192 placeholder is given for images that are not loaded.", + " const texture = imageManager.getPIXITexture(imageResourceName);", + " return texture && texture !== imageManager.getInvalidPIXITexture() ? texture.baseTexture.getDrawableSource() : null;", + "};", + "", + "/**", + " * Loads heights from an image: black is the lowest, white the highest.", + " * Heights have 16 bits of precision when the red channel holds the most", + " * significant byte and the green one the least significant (a gray image", + " * works as expected too).", + " * @param {TerrainData} data", + " * @param {gdjs.RuntimeGame} game", + " * @param {string} imageResourceName", + " * @returns {boolean} true if the image could be read.", + " */", + "const loadHeightsFromImage = (data, game, imageResourceName) => {", + " const source = getLoadedImage(game, imageResourceName);", + " if (!source) return false;", + "", + " const canvas = document.createElement('canvas');", + " canvas.width = source.width;", + " canvas.height = source.height;", + " const context = canvas.getContext('2d', { willReadFrequently: true });", + " if (!context) return false;", + " context.drawImage(source, 0, 0);", + " const pixels = context.getImageData(0, 0, source.width, source.height).data;", + " // Pixels are decoded before being interpolated, to keep the 16 bits.", + " const pixelHeight = (x, y) => {", + " const offset = (y * source.width + x) * 4;", + " return (256 * pixels[offset] + pixels[offset + 1]) / 65535;", + " };", + " for (let j = 0; j < data.size; j++) {", + " const y = (j / data.resolution) * (source.height - 1);", + " const y0 = Math.floor(y);", + " const y1 = Math.min(y0 + 1, source.height - 1);", + " for (let i = 0; i < data.size; i++) {", + " const x = (i / data.resolution) * (source.width - 1);", + " const x0 = Math.floor(x);", + " const x1 = Math.min(x0 + 1, source.width - 1);", + " const top = pixelHeight(x0, y0) + (x - x0) * (pixelHeight(x1, y0) - pixelHeight(x0, y0));", + " const bottom = pixelHeight(x0, y1) + (x - x0) * (pixelHeight(x1, y1) - pixelHeight(x0, y1));", + " data.heights[j * data.size + i] = top + (y - y0) * (bottom - top);", + " }", + " }", + " return true;", + "};", + "", + "/**", + " * Stores the data in a string. Only the differences with the base data (from", + " * the relief or the heightmap) are stored: heights on 16 bits, layer weights", + " * on 8 bits, then (since format 2) the tint and wetness on 8 bits. Each value is predicted from its neighbors and the", + " * prediction errors are written as variable length integers, with runs of", + " * zeros collapsed. Untouched or smoothly changed areas take almost no space.", + " */", + "const codec = (() => {", + " const formatVersion = '2';", + " // Format 1 has no tint and wetness.", + " const readableFormatVersions = ['1', '2'];", + "", + " class ByteWriter {", + " constructor() {", + " this.bytes = new Uint8Array(1024);", + " this.length = 0;", + " }", + " writeVarUint(value) {", + " if (this.length + 5 > this.bytes.length) {", + " const bytes = new Uint8Array(this.bytes.length * 2);", + " bytes.set(this.bytes);", + " this.bytes = bytes;", + " }", + " while (value >= 0x80) {", + " this.bytes[this.length++] = (value & 0x7f) | 0x80;", + " value >>>= 7;", + " }", + " this.bytes[this.length++] = value;", + " }", + " }", + "", + " class ByteReader {", + " /** @param {Uint8Array} bytes */", + " constructor(bytes) {", + " this.bytes = bytes;", + " this.position = 0;", + " }", + " readVarUint() {", + " let value = 0;", + " let shift = 0;", + " let byte;", + " do {", + " if (this.position >= this.bytes.length) throw new Error('Truncated terrain data.');", + " byte = this.bytes[this.position++];", + " value += (byte & 0x7f) * Math.pow(2, shift);", + " shift += 7;", + " } while (byte & 0x80);", + " return value;", + " }", + " }", + "", + " /** Predicts a value from the left, top and top-left ones. */", + " const predict = (values, size, index) => {", + " const i = index % size;", + " if (index < size) return i === 0 ? 0 : values[index - 1];", + " if (i === 0) return values[index - size];", + " return values[index - 1] + values[index - size] - values[index - size - 1];", + " };", + "", + " /**", + " * @param {ByteWriter} writer", + " * @param {Int32Array} values A grid of `size * size` values.", + " */", + " const writeGrid = (writer, values, size) => {", + " let zeroRun = 0;", + " for (let index = 0; index < values.length; index++) {", + " const error = values[index] - predict(values, size, index);", + " if (error === 0) {", + " zeroRun++;", + " continue;", + " }", + " if (zeroRun > 0) {", + " writer.writeVarUint(0);", + " writer.writeVarUint(zeroRun);", + " zeroRun = 0;", + " }", + " // Zigzag: small negative and positive numbers get small codes.", + " writer.writeVarUint(error > 0 ? error * 2 : -error * 2 - 1);", + " }", + " if (zeroRun > 0) {", + " writer.writeVarUint(0);", + " writer.writeVarUint(zeroRun);", + " }", + " };", + "", + " /** @param {ByteReader} reader */", + " const readGrid = (reader, size) => {", + " const values = new Int32Array(size * size);", + " let zeroRun = 0;", + " for (let index = 0; index < values.length; index++) {", + " let error = 0;", + " if (zeroRun > 0) {", + " zeroRun--;", + " } else {", + " const code = reader.readVarUint();", + " if (code === 0) {", + " zeroRun = reader.readVarUint() - 1;", + " } else {", + " error = code % 2 === 0 ? code / 2 : -(code + 1) / 2;", + " }", + " }", + " values[index] = predict(values, size, index) + error;", + " }", + " return values;", + " };", + "", + " const toBase64 = (bytes, length) => {", + " let binary = '';", + " for (let offset = 0; offset < length; offset += 0x8000) {", + " binary += String.fromCharCode.apply(null, bytes.subarray(offset, Math.min(offset + 0x8000, length)));", + " }", + " return btoa(binary);", + " };", + "", + " const fromBase64 = (base64) => {", + " const binary = atob(base64);", + " const bytes = new Uint8Array(binary.length);", + " for (let index = 0; index < binary.length; index++) {", + " bytes[index] = binary.charCodeAt(index);", + " }", + " return bytes;", + " };", + "", + " return {", + " /**", + " * @param {TerrainData} data", + " * @param {TerrainData} baseData The data without changes, with the same resolution.", + " */", + " encode(data, baseData) {", + " const writer = new ByteWriter();", + " const values = new Int32Array(data.heights.length);", + " for (let index = 0; index < values.length; index++) {", + " values[index] = Math.round((data.heights[index] - baseData.heights[index]) * 65535);", + " }", + " writeGrid(writer, values, data.size);", + " for (let layer = 0; layer < LAYER_COUNT; layer++) {", + " for (let index = 0; index < values.length; index++) {", + " const offset = index * LAYER_COUNT + layer;", + " values[index] = data.splat[offset] - baseData.splat[offset];", + " }", + " writeGrid(writer, values, data.size);", + " }", + " // Not painted, these grids are zeros: a few bytes each.", + " for (let channel = 0; channel < 4; channel++) {", + " for (let index = 0; index < values.length; index++) {", + " const offset = index * 4 + channel;", + " values[index] = data.colors[offset] - baseData.colors[offset];", + " }", + " writeGrid(writer, values, data.size);", + " }", + " return formatVersion + ';' + data.resolution + ';' + toBase64(writer.bytes, writer.length);", + " },", + "", + " /**", + " * Store grids of integers in a string, like the terrain data. Used by other objects (the scattered models).", + " * @param {Int32Array[]} grids Grids of `size * size` values.", + " * @param {number} size", + " */", + " encodeGrids(grids, size) {", + " const writer = new ByteWriter();", + " for (const grid of grids) writeGrid(writer, grid, size);", + " return toBase64(writer.bytes, writer.length);", + " },", + "", + " /**", + " * @param {string} base64 A string from `encodeGrids`.", + " * @param {number} size", + " * @param {number} count The number of grids.", + " * @returns {Int32Array[]} The grids (an error is thrown if the string is not valid).", + " */", + " decodeGrids(base64, size, count) {", + " const reader = new ByteReader(fromBase64(base64));", + " return Array.from({ length: count }, () => readGrid(reader, size));", + " },", + "", + " /**", + " * @param {string} text", + " * @param {(resolution: number) => TerrainData} createBaseData", + " * @returns {TerrainData | null} null if the text is empty or not valid.", + " */", + " decode(text, createBaseData) {", + " if (!text) return null;", + " const parts = text.split(';');", + " const resolution = parseInt(parts[1], 10);", + " if (readableFormatVersions.indexOf(parts[0]) === -1 || RESOLUTIONS.indexOf(resolution) === -1) {", + " return null;", + " }", + " try {", + " const reader = new ByteReader(fromBase64(parts[2]));", + " const data = createBaseData(resolution);", + " const heightChanges = readGrid(reader, data.size);", + " for (let index = 0; index < heightChanges.length; index++) {", + " data.heights[index] = clamp01(data.heights[index] + heightChanges[index] / 65535);", + " }", + " for (let layer = 0; layer < LAYER_COUNT; layer++) {", + " const weightChanges = readGrid(reader, data.size);", + " for (let index = 0; index < weightChanges.length; index++) {", + " const offset = index * LAYER_COUNT + layer;", + " // The base may have changed since the data was saved.", + " data.splat[offset] = Math.min(Math.max(data.splat[offset] + weightChanges[index], 0), 255);", + " }", + " }", + " if (parts[0] !== '1') {", + " for (let channel = 0; channel < 4; channel++) {", + " const colorChanges = readGrid(reader, data.size);", + " for (let index = 0; index < colorChanges.length; index++) {", + " const offset = index * 4 + channel;", + " data.colors[offset] = Math.min(Math.max(data.colors[offset] + colorChanges[index], 0), 255);", + " }", + " }", + " }", + " return data;", + " } catch (error) {", + " console.error('Terrain data could not be read:', error);", + " return null;", + " }", + " },", + " };", + "})();", + "", + "const lodIndices = new Map();", + "/**", + " * The triangles of a chunk, shared by all chunks of the same size. Chunk", + " * borders have a skirt going down, hiding the gaps between chunks drawn", + " * with different levels of detail.", + " * @param {number} chunkCells", + " * @param {number} step", + " */", + "const getLodIndex = (chunkCells, step) => {", + " const key = chunkCells + ';' + step;", + " const existingIndex = lodIndices.get(key);", + " if (existingIndex) return existingIndex;", + "", + " const sideVertices = chunkCells + 1;", + " const gridVertices = sideVertices * sideVertices;", + " const indices = [];", + " const grid = (i, j) => j * sideVertices + i;", + " for (let j = 0; j < chunkCells; j += step) {", + " for (let i = 0; i < chunkCells; i += step) {", + " const topLeft = grid(i, j);", + " const topRight = grid(i + step, j);", + " const bottomLeft = grid(i, j + step);", + " const bottomRight = grid(i + step, j + step);", + " indices.push(topLeft, topRight, bottomLeft, topRight, bottomRight, bottomLeft);", + " }", + " }", + " const borders = [", + " (k) => grid(k, 0),", + " (k) => grid(k, chunkCells),", + " (k) => grid(0, k),", + " (k) => grid(chunkCells, k),", + " ];", + " borders.forEach((borderVertex, border) => {", + " const skirtVertex = (k) => gridVertices + border * sideVertices + k;", + " for (let k = 0; k < chunkCells; k += step) {", + " const a = borderVertex(k);", + " const b = borderVertex(k + step);", + " const skirtA = skirtVertex(k);", + " const skirtB = skirtVertex(k + step);", + " // Both faces, as a skirt can be seen from either side.", + " indices.push(a, b, skirtB, a, skirtB, skirtA, a, skirtB, b, a, skirtA, skirtB);", + " }", + " });", + " const index = new THREE.BufferAttribute(new Uint16Array(indices), 1);", + " lodIndices.set(key, index);", + " return index;", + "};", + "", + "const vertexShaderDeclarations = `", + "varying vec2 vTerrainLocalXY;", + "varying float vTerrainLocalZ;", + "uniform float terrainWorldScale;", + "varying vec2 vTerrainWorldXY;", + "varying float vTerrainUp;", + "`;", + "const vertexShaderCode = `", + "vTerrainLocalXY = position.xy;", + "vTerrainLocalZ = position.z;", + "vTerrainWorldXY = (modelMatrix * vec4(transformed, 1.0)).xy * terrainWorldScale;", + "vTerrainUp = normalize(transpose(inverse(mat3(modelMatrix))) * objectNormal).z;", + "`;", + "const fragmentShaderDeclarations = `", + "uniform sampler2D terrainSplatMap;", + "uniform vec2 terrainSplatUvScaleAndOffset;", + "uniform vec3 terrainLayerColors[4];", + "uniform sampler2D terrainLayerMap0;", + "uniform sampler2D terrainLayerMap1;", + "uniform sampler2D terrainLayerMap2;", + "uniform sampler2D terrainLayerMap3;", + "uniform sampler2D terrainLayerNormalMap0;", + "uniform sampler2D terrainLayerNormalMap1;", + "uniform sampler2D terrainLayerNormalMap2;", + "uniform sampler2D terrainLayerNormalMap3;", + "uniform float terrainHasNormalMaps;", + "uniform float terrainNormalMapStrength;", + "uniform float terrainTextureSize;", + "uniform float terrainHeightBlending;", + "uniform float terrainTextureVariation;", + "uniform vec4 terrainCliffLayer;", + "uniform vec2 terrainCliffCosines;", + "uniform vec4 terrainBrush;", + "uniform sampler2D terrainColorMap;", + "uniform vec3 terrainWetShore;", + "varying vec2 vTerrainLocalXY;", + "varying float vTerrainLocalZ;", + "varying vec2 vTerrainWorldXY;", + "varying float vTerrainUp;", + "", + "float terrainHash(vec2 p) {", + " return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);", + "}", + "// The rotation of the copies of textures used to make repetition less visible.", + "const mat2 terrainCopyRotation = mat2(0.8, -0.6, 0.6, 0.8);", + "/** A bump of a normal map (from -1 to 1), mixed with its rotated copy in patches. */", + "vec3 terrainSampleNormal(sampler2D normalMap, vec2 uv, vec2 uv2, float patches) {", + " vec3 bump = texture2D(normalMap, uv).xyz * 2.0 - 1.0;", + " if (patches <= 0.0) return bump;", + " vec3 copyBump = texture2D(normalMap, uv2).xyz * 2.0 - 1.0;", + " // The bumps of the copy are rotated back with the inverse rotation.", + " copyBump.xy = transpose(terrainCopyRotation) * copyBump.xy;", + " return mix(bump, copyBump, patches);", + "}", + "float terrainNoise(vec2 p) {", + " vec2 i = floor(p);", + " vec2 f = fract(p);", + " vec2 u = f * f * (3.0 - 2.0 * f);", + " return mix(", + " mix(terrainHash(i), terrainHash(i + vec2(1.0, 0.0)), u.x),", + " mix(terrainHash(i + vec2(0.0, 1.0)), terrainHash(i + vec2(1.0, 1.0)), u.x),", + " u.y", + " );", + "}", + "`;", + "// Holes are samples without any painted layer: the interpolated sum of the", + "// weights gives smooth edges. Only compiled for terrains having holes, as", + "// discard makes drawing slower on some GPUs.", + "const holeShaderCode = `", + "#ifdef TERRAIN_HOLES", + "if (dot(texture2D(terrainSplatMap, vTerrainLocalXY * terrainSplatUvScaleAndOffset.x + terrainSplatUvScaleAndOffset.y), vec4(1.0)) < 0.5) discard;", + "#endif", + "`;", + "// For the shadows: the depth of holes is not drawn.", + "const holeDepthShaderDeclarations = `", + "uniform sampler2D terrainSplatMap;", + "uniform vec2 terrainSplatUvScaleAndOffset;", + "varying vec2 vTerrainLocalXY;", + "`;", + "", + "const fragmentShaderCode = holeShaderCode + `", + "// Borders between layers are made irregular and sharper, to look painted.", + "vec2 terrainNoisePosition = vTerrainWorldXY / 24.0;", + "vec4 terrainJitter = vec4(", + " terrainNoise(terrainNoisePosition),", + " terrainNoise(terrainNoisePosition + 19.1),", + " terrainNoise(terrainNoisePosition + 37.7),", + " terrainNoise(terrainNoisePosition + 53.3)", + ");", + "// Layer weights are in a texture, so that painted details stay the same", + "// whatever the level of detail of the ground.", + "vec2 terrainSplatUv = vTerrainLocalXY * terrainSplatUvScaleAndOffset.x + terrainSplatUvScaleAndOffset.y;", + "vec4 terrainWeights = texture2D(terrainSplatMap, terrainSplatUv) * (0.7 + 0.6 * terrainJitter);", + "terrainWeights = terrainWeights * terrainWeights;", + "terrainWeights /= max(dot(terrainWeights, vec4(1.0)), 0.00001);", + "float terrainCliff = 1.0 - smoothstep(terrainCliffCosines.x, terrainCliffCosines.y, vTerrainUp);", + "terrainWeights = mix(terrainWeights, terrainCliffLayer, terrainCliff * dot(terrainCliffLayer, vec4(1.0)));", + "", + "vec2 terrainUv = vTerrainWorldXY / terrainTextureSize;", + "vec3 terrainTexture0 = texture2D(terrainLayerMap0, terrainUv).rgb;", + "vec3 terrainTexture1 = texture2D(terrainLayerMap1, terrainUv).rgb;", + "vec3 terrainTexture2 = texture2D(terrainLayerMap2, terrainUv).rgb;", + "vec3 terrainTexture3 = texture2D(terrainLayerMap3, terrainUv).rgb;", + "", + "// Less visible repetition: in irregular patches, textures are replaced by a", + "// rotated and shifted copy of themselves, and colors slowly vary.", + "float terrainPatches = 0.0;", + "vec2 terrainUv2 = terrainUv;", + "float terrainColorVariation = 1.0;", + "if (terrainTextureVariation > 0.0) {", + " // Up to half of the ground is covered by the copies, each patch showing", + " // only one of them (mixing them would make them blurry).", + " float terrainPatchThreshold = 1.0 - 0.5 * terrainTextureVariation;", + " terrainPatches = smoothstep(terrainPatchThreshold - 0.05, terrainPatchThreshold + 0.05, terrainNoise(terrainUv * 0.23 + 7.3));", + " terrainUv2 = terrainCopyRotation * terrainUv + vec2(0.31, 0.73);", + " terrainTexture0 = mix(terrainTexture0, texture2D(terrainLayerMap0, terrainUv2).rgb, terrainPatches);", + " terrainTexture1 = mix(terrainTexture1, texture2D(terrainLayerMap1, terrainUv2).rgb, terrainPatches);", + " terrainTexture2 = mix(terrainTexture2, texture2D(terrainLayerMap2, terrainUv2).rgb, terrainPatches);", + " terrainTexture3 = mix(terrainTexture3, texture2D(terrainLayerMap3, terrainUv2).rgb, terrainPatches);", + " terrainColorVariation += terrainTextureVariation * 0.3 *", + " (terrainNoise(terrainUv * 0.11) + 0.5 * terrainNoise(terrainUv * 0.37 + 3.1) - 0.75);", + "}", + "", + "// Height-based blending: the brightness of a texture, compared to its", + "// average, is used as its height. Where layers meet, the high parts (stones,", + "// tufts) cover the low parts (sand, gaps) instead of fading into each other.", + "if (terrainHeightBlending > 0.0) {", + " const vec3 terrainLuminance = vec3(0.299, 0.587, 0.114);", + " vec4 terrainHeights = vec4(", + " dot(terrainTexture0 - textureLod(terrainLayerMap0, vec2(0.5), 16.0).rgb, terrainLuminance),", + " dot(terrainTexture1 - textureLod(terrainLayerMap1, vec2(0.5), 16.0).rgb, terrainLuminance),", + " dot(terrainTexture2 - textureLod(terrainLayerMap2, vec2(0.5), 16.0).rgb, terrainLuminance),", + " dot(terrainTexture3 - textureLod(terrainLayerMap3, vec2(0.5), 16.0).rgb, terrainLuminance)", + " );", + " vec4 terrainHeightWeights = terrainWeights + terrainHeights;", + " float terrainTop = max(max(terrainHeightWeights.x, terrainHeightWeights.y), max(terrainHeightWeights.z, terrainHeightWeights.w));", + " // Only painted layers (and the cliff layer) can show.", + " vec4 terrainHeightBlend = max(terrainHeightWeights - terrainTop + 0.1, 0.0) * step(0.004, terrainWeights);", + " terrainHeightBlend /= max(dot(terrainHeightBlend, vec4(1.0)), 0.00001);", + " terrainWeights = mix(terrainWeights, terrainHeightBlend, terrainHeightBlending);", + "}", + "", + "vec3 terrainColor = terrainColorVariation * (", + " terrainWeights.x * terrainLayerColors[0] * terrainTexture0 +", + " terrainWeights.y * terrainLayerColors[1] * terrainTexture1 +", + " terrainWeights.z * terrainLayerColors[2] * terrainTexture2 +", + " terrainWeights.w * terrainLayerColors[3] * terrainTexture3);", + "", + "// The painted tint multiplies all layers. Wet ground is darker (and", + "// shinier, see the roughness).", + "vec4 terrainTintAndWetness = texture2D(terrainColorMap, terrainSplatUv);", + "float terrainWetness = max(", + " terrainTintAndWetness.a,", + " terrainWetShore.z * (1.0 - smoothstep(terrainWetShore.x, terrainWetShore.x + terrainWetShore.y, vTerrainLocalZ))", + ");", + "terrainColor *= terrainTintAndWetness.rgb * (1.0 - 0.4 * terrainWetness);", + "diffuseColor.rgb *= terrainColor;", + "", + "if (terrainBrush.w > 0.5) {", + " float brushDistance = distance(vTerrainWorldXY, terrainBrush.xy);", + " float ringWidth = max(terrainBrush.z * 0.04, 1.5);", + " float ring = 1.0 - smoothstep(0.0, ringWidth, abs(brushDistance - terrainBrush.z));", + " float disc = brushDistance < terrainBrush.z ? 0.12 : 0.0;", + " diffuseColor.rgb = mix(diffuseColor.rgb, vec3(1.0), max(ring * 0.85, disc));", + "}", + "`;", + "", + "const fragmentShaderRoughnessCode = `", + "roughnessFactor = mix(roughnessFactor, 0.3, terrainWetness);", + "`;", + "", + "const fragmentShaderNormalCode = `", + "if (terrainHasNormalMaps > 0.5) {", + " vec3 terrainMapNormal =", + " terrainWeights.x * terrainSampleNormal(terrainLayerNormalMap0, terrainUv, terrainUv2, terrainPatches) +", + " terrainWeights.y * terrainSampleNormal(terrainLayerNormalMap1, terrainUv, terrainUv2, terrainPatches) +", + " terrainWeights.z * terrainSampleNormal(terrainLayerNormalMap2, terrainUv, terrainUv2, terrainPatches) +", + " terrainWeights.w * terrainSampleNormal(terrainLayerNormalMap3, terrainUv, terrainUv2, terrainPatches);", + " terrainMapNormal.xy *= terrainNormalMapStrength;", + " // The X and Y of textures follow the X and Y of the 3D world, projected on", + " // the ground (the camera space of the normal is used).", + " vec3 terrainTangent = (viewMatrix * vec4(1.0, 0.0, 0.0, 0.0)).xyz;", + " terrainTangent -= normal * dot(normal, terrainTangent);", + " // A ground facing the X axis (a rotated terrain) uses the Y axis instead.", + " if (dot(terrainTangent, terrainTangent) < 0.000001) {", + " terrainTangent = (viewMatrix * vec4(0.0, 1.0, 0.0, 0.0)).xyz;", + " terrainTangent -= normal * dot(normal, terrainTangent);", + " }", + " terrainTangent = normalize(terrainTangent);", + " vec3 terrainBitangent = cross(normal, terrainTangent);", + " normal = normalize(mat3(terrainTangent, terrainBitangent, normal) * terrainMapNormal);", + "}", + "`;", + "", + "let whiteTexture = null;", + "const getWhiteTexture = () => {", + " if (!whiteTexture) {", + " whiteTexture = new THREE.DataTexture(new Uint8Array([255, 255, 255, 255]), 1, 1);", + " whiteTexture.needsUpdate = true;", + " }", + " return whiteTexture;", + "};", + "", + "let flatNormalTexture = null;", + "const getFlatNormalTexture = () => {", + " if (!flatNormalTexture) {", + " flatNormalTexture = new THREE.DataTexture(new Uint8Array([128, 128, 255, 255]), 1, 1);", + " flatNormalTexture.needsUpdate = true;", + " }", + " return flatNormalTexture;", + "};", + "", + "const layerTextures = new WeakMap();", + "const layerNormalMaps = new WeakMap();", + "/**", + " * Textures of the image manager have no mipmaps: they would shimmer when", + " * repeated in the distance.", + " * @param {gdjs.RuntimeGame} game", + " * @param {string} resourceName", + " * @param {boolean} isNormalMap Normal maps are directions, not colors.", + " */", + "const getLayerTexture = (game, resourceName, isNormalMap) => {", + " if (!resourceName) return isNormalMap ? getFlatNormalTexture() : getWhiteTexture();", + " const imageTexture = game.getImageManager().getThreeTexture(resourceName);", + " const textures = isNormalMap ? layerNormalMaps : layerTextures;", + " let texture = textures.get(imageTexture);", + " if (!texture) {", + " texture = imageTexture.clone();", + " if (isNormalMap) texture.colorSpace = THREE.NoColorSpace;", + " texture.wrapS = THREE.RepeatWrapping;", + " texture.wrapT = THREE.RepeatWrapping;", + " texture.minFilter = THREE.LinearMipmapLinearFilter;", + " texture.generateMipmaps = true;", + " texture.anisotropy = 4;", + " texture.needsUpdate = true;", + " textures.set(imageTexture, texture);", + " }", + " return texture;", + "};", + "", + "/**", + " * A texture of 4 bytes per sample of a grid of `size * size` samples (layer", + " * weights, tint and wetness...), interpolated between samples.", + " * @param {Uint8Array} bytes", + " * @param {number} size", + " */", + "const createGridTexture = (bytes, size) => {", + " const texture = new THREE.DataTexture(bytes, size, size);", + " texture.magFilter = THREE.LinearFilter;", + " texture.minFilter = THREE.LinearFilter;", + " texture.userData.isUploaded = false;", + " texture.onUpdate = () => {", + " texture.userData.isUploaded = true;", + " };", + " return texture;", + "};", + "", + "/**", + " * Send the rows of the changed samples to the GPU, or the whole texture", + " * before its first upload (and in older GDevelop versions).", + " * @param {THREE.DataTexture} texture", + " * @param {SampleRectangle} samples", + " */", + "const uploadGridTextureRows = (texture, samples) => {", + " if (texture.userData.isUploaded && typeof texture.addUpdateRange === 'function') {", + " const size = texture.image.width;", + " for (let j = samples.minJ; j <= samples.maxJ; j++) {", + " texture.addUpdateRange((j * size + samples.minI) * 4, (samples.maxI - samples.minI + 1) * 4);", + " }", + " }", + " texture.needsUpdate = true;", + "};", + "", + "class TerrainRenderer {", + " /** @param {Terrain} terrain */", + " constructor(terrain) {", + " this.terrain = terrain;", + " this.group = new THREE.Group();", + " terrain.object.get3DRendererObject().add(this.group);", + " /** @type {Array<{mesh: THREE.Mesh, geometry: THREE.BufferGeometry, firstI: number, firstJ: number, center: THREE.Vector3, lod: number}>} */", + " this.chunks = [];", + " this.chunksPerSide = 0;", + " this.chunkCells = 0;", + " this.chunkSideVertices = 0;", + " this.chunkGridVertices = 0;", + " this.chunkVertices = 0;", + " /** @type {THREE.DataTexture | null} */", + " this.splatTexture = null;", + " /** @type {THREE.DataTexture | null} */", + " this.colorTexture = null;", + " this.uniforms = {", + " terrainSplatMap: { value: getWhiteTexture() },", + " terrainSplatUvScaleAndOffset: { value: new THREE.Vector2() },", + " terrainLayerColors: { value: [new THREE.Color(), new THREE.Color(), new THREE.Color(), new THREE.Color()] },", + " terrainLayerMap0: { value: getWhiteTexture() },", + " terrainLayerMap1: { value: getWhiteTexture() },", + " terrainLayerMap2: { value: getWhiteTexture() },", + " terrainLayerMap3: { value: getWhiteTexture() },", + " terrainLayerNormalMap0: { value: getFlatNormalTexture() },", + " terrainLayerNormalMap1: { value: getFlatNormalTexture() },", + " terrainLayerNormalMap2: { value: getFlatNormalTexture() },", + " terrainLayerNormalMap3: { value: getFlatNormalTexture() },", + " terrainHasNormalMaps: { value: 0 },", + " terrainNormalMapStrength: { value: 1 },", + " terrainTextureSize: { value: 256 },", + " terrainHeightBlending: { value: 0 },", + " terrainTextureVariation: { value: 0 },", + " terrainCliffLayer: { value: new THREE.Vector4() },", + " terrainCliffCosines: { value: new THREE.Vector2() },", + " terrainBrush: { value: new THREE.Vector4() },", + " terrainColorMap: { value: getWhiteTexture() },", + " // Z (in the terrain group) under which the ground is wet, fade height, and 1 if it's used.", + " terrainWetShore: { value: new THREE.Vector3() },", + " // Scene units by 3D world unit.", + " terrainWorldScale: { value: 1 },", + " };", + " this.material = new THREE.MeshStandardMaterial({ roughness: 1, metalness: 0 });", + " this.material.onBeforeCompile = (shader) => {", + " Object.assign(shader.uniforms, this.uniforms);", + " shader.vertexShader = patchShaderCode(shader.vertexShader, [", + " ['#include ', '#include \\n' + vertexShaderDeclarations],", + " ['#include ', '#include \\n' + vertexShaderCode],", + " ], 'Terrain3D');", + " shader.fragmentShader = patchShaderCode(shader.fragmentShader, [", + " ['#include ', '#include \\n' + fragmentShaderDeclarations],", + " ['#include ', fragmentShaderCode],", + " ['#include ', '#include \\n' + fragmentShaderRoughnessCode],", + " ['#include ', '#include \\n' + fragmentShaderNormalCode],", + " ], 'Terrain3D');", + " };", + " this.material.customProgramCacheKey = () => (this.hasHoles ? 'Terrain3DWithHoles' : 'Terrain3D');", + " this.hasHoles = false;", + " // Without them, holes would cast shadows.", + " this.depthMaterial = this._createHoleDepthMaterial(new THREE.MeshDepthMaterial());", + " this.distanceMaterial = this._createHoleDepthMaterial(new THREE.MeshDistanceMaterial());", + " this._cameraPosition = new THREE.Vector3();", + " this._chunkPosition = new THREE.Vector3();", + " this._scale = new THREE.Vector3();", + " }", + "", + " /**", + " * @param {THREE.MeshDepthMaterial | THREE.MeshDistanceMaterial} material", + " * @returns The material, drawing nothing in holes.", + " */", + " _createHoleDepthMaterial(material) {", + " material.defines = { TERRAIN_HOLES: '' };", + " material.onBeforeCompile = (shader) => {", + " Object.assign(shader.uniforms, this.uniforms);", + " shader.vertexShader = patchShaderCode(shader.vertexShader, [", + " ['#include ', '#include \\nvarying vec2 vTerrainLocalXY;'],", + " ['#include ', '#include \\nvTerrainLocalXY = position.xy;'],", + " ], 'Terrain3D shadows');", + " shader.fragmentShader = patchShaderCode(shader.fragmentShader, [", + " ['#include ', '#include \\n' + holeDepthShaderDeclarations],", + " ['#include ', holeShaderCode + '#include '],", + " ], 'Terrain3D shadows');", + " };", + " material.customProgramCacheKey = () => 'Terrain3DHoles';", + " return material;", + " }", + "", + " /**", + " * Use the shaders drawing holes, or the faster ones without them.", + " * @param {boolean} hasHoles", + " */", + " setHasHoles(hasHoles) {", + " if (hasHoles === this.hasHoles) return;", + " this.hasHoles = hasHoles;", + " // The material has its own defines (STANDARD).", + " if (hasHoles) this.material.defines.TERRAIN_HOLES = '';", + " else delete this.material.defines.TERRAIN_HOLES;", + " this.material.needsUpdate = true;", + " for (const chunk of this.chunks) this._setChunkShadowMaterials(chunk.mesh);", + " }", + "", + " /** @param {THREE.Mesh} mesh */", + " _setChunkShadowMaterials(mesh) {", + " mesh.customDepthMaterial = this.hasHoles ? this.depthMaterial : undefined;", + " mesh.customDistanceMaterial = this.hasHoles ? this.distanceMaterial : undefined;", + " }", + "", + " /** Recreate all the chunks, for example after the resolution changed. */", + " rebuild() {", + " this.disposeChunks();", + " const { data } = this.terrain;", + " this.chunkCells = getChunkCells(data.resolution);", + " this.chunkSideVertices = this.chunkCells + 1;", + " this.chunkGridVertices = this.chunkSideVertices * this.chunkSideVertices;", + " this.chunkVertices = this.chunkGridVertices + 4 * this.chunkSideVertices;", + " this.chunksPerSide = data.resolution / this.chunkCells;", + " const cellSize = AREA_SIZE / data.resolution;", + " // Sample centers are at the texel centers.", + " this.splatTexture = createGridTexture(data.splat, data.size);", + " this.uniforms.terrainSplatMap.value = this.splatTexture;", + " this.colorTexture = createGridTexture(data.colors, data.size);", + " // Tints are colors chosen like the layer colors.", + " this.colorTexture.colorSpace = THREE.SRGBColorSpace;", + " this.uniforms.terrainColorMap.value = this.colorTexture;", + " this.uniforms.terrainSplatUvScaleAndOffset.value.set(1 / (cellSize * data.size), 0.5 / data.size);", + " for (let chunkJ = 0; chunkJ < this.chunksPerSide; chunkJ++) {", + " for (let chunkI = 0; chunkI < this.chunksPerSide; chunkI++) {", + " const firstI = chunkI * this.chunkCells;", + " const firstJ = chunkJ * this.chunkCells;", + " const positions = new Float32Array(this.chunkVertices * 3);", + " for (let j = 0; j < this.chunkSideVertices; j++) {", + " for (let i = 0; i < this.chunkSideVertices; i++) {", + " const vertex = j * this.chunkSideVertices + i;", + " positions[vertex * 3] = (firstI + i) * cellSize;", + " positions[vertex * 3 + 1] = (firstJ + j) * cellSize;", + " }", + " }", + " // Skirt vertices are below border vertices.", + " this._forEachSkirtVertex((skirtVertex, borderVertex) => {", + " positions[skirtVertex * 3] = positions[borderVertex * 3];", + " positions[skirtVertex * 3 + 1] = positions[borderVertex * 3 + 1];", + " });", + " const geometry = new THREE.BufferGeometry();", + " geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));", + " geometry.setAttribute('normal', new THREE.BufferAttribute(new Float32Array(this.chunkVertices * 3), 3));", + " geometry.setIndex(getLodIndex(this.chunkCells, LOD_STEPS[0]));", + " geometry.boundingBox = new THREE.Box3();", + " geometry.boundingSphere = new THREE.Sphere();", + " const mesh = new THREE.Mesh(geometry, this.material);", + " mesh.matrixAutoUpdate = false;", + " // Rays hit the ground at its full resolution, whatever the level of", + " // detail drawn, and go through holes.", + " const chunkCells = this.chunkCells;", + " const terrainData = () => this.terrain.data;", + " const localPoint = new THREE.Vector3();", + " mesh.raycast = function (raycaster, intersects) {", + " const drawnIndex = geometry.index;", + " const firstIntersection = intersects.length;", + " geometry.setIndex(getLodIndex(chunkCells, LOD_STEPS[0]));", + " THREE.Mesh.prototype.raycast.call(this, raycaster, intersects);", + " geometry.setIndex(drawnIndex);", + " const data = terrainData();", + " const cellSize = AREA_SIZE / data.resolution;", + " for (let index = intersects.length - 1; index >= firstIntersection; index--) {", + " this.worldToLocal(localPoint.copy(intersects[index].point));", + " if (data.sampleSolidity(localPoint.x / cellSize, localPoint.y / cellSize) < 0.5) {", + " intersects.splice(index, 1);", + " }", + " }", + " };", + " this._setChunkShadowMaterials(mesh);", + " this.group.add(mesh);", + " this.chunks.push({", + " mesh,", + " geometry,", + " firstI,", + " firstJ,", + " center: new THREE.Vector3(", + " (firstI + this.chunkCells / 2) * cellSize,", + " (firstJ + this.chunkCells / 2) * cellSize,", + " 0", + " ),", + " lod: 0,", + " });", + " }", + " }", + " this.updateSamples(new SampleRectangle().set(0, 0, data.resolution, data.resolution), true);", + " this.updateMaterial();", + " }", + "", + " _forEachSkirtVertex(callback) {", + " for (let k = 0; k < this.chunkSideVertices; k++) {", + " callback(this.chunkGridVertices + k, k);", + " callback(this.chunkGridVertices + this.chunkSideVertices + k, this.chunkCells * this.chunkSideVertices + k);", + " callback(this.chunkGridVertices + 2 * this.chunkSideVertices + k, k * this.chunkSideVertices);", + " callback(", + " this.chunkGridVertices + 3 * this.chunkSideVertices + k,", + " k * this.chunkSideVertices + this.chunkCells", + " );", + " }", + " }", + "", + " /**", + " * Skirts go down only to the lowest point a coarser neighbor chunk can", + " * draw along the border, so that they can't be seen through holes.", + " * @param {Float32Array} positions", + " */", + " _updateSkirtHeights(positions) {", + " const coarsestStep = LOD_STEPS[LOD_STEPS.length - 1];", + " const borderVertices = [", + " (k) => k,", + " (k) => this.chunkCells * this.chunkSideVertices + k,", + " (k) => k * this.chunkSideVertices,", + " (k) => k * this.chunkSideVertices + this.chunkCells,", + " ];", + " borderVertices.forEach((borderVertex, border) => {", + " for (let k = 0; k < this.chunkSideVertices; k++) {", + " let minZ = Infinity;", + " const from = Math.max(k - coarsestStep, 0);", + " const to = Math.min(k + coarsestStep, this.chunkCells);", + " for (let other = from; other <= to; other++) {", + " minZ = Math.min(minZ, positions[borderVertex(other) * 3 + 2]);", + " }", + " const skirtVertex = this.chunkGridVertices + border * this.chunkSideVertices + k;", + " positions[skirtVertex * 3 + 2] = minZ;", + " }", + " });", + " }", + "", + " /**", + " * Update the vertices of the chunks sharing the given samples.", + " * @param {SampleRectangle} samples", + " * @param {boolean} hasPaintChanged", + " */", + " updateSamples(samples, hasPaintChanged) {", + " if (samples.isEmpty()) return;", + " const { data } = this.terrain;", + " const cellSize = AREA_SIZE / data.resolution;", + " const slopeFactor = AREA_DEPTH / cellSize;", + " // Normals of samples next to changed ones change too.", + " const minI = Math.max(samples.minI - 1, 0);", + " const minJ = Math.max(samples.minJ - 1, 0);", + " const maxI = Math.min(samples.maxI + 1, data.resolution);", + " const maxJ = Math.min(samples.maxJ + 1, data.resolution);", + " const firstChunkI = Math.max(Math.ceil(minI / this.chunkCells) - 1, 0);", + " const firstChunkJ = Math.max(Math.ceil(minJ / this.chunkCells) - 1, 0);", + " const lastChunkI = Math.min(Math.floor(maxI / this.chunkCells), this.chunksPerSide - 1);", + " const lastChunkJ = Math.min(Math.floor(maxJ / this.chunkCells), this.chunksPerSide - 1);", + "", + " for (let chunkJ = firstChunkJ; chunkJ <= lastChunkJ; chunkJ++) {", + " for (let chunkI = firstChunkI; chunkI <= lastChunkI; chunkI++) {", + " const chunk = this.chunks[chunkJ * this.chunksPerSide + chunkI];", + " const positions = chunk.geometry.attributes.position.array;", + " const normals = chunk.geometry.attributes.normal.array;", + " const fromI = Math.max(minI - chunk.firstI, 0);", + " const toI = Math.min(maxI - chunk.firstI, this.chunkCells);", + " const fromJ = Math.max(minJ - chunk.firstJ, 0);", + " const toJ = Math.min(maxJ - chunk.firstJ, this.chunkCells);", + " for (let j = fromJ; j <= toJ; j++) {", + " const gridJ = chunk.firstJ + j;", + " for (let i = fromI; i <= toI; i++) {", + " const gridI = chunk.firstI + i;", + " const vertex = j * this.chunkSideVertices + i;", + " const sample = gridJ * data.size + gridI;", + " positions[vertex * 3 + 2] = data.heights[sample] * AREA_DEPTH;", + "", + " const leftI = Math.max(gridI - 1, 0);", + " const rightI = Math.min(gridI + 1, data.resolution);", + " const upJ = Math.max(gridJ - 1, 0);", + " const downJ = Math.min(gridJ + 1, data.resolution);", + " const nx =", + " ((data.getHeight(leftI, gridJ) - data.getHeight(rightI, gridJ)) * slopeFactor) /", + " (rightI - leftI);", + " const ny =", + " ((data.getHeight(gridI, upJ) - data.getHeight(gridI, downJ)) * slopeFactor) /", + " (downJ - upJ);", + " const inverseLength = 1 / Math.sqrt(nx * nx + ny * ny + 1);", + " normals[vertex * 3] = nx * inverseLength;", + " normals[vertex * 3 + 1] = ny * inverseLength;", + " normals[vertex * 3 + 2] = inverseLength;", + " }", + " }", + " this._forEachSkirtVertex((skirtVertex, borderVertex) => {", + " for (let axis = 0; axis < 3; axis++) {", + " normals[skirtVertex * 3 + axis] = normals[borderVertex * 3 + axis];", + " }", + " });", + " this._updateSkirtHeights(positions);", + " let maxZ = 0;", + " for (let vertex = 0; vertex < this.chunkGridVertices; vertex++) {", + " maxZ = Math.max(maxZ, positions[vertex * 3 + 2]);", + " }", + " const chunkSize = this.chunkCells * cellSize;", + " chunk.geometry.boundingBox.min.set(chunk.firstI * cellSize, chunk.firstJ * cellSize, 0);", + " chunk.geometry.boundingBox.max.set(", + " chunk.firstI * cellSize + chunkSize,", + " chunk.firstJ * cellSize + chunkSize,", + " maxZ", + " );", + " chunk.geometry.boundingBox.getBoundingSphere(chunk.geometry.boundingSphere);", + " chunk.center.z = maxZ / 2;", + " chunk.geometry.attributes.position.needsUpdate = true;", + " chunk.geometry.attributes.normal.needsUpdate = true;", + " }", + " }", + " if (hasPaintChanged) {", + " // The data may have been replaced by one with the same resolution.", + " this.splatTexture.image.data = data.splat;", + " uploadGridTextureRows(this.splatTexture, samples);", + " this.colorTexture.image.data = data.colors;", + " uploadGridTextureRows(this.colorTexture, samples);", + " }", + " }", + "", + " updateMaterial() {", + " const { object } = this.terrain;", + " const game = object.getRuntimeScene().getGame();", + " const colors = [object._getLayer1Color(), object._getLayer2Color(), object._getLayer3Color(), object._getLayer4Color()];", + " const textures = [", + " object._getLayer1Texture(),", + " object._getLayer2Texture(),", + " object._getLayer3Texture(),", + " object._getLayer4Texture(),", + " ];", + " const normalMaps = [", + " object._getLayer1NormalMap(),", + " object._getLayer2NormalMap(),", + " object._getLayer3NormalMap(),", + " object._getLayer4NormalMap(),", + " ];", + " for (let layer = 0; layer < LAYER_COUNT; layer++) {", + " this.uniforms.terrainLayerColors.value[layer].set(gdjs.rgbOrHexStringToNumber(colors[layer]));", + " this.uniforms['terrainLayerMap' + layer].value = getLayerTexture(game, textures[layer], false);", + " this.uniforms['terrainLayerNormalMap' + layer].value = getLayerTexture(game, normalMaps[layer], true);", + " }", + " this.uniforms.terrainHasNormalMaps.value = normalMaps.some((normalMap) => !!normalMap) ? 1 : 0;", + " this.uniforms.terrainNormalMapStrength.value = Math.max(object._getNormalMapStrength(), 0);", + " this.uniforms.terrainTextureSize.value = Math.max(object._getTextureSize(), 1);", + " this.uniforms.terrainHeightBlending.value = clamp01(object._getHeightBlending());", + " this.uniforms.terrainTextureVariation.value = clamp01(object._getTextureVariation());", + "", + " const cliffLayer = parseInt(object._getCliffLayer(), 10);", + " const cliffAngle = gdjs.toRad(object._getCliffAngle());", + " const cliffBlendAngle = gdjs.toRad(4);", + " this.uniforms.terrainCliffLayer.value.set(", + " cliffLayer === 1 ? 1 : 0,", + " cliffLayer === 2 ? 1 : 0,", + " cliffLayer === 3 ? 1 : 0,", + " cliffLayer === 4 ? 1 : 0", + " );", + " this.uniforms.terrainCliffCosines.value.set(", + " Math.cos(cliffAngle + cliffBlendAngle),", + " Math.cos(cliffAngle - cliffBlendAngle)", + " );", + "", + " const castShadow = object._getCastShadow();", + " const receiveShadow = object._getReceiveShadow();", + " for (const chunk of this.chunks) {", + " chunk.mesh.castShadow = castShadow;", + " chunk.mesh.receiveShadow = receiveShadow;", + " }", + " }", + "", + " /**", + " * Show the brush circle of the editor.", + " * @param {THREE.Vector3 | null} localPosition", + " * @param {number} radius In scene units.", + " */", + " setBrush(localPosition, radius) {", + " const brush = this.uniforms.terrainBrush.value;", + " if (!localPosition) {", + " brush.w = 0;", + " return;", + " }", + " this.group.updateWorldMatrix(true, false);", + " const worldPosition = this._chunkPosition.copy(localPosition).applyMatrix4(this.group.matrixWorld);", + " const worldScale = this.uniforms.terrainWorldScale.value;", + " brush.set(worldPosition.x * worldScale, worldPosition.y * worldScale, radius, 1);", + " }", + "", + " /** @param {THREE.Camera | null} camera */", + " updateLevelsOfDetail(camera) {", + " if (!camera || !this.group.visible) return;", + " const matrixWorld = this.group.matrixWorld;", + " this._cameraPosition.setFromMatrixPosition(camera.matrixWorld);", + " this._scale.setFromMatrixScale(matrixWorld);", + " const cellSize = AREA_SIZE / this.terrain.data.resolution;", + " const chunkWorldSize = this.chunkCells * cellSize * Math.max(this._scale.x, this._scale.y);", + " for (const chunk of this.chunks) {", + " const distance =", + " this._chunkPosition.copy(chunk.center).applyMatrix4(matrixWorld).distanceTo(this._cameraPosition) /", + " chunkWorldSize;", + " let lod = LOD_DISTANCES.length - 1;", + " while (lod > 0 && distance < LOD_DISTANCES[lod]) lod--;", + " if (lod !== chunk.lod) {", + " chunk.lod = lod;", + " chunk.geometry.setIndex(getLodIndex(this.chunkCells, LOD_STEPS[lod]));", + " }", + " }", + " }", + "", + " disposeChunks() {", + " for (const chunk of this.chunks) {", + " chunk.mesh.removeFromParent();", + " // The index is shared by all chunks: it must stay on the GPU.", + " chunk.geometry.setIndex(null);", + " chunk.geometry.dispose();", + " }", + " this.chunks.length = 0;", + " if (this.splatTexture) this.splatTexture.dispose();", + " this.splatTexture = null;", + " if (this.colorTexture) this.colorTexture.dispose();", + " this.colorTexture = null;", + " }", + "", + " dispose() {", + " this.disposeChunks();", + " this.material.dispose();", + " this.depthMaterial.dispose();", + " this.distanceMaterial.dispose();", + " this.group.removeFromParent();", + " }", + "}", + "", + "/**", + " * Makes a Physics3D behavior use a height field shape following the terrain.", + " * Changes of heights are applied to the shape without recreating it.", + " * @implements {gdjs.Physics3DRuntimeBehavior.BodyUpdater}", + " */", + "class TerrainBodyUpdater {", + " /**", + " * @param {Terrain} terrain", + " * @param {gdjs.Physics3DRuntimeBehavior} physics", + " */", + " constructor(terrain, physics) {", + " this.terrain = terrain;", + " this.physics = physics;", + " this.defaultBodyUpdater = physics.bodyUpdater;", + " this.changedSamples = new SampleRectangle();", + " /** @type {Jolt.HeightFieldShape | null} */", + " this.heightField = null;", + " this.heightScale = 1;", + " this.heightsBufferPointer = 0;", + " this.heightsBufferLength = 0;", + " this.bodyX = 0;", + " this.bodyY = 0;", + " this.bodyZ = 0;", + " this.bodyHeight = 0;", + " physics.bodyType = 'Static';", + " physics.bodyUpdater = this;", + " }", + "", + " createShape() {", + " const { data, object } = this.terrain;", + " const worldInvScale = this.physics._sharedData.worldInvScale;", + " const { size } = data;", + " this.heightScale = object.getDepth() * worldInvScale;", + "", + " // Changes of heights (SetHeights) break collisions unless the sample", + " // count is a power of 2: extra samples have no collision.", + " const sampleCount = Math.pow(2, Math.ceil(Math.log2(size)));", + " const settings = new Jolt.HeightFieldShapeSettings();", + " settings.mScale.Set(", + " (object.getWidth() / data.resolution) * worldInvScale,", + " 1,", + " (object.getHeight() / data.resolution) * worldInvScale", + " );", + " settings.mSampleCount = sampleCount;", + " settings.mBlockSize = 4;", + " // Allows later changes of heights on the whole depth of the object.", + " settings.mMinHeightValue = 0;", + " settings.mMaxHeightValue = this.heightScale;", + " settings.mHeightSamples.resize(sampleCount * sampleCount);", + " const samples = new Float32Array(", + " Jolt.HEAPF32.buffer,", + " Jolt.getPointer(settings.mHeightSamples.data()),", + " sampleCount * sampleCount", + " );", + " const noCollision = Jolt.HeightFieldShapeConstantValues.prototype.cNoCollisionValue;", + " samples.fill(noCollision);", + " // Height fields go along X and Z: rows are flipped so that, once", + " // rotated to have heights along Z, rows go along Y.", + " for (let j = 0; j < size; j++) {", + " const row = (size - 1 - j) * sampleCount;", + " for (let i = 0; i < size; i++) {", + " samples[row + i] = this._getSampleHeight(j * size + i, noCollision);", + " }", + " }", + " const shape = settings.Create().Get();", + " Jolt.destroy(settings);", + " this.heightField = Jolt.castObject(shape, Jolt.HeightFieldShape);", + " this.changedSamples.clear();", + " return shape;", + " }", + "", + " /** @returns {number} The height of a sample in the shape, or no collision in a hole. */", + " _getSampleHeight(index, noCollision) {", + " const height = this.terrain.surfaceHeights[index];", + " return Number.isNaN(height) ? noCollision : height * this.heightScale;", + " }", + "", + " _getBodyPosition() {", + " const { object } = this.terrain;", + " const worldInvScale = this.physics._sharedData.worldInvScale;", + " this.bodyX = object.getX();", + " this.bodyY = object.getY();", + " this.bodyZ = object.getZ();", + " // The origin of the height field is at the bottom of the terrain (see createShape).", + " this.bodyHeight = object.getHeight();", + " return this.physics._sharedData.getRVec3(", + " object.getX() * worldInvScale,", + " (object.getY() + object.getHeight()) * worldInvScale,", + " object.getZ() * worldInvScale", + " );", + " }", + "", + " _getBodyRotation() {", + " // A quarter turn around X: heights go along Z.", + " return this.physics._sharedData.getQuat(Math.SQRT1_2, 0, 0, Math.SQRT1_2);", + " }", + "", + " createAndAddBody() {", + " const { physics } = this;", + " const { bodyInterface } = physics._sharedData;", + " const shape = this.createShape();", + " const settings = new Jolt.BodyCreationSettings(", + " shape,", + " this._getBodyPosition(),", + " this._getBodyRotation(),", + " Jolt.EMotionType_Static,", + " physics.getBodyLayer()", + " );", + " settings.mFriction = physics.friction;", + " settings.mRestitution = physics.restitution;", + " const body = bodyInterface.CreateBody(settings);", + " Jolt.destroy(settings);", + " bodyInterface.AddBody(body.GetID(), Jolt.EActivation_DontActivate);", + " return body;", + " }", + "", + " updateObjectFromBody() {", + " // The terrain is static: the body never moves it.", + " }", + "", + " updateBodyFromObject() {", + " const { physics } = this;", + " const body = physics._body;", + " if (!body) return;", + " const { object } = this.terrain;", + " const { bodyInterface } = physics._sharedData;", + " if (", + " object.getX() !== this.bodyX ||", + " object.getY() !== this.bodyY ||", + " object.getZ() !== this.bodyZ ||", + " object.getHeight() !== this.bodyHeight", + " ) {", + " bodyInterface.SetPositionAndRotation(", + " body.GetID(),", + " this._getBodyPosition(),", + " this._getBodyRotation(),", + " Jolt.EActivation_DontActivate", + " );", + " }", + " if (!this.changedSamples.isEmpty()) {", + " this._applyChangedHeights(body);", + " }", + " }", + "", + " _applyChangedHeights(body) {", + " const { data } = this.terrain;", + " const { size } = data;", + " const { minI, minJ, maxI, maxJ } = this.changedSamples;", + " // Jolt only changes blocks of samples. Rows are flipped and extra", + " // samples have no collision (see createShape).", + " const blockSize = this.heightField.GetBlockSize();", + " const sampleCount = this.heightField.GetSampleCount();", + " const alignDown = (value) => Math.floor(value / blockSize) * blockSize;", + " const alignUp = (value) => Math.min(Math.ceil(value / blockSize) * blockSize, sampleCount);", + " const firstX = alignDown(minI);", + " const firstRow = alignDown(size - 1 - maxJ);", + " const width = alignUp(maxI + 1) - firstX;", + " const height = alignUp(size - minJ) - firstRow;", + " if (width * height > this.heightsBufferLength) {", + " if (this.heightsBufferPointer) Jolt._webidl_free(this.heightsBufferPointer);", + " this.heightsBufferLength = width * height;", + " this.heightsBufferPointer = Jolt._webidl_malloc(this.heightsBufferLength * 4);", + " }", + " const heights = new Float32Array(Jolt.HEAPF32.buffer, this.heightsBufferPointer, width * height);", + " const noCollision = Jolt.HeightFieldShapeConstantValues.prototype.cNoCollisionValue;", + " for (let row = 0; row < height; row++) {", + " const j = size - 1 - (firstRow + row);", + " for (let column = 0; column < width; column++) {", + " const i = firstX + column;", + " heights[row * width + column] =", + " i < size && j >= 0 ? this._getSampleHeight(j * size + i, noCollision) : noCollision;", + " }", + " }", + " const sharedData = this.physics._sharedData;", + " this.heightField.SetHeights(", + " firstX,", + " firstRow,", + " width,", + " height,", + " this.heightsBufferPointer,", + " width,", + " sharedData.jolt.GetTempAllocator()", + " );", + " sharedData.bodyInterface.NotifyShapeChanged(", + " body.GetID(),", + " sharedData.getVec3(0, 0, 0),", + " false,", + " Jolt.EActivation_DontActivate", + " );", + " this._wakeUpBodiesOnChangedSamples();", + " this.changedSamples.clear();", + " }", + "", + " /** Bodies sleeping on the changed area would otherwise float or sink. */", + " _wakeUpBodiesOnChangedSamples() {", + " const { object, data } = this.terrain;", + " const worldInvScale = this.physics._sharedData.worldInvScale;", + " const cellWidth = object.getWidth() / data.resolution;", + " const cellHeight = object.getHeight() / data.resolution;", + " const { minI, minJ, maxI, maxJ } = this.changedSamples;", + " const min = new Jolt.Vec3(", + " (object.getX() + (minI - 1) * cellWidth) * worldInvScale,", + " (object.getY() + (minJ - 1) * cellHeight) * worldInvScale,", + " (object.getZ() - 1) * worldInvScale", + " );", + " const max = new Jolt.Vec3(", + " (object.getX() + (maxI + 1) * cellWidth) * worldInvScale,", + " (object.getY() + (maxJ + 1) * cellHeight) * worldInvScale,", + " (object.getZ() + object.getDepth() + 1) * worldInvScale", + " );", + " const box = new Jolt.AABox(min, max);", + " const broadPhaseLayerFilter = new Jolt.BroadPhaseLayerFilter();", + " const objectLayerFilter = new Jolt.ObjectLayerFilter();", + " this.physics._sharedData.bodyInterface.ActivateBodiesInAABox(box, broadPhaseLayerFilter, objectLayerFilter);", + " Jolt.destroy(min);", + " Jolt.destroy(max);", + " Jolt.destroy(box);", + " Jolt.destroy(broadPhaseLayerFilter);", + " Jolt.destroy(objectLayerFilter);", + " }", + "", + " recreateShape() {", + " const body = this.physics._body;", + " if (!body) return;", + " this.physics._sharedData.bodyInterface.SetShape(", + " body.GetID(),", + " this.createShape(),", + " false,", + " Jolt.EActivation_DontActivate", + " );", + " const { resolution } = this.terrain.data;", + " this.changedSamples.set(0, 0, resolution, resolution);", + " this._wakeUpBodiesOnChangedSamples();", + " this.changedSamples.clear();", + " }", + "", + " destroyBody() {", + " this.defaultBodyUpdater.destroyBody();", + " this.heightField = null;", + " }", + "", + " dispose() {", + " if (this.heightsBufferPointer) Jolt._webidl_free(this.heightsBufferPointer);", + " this.heightsBufferPointer = 0;", + " this.heightsBufferLength = 0;", + " }", + "}", + "", + "/** @param {TerrainData} data */", + "const copyTerrainData = (data) => {", + " const copy = new TerrainData(data.resolution);", + " copy.copyFrom(data);", + " return copy;", + "};", + "", + "// Generating a relief or reading an image is long for big terrains, and done", + "// for every instance and every hot-reload: the results are kept.", + "const baseDataCache = new Map();", + "const MAX_CACHED_BASE_DATA = 8;", + "// Changes of the shape of a terrain kept for consumers of its surface that", + "// missed some versions: older changes update the whole surface.", + "const MAX_SURFACE_CHANGES = 64;", + "// Unique among all terrains: a terrain replaced by another one never has its version.", + "let lastGroundVersion = 0;", + "", + "/**", + " * The terrain of a Terrain3D object: its data, how it's drawn and its", + " * surface (for collisions and navigation meshes). Positions are in the scene.", + " */", + "class Terrain {", + " /** @param {gdjs.CustomRuntimeObject3D} object */", + " constructor(object) {", + " this.object = object;", + " /** @type {TerrainData} */", + " this.data = new TerrainData(DEFAULT_RESOLUTION);", + " /** The data before any sculpting or painting, from the relief or heightmap. */", + " this.baseData = this.data;", + " this.renderer = new TerrainRenderer(this);", + " /** @type {TerrainBodyUpdater[]} */", + " this.bodyUpdaters = [];", + " this.changedSamples = new SampleRectangle();", + " this.hasPaintChanged = false;", + " /** Increased each time the ground changes, for objects following it (like grass). */", + " this.version = 0;", + " /** Increased each time the shape of the ground changes (not its paint or position). */", + " this.surfaceVersion = 0;", + " /**", + " * The last changes of the shape of the ground, for `getChangedArea`.", + " * @type {Array<{version: number, samples: SampleRectangle}>}", + " */", + " this.surfaceChanges = [];", + " /** The changes since this version are all in `surfaceChanges`. */", + " this.surfaceChangesStartVersion = 0;", + " this._stroke = { cellWidth: 1, cellHeight: 1, ax: 0, ay: 0, bx: 0, by: 0, radius: 0 };", + " this._strokeChangedSamples = new SampleRectangle();", + " /** The properties the data was loaded from, to reload it only if they change. */", + " this.loadedFrom = '';", + " /** The heightmap image being loaded: the relief is used until it is. */", + " this.loadingHeightmapImage = '';", + " this.isDisposed = false;", + " /** The last value of the sculpt data property read or saved. */", + " this.sculptData = '';", + " /**", + " * Values saved by the editor tools: the editor sends them back with a", + " * hot-reload, possibly after newer ones were saved.", + " * @type {string[]}", + " */", + " this.savedSculptData = [];", + " /** The heights of the surface: the heights of the data, and NaN in holes. */", + " this.surfaceHeights = new Float32Array(0);", + " this.loadFromProperties();", + " this._transform = this._getTransform();", + " getGrounds(object.getRuntimeScene()).add(this);", + " /**", + " * The shape of the ground, for the physics engine and navigation meshes.", + " * @type {gdjs.Surface}", + " */", + " this.surface = {", + " getVersion: () => this.surfaceVersion,", + " getChangedArea: (sinceVersion) => this.getChangedArea(sinceVersion),", + " getTriangles: () => null,", + " getHeightField: () => ({ columns: this.data.size, rows: this.data.size, heights: this.surfaceHeights }),", + " };", + " if (typeof object.setSurface === 'function') {", + " object.setSurface(this.surface);", + " } else if (gdjs.Physics3DRuntimeBehavior) {", + " // Older GDevelop versions don't use surfaces.", + " for (const behavior of object._behaviors) {", + " if (behavior instanceof gdjs.Physics3DRuntimeBehavior) {", + " this.bodyUpdaters.push(new TerrainBodyUpdater(this, behavior));", + " }", + " }", + " }", + " }", + "", + " _getSourceProperties() {", + " const { object } = this;", + " return [", + " object._getResolution(),", + " object._getRelief(),", + " object._getSeed(),", + " object._getHeightmapImage(),", + " object._getEdits(),", + " ].join('|');", + " }", + "", + " loadFromProperties() {", + " const { object } = this;", + " const resolution = parseInt(object._getResolution(), 10);", + " const validResolution = RESOLUTIONS.indexOf(resolution) !== -1 ? resolution : DEFAULT_RESOLUTION;", + " this.sculptData = object._getSculptData();", + " this.loadedFrom = this._getSourceProperties();", + "", + " this.baseData = this._getBaseData(validResolution);", + " let data = codec.decode(this.sculptData, (resolution) => copyTerrainData(this._getBaseData(resolution)));", + " if (data && data.resolution !== validResolution) {", + " data = data.resampled(validResolution);", + " }", + " this.setData(data || copyTerrainData(this.baseData));", + " }", + "", + " /**", + " * @param {number} resolution", + " * @returns {TerrainData} The data from the relief or the heightmap, shared: it must not be changed.", + " */", + " _getBaseData(resolution) {", + " const { object } = this;", + " const key = [resolution, object._getRelief(), object._getSeed(), object._getHeightmapImage(), object._getEdits()].join('|');", + " const cachedData = baseDataCache.get(key);", + " if (cachedData) return cachedData;", + "", + " const data = new TerrainData(resolution);", + " const heightmapImage = object._getHeightmapImage();", + " const isHeightmapLoaded =", + " !!heightmapImage && loadHeightsFromImage(data, object.getRuntimeScene().getGame(), heightmapImage);", + " if (!isHeightmapLoaded) {", + " relief.generate(data, object._getRelief(), object._getSeed());", + " if (heightmapImage) this._reloadOnceLoaded(heightmapImage);", + " }", + " edits.apply(data, object._getEdits());", + " // Not kept if the image is missing: it may be loaded later.", + " if (isHeightmapLoaded || !heightmapImage) baseDataCache.set(key, data);", + " if (baseDataCache.size > MAX_CACHED_BASE_DATA) {", + " baseDataCache.delete(baseDataCache.keys().next().value);", + " }", + " return data;", + " }", + "", + " /** @param {TerrainData} data */", + " setData(data) {", + " this.version = ++lastGroundVersion;", + " this.surfaceVersion = this.version;", + " this.surfaceChanges.length = 0;", + " this.surfaceChangesStartVersion = this.version;", + " const hasSameResolution = data.resolution === this.data.resolution && this.renderer.chunks.length > 0;", + " this.data = data;", + " const allSamples = new SampleRectangle().set(0, 0, data.resolution, data.resolution);", + " this.surfaceHeights = new Float32Array(data.heights.length);", + " this.renderer.setHasHoles(false);", + " this._updateSurfaceHeights(allSamples);", + " if (hasSameResolution) {", + " this.renderer.updateSamples(allSamples, true);", + " } else {", + " this.renderer.rebuild();", + " }", + " for (const bodyUpdater of this.bodyUpdaters) {", + " bodyUpdater.recreateShape();", + " }", + " }", + "", + " onHotReloading() {", + " const sculptData = this.object._getSculptData();", + " const savedIndex = this.savedSculptData.indexOf(sculptData);", + " if (savedIndex !== -1 && this._getSourceProperties() === this.loadedFrom) {", + " // The terrain already has this data, or newer changes.", + " this.savedSculptData.splice(0, savedIndex + 1);", + " } else if (sculptData !== this.sculptData || this._getSourceProperties() !== this.loadedFrom) {", + " this.savedSculptData.length = 0;", + " this.loadFromProperties();", + " }", + " this.renderer.updateMaterial();", + " }", + "", + " /** @returns {string} The data to save in the sculpt data property. */", + " save() {", + " this.sculptData = codec.encode(this.data, this.baseData);", + " this.savedSculptData.push(this.sculptData);", + " return this.sculptData;", + " }", + "", + " getCellWidth() {", + " return this.object.getWidth() / this.data.resolution;", + " }", + "", + " getCellHeight() {", + " return this.object.getHeight() / this.data.resolution;", + " }", + "", + " _toGridX(x) {", + " return (x - this.object.getX()) / this.getCellWidth();", + " }", + "", + " _toGridY(y) {", + " return (y - this.object.getY()) / this.getCellHeight();", + " }", + "", + " /** @returns {number} The Z position of the ground. */", + " getHeightAt(x, y) {", + " return this.object.getZ() + this.data.sampleHeight(this._toGridX(x), this._toGridY(y)) * this.object.getDepth();", + " }", + "", + " /** @returns {number} The angle of the ground with the horizontal, in degrees. */", + " getSlopeAt(x, y) {", + " const gridX = this._toGridX(x);", + " const gridY = this._toGridY(y);", + " const { data } = this;", + " const depth = this.object.getDepth();", + " const slopeX = ((data.sampleHeight(gridX + 0.5, gridY) - data.sampleHeight(gridX - 0.5, gridY)) * depth) / this.getCellWidth();", + " const slopeY = ((data.sampleHeight(gridX, gridY + 0.5) - data.sampleHeight(gridX, gridY - 0.5)) * depth) / this.getCellHeight();", + " return gdjs.toDegrees(Math.atan(Math.hypot(slopeX, slopeY)));", + " }", + "", + " /** @returns {number} The layer painted the most, from 1 to 4. */", + " getLayerAt(x, y) {", + " return this.data.getDominantLayer(this._toGridX(x), this._toGridY(y)) + 1;", + " }", + "", + " /**", + " * @param {number} layer From 1 to 4.", + " * @returns {number} How much the layer is painted, from 0 to 1.", + " */", + " getLayerWeightAt(x, y, layer) {", + " const layerIndex = clampInteger(layer - 1, 0, LAYER_COUNT - 1);", + " return this.data.sampleLayerWeight(this._toGridX(x), this._toGridY(y), layerIndex);", + " }", + "", + " /** @returns {number[]} The painted tint: red, green and blue, from 0 to 255 (white when there is none). */", + " getTintAt(x, y) {", + " const gridX = this._toGridX(x);", + " const gridY = this._toGridY(y);", + " return [0, 1, 2].map((channel) => Math.round(this.data.sampleColor(gridX, gridY, channel) * 255));", + " }", + "", + " /** @returns {number} How wet the ground is, from 0 (dry) to 1, painted or near the water (see the wet shore). */", + " getWetnessAt(x, y) {", + " const paintedWetness = this.data.sampleColor(this._toGridX(x), this._toGridY(y), 3);", + " const { object } = this;", + " if (!object._getWetShore()) return paintedWetness;", + " const fade = Math.max(object._getWetShoreFade(), 0.001);", + " const shoreWetness = clamp01((object._getWetShoreZ() + fade - this.getHeightAt(x, y)) / fade);", + " // Like the smoothstep of the shader.", + " return Math.max(paintedWetness, shoreWetness * shoreWetness * (3 - 2 * shoreWetness));", + " }", + "", + " _isInArea(x, y) {", + " const { object } = this;", + " return (", + " x >= object.getX() &&", + " x <= object.getX() + object.getWidth() &&", + " y >= object.getY() &&", + " y <= object.getY() + object.getHeight()", + " );", + " }", + "", + " /** @returns {boolean} true if the position is above or under the terrain, and not in a hole. */", + " containsPoint(x, y) {", + " return this._isInArea(x, y) && this.data.sampleSolidity(this._toGridX(x), this._toGridY(y)) >= 0.5;", + " }", + "", + " /** @returns {boolean} true if there is a hole in the terrain at the position, like what is drawn. */", + " isHoleAt(x, y) {", + " return this._isInArea(x, y) && this.data.sampleSolidity(this._toGridX(x), this._toGridY(y)) < 0.5;", + " }", + "", + " _getStroke(x1, y1, x2, y2, radius) {", + " const stroke = this._stroke;", + " stroke.cellWidth = this.getCellWidth();", + " stroke.cellHeight = this.getCellHeight();", + " stroke.ax = x1 - this.object.getX();", + " stroke.ay = y1 - this.object.getY();", + " stroke.bx = x2 - this.object.getX();", + " stroke.by = y2 - this.object.getY();", + " stroke.radius = Math.max(radius, 0);", + " return stroke;", + " }", + "", + " /**", + " * @param {SampleRectangle} samples", + " * @param {boolean} haveHeightsChanged false when only the paint changed.", + " */", + " _onSamplesChanged(samples, haveHeightsChanged) {", + " if (samples.isEmpty()) return;", + " this.version = ++lastGroundVersion;", + " this.changedSamples.add(samples);", + " // Painting changes the surface when it makes holes or fills them.", + " const hasSurfaceChanged = this._updateSurfaceHeights(samples);", + " if (!haveHeightsChanged && !hasSurfaceChanged) return;", + " this.surfaceVersion = this.version;", + " this.surfaceChanges.push({ version: this.version, samples: new SampleRectangle().set(samples.minI, samples.minJ, samples.maxI, samples.maxJ) });", + " if (this.surfaceChanges.length > MAX_SURFACE_CHANGES) {", + " this.surfaceChangesStartVersion = this.surfaceChanges.shift().version;", + " }", + " for (const bodyUpdater of this.bodyUpdaters) {", + " bodyUpdater.changedSamples.add(samples);", + " }", + " }", + "", + " /**", + " * @param {SampleRectangle} samples", + " * @returns {boolean} true if heights of the surface changed, or holes were made or filled.", + " */", + " _updateSurfaceHeights(samples) {", + " const { data, surfaceHeights } = this;", + " let hasChanged = false;", + " let hasHoles = false;", + " for (let j = samples.minJ; j <= samples.maxJ; j++) {", + " for (let i = samples.minI; i <= samples.maxI; i++) {", + " const index = j * data.size + i;", + " const isHole = data.isHole(index);", + " const height = isHole ? NaN : data.heights[index];", + " const previousHeight = surfaceHeights[index];", + " if (isHole) {", + " hasHoles = true;", + " if (Number.isNaN(previousHeight)) continue;", + " } else if (height === previousHeight) {", + " continue;", + " }", + " surfaceHeights[index] = height;", + " hasChanged = true;", + " }", + " }", + " // Filled holes keep the shaders drawing holes until the data is loaded again.", + " if (hasHoles) this.renderer.setHasHoles(true);", + " return hasChanged;", + " }", + "", + " /** @param {number} height In scene units, negative to lower the ground. */", + " raise(x1, y1, x2, y2, radius, height) {", + " const samples = this._strokeChangedSamples;", + " brushes.raise(this.data, this._getStroke(x1, y1, x2, y2, radius), height / this.object.getDepth(), samples);", + " this._onSamplesChanged(samples, true);", + " }", + "", + " /** @param {number} z The Z position to flatten the ground to. */", + " /** @param {number} [toZ] The Z position at the end of the segment, for a slope. The same as z by default. */", + " flatten(x1, y1, x2, y2, radius, z, strength, toZ = z) {", + " const samples = this._strokeChangedSamples;", + " const toHeight = (value) => (value - this.object.getZ()) / this.object.getDepth();", + " brushes.flatten(this.data, this._getStroke(x1, y1, x2, y2, radius), toHeight(z), strength, samples, toHeight(toZ));", + " this._onSamplesChanged(samples, true);", + " }", + "", + " smooth(x1, y1, x2, y2, radius, strength) {", + " const samples = this._strokeChangedSamples;", + " brushes.smooth(this.data, this._getStroke(x1, y1, x2, y2, radius), strength, samples);", + " this._onSamplesChanged(samples, true);", + " }", + "", + " /** @param {number} layer From 1 to 4. */", + " paint(x1, y1, x2, y2, radius, layer, strength) {", + " const layerIndex = clampInteger(layer - 1, 0, LAYER_COUNT - 1);", + " const samples = this._strokeChangedSamples;", + " brushes.paint(this.data, this._getStroke(x1, y1, x2, y2, radius), layerIndex, strength, samples);", + " this.hasPaintChanged = true;", + " this._onSamplesChanged(samples, false);", + " }", + "", + " /** @param {number[]} color The red, green and blue (0-255) of the tint: white removes it. */", + " tint(x1, y1, x2, y2, radius, color, strength) {", + " const samples = this._strokeChangedSamples;", + " brushes.tint(this.data, this._getStroke(x1, y1, x2, y2, radius), color, strength, samples);", + " this.hasPaintChanged = true;", + " this._onSamplesChanged(samples, false);", + " }", + "", + " /** @param {number} wetness From 0 (dry) to 1. */", + " wet(x1, y1, x2, y2, radius, wetness, strength) {", + " const samples = this._strokeChangedSamples;", + " brushes.wet(this.data, this._getStroke(x1, y1, x2, y2, radius), wetness, strength, samples);", + " this.hasPaintChanged = true;", + " this._onSamplesChanged(samples, false);", + " }", + "", + " /** Make a hole around a segment (`fillHoles` fills it again). */", + " makeHole(x1, y1, x2, y2, radius) {", + " const samples = this._strokeChangedSamples;", + " brushes.hole(this.data, this._getStroke(x1, y1, x2, y2, radius), samples);", + " this.hasPaintChanged = true;", + " this._onSamplesChanged(samples, false);", + " }", + "", + " /** Fill the holes around a segment with the layers of the ground around them. */", + " fillHoles(x1, y1, x2, y2, radius) {", + " const samples = this._strokeChangedSamples;", + " brushes.fill(this.data, this._getStroke(x1, y1, x2, y2, radius), samples);", + " this.hasPaintChanged = true;", + " this._onSamplesChanged(samples, false);", + " }", + "", + " /**", + " * Make the entrance of a tunnel or a cave: a hole, with the ground in", + " * front of it flattened to the floor of the tunnel.", + " * @param {number} angle The direction going into the ground, in degrees.", + " * @param {number} floorZ", + " * @param {number} length The length of the flat ground in front of the entrance.", + " */", + " carveEntrance(x, y, radius, angle, floorZ, length) {", + " const directionX = Math.cos(gdjs.toRad(angle));", + " const directionY = Math.sin(gdjs.toRad(angle));", + " this.flatten(x - directionX * length, y - directionY * length, x, y, radius * 1.5, floorZ, 1);", + " this.makeHole(x, y, x + directionX * radius, y + directionY * radius, radius);", + " }", + "", + " generate(reliefName, seed) {", + " this.baseData = new TerrainData(this.data.resolution);", + " relief.generate(this.baseData, reliefName, seed);", + " this.setData(copyTerrainData(this.baseData));", + " }", + "", + " loadHeightmap(imageResourceName) {", + " const baseData = new TerrainData(this.data.resolution);", + " if (loadHeightsFromImage(baseData, this.object.getRuntimeScene().getGame(), imageResourceName)) {", + " baseData.splat.set(this.data.splat);", + " baseData.colors.set(this.data.colors);", + " this.baseData = baseData;", + " this.setData(copyTerrainData(baseData));", + " }", + " }", + "", + " /** @param {gdjs.RuntimeObject[]} objects */", + " placeOnGround(objects) {", + " for (const object of objects) {", + " if (object === this.object || !gdjs.Base3DHandler.is3D(object)) continue;", + " const groundZ = this.getHeightAt(object.getCenterXInScene(), object.getCenterYInScene());", + " // The origin of the object is not always at its bottom.", + " object.setZ(groundZ + object.getZ() - object.getUnrotatedAABBMinZ());", + " }", + " }", + "", + " /**", + " * @returns {THREE.Vector3 | null} The point of the ground under the cursor,", + " * in the terrain group coordinates. Holes are hit too, so that brushes can fill them.", + " */", + " raycastFromCursor(game, raycaster, ndc) {", + " const layer = this.object.getInstanceContainer().getLayer(this.object.getLayer());", + " const camera = layer.getRenderer().getThreeCamera();", + " if (!camera) return null;", + " const inputManager = game.getInputManager();", + " ndc.set(", + " (inputManager.getCursorX() / game.getGameResolutionWidth()) * 2 - 1,", + " -(inputManager.getCursorY() / game.getGameResolutionHeight()) * 2 + 1", + " );", + " raycaster.setFromCamera(ndc, camera);", + " this.renderer.group.updateWorldMatrix(true, false);", + " const ray = raycaster.ray.clone().applyMatrix4(this.renderer.group.matrixWorld.clone().invert());", + " ray.direction.normalize();", + "", + " const box = new THREE.Box3(new THREE.Vector3(0, 0, 0), new THREE.Vector3(AREA_SIZE, AREA_SIZE, AREA_DEPTH));", + " const entry = box.containsPoint(ray.origin) ? ray.origin.clone() : ray.intersectBox(box, new THREE.Vector3());", + " if (!entry) return null;", + " const cellSize = AREA_SIZE / this.data.resolution;", + " const groundZAt = (point) => this.data.sampleHeight(point.x / cellSize, point.y / cellSize) * AREA_DEPTH;", + " const step = cellSize / 2;", + " const point = entry.clone();", + " const previousPoint = entry.clone();", + " for (let distance = 0; distance < AREA_SIZE * 2; distance += step) {", + " if (point.z <= groundZAt(point)) {", + " // Refine between the last point above the ground and this one.", + " for (let iteration = 0; iteration < 8; iteration++) {", + " const middle = previousPoint.clone().lerp(point, 0.5);", + " if (middle.z <= groundZAt(middle)) point.copy(middle);", + " else previousPoint.copy(middle);", + " }", + " return point;", + " }", + " if (!box.containsPoint(point)) return null;", + " previousPoint.copy(point);", + " point.addScaledVector(ray.direction, step);", + " }", + " return null;", + " }", + "", + " /**", + " * @param {THREE.Vector3} localPosition In the terrain group coordinates (like from `raycastFromCursor`).", + " * @returns {{x: number, y: number, z: number}} The position in the scene.", + " */", + " toScenePosition(localPosition) {", + " const { object } = this;", + " return {", + " x: object.getX() + (localPosition.x / AREA_SIZE) * object.getWidth(),", + " y: object.getY() + (localPosition.y / AREA_SIZE) * object.getHeight(),", + " z: object.getZ() + (localPosition.z / AREA_DEPTH) * object.getDepth(),", + " };", + " }", + "", + " /** Draw the changes of the ground (collisions are updated by the physics engine). */", + " applyChanges() {", + " this.renderer.updateSamples(this.changedSamples, this.hasPaintChanged);", + " this.changedSamples.clear();", + " this.hasPaintChanged = false;", + " }", + "", + " /** @returns {number} A number changing each time the ground changes (see `getGrounds`). */", + " getVersion() {", + " return this.version;", + " }", + "", + " /**", + " * @param {number} sinceVersion", + " * @returns {gdjs.SurfaceArea | null} The part of the ground whose shape changed since the version (see `gdjs.Surface`).", + " */", + " getChangedArea(sinceVersion) {", + " if (sinceVersion < this.surfaceChangesStartVersion) return null;", + " const samples = new SampleRectangle();", + " for (const change of this.surfaceChanges) {", + " if (change.version > sinceVersion) samples.add(change.samples);", + " }", + " if (samples.isEmpty()) return { minX: 0, minY: 0, maxX: 0, maxY: 0 };", + " const { resolution } = this.data;", + " return {", + " minX: samples.minI / resolution,", + " minY: samples.minJ / resolution,", + " maxX: samples.maxI / resolution,", + " maxY: samples.maxJ / resolution,", + " };", + " }", + "", + " _getTransform() {", + " const { object } = this;", + " return [object.getX(), object.getY(), object.getZ(), object.getWidth(), object.getHeight(), object.getDepth()].join(',');", + " }", + "", + " /** @param {string} imageResourceName A heightmap image that is not loaded yet. */", + " _reloadOnceLoaded(imageResourceName) {", + " if (this.loadingHeightmapImage === imageResourceName) return;", + " this.loadingHeightmapImage = imageResourceName;", + " const game = this.object.getRuntimeScene().getGame();", + " game.getImageManager()", + " .loadResource(imageResourceName)", + " .then(() => {", + " if (this.isDisposed || this.loadingHeightmapImage !== imageResourceName) return;", + " this.loadingHeightmapImage = '';", + " if (getLoadedImage(game, imageResourceName)) this.loadFromProperties();", + " })", + " .catch(() => {});", + " }", + "", + " update() {", + " const transform = this._getTransform();", + " if (transform !== this._transform) {", + " this._transform = transform;", + " this.version = ++lastGroundVersion;", + " }", + " this.applyChanges();", + " this.renderer.uniforms.terrainWorldScale.value = getWorldScale(this.object);", + " this._updateWetShore();", + " const layer = this.object.getInstanceContainer().getLayer(this.object.getLayer());", + " this.renderer.updateLevelsOfDetail(layer.getRenderer().getThreeCamera());", + " }", + "", + " _updateWetShore() {", + " const { object } = this;", + " // In the coordinates of the terrain group, like the vertices.", + " const depthScale = AREA_DEPTH / Math.max(object.getDepth(), 0.0001);", + " this.renderer.uniforms.terrainWetShore.value.set(", + " (object._getWetShoreZ() - object.getZ()) * depthScale,", + " Math.max(object._getWetShoreFade(), 0.001) * depthScale,", + " object._getWetShore() ? 1 : 0", + " );", + " }", + "", + " dispose() {", + " this.isDisposed = true;", + " getGrounds(this.object.getRuntimeScene()).delete(this);", + " if (typeof this.object.setSurface === 'function' && this.object.getSurface() === this.surface) {", + " this.object.setSurface(null);", + " }", + " this.renderer.dispose();", + " for (const bodyUpdater of this.bodyUpdaters) {", + " bodyUpdater.dispose();", + " }", + " }", + "}", + "", + "/**", + " * @param {gdjs.RuntimeObject} object", + " * @returns {number} The number of scene units by 3D world unit (1 in GDevelop versions before the world scale).", + " */", + "const getWorldScale = (object) => {", + " const scene = object.getRuntimeScene();", + " return typeof scene.getRenderer3DWorldScale === 'function' ? scene.getRenderer3DWorldScale() : 1;", + "};", + "", + "const averageColors = new WeakMap();", + "let white = null;", + "/**", + " * @param {THREE.Texture} texture", + " * @returns {THREE.Color} The average color of the texture (white when it's not loaded).", + " */", + "const getAverageColor = (texture) => {", + " const cachedColor = averageColors.get(texture);", + " if (cachedColor) return cachedColor;", + " const image = texture.image;", + " const isDrawable =", + " typeof document !== 'undefined' &&", + " ((image instanceof HTMLImageElement && image.complete && image.naturalWidth > 0) ||", + " image instanceof HTMLCanvasElement ||", + " (typeof ImageBitmap !== 'undefined' && image instanceof ImageBitmap));", + " if (!isDrawable) return white || (white = new THREE.Color(1, 1, 1));", + " const size = 8;", + " const canvas = document.createElement('canvas');", + " canvas.width = size;", + " canvas.height = size;", + " const context = canvas.getContext('2d');", + " context.drawImage(image, 0, 0, size, size);", + " const pixels = context.getImageData(0, 0, size, size).data;", + " let red = 0;", + " let green = 0;", + " let blue = 0;", + " for (let index = 0; index < pixels.length; index += 4) {", + " red += pixels[index];", + " green += pixels[index + 1];", + " blue += pixels[index + 2];", + " }", + " const count = size * size * 255;", + " const color = new THREE.Color().setRGB(red / count, green / count, blue / count, THREE.SRGBColorSpace);", + " averageColors.set(texture, color);", + " return color;", + "};", + "", + "const svgIcon = (content) =>", + " 'data:image/svg+xml,' +", + " encodeURIComponent('' + content + '');", + "const strokes = 'fill=\"none\" stroke=\"#000\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"';", + "const toolIcons = {", + " Raise: svgIcon(''),", + " Lower: svgIcon(''),", + " Smooth: svgIcon(''),", + " Flatten: svgIcon(''),", + " Size: svgIcon(''),", + " Strength: svgIcon(''),", + " Undo: svgIcon(''),", + " Hole: svgIcon(''),", + " FillHoles: svgIcon(''),", + " Wetness: svgIcon(''),", + "};", + "// Tints for the color brush when the editor has no color picker (older GDevelop versions).", + "const tintPresets = ['#e2c879', '#a5c45a', '#8a6a48', '#c98a62', '#9a9a92', '#ffffff'];", + "", + "/**", + " * The sculpt and paint tools shown in the scene editor when a terrain is", + " * selected. Without the needed editor functions (older GDevelop versions),", + " * nothing is shown: the terrain can still be changed with events.", + " */", + "class TerrainEditorTools {", + " constructor() {", + " /** '' when no brush is used. */", + " this.toolName = '';", + " this.sizePercent = 30;", + " this.strengthPercent = 50;", + " /** @type {Terrain | null} */", + " this.terrain = null;", + " /** @type {gdjs.InGameEditor | null} */", + " this.editor = null;", + " this.isStroking = false;", + " this.flattenHeight = 0;", + " /** The color of the \"Tint\" brush, as \"#rrggbb\". */", + " this.tintColor = tintPresets[0];", + " this.wetnessPercent = 100;", + " this.lastStrokeTime = 0;", + " /** @type {{data: TerrainData, baseData: TerrainData}[]} */", + " this.undoSteps = [];", + " this.raycaster = new THREE.Raycaster();", + " this.ndc = new THREE.Vector2();", + " this._swatchColor = new THREE.Color();", + " }", + "", + " /** @param {gdjs.InGameEditor} editor */", + " update(editor) {", + " if (typeof editor.showToolbar !== 'function') {", + " // Older GDevelop versions: the terrain can only be changed with events.", + " return;", + " }", + " this.editor = editor;", + " const selectedObjects = editor.getSelectedObjects();", + " const terrain =", + " selectedObjects.length === 1 && selectedObjects[0].__terrain3D ? selectedObjects[0].__terrain3D : null;", + " if (terrain !== this.terrain) {", + " this._endStroke();", + " if (this.terrain) this.terrain.renderer.setBrush(null, 0);", + " this.terrain = terrain;", + " this.undoSteps.length = 0;", + " }", + " if (terrain) editor.showToolbar('Terrain3D', this._getToolbarItems(terrain));", + " const game = editor.getRuntimeGame();", + " if (!terrain || !this.toolName) {", + " if (terrain) terrain.renderer.setBrush(null, 0);", + " return;", + " }", + "", + " const inputManager = game.getInputManager();", + " if (inputManager.wasKeyJustPressed(gdjs.evtTools.input.keysNameToCode.Escape)) {", + " this._endStroke();", + " this.toolName = '';", + " terrain.renderer.setBrush(null, 0);", + " return;", + " }", + " editor.captureLeftMouseButton();", + " const hit = terrain.raycastFromCursor(game, this.raycaster, this.ndc);", + " const radius = this._getRadius();", + " terrain.renderer.setBrush(hit, radius);", + "", + " // Older GDevelop versions don't have it.", + " const isPressedForTool =", + " typeof editor.isCapturedLeftMouseButtonPressed === 'function'", + " ? editor.isCapturedLeftMouseButtonPressed()", + " : inputManager.isMouseButtonPressed(0);", + " // The button is released outside the canvas when the mouse is on a toolbar.", + " const isPressed = isPressedForTool && inputManager.isMouseInsideCanvas();", + " if (isPressed && hit && !this.isStroking) {", + " this._startStroke(terrain, hit);", + " }", + " if (this.isStroking && isPressed && hit) {", + " this._applyBrush(terrain, hit, radius);", + " }", + " if (!isPressed) {", + " this._endStroke();", + " }", + " }", + "", + " _getRadius() {", + " const { object } = this.terrain;", + " const maxRadius = Math.max(object.getWidth(), object.getHeight()) / 4;", + " return Math.max(maxRadius * Math.pow(this.sizePercent / 100, 2), this.terrain.getCellWidth());", + " }", + "", + " _startStroke(terrain, hit) {", + " this.isStroking = true;", + " this.lastStrokeTime = performance.now();", + " this.flattenHeight = terrain.toScenePosition(hit).z;", + " this.undoSteps.push({ data: copyTerrainData(terrain.data), baseData: terrain.baseData });", + " const maxUndoSteps = Math.max(Math.floor(UNDO_STEPS_SAMPLES / terrain.data.heights.length), 2);", + " while (this.undoSteps.length > maxUndoSteps) this.undoSteps.shift();", + " }", + "", + " _endStroke() {", + " if (!this.isStroking) return;", + " this.isStroking = false;", + " this._save();", + " }", + "", + " _applyBrush(terrain, hit, radius) {", + " const now = performance.now();", + " const elapsedSeconds = Math.min((now - this.lastStrokeTime) / 1000, 0.1);", + " this.lastStrokeTime = now;", + " const { x, y } = terrain.toScenePosition(hit);", + " const amount = (this.strengthPercent / 100) * elapsedSeconds;", + " const depth = terrain.object.getDepth();", + " if (this.toolName === 'Raise') terrain.raise(x, y, x, y, radius, amount * depth * 0.5);", + " else if (this.toolName === 'Lower') terrain.raise(x, y, x, y, radius, -amount * depth * 0.5);", + " else if (this.toolName === 'Smooth') terrain.smooth(x, y, x, y, radius, amount * 10);", + " else if (this.toolName === 'Flatten') terrain.flatten(x, y, x, y, radius, this.flattenHeight, amount * 10);", + " else if (this.toolName.startsWith('Paint')) {", + " terrain.paint(x, y, x, y, radius, parseInt(this.toolName.slice(5), 10), amount * 8);", + " } else if (this.toolName === 'Hole') {", + " terrain.makeHole(x, y, x, y, radius);", + " } else if (this.toolName === 'FillHoles') {", + " terrain.fillHoles(x, y, x, y, radius);", + " } else if (this.toolName === 'Tint') {", + " terrain.tint(x, y, x, y, radius, gdjs.rgbOrHexToRGBColor(this.tintColor), amount * 8);", + " } else if (this.toolName === 'Wetness') {", + " terrain.wet(x, y, x, y, radius, this.wetnessPercent / 100, amount * 8);", + " }", + " // Drawn now, as the terrain was updated earlier in this frame.", + " terrain.applyChanges();", + " }", + "", + " _save() {", + " const { terrain, editor } = this;", + " if (!terrain || !editor) return;", + " editor.updateObjectProperties(terrain.object.getName(), { SculptData: terrain.save() });", + " }", + "", + " _undo() {", + " const undoStep = this.undoSteps.pop();", + " // The relief, the edits or the resolution may have changed since the stroke.", + " if (!this.terrain || !undoStep || undoStep.baseData !== this.terrain.baseData) {", + " this.undoSteps.length = 0;", + " return;", + " }", + " this.terrain.setData(undoStep.data);", + " this._save();", + " }", + "", + " /** @returns {Array} The settings of the tint or wetness brush, when it's used. */", + " _getColorToolItems() {", + " const { editor } = this;", + " if (this.toolName === 'Tint') {", + " if (editor && typeof editor.isToolbarItemTypeSupported === 'function' && editor.isToolbarItemTypeSupported('color')) {", + " return [", + " {", + " type: 'color',", + " id: 'TintColor',", + " tooltip: 'Color of the tint',", + " value: this.tintColor,", + " onChange: (color) => (this.tintColor = color),", + " },", + " ];", + " }", + " return tintPresets.map((color) => ({", + " type: 'button',", + " id: 'TintPreset' + color,", + " tooltip: color === '#ffffff' ? 'No tint' : 'Tint with this color',", + " color,", + " isActive: this.tintColor === color,", + " onClick: () => (this.tintColor = color),", + " }));", + " }", + " if (this.toolName === 'Wetness') {", + " return [", + " {", + " type: 'slider',", + " id: 'WetnessAmount',", + " tooltip: 'Wetness',", + " iconUrl: toolIcons.Wetness,", + " min: 0,", + " max: 100,", + " value: this.wetnessPercent,", + " onChange: (value) => (this.wetnessPercent = value),", + " },", + " ];", + " }", + " return [];", + " }", + "", + " /**", + " * @param {Terrain} terrain", + " * @returns {Array} The toolbar for the current state (the editor only", + " * updates what changed).", + " */", + " _getToolbarItems(terrain) {", + " const { object } = terrain;", + " const toolButton = (toolName, tooltip, color) => ({", + " type: 'button',", + " id: toolName,", + " tooltip,", + " iconUrl: toolIcons[toolName],", + " color,", + " isActive: this.toolName === toolName,", + " onClick: () => (this.toolName = this.toolName === toolName ? '' : toolName),", + " });", + " const layerColors = [object._getLayer1Color(), object._getLayer2Color(), object._getLayer3Color(), object._getLayer4Color()];", + " // Layers are shown with their color, tinting their texture.", + " const swatchColors = layerColors.map((color, index) =>", + " this._swatchColor", + " .set(gdjs.rgbOrHexStringToNumber(color))", + " .multiply(getAverageColor(terrain.renderer.uniforms['terrainLayerMap' + index].value))", + " .getHexString()", + " );", + " return [", + " toolButton('Raise', 'Raise the ground (drag on the terrain)'),", + " toolButton('Lower', 'Lower the ground'),", + " toolButton('Smooth', 'Smooth the ground'),", + " toolButton('Flatten', 'Flatten the ground to the height where the drag starts'),", + " { type: 'divider', id: 'PaintDivider' },", + " ...swatchColors.map((swatchColor, index) =>", + " toolButton('Paint' + (index + 1), 'Paint layer ' + (index + 1), '#' + swatchColor)", + " ),", + " toolButton('Hole', 'Make a hole, for the entrance of a tunnel or a cave'),", + " toolButton('FillHoles', 'Fill holes, with the layers of the ground around them'),", + " { type: 'divider', id: 'ColorDivider' },", + " toolButton('Tint', 'Tint the ground (white removes the tint)', this.tintColor),", + " toolButton('Wetness', 'Make the ground wet, or dry with a wetness of 0'),", + " ...this._getColorToolItems(),", + " { type: 'divider', id: 'BrushDivider' },", + " {", + " type: 'slider',", + " id: 'Size',", + " tooltip: 'Brush size',", + " iconUrl: toolIcons.Size,", + " min: 1,", + " max: 100,", + " value: this.sizePercent,", + " onChange: (value) => (this.sizePercent = value),", + " },", + " {", + " type: 'slider',", + " id: 'Strength',", + " tooltip: 'Brush strength',", + " iconUrl: toolIcons.Strength,", + " min: 5,", + " max: 100,", + " value: this.strengthPercent,", + " onChange: (value) => (this.strengthPercent = value),", + " },", + " { type: 'button', id: 'Undo', tooltip: 'Undo the last stroke on this terrain', iconUrl: toolIcons.Undo, onClick: () => this._undo() },", + " ];", + " }", + "}", + "", + "const editorTools = new TerrainEditorTools();", + "if (gdjs.registerInGameEditorPostStepCallback) {", + " gdjs.registerInGameEditorPostStepCallback((editor) => editorTools.update(editor));", + "}", + "", + "gdjs.__terrain3DExtension = {", + " Terrain,", + " // Used by the terrain grass and the scattered models.", + " getWorldScale,", + " getGrounds,", + " patchShaderCode,", + " createRandom,", + " findHighestGround,", + " GroundPlacement,", + " svgIcon,", + " toolIcons,", + " // Exposed for tests.", + " TerrainBodyUpdater,", + " TerrainData,", + " SampleRectangle,", + " brushes,", + " edits,", + " codec,", + " relief,", + "};" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [], + "objectGroups": [] + }, + { + "description": "Define grass helper classes JavaScript code.", + "fullName": "Define grass helper classes", + "functionType": "Action", + "name": "DefineGrassHelperClasses", + "private": true, + "sentence": "Define grass helper classes JavaScript code", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "if (gdjs.__terrainGrassExtension) {", + " return;", + "}", + "// Blades grow on the grounds of the scene: the 3D terrains (see `getGrounds`", + "// in the terrain helper).", + "", + "// Size of the object inner area: instances are scaled from it.", + "const AREA_SIZE = 1000;", + "const AREA_DEPTH = 40;", + "const CHUNK_SIZE = 500;", + "const MAX_BENDING_OBJECTS = 8;", + "", + "/**", + " * A blade: a thin, tapered strip. Its width and height are 1: blades are", + " * scaled by the shader.", + " */", + "const createBladeGeometry = () => {", + " const halfWidths = [0.5, 0.42, 0.28, 0];", + " const heights = [0, 0.35, 0.7, 1];", + " const positions = [];", + " halfWidths.forEach((halfWidth, level) => {", + " positions.push(-halfWidth, 0, heights[level]);", + " if (halfWidth > 0) positions.push(halfWidth, 0, heights[level]);", + " });", + " const indices = [0, 1, 2, 1, 3, 2, 2, 3, 4, 3, 5, 4, 4, 5, 6];", + " const geometry = new THREE.InstancedBufferGeometry();", + " geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));", + " geometry.setAttribute('normal', new THREE.Float32BufferAttribute(new Array(positions.length).fill(0), 3));", + " geometry.setIndex(indices);", + " return geometry;", + "};", + "", + "const vertexShaderDeclarations = `", + "attribute vec4 grassRoot;", + "attribute vec2 grassShape;", + "// The tint of the ground at the root (linear, from 0 to 1).", + "attribute vec3 grassGroundTint;", + "uniform mat3 grassWorldToLocal;", + "uniform float grassWorldScale;", + "uniform float grassBladeWidth;", + "uniform float grassTime;", + "uniform float grassWindStrength;", + "uniform float grassWindSpeed;", + "uniform vec4 grassBenders[${MAX_BENDING_OBJECTS}];", + "uniform vec3 grassBaseColor;", + "uniform vec3 grassTipColor;", + "varying vec3 vGrassColor;", + "`;", + "const vertexShaderPosition = `", + "// Blades are shaped in scene units (with Y flipped like the 3D world), then", + "// moved back in the (scaled) object space.", + "float bladeHeight = position.z;", + "float grassAngle = grassRoot.w;", + "float bladeWidth = position.x * grassBladeWidth;", + "vec3 blade = vec3(", + " bladeWidth * cos(grassAngle),", + " bladeWidth * sin(grassAngle),", + " bladeHeight * grassShape.x", + ");", + "vec3 rootWorld = (modelMatrix * vec4(grassRoot.xyz, 1.0)).xyz * grassWorldScale;", + "// Tips bend more than the base.", + "float bendFactor = bladeHeight * bladeHeight * grassShape.x;", + "float gust = sin(grassTime * grassWindSpeed + rootWorld.x * 0.013 + rootWorld.y * 0.011);", + "float flutter = sin(grassTime * grassWindSpeed * 2.7 + grassShape.y * 40.0) * 0.3;", + "blade.xy += vec2(0.8, 0.5) * (gust + flutter + 0.6) * grassWindStrength * 0.35 * bendFactor;", + "for (int i = 0; i < ${MAX_BENDING_OBJECTS}; i++) {", + " vec4 bender = grassBenders[i];", + " if (bender.z <= 0.0) continue;", + " vec2 away = rootWorld.xy - bender.xy;", + " float distanceToBender = length(away);", + " float push = 1.0 - smoothstep(bender.z * 0.3, bender.z, distanceToBender);", + " if (push > 0.0) {", + " blade.xy += normalize(away + vec2(0.001)) * push * bendFactor * 0.9;", + " blade.z *= 1.0 - push * 0.5;", + " }", + "}", + "vec3 transformed = grassRoot.xyz + grassWorldToLocal * (blade / grassWorldScale);", + "vGrassColor = mix(grassBaseColor, grassTipColor, bladeHeight) * (0.85 + 0.3 * grassShape.y) * grassGroundTint;", + "`;", + "const vertexShaderNormal = `", + "// Blades are lit like the ground below them.", + "transformedNormal = normalize((viewMatrix * vec4(0.0, 0.0, 1.0, 0.0)).xyz);", + "`;", + "const fragmentShaderDeclarations = `", + "varying vec3 vGrassColor;", + "`;", + "const fragmentShaderColor = `", + "diffuseColor.rgb *= vGrassColor;", + "`;", + "// Both faces of blades are lit like the ground (not flipped for back faces).", + "const fragmentShaderNormal = `", + "#include ", + "normal = normalize(vNormal);", + "nonPerturbedNormal = normal;", + "`;", + "", + "const { getWorldScale, patchShaderCode, createRandom, findHighestGround, GroundPlacement } = gdjs.__terrain3DExtension;", + "", + "/**", + " * The grass of a terrain grass object: blades drawn with GPU instancing, in chunks", + " * that are hidden when off-screen and thinned out with the distance. They grow", + " * on the grounds below, like 3D terrains (only where a given layer is painted).", + " */", + "class Grass {", + " /** @param {gdjs.CustomRuntimeObject3D} object */", + " constructor(object) {", + " this.object = object;", + " this.group = new THREE.Group();", + " object.get3DRendererObject().add(this.group);", + " this.bladeGeometry = createBladeGeometry();", + " /** @type {Array<{mesh: THREE.Mesh, blades: number, center: THREE.Vector3}>} */", + " this.chunks = [];", + " this.placement = new GroundPlacement(object.getRuntimeScene());", + " this._groundAtBlade = { ground: null, z: 0 };", + " this.uniforms = {", + " grassWorldToLocal: { value: new THREE.Matrix3() },", + " // Scene units by 3D world unit.", + " grassWorldScale: { value: 1 },", + " grassBladeWidth: { value: 1 },", + " grassTime: { value: 0 },", + " grassWindStrength: { value: 1 },", + " grassWindSpeed: { value: 1 },", + " grassBenders: { value: Array.from({ length: MAX_BENDING_OBJECTS }, () => new THREE.Vector4()) },", + " grassBaseColor: { value: new THREE.Color() },", + " grassTipColor: { value: new THREE.Color() },", + " };", + " this.material = new THREE.MeshStandardMaterial({ roughness: 1, metalness: 0, side: THREE.DoubleSide });", + " this.material.onBeforeCompile = (shader) => {", + " Object.assign(shader.uniforms, this.uniforms);", + " shader.vertexShader = patchShaderCode(shader.vertexShader, [", + " ['#include ', '#include \\n' + vertexShaderDeclarations],", + " ['#include ', vertexShaderPosition],", + " ['#include ', '#include \\n' + vertexShaderNormal],", + " ], 'TerrainGrass');", + " shader.fragmentShader = patchShaderCode(shader.fragmentShader, [", + " ['#include ', '#include \\n' + fragmentShaderDeclarations],", + " ['#include ', '#include \\n' + fragmentShaderColor],", + " ['#include ', fragmentShaderNormal],", + " ], 'TerrainGrass');", + " };", + " this.material.customProgramCacheKey = () => 'TerrainGrass';", + "", + " this._matrix = new THREE.Matrix4();", + " this._cameraPosition = new THREE.Vector3();", + " this._chunkPosition = new THREE.Vector3();", + " this._scale = new THREE.Vector3();", + " this.updateFromProperties();", + " }", + "", + " updateFromProperties() {", + " const { object } = this;", + " this.uniforms.grassBaseColor.value.set(gdjs.rgbOrHexStringToNumber(object._getBaseColor()));", + " this.uniforms.grassTipColor.value.set(gdjs.rgbOrHexStringToNumber(object._getTipColor()));", + " this.uniforms.grassWindStrength.value = object._getWindStrength();", + " this.uniforms.grassWindSpeed.value = object._getWindSpeed();", + " this.uniforms.grassBladeWidth.value = Math.max(object._getBladeWidth(), 0);", + " this.material.needsUpdate = true;", + " this.placement.placeAgain();", + " }", + "", + " _getPlacementKey() {", + " const { object } = this;", + " return [", + " object.getX(), object.getY(), object.getZ(), object.getWidth(), object.getHeight(), object.getDepth(),", + " object._getDensity(), object._getBladeHeight(), object._getGroundLayer(), object._getMaxSlope(),", + " object._getSeed(),", + " ].join('|');", + " }", + "", + " /** Place the blades again, for example after the ground or the object changed. */", + " rebuild() {", + " const grounds = Array.from(this.placement.grounds);", + " for (const chunk of this.chunks) {", + " chunk.mesh.removeFromParent();", + " chunk.mesh.geometry.dispose();", + " }", + " this.chunks.length = 0;", + "", + " const { object } = this;", + " const width = object.getWidth();", + " const height = object.getHeight();", + " const scaleX = width / AREA_SIZE;", + " const scaleY = height / AREA_SIZE;", + " const scaleZ = Math.max(object.getDepth() / AREA_DEPTH, 0.0001);", + " const bladesPerChunk = Math.round((Math.max(object._getDensity(), 0) * CHUNK_SIZE * CHUNK_SIZE) / 10000);", + " const groundLayer = parseInt(object._getGroundLayer(), 10) || 0;", + " const maxSlope = object._getMaxSlope();", + " const chunksX = Math.max(Math.ceil(width / CHUNK_SIZE), 1);", + " const chunksY = Math.max(Math.ceil(height / CHUNK_SIZE), 1);", + " const nextRandom = createRandom(object._getSeed() * 7919 + 1);", + " const bladeHeight = Math.max(object._getBladeHeight(), 0);", + "", + " for (let chunkY = 0; chunkY < chunksY; chunkY++) {", + " for (let chunkX = 0; chunkX < chunksX; chunkX++) {", + " const roots = new Float32Array(bladesPerChunk * 4);", + " const shapes = new Float32Array(bladesPerChunk * 2);", + " const groundTints = new Uint8Array(bladesPerChunk * 3).fill(255);", + " let blades = 0;", + " let minZ = Infinity;", + " let maxZ = -Infinity;", + " for (let attempt = 0; attempt < bladesPerChunk; attempt++) {", + " // In scene units, relative to the object.", + " const x = (chunkX + nextRandom()) * CHUNK_SIZE;", + " const y = (chunkY + nextRandom()) * CHUNK_SIZE;", + " const angle = nextRandom() * Math.PI * 2;", + " const heightFactor = 0.6 + 0.8 * nextRandom();", + " const tint = nextRandom();", + " const growth = nextRandom();", + " if (x > width || y > height) continue;", + " const sceneX = object.getX() + x;", + " const sceneY = object.getY() + y;", + " const { ground, z } = findHighestGround(grounds, sceneX, sceneY, this._groundAtBlade);", + " const groundZ = ground ? z : object.getZ();", + " if (ground) {", + " if (groundLayer > 0 && growth > ground.getLayerWeightAt(sceneX, sceneY, groundLayer)) continue;", + " if (ground.getSlopeAt(sceneX, sceneY) > maxSlope) continue;", + " if (typeof ground.getTintAt === 'function') {", + " const tint = ground.getTintAt(sceneX, sceneY);", + " for (let channel = 0; channel < 3; channel++) {", + " groundTints[blades * 3 + channel] = Math.round(255 * Math.pow(tint[channel] / 255, 2.2));", + " }", + " }", + " }", + " const localZ = (groundZ - object.getZ()) / scaleZ;", + " roots[blades * 4] = x / scaleX;", + " roots[blades * 4 + 1] = y / scaleY;", + " roots[blades * 4 + 2] = localZ;", + " roots[blades * 4 + 3] = angle;", + " shapes[blades * 2] = heightFactor * bladeHeight;", + " shapes[blades * 2 + 1] = tint;", + " minZ = Math.min(minZ, localZ);", + " maxZ = Math.max(maxZ, localZ);", + " blades++;", + " }", + " if (blades === 0) continue;", + "", + " const geometry = new THREE.InstancedBufferGeometry();", + " geometry.index = this.bladeGeometry.index;", + " geometry.setAttribute('position', this.bladeGeometry.attributes.position);", + " geometry.setAttribute('normal', this.bladeGeometry.attributes.normal);", + " geometry.setAttribute('grassRoot', new THREE.InstancedBufferAttribute(roots.subarray(0, blades * 4), 4));", + " geometry.setAttribute('grassShape', new THREE.InstancedBufferAttribute(shapes.subarray(0, blades * 2), 2));", + " geometry.setAttribute('grassGroundTint', new THREE.InstancedBufferAttribute(groundTints.subarray(0, blades * 3), 3, true));", + " geometry.instanceCount = blades;", + " // The furthest tips can go with the wind and the bending objects (see the shader).", + " const maxBladeHeight = bladeHeight * 1.4;", + " const lean =", + " (0.35 * 1.9 * 0.943 * Math.max(object._getWindStrength(), 0) + 0.9) * maxBladeHeight +", + " Math.max(object._getBladeWidth(), 0) / 2;", + " const leanX = lean / scaleX;", + " const leanY = lean / scaleY;", + " geometry.boundingBox = new THREE.Box3(", + " new THREE.Vector3((chunkX * CHUNK_SIZE) / scaleX - leanX, (chunkY * CHUNK_SIZE) / scaleY - leanY, minZ),", + " new THREE.Vector3(", + " Math.min((chunkX + 1) * CHUNK_SIZE, width) / scaleX + leanX,", + " Math.min((chunkY + 1) * CHUNK_SIZE, height) / scaleY + leanY,", + " maxZ + (bladeHeight * 1.4) / scaleZ", + " )", + " );", + " geometry.boundingSphere = geometry.boundingBox.getBoundingSphere(new THREE.Sphere());", + " const mesh = new THREE.Mesh(geometry, this.material);", + " mesh.receiveShadow = true;", + " // Raycasts would only test one blade: the selection plane is used instead.", + " mesh.raycast = () => {};", + " this.group.add(mesh);", + " this.chunks.push({ mesh, blades, center: geometry.boundingSphere.center.clone() });", + " }", + " }", + " }", + "", + " _updateBendingObjects() {", + " const benders = this.uniforms.grassBenders.value;", + " const objectName = this.object._getBendingObject();", + " const bendingObjects = objectName ? this.object.getInstanceContainer().getObjects(objectName) || [] : [];", + " for (let index = 0; index < MAX_BENDING_OBJECTS; index++) {", + " const bendingObject = bendingObjects[index];", + " if (!bendingObject) {", + " benders[index].set(0, 0, 0, 0);", + " continue;", + " }", + " // Blades are pushed in the 3D scene, which is flipped on Y.", + " const radius = Math.max(bendingObject.getWidth(), bendingObject.getHeight()) * 1.2;", + " benders[index].set(bendingObject.getCenterXInScene(), -bendingObject.getCenterYInScene(), radius, 0);", + " }", + " }", + "", + " /**", + " * @param {number} elapsedSeconds", + " * @param {number} time In seconds, the same for all grass objects.", + " */", + " update(elapsedSeconds, time, camera) {", + " if (this.placement.shouldPlaceAgain(elapsedSeconds, this._getPlacementKey())) {", + " this.rebuild();", + " }", + "", + " this.uniforms.grassTime.value = time;", + " this.uniforms.grassWorldScale.value = getWorldScale(this.object);", + " // The object transformation is applied just before rendering, it's needed now.", + " this.object.getRenderer().ensureUpToDate();", + " this.group.updateWorldMatrix(true, false);", + " this.uniforms.grassWorldToLocal.value.setFromMatrix4(this._matrix.copy(this.group.matrixWorld).invert());", + " this._updateBendingObjects();", + " if (!camera) return;", + "", + " // Far chunks are hidden, and thinned out before (in 3D world units).", + " const fadeDistance = Math.max(this.object._getFadeDistance(), 1) / getWorldScale(this.object);", + " this._cameraPosition.setFromMatrixPosition(camera.matrixWorld);", + " for (const chunk of this.chunks) {", + " const distance = this._chunkPosition", + " .copy(chunk.center)", + " .applyMatrix4(this.group.matrixWorld)", + " .distanceTo(this._cameraPosition);", + " const density = gdjs.evtTools.common.clamp((fadeDistance - distance) / (fadeDistance * 0.5), 0, 1);", + " chunk.mesh.visible = density > 0;", + " chunk.mesh.geometry.instanceCount = Math.ceil(chunk.blades * density);", + " }", + " }", + "", + " dispose() {", + " for (const chunk of this.chunks) {", + " chunk.mesh.geometry.dispose();", + " }", + " this.bladeGeometry.dispose();", + " this.material.dispose();", + " this.group.removeFromParent();", + " }", + "}", + "", + "gdjs.__terrainGrassExtension = { Grass };" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [], + "objectGroups": [] + }, + { + "description": "Define scattered models helper classes JavaScript code.", + "fullName": "Define scattered models helper classes", + "functionType": "Action", + "name": "DefineScatterHelperClasses", + "private": true, + "sentence": "Define scattered models helper classes JavaScript code", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "if (gdjs.__terrainScatterExtension) {", + " return;", + "}", + "// Copies grow on the grounds of the scene, like the terrain grass (see", + "// `getGrounds` in the terrain helper).", + "", + "const {", + " getWorldScale,", + " getGrounds,", + " patchShaderCode,", + " createRandom,", + " findHighestGround,", + " GroundPlacement,", + " SampleRectangle,", + " brushes,", + " codec,", + " svgIcon,", + " toolIcons,", + "} = gdjs.__terrain3DExtension;", + "", + "// Size of the object inner area: instances are scaled from it.", + "const AREA_SIZE = 2000;", + "const AREA_DEPTH = 200;", + "// The painted density is stored in a grid of this many cells on each side,", + "// whatever the size of the object.", + "const MASK_RESOLUTION = 256;", + "const MASK_FORMAT_VERSION = '1';", + "// Copies are hidden in chunks: big chunks mean fewer draw calls, small ones", + "// hide more copies out of the screen.", + "const MIN_CHUNK_SIZE = 500;", + "const MAX_CHUNK_SIZE = 2000;", + "// Copies shrink to nothing over this part of the draw distance, instead of popping out.", + "const FADE_PART = 0.15;", + "const MAX_COPIES = 200000;", + "// Prisms of the trunk collisions.", + "const TRUNK_SIDES = 6;", + "", + "const clamp01 = (value) => (value < 0 ? 0 : value > 1 ? 1 : value);", + "", + "/**", + " * @returns {number} A number for a cell of the copies grid, changing with the seed.", + " */", + "const hashCell = (seed, i, j) => {", + " let h = Math.imul(i, 374761393) + Math.imul(j, 668265263) + Math.imul(seed, 982451653);", + " h = Math.imul(h ^ (h >>> 13), 1274126177);", + " return (h ^ (h >>> 16)) >>> 0;", + "};", + "", + "/**", + " * How dense copies are, from 0 to 1, in a grid covering the object: painted", + " * with the editor brushes, the edits or events.", + " */", + "class DensityMask {", + " /** @param {number} fillValue From 0 to 255. */", + " constructor(fillValue) {", + " // `resolution` and `size` let terrain brushes paint it.", + " this.resolution = MASK_RESOLUTION;", + " this.size = MASK_RESOLUTION + 1;", + " this.values = new Uint8Array(this.size * this.size).fill(fillValue);", + " }", + "", + " /**", + " * @param {number} u From 0 to 1, along the object width.", + " * @param {number} v From 0 to 1, along the object height.", + " * @returns {number} The density, from 0 to 1, interpolated between samples.", + " */", + " sample(u, v) {", + " const x = clamp01(u) * this.resolution;", + " const y = clamp01(v) * this.resolution;", + " const i = Math.min(Math.floor(x), this.resolution - 1);", + " const j = Math.min(Math.floor(y), this.resolution - 1);", + " const fx = x - i;", + " const fy = y - j;", + " const value = (i, j) => this.values[j * this.size + i];", + " const top = value(i, j) + fx * (value(i + 1, j) - value(i, j));", + " const bottom = value(i, j + 1) + fx * (value(i + 1, j + 1) - value(i, j + 1));", + " return (top + fy * (bottom - top)) / 255;", + " }", + "", + " /**", + " * Bring the density towards a target around a segment: fully up to 70%", + " * of the radius, then fading out (forests have dense centers and soft edges).", + " * @param {{cellWidth: number, cellHeight: number, ax: number, ay: number, bx: number, by: number, radius: number}} stroke", + " * @param {number} density The target, from 0 to 1.", + " * @param {number} strength From 0 to 1.", + " * @param {SampleRectangle} changedSamples", + " */", + " paint(stroke, density, strength, changedSamples) {", + " const { values } = this;", + " const target = Math.round(clamp01(density) * 255);", + " brushes.forEachSample(", + " this,", + " stroke,", + " (index, falloff) => {", + " // The falloff of terrain brushes is (1 - distance² / radius²)².", + " const weight = clamp01(2 * Math.sqrt(falloff));", + " // Rounded towards the target, so that light strokes still paint.", + " const change = (target - values[index]) * clamp01(weight * strength);", + " values[index] += change > 0 ? Math.ceil(change) : Math.floor(change);", + " },", + " changedSamples", + " );", + " }", + "", + " /** @returns {DensityMask} */", + " clone() {", + " const mask = new DensityMask(0);", + " mask.values.set(this.values);", + " return mask;", + " }", + "}", + "", + "/**", + " * Edits written in the Edits property: a JSON list of brush strokes, like", + " * `[{\"tool\": \"add\", \"x\": 0.5, \"y\": 0.5, \"radius\": 0.2}]`. Positions and", + " * radius are fractions of the object size, like the terrain edits.", + " */", + "const applyEdits = (mask, text) => {", + " if (!text.trim()) return;", + " let editList;", + " try {", + " editList = JSON.parse(text);", + " } catch (error) {", + " console.warn('The edits of scattered models are not valid JSON: ' + error.message);", + " return;", + " }", + " if (!Array.isArray(editList)) {", + " console.warn('The edits of scattered models must be a JSON list (like [{\"tool\": \"add\", ...}]).');", + " return;", + " }", + " const numberOr = (value, defaultValue) => (typeof value === 'number' && isFinite(value) ? value : defaultValue);", + " const changedSamples = new SampleRectangle();", + " for (const edit of editList) {", + " if (!edit || typeof edit !== 'object') continue;", + " const x = numberOr(edit.x, 0.5);", + " const y = numberOr(edit.y, 0.5);", + " const stroke = {", + " cellWidth: 1 / mask.resolution,", + " cellHeight: 1 / mask.resolution,", + " ax: x,", + " ay: y,", + " bx: numberOr(edit.toX, x),", + " by: numberOr(edit.toY, y),", + " radius: numberOr(edit.radius, 0.1),", + " };", + " const strength = clamp01(numberOr(edit.strength, 1));", + " if (edit.tool === 'add') {", + " mask.paint(stroke, numberOr(edit.density, 1), strength, changedSamples);", + " } else if (edit.tool === 'erase') {", + " mask.paint(stroke, 0, strength, changedSamples);", + " } else {", + " console.warn('Unknown tool in the edits of scattered models: \"' + edit.tool + '\" (use add or erase).');", + " }", + " }", + "};", + "", + "/**", + " * The density painted in the editor, stored as its difference with the", + " * density from the properties (the coverage and the edits), like the terrain", + " * sculpt data.", + " */", + "const maskCodec = {", + " /**", + " * @param {DensityMask} mask", + " * @param {DensityMask} baseMask", + " */", + " encode(mask, baseMask) {", + " const changes = new Int32Array(mask.values.length);", + " let hasChanges = false;", + " for (let index = 0; index < changes.length; index++) {", + " changes[index] = mask.values[index] - baseMask.values[index];", + " if (changes[index] !== 0) hasChanges = true;", + " }", + " if (!hasChanges) return '';", + " return MASK_FORMAT_VERSION + ';' + MASK_RESOLUTION + ';' + codec.encodeGrids([changes], mask.size);", + " },", + "", + " /**", + " * @param {string} text", + " * @param {DensityMask} baseMask", + " * @returns {DensityMask | null} null if the text is empty or not valid.", + " */", + " decode(text, baseMask) {", + " if (!text) return null;", + " const parts = text.split(';');", + " if (parts[0] !== MASK_FORMAT_VERSION || parseInt(parts[1], 10) !== MASK_RESOLUTION) return null;", + " try {", + " const mask = baseMask.clone();", + " const [changes] = codec.decodeGrids(parts[2], mask.size, 1);", + " for (let index = 0; index < changes.length; index++) {", + " // The base may have changed since the data was saved.", + " mask.values[index] = Math.min(Math.max(mask.values[index] + changes[index], 0), 255);", + " }", + " return mask;", + " } catch (error) {", + " console.error('The painted data of scattered models could not be read:', error);", + " return null;", + " }", + " },", + "};", + "", + "const fadeShaderDeclarations = `", + "uniform float scatterDrawDistance;", + "`;", + "// Instance matrices are applied after this: copies shrink towards their base.", + "const fadeShaderCode = `", + "#ifdef USE_INSTANCING", + "vec3 scatterCopyPosition = (modelMatrix * instanceMatrix * vec4(0.0, 0.0, 0.0, 1.0)).xyz;", + "transformed *= clamp(", + " (scatterDrawDistance - distance(scatterCopyPosition, cameraPosition)) / (scatterDrawDistance * ${FADE_PART}),", + " 0.0,", + " 1.0", + ");", + "#endif", + "`;", + "", + "const flipTriangles = (geometry) => {", + " const index = geometry.index;", + " for (let offset = 0; offset + 2 < index.count; offset += 3) {", + " const second = index.getX(offset + 1);", + " index.setX(offset + 1, index.getX(offset + 2));", + " index.setX(offset + 2, second);", + " }", + "};", + "", + "/**", + " * The parts of a model, ready to be drawn with instancing: each glTF", + " * primitive with its material. Geometries are moved so that the model base", + " * is at the origin, its height is 1 and it stands along Z (like 3D model", + " * objects with a 90° rotation around X).", + " * @param {THREE.Object3D} modelScene", + " * @param {object} uniforms Uniforms added to the materials.", + " * @returns {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}}", + " */", + "const createModelParts = (modelScene, uniforms) => {", + " modelScene.updateMatrixWorld(true);", + " const rootInverse = modelScene.matrixWorld.clone().invert();", + " const standUp = new THREE.Matrix4().makeRotationX(Math.PI / 2);", + " /** @type {Array<{mesh: THREE.Mesh, matrix: THREE.Matrix4}>} */", + " const meshes = [];", + " const box = new THREE.Box3();", + " modelScene.traverse((node) => {", + " if (!node.isMesh || !node.geometry || !node.geometry.attributes.position) return;", + " const matrix = standUp.clone().multiply(rootInverse).multiply(node.matrixWorld);", + " if (!node.geometry.boundingBox) node.geometry.computeBoundingBox();", + " box.union(node.geometry.boundingBox.clone().applyMatrix4(matrix));", + " meshes.push({ mesh: node, matrix });", + " });", + " if (box.isEmpty()) return { parts: [], width: 0, depth: 0 };", + "", + " const height = Math.max(box.max.z - box.min.z, 0.000001);", + " // The Y axis of the scene goes down: the model is flipped, like 3D model objects.", + " const normalization = new THREE.Matrix4()", + " .makeScale(1 / height, -1 / height, 1 / height)", + " .multiply(", + " new THREE.Matrix4().makeTranslation(", + " -(box.min.x + box.max.x) / 2,", + " -(box.min.y + box.max.y) / 2,", + " -box.min.z", + " )", + " );", + " const parts = meshes.map(({ mesh, matrix }) => {", + " let geometry = mesh.geometry.clone();", + " if (!geometry.index) {", + " const indices = Array.from({ length: geometry.attributes.position.count }, (_, index) => index);", + " geometry.setIndex(indices);", + " }", + " geometry.applyMatrix4(normalization.clone().multiply(matrix));", + " // The flip turned the faces inside out.", + " flipTriangles(geometry);", + " geometry.computeBoundingSphere();", + " geometry.computeBoundingBox();", + " const patchMaterial = (originalMaterial) => {", + " const material = originalMaterial.clone();", + " // Like 3D model objects (\"Standard without metalness\"), which look better without an environment map.", + " if (material.metalness) material.metalness = 0;", + " material.onBeforeCompile = (shader) => {", + " Object.assign(shader.uniforms, uniforms);", + " shader.vertexShader = patchShaderCode(shader.vertexShader, [", + " ['#include ', '#include \\n' + fadeShaderDeclarations],", + " ['#include ', '#include \\n' + fadeShaderCode],", + " ], 'ScatteredModels');", + " };", + " material.customProgramCacheKey = () => 'ScatteredModels';", + " return material;", + " };", + " const material = Array.isArray(mesh.material) ? mesh.material.map(patchMaterial) : patchMaterial(mesh.material);", + " return { geometry, material };", + " });", + " return {", + " parts,", + " width: (box.max.x - box.min.x) / height,", + " depth: (box.max.y - box.min.y) / height,", + " };", + "};", + "", + "/**", + " * A copy of the model: its base position in the scene, its scale (the", + " * height of the copy) and the key of its cell in the grid of copies.", + " * @typedef {{x: number, y: number, z: number, angle: number, height: number, key: number}} Copy", + " */", + "", + "/**", + " * The scattered models of an object: copies of a 3D model placed on the", + " * grounds of the scene, drawn with GPU instancing in chunks that are hidden", + " * when far or off-screen.", + " */", + "class Scatter {", + " /** @param {gdjs.CustomRuntimeObject3D} object */", + " constructor(object) {", + " this.object = object;", + " this.group = new THREE.Group();", + " object.get3DRendererObject().add(this.group);", + " /** @type {Array<{firstCellI: number, firstCellJ: number, meshes: THREE.InstancedMesh[], copies: Copy[], center: THREE.Vector3, radius: number}>} */", + " this.chunks = [];", + " /**", + " * The grid of copies: one copy at most in each cell, cells grouped in chunks.", + " * @type {{spacing: number, cellsPerChunk: number, cellsX: number, cellsY: number} | null}", + " */", + " this.grid = null;", + " this.placement = new GroundPlacement(object.getRuntimeScene());", + " this.uniforms = {", + " // In 3D world units.", + " scatterDrawDistance: { value: 1 },", + " };", + " /** @type {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}} */", + " this.model = { parts: [], width: 0, depth: 0 };", + " this.modelResourceName = '';", + " this.loadingModelResourceName = '';", + " /**", + " * Copies removed by events (for example, cut trees), by key.", + " * @type {Map}", + " */", + " this.removedCopies = new Map();", + " /** Increased each time copies change, for the collisions. */", + " this.version = 0;", + " this.isDisposed = false;", + " /** The last value of the painted data property read or saved. */", + " this.paintData = '';", + " /**", + " * Values saved by the editor tools: the editor sends them back with a", + " * hot-reload, possibly after newer ones were saved.", + " * @type {string[]}", + " */", + " this.savedPaintData = [];", + " this.maskSource = '';", + " /** @type {DensityMask} */", + " this.baseMask = new DensityMask(255);", + " /** @type {DensityMask} */", + " this.mask = this.baseMask;", + " this.changedSamples = new SampleRectangle();", + " this._stroke = { cellWidth: 1, cellHeight: 1, ax: 0, ay: 0, bx: 0, by: 0, radius: 0 };", + " this._groundAtCopy = { ground: null, z: 0 };", + " this._matrix = new THREE.Matrix4();", + " this._raycastSphere = new THREE.Sphere();", + " this._raycastRay = new THREE.Ray();", + " this._raycastBox = new THREE.Box3();", + " this._raycastInverseMatrix = new THREE.Matrix4();", + " this._raycastScale = new THREE.Vector3();", + " /** Placed on a copy to test the triangles of the model. */", + " this._raycastMesh = new THREE.Mesh();", + " /** @type {THREE.Intersection[]} */", + " this._raycastHits = [];", + " this._color = new THREE.Color();", + " this._tint = new THREE.Color();", + " this._cameraPosition = new THREE.Vector3();", + " this._chunkPosition = new THREE.Vector3();", + " /**", + " * Trunks, for the 3D physics behavior and navigation meshes. Always given,", + " * empty without trunk collisions: otherwise these behaviors would use the", + " * object box, covering the whole area.", + " * @type {gdjs.Surface}", + " */", + " this.surface = {", + " getVersion: () => this.version,", + " getChangedArea: () => null,", + " getTriangles: () => this._getTrunkTriangles(),", + " getHeightField: () => null,", + " };", + " /** The trunk collision properties, changing the surface. */", + " this.trunkKey = '';", + " this.loadMaskFromProperties();", + " this.updateFromProperties();", + " // Before the behaviors create their shapes.", + " this._updateSurface();", + " }", + "", + " _getMaskSource() {", + " return this.object._getCoverage() + '|' + this.object._getEdits();", + " }", + "", + " loadMaskFromProperties() {", + " const { object } = this;", + " this.maskSource = this._getMaskSource();", + " this.paintData = object._getPaintData();", + " this.baseMask = new DensityMask(object._getCoverage() === 'Painted' ? 0 : 255);", + " applyEdits(this.baseMask, object._getEdits());", + " this.mask = maskCodec.decode(this.paintData, this.baseMask) || this.baseMask.clone();", + " this.placement.placeAgain();", + " }", + "", + " onHotReloading() {", + " const paintData = this.object._getPaintData();", + " const savedIndex = this.savedPaintData.indexOf(paintData);", + " if (savedIndex !== -1 && this._getMaskSource() === this.maskSource) {", + " // The object already has this data, or newer changes.", + " this.savedPaintData.splice(0, savedIndex + 1);", + " } else if (paintData !== this.paintData || this._getMaskSource() !== this.maskSource) {", + " this.savedPaintData.length = 0;", + " this.loadMaskFromProperties();", + " }", + " this.updateFromProperties();", + " }", + "", + " /** @returns {string} The data to save in the painted data property. */", + " save() {", + " this.paintData = maskCodec.encode(this.mask, this.baseMask);", + " this.savedPaintData.push(this.paintData);", + " return this.paintData;", + " }", + "", + " updateFromProperties() {", + " const { object } = this;", + " const modelResourceName = object._getModel();", + " if (modelResourceName !== this.modelResourceName) {", + " this.modelResourceName = modelResourceName;", + " this._loadModel();", + " }", + " this.placement.placeAgain();", + " }", + "", + " _disposeModel() {", + " for (const part of this.model.parts) {", + " part.geometry.dispose();", + " for (const material of Array.isArray(part.material) ? part.material : [part.material]) {", + " material.dispose();", + " }", + " }", + " this.model = { parts: [], width: 0, depth: 0 };", + " }", + "", + " _loadModel() {", + " this._disposeChunks(this.chunks);", + " this._disposeModel();", + " const resourceName = this.modelResourceName;", + " if (!resourceName) return;", + " const game = this.object.getRuntimeScene().getGame();", + " const modelManager = game.getModel3DManager();", + " const gltf = modelManager.getModel(resourceName);", + " if (gltf === modelManager._invalidModel) {", + " // Not loaded yet, for example a model just chosen in the editor.", + " if (this.loadingModelResourceName === resourceName) return;", + " this.loadingModelResourceName = resourceName;", + " game.getResourceLoader()", + " .loadResource(resourceName)", + " .then(() => {", + " if (this.isDisposed || this.modelResourceName !== resourceName) return;", + " this.loadingModelResourceName = '';", + " if (modelManager.getModel(resourceName) !== modelManager._invalidModel) {", + " this._loadModel();", + " this.placement.placeAgain();", + " }", + " })", + " .catch(() => {});", + " return;", + " }", + " this.model = createModelParts(gltf.scene, this.uniforms);", + " }", + "", + " _getPlacementKey() {", + " const { object } = this;", + " return [", + " object.getX(), object.getY(), object.getZ(), object.getWidth(), object.getHeight(), object.getDepth(),", + " object._getDensity(), object._getSize(), object._getMinScale(), object._getMaxScale(),", + " object._getRandomRotation(), object._getRandomTint(), object._getGroundLayer(), object._getMaxSlope(),", + " object._getMinGroundZ(), object._getMaxGroundZ(), object._getSeed(), object._getDrawDistance(),", + " object._getCastShadow(), object._getReceiveShadow(), this.model.parts.length,", + " ].join('|');", + " }", + "", + " _getChunkSize() {", + " return Math.min(Math.max(this.object._getDrawDistance() / 4, MIN_CHUNK_SIZE), MAX_CHUNK_SIZE);", + " }", + "", + " /** @returns {number} The distance between copies (one copy per cell of this size at most). */", + " _getSpacing() {", + " const { object } = this;", + " const density = Math.max(object._getDensity(), 0);", + " if (density <= 0) return 0;", + " const spacing = 100 / Math.sqrt(density);", + " // Too many cells would freeze the game: the density is lowered.", + " const cellCount = (object.getWidth() / spacing) * (object.getHeight() / spacing);", + " return cellCount > MAX_COPIES ? spacing * Math.sqrt(cellCount / MAX_COPIES) : spacing;", + " }", + "", + " /**", + " * Find the copy of a cell of the grid, if there is one.", + " * @returns {Copy | null}", + " */", + " _placeCopy(cellI, cellJ, spacing, grounds) {", + " const { object } = this;", + " const width = object.getWidth();", + " const height = object.getHeight();", + " const nextRandom = createRandom(hashCell(Math.floor(object._getSeed()), cellI, cellJ));", + " // Always drawn in the same order, so that a copy stays the same when properties change.", + " const x = (cellI + nextRandom()) * spacing;", + " const y = (cellJ + nextRandom()) * spacing;", + " const presence = nextRandom();", + " const growth = nextRandom();", + " const scaleFactor = nextRandom();", + " const angle = nextRandom() * Math.PI * 2;", + " if (x > width || y > height) return null;", + " if (presence >= this.mask.sample(x / width, y / height)) return null;", + " const key = cellJ * 65536 + cellI;", + " if (this.removedCopies.has(key)) return null;", + "", + " const sceneX = object.getX() + x;", + " const sceneY = object.getY() + y;", + " const { ground, z } = findHighestGround(grounds, sceneX, sceneY, this._groundAtCopy);", + " const groundZ = ground ? z : object.getZ();", + " if (groundZ < object._getMinGroundZ() || groundZ > object._getMaxGroundZ()) return null;", + " const copyHeight =", + " Math.max(object._getSize(), 0) *", + " (object._getMinScale() + (object._getMaxScale() - object._getMinScale()) * scaleFactor);", + " if (ground) {", + " if (ground.getSlopeAt(sceneX, sceneY) > object._getMaxSlope()) return null;", + " const groundLayer = parseInt(object._getGroundLayer(), 10) || 0;", + " if (groundLayer > 0) {", + " if (growth > ground.getLayerWeightAt(sceneX, sceneY, groundLayer)) return null;", + " // Copies don't stand at the border of their layer, so that trees don't cover roads.", + " const margin = (copyHeight * Math.max(this.model.width, this.model.depth)) / 4;", + " for (const [offsetX, offsetY] of [[margin, 0], [-margin, 0], [0, margin], [0, -margin]]) {", + " if (ground.getLayerWeightAt(sceneX + offsetX, sceneY + offsetY, groundLayer) < 0.5) return null;", + " }", + " }", + " }", + " return {", + " x: sceneX,", + " y: sceneY,", + " z: groundZ,", + " angle: object._getRandomRotation() ? angle : 0,", + " height: copyHeight,", + " key,", + " };", + " }", + "", + " _disposeChunks(chunks) {", + " for (const chunk of chunks) {", + " for (const mesh of chunk.meshes) {", + " mesh.removeFromParent();", + " // Geometries and materials are the ones of the model.", + " mesh.dispose();", + " }", + " }", + " if (chunks === this.chunks) this.chunks = [];", + " }", + "", + " /** Place the copies again, for example after the ground or the object changed. */", + " rebuild() {", + " this._disposeChunks(this.chunks);", + " this.version++;", + " const { object } = this;", + " const spacing = this._getSpacing();", + " if (spacing <= 0 || this.model.parts.length === 0) {", + " this.grid = null;", + " return;", + " }", + " const cellsPerChunk = Math.max(Math.round(this._getChunkSize() / spacing), 1);", + " const grid = {", + " spacing,", + " cellsPerChunk,", + " cellsX: Math.ceil(object.getWidth() / spacing),", + " cellsY: Math.ceil(object.getHeight() / spacing),", + " };", + " this.grid = grid;", + " for (let firstCellJ = 0; firstCellJ < grid.cellsY; firstCellJ += cellsPerChunk) {", + " for (let firstCellI = 0; firstCellI < grid.cellsX; firstCellI += cellsPerChunk) {", + " this._createChunk(firstCellI, firstCellJ);", + " }", + " }", + " }", + "", + " /**", + " * Place again the copies of the chunks touching a rectangle of the scene,", + " * for example after painting.", + " */", + " _rebuildArea(minX, minY, maxX, maxY) {", + " const { grid, object } = this;", + " if (!grid) return;", + " const chunkSize = grid.spacing * grid.cellsPerChunk;", + " const firstChunkI = Math.max(Math.floor((minX - object.getX()) / chunkSize), 0);", + " const firstChunkJ = Math.max(Math.floor((minY - object.getY()) / chunkSize), 0);", + " const lastChunkI = Math.min(Math.floor((maxX - object.getX()) / chunkSize), Math.ceil(grid.cellsX / grid.cellsPerChunk) - 1);", + " const lastChunkJ = Math.min(Math.floor((maxY - object.getY()) / chunkSize), Math.ceil(grid.cellsY / grid.cellsPerChunk) - 1);", + " if (firstChunkI > lastChunkI || firstChunkJ > lastChunkJ) return;", + " const isInArea = (chunk) => {", + " const chunkI = chunk.firstCellI / grid.cellsPerChunk;", + " const chunkJ = chunk.firstCellJ / grid.cellsPerChunk;", + " return chunkI >= firstChunkI && chunkI <= lastChunkI && chunkJ >= firstChunkJ && chunkJ <= lastChunkJ;", + " };", + " this._disposeChunks(this.chunks.filter(isInArea));", + " this.chunks = this.chunks.filter((chunk) => !isInArea(chunk));", + " for (let chunkJ = firstChunkJ; chunkJ <= lastChunkJ; chunkJ++) {", + " for (let chunkI = firstChunkI; chunkI <= lastChunkI; chunkI++) {", + " this._createChunk(chunkI * grid.cellsPerChunk, chunkJ * grid.cellsPerChunk);", + " }", + " }", + " this.version++;", + " }", + "", + " _createChunk(firstCellI, firstCellJ) {", + " const { grid } = this;", + " const grounds = Array.from(this.placement.grounds);", + " /** @type {Copy[]} */", + " const copies = [];", + " for (let cellJ = firstCellJ; cellJ < Math.min(firstCellJ + grid.cellsPerChunk, grid.cellsY); cellJ++) {", + " for (let cellI = firstCellI; cellI < Math.min(firstCellI + grid.cellsPerChunk, grid.cellsX); cellI++) {", + " const copy = this._placeCopy(cellI, cellJ, grid.spacing, grounds);", + " if (copy) copies.push(copy);", + " }", + " }", + " if (copies.length === 0) return;", + "", + " const { object } = this;", + " // Copies are placed in scene units, and the object group is scaled", + " // from its inner area: they are scaled back to keep their proportions.", + " const scaleX = Math.max(object.getWidth() / AREA_SIZE, 0.0001);", + " const scaleY = Math.max(object.getHeight() / AREA_SIZE, 0.0001);", + " const scaleZ = Math.max(object.getDepth() / AREA_DEPTH, 0.0001);", + " const matrices = new Float32Array(copies.length * 16);", + " const tintColor = gdjs.rgbOrHexStringToNumber(object._getRandomTint());", + " const hasTint = tintColor !== 0xffffff;", + " const colors = hasTint ? new Float32Array(copies.length * 3) : null;", + " this._tint.setHex(tintColor);", + " const min = new THREE.Vector3(Infinity, Infinity, Infinity);", + " const max = new THREE.Vector3(-Infinity, -Infinity, -Infinity);", + " copies.forEach((copy, index) => {", + " const cos = Math.cos(copy.angle) * copy.height;", + " const sin = Math.sin(copy.angle) * copy.height;", + " const localX = (copy.x - object.getX()) / scaleX;", + " const localY = (copy.y - object.getY()) / scaleY;", + " const localZ = (copy.z - object.getZ()) / scaleZ;", + " this._matrix.set(", + " cos / scaleX, -sin / scaleX, 0, localX,", + " sin / scaleY, cos / scaleY, 0, localY,", + " 0, 0, copy.height / scaleZ, localZ,", + " 0, 0, 0, 1", + " );", + " this._matrix.toArray(matrices, index * 16);", + " if (colors) {", + " // The tint of each copy is between white and the tint color.", + " this._color.setRGB(1, 1, 1).lerp(this._tint, createRandom(copy.key + 1)());", + " this._color.toArray(colors, index * 3);", + " }", + " const reach = (copy.height * Math.max(this.model.width, this.model.depth)) / 2;", + " min.min(this._chunkPosition.set(copy.x - reach, copy.y - reach, copy.z));", + " max.max(this._chunkPosition.set(copy.x + reach, copy.y + reach, copy.z + copy.height));", + " });", + " const instanceMatrix = new THREE.InstancedBufferAttribute(matrices, 16);", + " const instanceColor = colors ? new THREE.InstancedBufferAttribute(colors, 3) : null;", + " const castShadow = object._getCastShadow();", + " const receiveShadow = object._getReceiveShadow();", + " const meshes = this.model.parts.map((part) => {", + " const mesh = new THREE.InstancedMesh(part.geometry, part.material, 0);", + " // All the parts of the model share the positions of the copies.", + " mesh.instanceMatrix = instanceMatrix;", + " mesh.instanceColor = instanceColor;", + " mesh.count = copies.length;", + " mesh.computeBoundingSphere();", + " mesh.castShadow = castShadow;", + " mesh.receiveShadow = receiveShadow;", + " // The first mesh tests all the parts of the copies.", + " mesh.raycast = () => {};", + " this.group.add(mesh);", + " return mesh;", + " });", + " const center = new THREE.Vector3(", + " ((min.x + max.x) / 2 - object.getX()) / scaleX,", + " ((min.y + max.y) / 2 - object.getY()) / scaleY,", + " ((min.z + max.z) / 2 - object.getZ()) / scaleZ", + " );", + " const chunk = { firstCellI, firstCellJ, meshes, copies, center, radius: min.distanceTo(max) / 2 };", + " // Used by the scene editor to select the object when a copy is clicked.", + " meshes[0].raycast = (raycaster, intersects) => this._raycastChunk(chunk, raycaster, intersects);", + " this.chunks.push(chunk);", + " }", + "", + " /**", + " * Add the hits of a ray on the copies of a chunk, like they are drawn. There", + " * can be thousands of copies: only the ones whose box is crossed by the ray", + " * are tested with the triangles of the model.", + " * @param {{meshes: THREE.InstancedMesh[], copies: Copy[], center: THREE.Vector3, radius: number}} chunk", + " * @param {THREE.Raycaster} raycaster", + " * @param {THREE.Intersection[]} intersects", + " */", + " _raycastChunk(chunk, raycaster, intersects) {", + " const { object, group } = this;", + " this._raycastSphere.center.copy(chunk.center).applyMatrix4(group.matrixWorld);", + " this._raycastSphere.radius = chunk.radius / getWorldScale(object);", + " if (!raycaster.ray.intersectsSphere(this._raycastSphere)) return;", + "", + " // The ray in the scene coordinates, like the copies.", + " const scale = this._raycastScale.set(", + " Math.max(object.getWidth() / AREA_SIZE, 0.0001),", + " Math.max(object.getHeight() / AREA_SIZE, 0.0001),", + " Math.max(object.getDepth() / AREA_DEPTH, 0.0001)", + " );", + " const ray = this._raycastRay.copy(raycaster.ray).applyMatrix4(this._raycastInverseMatrix.copy(group.matrixWorld).invert());", + " ray.origin.multiply(scale).add(this._chunkPosition.set(object.getX(), object.getY(), object.getZ()));", + " ray.direction.multiply(scale);", + "", + " const reachPerHeight = Math.max(this.model.width, this.model.depth) / 2;", + " const instanceMatrices = chunk.meshes[0].instanceMatrix.array;", + " chunk.copies.forEach((copy, index) => {", + " const reach = copy.height * reachPerHeight;", + " this._raycastBox.min.set(copy.x - reach, copy.y - reach, copy.z);", + " this._raycastBox.max.set(copy.x + reach, copy.y + reach, copy.z + copy.height);", + " if (!ray.intersectsBox(this._raycastBox)) return;", + " this._raycastMesh.matrixWorld.fromArray(instanceMatrices, index * 16).premultiply(group.matrixWorld);", + " for (const mesh of chunk.meshes) {", + " this._raycastMesh.geometry = mesh.geometry;", + " this._raycastMesh.material = mesh.material;", + " this._raycastHits.length = 0;", + " this._raycastMesh.raycast(raycaster, this._raycastHits);", + " for (const hit of this._raycastHits) {", + " hit.object = mesh;", + " hit.instanceId = index;", + " intersects.push(hit);", + " }", + " }", + " });", + " }", + "", + " /** @param {THREE.Camera | null} camera */", + " _hideFarChunks(camera) {", + " if (!camera) return;", + " const worldScale = getWorldScale(this.object);", + " const drawDistance = Math.max(this.object._getDrawDistance(), 1) / worldScale;", + " this.uniforms.scatterDrawDistance.value = drawDistance;", + " this._cameraPosition.setFromMatrixPosition(camera.matrixWorld);", + " for (const chunk of this.chunks) {", + " const distance = this._chunkPosition", + " .copy(chunk.center)", + " .applyMatrix4(this.group.matrixWorld)", + " .distanceTo(this._cameraPosition);", + " const isVisible = distance - chunk.radius / worldScale < drawDistance;", + " for (const mesh of chunk.meshes) mesh.visible = isVisible;", + " }", + " }", + "", + " /**", + " * @param {number} elapsedSeconds", + " * @param {THREE.Camera | null} camera", + " */", + " update(elapsedSeconds, camera) {", + " if (this.placement.shouldPlaceAgain(elapsedSeconds, this._getPlacementKey())) {", + " this.rebuild();", + " }", + " if (!this.changedSamples.isEmpty()) {", + " const { object } = this;", + " const sampleWidth = object.getWidth() / MASK_RESOLUTION;", + " const sampleHeight = object.getHeight() / MASK_RESOLUTION;", + " const { minI, minJ, maxI, maxJ } = this.changedSamples;", + " // Copies between samples use the density of the samples around them.", + " this._rebuildArea(", + " object.getX() + (minI - 1) * sampleWidth,", + " object.getY() + (minJ - 1) * sampleHeight,", + " object.getX() + (maxI + 1) * sampleWidth,", + " object.getY() + (maxJ + 1) * sampleHeight", + " );", + " this.changedSamples.clear();", + " }", + " // The object transformation is applied just before rendering, it's needed now.", + " this.object.getRenderer().ensureUpToDate();", + " this.group.updateWorldMatrix(true, false);", + " this._hideFarChunks(camera);", + " this._updateSurface();", + " }", + "", + " _updateSurface() {", + " const { object } = this;", + " if (typeof object.setSurface !== 'function') return;", + " const trunkKey = object._getTrunkCollisions() + '|' + object._getTrunkDiameter();", + " if (trunkKey !== this.trunkKey) {", + " this.trunkKey = trunkKey;", + " this.version++;", + " }", + " if (object.getSurface() !== this.surface) object.setSurface(this.surface);", + " }", + "", + " /** @returns {Copy[]} */", + " getCopies() {", + " const copies = [];", + " for (const chunk of this.chunks) copies.push(...chunk.copies);", + " return copies;", + " }", + "", + " /**", + " * @returns {Copy | null} The copy whose base is the nearest to a position (on X and Y), if any.", + " */", + " findNearestCopy(x, y) {", + " let nearestCopy = null;", + " let nearestDistanceSquared = Infinity;", + " for (const chunk of this.chunks) {", + " for (const copy of chunk.copies) {", + " const distanceSquared = (copy.x - x) * (copy.x - x) + (copy.y - y) * (copy.y - y);", + " if (distanceSquared < nearestDistanceSquared) {", + " nearestDistanceSquared = distanceSquared;", + " nearestCopy = copy;", + " }", + " }", + " }", + " return nearestCopy;", + " }", + "", + " getCopyCount() {", + " let count = 0;", + " for (const chunk of this.chunks) count += chunk.copies.length;", + " return count;", + " }", + "", + " /** Remove the copies whose base is in a circle (they come back when the scene is restarted). */", + " removeCopies(x, y, radius) {", + " let hasRemovedCopies = false;", + " for (const chunk of this.chunks) {", + " for (const copy of chunk.copies) {", + " if ((copy.x - x) * (copy.x - x) + (copy.y - y) * (copy.y - y) <= radius * radius) {", + " this.removedCopies.set(copy.key, copy);", + " hasRemovedCopies = true;", + " }", + " }", + " }", + " if (hasRemovedCopies) this._rebuildArea(x - radius, y - radius, x + radius, y + radius);", + " }", + "", + " /** Paint the full density around a position, and bring back the copies removed there. */", + " addCopies(x, y, radius) {", + " for (const [key, copy] of this.removedCopies) {", + " if ((copy.x - x) * (copy.x - x) + (copy.y - y) * (copy.y - y) <= radius * radius) {", + " this.removedCopies.delete(key);", + " }", + " }", + " this.paint(x, y, x, y, radius, 1, 1);", + " }", + "", + " _getStroke(x1, y1, x2, y2, radius) {", + " const { object } = this;", + " const stroke = this._stroke;", + " stroke.cellWidth = object.getWidth() / MASK_RESOLUTION;", + " stroke.cellHeight = object.getHeight() / MASK_RESOLUTION;", + " stroke.ax = x1 - object.getX();", + " stroke.ay = y1 - object.getY();", + " stroke.bx = x2 - object.getX();", + " stroke.by = y2 - object.getY();", + " stroke.radius = Math.max(radius, 0);", + " return stroke;", + " }", + "", + " /**", + " * Paint the density around a segment (copies are placed again at the next frame).", + " * @param {number} density From 0 (erase) to 1 (as dense as the Density property).", + " * @param {number} strength From 0 to 1.", + " */", + " paint(x1, y1, x2, y2, radius, density, strength) {", + " const changedSamples = new SampleRectangle();", + " this.mask.paint(this._getStroke(x1, y1, x2, y2, radius), density, strength, changedSamples);", + " this.changedSamples.add(changedSamples);", + " }", + "", + " /** @param {DensityMask} mask */", + " setMask(mask) {", + " this.mask = mask;", + " this.rebuild();", + " }", + "", + " /**", + " * Prisms around the trunks, from 0 to 1 in the object box (see `gdjs.Surface`).", + " * @returns {gdjs.SurfaceTriangles}", + " */", + " _getTrunkTriangles() {", + " const { object } = this;", + " const copies = object._getTrunkCollisions() ? this.getCopies() : [];", + " const verticesPerCopy = TRUNK_SIDES * 2 + 1;", + " const positions = new Float32Array(copies.length * verticesPerCopy * 3);", + " const indices = new Uint32Array(copies.length * TRUNK_SIDES * 3 * 3);", + " const width = object.getWidth() || 1;", + " const height = object.getHeight() || 1;", + " const depth = object.getDepth() || 1;", + " let indexOffset = 0;", + " copies.forEach((copy, copyIndex) => {", + " const radius = (Math.max(object._getTrunkDiameter(), 0) / 2) * (copy.height / Math.max(object._getSize(), 1));", + " const top = copy.z + copy.height * 0.8;", + " const firstVertex = copyIndex * verticesPerCopy;", + " const setVertex = (vertex, x, y, z) => {", + " positions[vertex * 3] = (x - object.getX()) / width;", + " positions[vertex * 3 + 1] = (y - object.getY()) / height;", + " positions[vertex * 3 + 2] = (z - object.getZ()) / depth;", + " };", + " for (let side = 0; side < TRUNK_SIDES; side++) {", + " const angle = ((side + 0.5) / TRUNK_SIDES) * Math.PI * 2;", + " const x = copy.x + Math.cos(angle) * radius;", + " const y = copy.y + Math.sin(angle) * radius;", + " // Starting a bit under the ground, which can be sloped.", + " setVertex(firstVertex + side * 2, x, y, copy.z - radius);", + " setVertex(firstVertex + side * 2 + 1, x, y, top);", + " }", + " const center = firstVertex + TRUNK_SIDES * 2;", + " setVertex(center, copy.x, copy.y, top);", + " for (let side = 0; side < TRUNK_SIDES; side++) {", + " const bottom = firstVertex + side * 2;", + " const nextBottom = firstVertex + ((side + 1) % TRUNK_SIDES) * 2;", + " // Triangles face out of the prism (vertices at odd indices are at the top).", + " indices.set([bottom, nextBottom, bottom + 1, nextBottom, nextBottom + 1, bottom + 1], indexOffset);", + " indices.set([center, bottom + 1, nextBottom + 1], indexOffset + 6);", + " indexOffset += 9;", + " }", + " });", + " if (copies.length === 0) {", + " // Without triangles, the physics engine and navigation meshes would use", + " // the object box: a tiny triangle under the object is used instead.", + " return { positions: new Float32Array([0, 0, -1, 0.0001, 0, -1, 0, 0.0001, -1]), indices: new Uint32Array([0, 2, 1]) };", + " }", + " return { positions, indices };", + " }", + "", + " dispose() {", + " this.isDisposed = true;", + " if (typeof this.object.setSurface === 'function' && this.object.getSurface() === this.surface) {", + " this.object.setSurface(null);", + " }", + " this._disposeChunks(this.chunks);", + " this._disposeModel();", + " this.group.removeFromParent();", + " }", + "}", + "", + "const scatterToolIcons = {", + " Add: svgIcon(", + " ''", + " ),", + " Erase: svgIcon(", + " ''", + " ),", + "};", + "", + "/**", + " * The brushes shown in the scene editor when scattered models are selected,", + " * to add or erase copies. Without the needed editor functions (older GDevelop", + " * versions), nothing is shown: copies can still be painted with events.", + " */", + "class ScatterEditorTools {", + " constructor() {", + " /** '' when no brush is used. */", + " this.toolName = '';", + " this.sizePercent = 30;", + " this.strengthPercent = 50;", + " /** @type {Scatter | null} */", + " this.scatter = null;", + " /** @type {gdjs.InGameEditor | null} */", + " this.editor = null;", + " this.isStroking = false;", + " this.lastStrokeTime = 0;", + " /** @type {Array<{mask: DensityMask, baseMask: DensityMask}>} */", + " this.undoSteps = [];", + " this.raycaster = new THREE.Raycaster();", + " this.ndc = new THREE.Vector2();", + " this._groundAtCursor = { ground: null, z: 0 };", + " /** A circle following the ground under the cursor. */", + " this.brushRing = new THREE.LineLoop(", + " new THREE.BufferGeometry().setAttribute('position', new THREE.BufferAttribute(new Float32Array(64 * 3), 3)),", + " new THREE.LineBasicMaterial({ color: 0xffffff, depthTest: false, transparent: true })", + " );", + " this.brushRing.renderOrder = 1000;", + " this.brushRing.frustumCulled = false;", + " this.brushRing.raycast = () => {};", + " }", + "", + " /** @param {gdjs.InGameEditor} editor */", + " update(editor) {", + " if (typeof editor.showToolbar !== 'function') return;", + " this.editor = editor;", + " const selectedObjects = editor.getSelectedObjects();", + " const scatter =", + " selectedObjects.length === 1 && selectedObjects[0].__terrainScatter ? selectedObjects[0].__terrainScatter : null;", + " if (scatter !== this.scatter) {", + " this._endStroke();", + " this.scatter = scatter;", + " this.undoSteps.length = 0;", + " }", + " if (scatter) editor.showToolbar('ScatteredModels', this._getToolbarItems());", + " const game = editor.getRuntimeGame();", + " if (!scatter || !this.toolName) {", + " this.brushRing.removeFromParent();", + " return;", + " }", + " const inputManager = game.getInputManager();", + " if (inputManager.wasKeyJustPressed(gdjs.evtTools.input.keysNameToCode.Escape)) {", + " this._endStroke();", + " this.toolName = '';", + " this.brushRing.removeFromParent();", + " return;", + " }", + " editor.captureLeftMouseButton();", + " const hit = this._getPositionUnderCursor(scatter, game);", + " const radius = this._getRadius();", + " this._showBrushRing(scatter, hit, radius);", + "", + " const isPressedForTool =", + " typeof editor.isCapturedLeftMouseButtonPressed === 'function'", + " ? editor.isCapturedLeftMouseButtonPressed()", + " : inputManager.isMouseButtonPressed(0);", + " // The button is released outside the canvas when the mouse is on a toolbar.", + " const isPressed = isPressedForTool && inputManager.isMouseInsideCanvas();", + " if (isPressed && hit && !this.isStroking) {", + " this._startStroke(scatter);", + " }", + " if (this.isStroking && isPressed && hit) {", + " this._applyBrush(scatter, hit, radius);", + " }", + " if (!isPressed) {", + " this._endStroke();", + " }", + " }", + "", + " _getRadius() {", + " const { object } = this.scatter;", + " const maxRadius = Math.max(object.getWidth(), object.getHeight()) / 4;", + " return Math.max(maxRadius * Math.pow(this.sizePercent / 100, 2), 10);", + " }", + "", + " /**", + " * @returns {{x: number, y: number} | null} The position of the scene under the cursor: on the", + " * highest ground hit, or on the plane at the object Z.", + " */", + " _getPositionUnderCursor(scatter, game) {", + " let hit = null;", + " for (const ground of getGrounds(scatter.object.getRuntimeScene())) {", + " if (typeof ground.raycastFromCursor !== 'function') continue;", + " const localHit = ground.raycastFromCursor(game, this.raycaster, this.ndc);", + " if (!localHit) continue;", + " const sceneHit = ground.toScenePosition(localHit);", + " if (!hit || sceneHit.z > hit.z) hit = sceneHit;", + " }", + " if (hit) return hit;", + "", + " const { object } = scatter;", + " const layer = object.getInstanceContainer().getLayer(object.getLayer());", + " const camera = layer.getRenderer().getThreeCamera();", + " if (!camera) return null;", + " const inputManager = game.getInputManager();", + " this.ndc.set(", + " (inputManager.getCursorX() / game.getGameResolutionWidth()) * 2 - 1,", + " -(inputManager.getCursorY() / game.getGameResolutionHeight()) * 2 + 1", + " );", + " this.raycaster.setFromCamera(this.ndc, camera);", + " scatter.group.updateWorldMatrix(true, false);", + " const ray = this.raycaster.ray.clone().applyMatrix4(scatter.group.matrixWorld.clone().invert());", + " const point = ray.intersectPlane(new THREE.Plane(new THREE.Vector3(0, 0, 1), 0), new THREE.Vector3());", + " if (!point) return null;", + " return {", + " x: object.getX() + (point.x / AREA_SIZE) * object.getWidth(),", + " y: object.getY() + (point.y / AREA_SIZE) * object.getHeight(),", + " };", + " }", + "", + " _showBrushRing(scatter, hit, radius) {", + " if (!hit) {", + " this.brushRing.removeFromParent();", + " return;", + " }", + " const { object } = scatter;", + " if (this.brushRing.parent !== scatter.group) scatter.group.add(this.brushRing);", + " const grounds = getGrounds(object.getRuntimeScene());", + " const positions = this.brushRing.geometry.attributes.position;", + " for (let index = 0; index < positions.count; index++) {", + " const angle = (index / positions.count) * Math.PI * 2;", + " const x = hit.x + Math.cos(angle) * radius;", + " const y = hit.y + Math.sin(angle) * radius;", + " const { ground, z } = findHighestGround(grounds, x, y, this._groundAtCursor);", + " positions.setXYZ(", + " index,", + " ((x - object.getX()) / object.getWidth()) * AREA_SIZE,", + " ((y - object.getY()) / object.getHeight()) * AREA_SIZE,", + " (((ground ? z : object.getZ()) + 2 - object.getZ()) / Math.max(object.getDepth(), 0.0001)) * AREA_DEPTH", + " );", + " }", + " positions.needsUpdate = true;", + " }", + "", + " _startStroke(scatter) {", + " this.isStroking = true;", + " this.lastStrokeTime = performance.now();", + " this.undoSteps.push({ mask: scatter.mask.clone(), baseMask: scatter.baseMask });", + " while (this.undoSteps.length > 30) this.undoSteps.shift();", + " }", + "", + " _endStroke() {", + " if (!this.isStroking) return;", + " this.isStroking = false;", + " this._save();", + " }", + "", + " _applyBrush(scatter, hit, radius) {", + " const now = performance.now();", + " const elapsedSeconds = Math.min((now - this.lastStrokeTime) / 1000, 0.1);", + " this.lastStrokeTime = now;", + " const strength = (this.strengthPercent / 100) * elapsedSeconds * 8;", + " scatter.paint(hit.x, hit.y, hit.x, hit.y, radius, this.toolName === 'Add' ? 1 : 0, strength);", + " }", + "", + " _save() {", + " const { scatter, editor } = this;", + " if (!scatter || !editor) return;", + " editor.updateObjectProperties(scatter.object.getName(), { PaintData: scatter.save() });", + " }", + "", + " _undo() {", + " const undoStep = this.undoSteps.pop();", + " // The coverage or the edits may have changed since the stroke.", + " if (!this.scatter || !undoStep || undoStep.baseMask !== this.scatter.baseMask) {", + " this.undoSteps.length = 0;", + " return;", + " }", + " this.scatter.setMask(undoStep.mask);", + " this._save();", + " }", + "", + " /** @returns {Array} */", + " _getToolbarItems() {", + " const toolButton = (toolName, tooltip) => ({", + " type: 'button',", + " id: toolName,", + " tooltip,", + " iconUrl: scatterToolIcons[toolName],", + " isActive: this.toolName === toolName,", + " onClick: () => (this.toolName = this.toolName === toolName ? '' : toolName),", + " });", + " return [", + " toolButton('Add', 'Add copies (drag on the ground)'),", + " toolButton('Erase', 'Erase copies'),", + " { type: 'divider', id: 'BrushDivider' },", + " {", + " type: 'slider',", + " id: 'Size',", + " tooltip: 'Brush size',", + " iconUrl: toolIcons.Size,", + " min: 1,", + " max: 100,", + " value: this.sizePercent,", + " onChange: (value) => (this.sizePercent = value),", + " },", + " {", + " type: 'slider',", + " id: 'Strength',", + " tooltip: 'Brush strength',", + " iconUrl: toolIcons.Strength,", + " min: 5,", + " max: 100,", + " value: this.strengthPercent,", + " onChange: (value) => (this.strengthPercent = value),", + " },", + " {", + " type: 'button',", + " id: 'Undo',", + " tooltip: 'Undo the last stroke on these scattered models',", + " iconUrl: toolIcons.Undo,", + " onClick: () => this._undo(),", + " },", + " ];", + " }", + "}", + "", + "const editorTools = new ScatterEditorTools();", + "if (gdjs.registerInGameEditorPostStepCallback) {", + " gdjs.registerInGameEditorPostStepCallback((editor) => editorTools.update(editor));", + "}", + "", + "gdjs.__terrainScatterExtension = {", + " Scatter,", + " // Exposed for tests.", + " DensityMask,", + " maskCodec,", + " applyEdits,", + "};", + "" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [], + "objectGroups": [] + } + ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "DefineHelperClasses" + }, + { + "functionName": "DefineGrassHelperClasses" + }, + { + "functionName": "DefineScatterHelperClasses" + } + ] + }, + "eventsBasedBehaviors": [], + "eventsBasedObjects": [ + { + "areaMaxX": 4096, + "areaMaxY": 4096, + "areaMaxZ": 600, + "areaMinX": 0, + "areaMinY": 0, + "areaMinZ": 0, + "assetStoreTag": "3d terrains", + "defaultName": "Terrain", + "description": "A ground with hills, mountains or valleys that can be sculpted and painted in the scene editor, or changed with events. Add the 3D physics behavior to it to make objects walk or roll on it.", + "fullName": "3D terrain", + "helpPath": "", + "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLXRlcnJhaW4iIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTQsNkwxMC4yNSwxMUwxMy4xLDE0LjhMMTEuNSwxNkM5LjgxLDEzLjc1IDcsMTAgNywxMEwxLDE4SDIzTDE0LDZaIiAvPjwvc3ZnPg==", + "is3D": true, + "isUsingLegacyInstancesRenderer": true, + "name": "Terrain3D", + "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", + "objects": [ + { + "assetStoreId": "", + "name": "AreaPlaceholder", + "type": "Scene3D::Cube3DObject", + "variables": [], + "effects": [], + "behaviors": [], + "content": { + "width": 4096, + "height": 4096, + "depth": 600, + "enableTextureTransparency": false, + "facesOrientation": "Y", + "backFaceUpThroughWhichAxisRotation": "X", + "materialType": "Basic", + "tint": "255;255;255", + "frontFaceResourceName": "", + "frontFaceVisible": false, + "frontFaceResourceRepeat": false, + "backFaceResourceName": "", + "backFaceVisible": false, + "backFaceResourceRepeat": false, + "leftFaceResourceName": "", + "leftFaceVisible": false, + "leftFaceResourceRepeat": false, + "rightFaceResourceName": "", + "rightFaceVisible": false, + "rightFaceResourceRepeat": false, + "topFaceResourceName": "", + "topFaceVisible": false, + "topFaceResourceRepeat": false, + "bottomFaceResourceName": "", + "bottomFaceVisible": false, + "bottomFaceResourceRepeat": false + } + } + ], + "objectsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "objectName": "AreaPlaceholder" + } + ] + }, + "objectsGroups": [], + "layers": [ + { + "ambientLightColorB": 200, + "ambientLightColorG": 200, + "ambientLightColorR": 200, + "camera2DPlaneMaxDrawingDistance": 5000, + "camera3DFarPlaneDistance": 10000, + "camera3DFieldOfView": 45, + "camera3DNearPlaneDistance": 3, + "cameraType": "", + "followBaseLayerCamera": false, + "isLightingLayer": false, + "isLocked": false, + "name": "", + "renderingType": "", + "visibility": true, + "cameras": [ + { + "defaultSize": true, + "defaultViewport": true, + "height": 0, + "viewportBottom": 1, + "viewportLeft": 0, + "viewportRight": 1, + "viewportTop": 0, + "width": 0 + } + ], + "effects": [] + } + ], + "instances": [ + { + "angle": 0, + "customSize": true, + "depth": 600, + "height": 4096, + "keepRatio": true, + "layer": "", + "name": "AreaPlaceholder", + "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-004096004096", + "width": 4096, + "x": 0, + "y": 0, + "zOrder": 1, + "numberProperties": [], + "stringProperties": [], + "initialVariables": [] + } + ], + "editionSettings": {}, + "eventsFunctions": [ + { + "fullName": "", + "functionType": "Action", + "name": "onCreated", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Terrain3D::DefineHelperClasses" + }, + "parameters": [ + "", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "for (const child of object.getChildrenContainer().getAdhocListOfAllInstances()) {", + " child.deleteFromScene();", + "}", + "if (object.__terrain3D) object.__terrain3D.dispose();", + "object.__terrain3D = new gdjs.__terrain3DExtension.Terrain(object);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "onDestroy", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrain3D.dispose();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "doStepPostEvents", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrain3D.update();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "onHotReloading", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrain3D.onHotReloading();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "description": "Raise the ground around a position (or lower it with a negative height). It's raised the most at the center and less and less up to the radius.", + "fullName": "Raise or lower the ground", + "functionType": "Action", + "group": "Sculpt", + "name": "RaiseGround", + "sentence": "Raise the ground of _PARAM0_ by _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const height = eventsFunctionContext.getArgument(\"Height\");", + "object.__terrain3D.raise(x, y, x, y, radius, height);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Height to add (negative to lower the ground)", + "name": "Height", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Raise the ground along a line, for example for a ridge, or lower it with a negative height to dig a river bed or a valley.", + "fullName": "Raise or lower the ground along a line", + "functionType": "Action", + "group": "Sculpt", + "name": "RaiseGroundAlongLine", + "sentence": "Raise the ground of _PARAM0_ by _PARAM6_ from _PARAM1_; _PARAM2_ to _PARAM3_; _PARAM4_ with a radius of _PARAM5_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x1 = eventsFunctionContext.getArgument(\"X1\");", + "const y1 = eventsFunctionContext.getArgument(\"Y1\");", + "const x2 = eventsFunctionContext.getArgument(\"X2\");", + "const y2 = eventsFunctionContext.getArgument(\"Y2\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const height = eventsFunctionContext.getArgument(\"Height\");", + "object.__terrain3D.raise(x1, y1, x2, y2, radius, height);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the start", + "name": "X1", + "type": "expression" + }, + { + "description": "Y position of the start", + "name": "Y1", + "type": "expression" + }, + { + "description": "X position of the end", + "name": "X2", + "type": "expression" + }, + { + "description": "Y position of the end", + "name": "Y2", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance from the line.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Height to add (negative to lower the ground)", + "name": "Height", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Flatten the ground around a position to a given height, for example to place a building.", + "fullName": "Flatten the ground", + "functionType": "Action", + "group": "Sculpt", + "name": "FlattenGround", + "sentence": "Flatten the ground of _PARAM0_ to a Z of _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const z = eventsFunctionContext.getArgument(\"Z\");", + "object.__terrain3D.flatten(x, y, x, y, radius, z, 1);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Z position of the flat ground", + "name": "Z", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Smooth the ground around a position, softening bumps and cliffs.", + "fullName": "Smooth the ground", + "functionType": "Action", + "group": "Sculpt", + "name": "SmoothGround", + "sentence": "Smooth the ground of _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_ (strength: _PARAM4_)", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const strength = eventsFunctionContext.getArgument(\"Strength\");", + "object.__terrain3D.smooth(x, y, x, y, radius, strength);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Strength (between 0 and 1)", + "name": "Strength", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Paint a layer (like grass, dirt, rock or sand) on the ground around a position.", + "fullName": "Paint a layer", + "functionType": "Action", + "group": "Paint", + "name": "PaintLayer", + "sentence": "Paint layer _PARAM4_ on _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const layer = eventsFunctionContext.getArgument(\"Layer\");", + "object.__terrain3D.paint(x, y, x, y, radius, layer, 1);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Layer (1 to 4)", + "name": "Layer", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Paint a layer on the ground along a line, for example for a path or a road.", + "fullName": "Paint a layer along a line", + "functionType": "Action", + "group": "Paint", + "name": "PaintLayerAlongLine", + "sentence": "Paint layer _PARAM6_ on _PARAM0_ from _PARAM1_; _PARAM2_ to _PARAM3_; _PARAM4_ with a radius of _PARAM5_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x1 = eventsFunctionContext.getArgument(\"X1\");", + "const y1 = eventsFunctionContext.getArgument(\"Y1\");", + "const x2 = eventsFunctionContext.getArgument(\"X2\");", + "const y2 = eventsFunctionContext.getArgument(\"Y2\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const layer = eventsFunctionContext.getArgument(\"Layer\");", + "object.__terrain3D.paint(x1, y1, x2, y2, radius, layer, 1);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the start", + "name": "X1", + "type": "expression" + }, + { + "description": "Y position of the start", + "name": "Y1", + "type": "expression" + }, + { + "description": "X position of the end", + "name": "X2", + "type": "expression" + }, + { + "description": "Y position of the end", + "name": "Y2", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance from the line.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Layer (1 to 4)", + "name": "Layer", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Tint the ground around a position with a color multiplying its layers, like dry grass or darker mud. White removes the tint.", + "fullName": "Tint the ground", + "functionType": "Action", + "group": "Paint", + "name": "TintGround", + "sentence": "Tint the ground of _PARAM0_ with _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const color = gdjs.rgbOrHexToRGBColor(eventsFunctionContext.getArgument(\"Color\"));", + "object.__terrain3D.tint(x, y, x, y, radius, color, 1);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Color", + "name": "Color", + "type": "color" + } + ], + "objectGroups": [] + }, + { + "description": "Make the ground around a position wet (darker and shiny, like after rain or near water) or dry.", + "fullName": "Change the wetness of the ground", + "functionType": "Action", + "group": "Paint", + "name": "WetGround", + "sentence": "Change the wetness of the ground of _PARAM0_ to _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const wetness = eventsFunctionContext.getArgument(\"Wetness\");", + "object.__terrain3D.wet(x, y, x, y, radius, wetness, 1);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Wetness (from 0 for dry to 1)", + "name": "Wetness", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "the wetness of the ground at a position, from 0 (dry) to 1, painted or near the water (wet shore).", + "fullName": "Ground wetness", + "functionType": "ExpressionAndCondition", + "group": "Paint", + "name": "GroundWetness", + "sentence": "the wetness of the ground at _PARAM1_; _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "eventsFunctionContext.returnValue = object.__terrain3D.getWetnessAt(x, y);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "The tint painted on the ground at a position, like \"255;255;255\" (white when there is none).", + "fullName": "Ground tint", + "functionType": "StringExpression", + "group": "Paint", + "name": "GroundTint", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "eventsFunctionContext.returnValue = object.__terrain3D.getTintAt(x, y).join(\";\");" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "string" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Remove the ground in a circle, for the entrance of a tunnel, a cave or a well: there is nothing drawn and no collision there. Fill it again with the \"Fill holes\" action. A height field can't go under itself: put a 3D model (with a collision shape) for the tunnel or the cave under the ground. The physics engine removes the triangles touching a hole, so the hole in the collisions can be up to a cell larger than the one drawn: make the model go a bit further than the hole, or put rocks around the entrance.", + "fullName": "Make a hole", + "functionType": "Action", + "group": "Holes", + "name": "MakeHole", + "sentence": "Make a hole in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "object.__terrain3D.makeHole(x, y, x, y, radius);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "name": "Radius", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Fill the holes in a circle with the layers of the ground around them: the ground is drawn and has collisions again.", + "fullName": "Fill holes", + "functionType": "Action", + "group": "Holes", + "name": "FillHoles", + "sentence": "Fill the holes in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "object.__terrain3D.fillHoles(x, y, x, y, radius);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "name": "Radius", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Make the entrance of a tunnel or a cave on a slope: a hole, with the ground in front of it flattened to the floor of the tunnel.", + "fullName": "Carve an entrance", + "functionType": "Action", + "group": "Holes", + "name": "CarveEntrance", + "sentence": "Carve an entrance in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_, going in at _PARAM4_° with its floor at Z _PARAM5_ and _PARAM6_ pixels of flat ground in front", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const angle = eventsFunctionContext.getArgument(\"Angle\");", + "const floorZ = eventsFunctionContext.getArgument(\"FloorZ\");", + "const length = eventsFunctionContext.getArgument(\"Length\");", + "object.__terrain3D.carveEntrance(x, y, radius, angle, floorZ, length);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "name": "Radius", + "type": "expression" + }, + { + "description": "Direction going into the ground (in degrees)", + "name": "Angle", + "type": "expression" + }, + { + "description": "Z position of the floor", + "name": "FloorZ", + "type": "expression" + }, + { + "description": "Length of the flat ground in front of the entrance (in pixels)", + "name": "Length", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Check if there is a hole in the ground at a position.", + "fullName": "Hole", + "functionType": "Condition", + "group": "Holes", + "name": "IsHole", + "sentence": "There is a hole in _PARAM0_ at _PARAM1_; _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "eventsFunctionContext.returnValue = object.__terrain3D.isHoleAt(x, y);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "The Z position of the ground at a position, or the given value where there is a hole.", + "fullName": "Ground Z position, with holes", + "functionType": "Expression", + "group": "Holes", + "name": "GroundZWithHoles", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const terrain = object.__terrain3D;", + "eventsFunctionContext.returnValue = terrain.isHoleAt(x, y) ? eventsFunctionContext.getArgument(\"ZInHoles\") : terrain.getHeightAt(x, y);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + }, + { + "description": "Value in holes", + "longDescription": "For example, a Z position far under the ground.", + "name": "ZInHoles", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Replace the ground with a generated relief. Painted layers are reset.", + "fullName": "Generate a relief", + "functionType": "Action", + "group": "Generation", + "name": "GenerateRelief", + "sentence": "Generate a relief of _PARAM1_ on _PARAM0_ with seed _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const relief = eventsFunctionContext.getArgument(\"Relief\");", + "const seed = eventsFunctionContext.getArgument(\"Seed\");", + "object.__terrain3D.generate(relief, seed);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "Relief", + "name": "Relief", + "supplementaryInformation": "[\"Flat\", \"Hills\", \"Mountains\", \"Island\"]", + "type": "stringWithSelector" + }, + { + "description": "Seed", + "longDescription": "Each seed gives a different terrain with the same relief.", + "name": "Seed", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Replace the ground with the heights of an image. Painted layers are kept.", + "fullName": "Load a heightmap", + "functionType": "Action", + "group": "Generation", + "name": "LoadHeightmap", + "sentence": "Load the heights of _PARAM0_ from _PARAM1_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const image = eventsFunctionContext.getArgument(\"Image\");", + "object.__terrain3D.loadHeightmap(image);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "Heightmap image", + "longDescription": "Black is the lowest, white the highest. For more precision, store the height on 16 bits in the red (most significant byte) and green channels.", + "name": "Image", + "type": "imageResource" + } + ], + "objectGroups": [] + }, + { + "description": "Move objects up or down so that their bottom is on the ground, under their center. Useful to place trees, rocks or characters on a terrain.", + "fullName": "Put objects on the ground", + "functionType": "Action", + "group": "Position", + "name": "PutOnGround", + "sentence": "Put _PARAM1_ on the ground of _PARAM0_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrain3D.placeOnGround(eventsFunctionContext.getObjects(\"Objects\"));" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "Objects to put on the ground", + "name": "Objects", + "type": "objectList" + } + ], + "objectGroups": [] + }, + { + "description": "the Z position of the ground at a position of the scene.", + "fullName": "Ground Z position", + "functionType": "ExpressionAndCondition", + "group": "Position", + "name": "GroundZ", + "sentence": "the ground Z position at _PARAM1_; _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "eventsFunctionContext.returnValue = object.__terrain3D.getHeightAt(x, y);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "the slope of the ground (angle with the horizontal, in degrees) at a position of the scene.", + "fullName": "Ground slope", + "functionType": "ExpressionAndCondition", + "group": "Position", + "name": "GroundSlope", + "sentence": "the ground slope at _PARAM1_; _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "eventsFunctionContext.returnValue = object.__terrain3D.getSlopeAt(x, y);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "the layer (from 1 to 4) painted the most at a position of the scene. Useful to play footstep sounds or to slow down characters in water or mud.", + "fullName": "Ground layer", + "functionType": "ExpressionAndCondition", + "group": "Paint", + "name": "GroundLayer", + "sentence": "the ground layer at _PARAM1_; _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "eventsFunctionContext.returnValue = object.__terrain3D.getLayerAt(x, y);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + } + ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + }, + { + "folderName": "Sculpt", + "children": [ + { + "functionName": "RaiseGround" + }, + { + "functionName": "RaiseGroundAlongLine" + }, + { + "functionName": "FlattenGround" + }, + { + "functionName": "SmoothGround" + } + ] + }, + { + "folderName": "Paint", + "children": [ + { + "functionName": "PaintLayer" + }, + { + "functionName": "PaintLayerAlongLine" + }, + { + "functionName": "TintGround" + }, + { + "functionName": "WetGround" + }, + { + "functionName": "GroundWetness" + }, + { + "functionName": "GroundTint" + }, + { + "functionName": "GroundLayer" + } + ] + }, + { + "folderName": "Holes", + "children": [ + { + "functionName": "MakeHole" + }, + { + "functionName": "FillHoles" + }, + { + "functionName": "CarveEntrance" + }, + { + "functionName": "IsHole" + }, + { + "functionName": "GroundZWithHoles" + } + ] + }, + { + "folderName": "Generation", + "children": [ + { + "functionName": "GenerateRelief" + }, + { + "functionName": "LoadHeightmap" + } + ] + }, + { + "folderName": "Position", + "children": [ + { + "functionName": "PutOnGround" + }, + { + "functionName": "GroundZ" + }, + { + "functionName": "GroundSlope" + } + ] + } + ] + }, + "propertyDescriptors": [ + { + "value": "Hills", + "type": "Choice", + "label": "Relief", + "description": "The initial shape, before sculpting.", + "group": "Shape", + "choices": [ + { + "label": "Flat", + "value": "Flat" + }, + { + "label": "Hills", + "value": "Hills" + }, + { + "label": "Mountains", + "value": "Mountains" + }, + { + "label": "Island", + "value": "Island" + } + ], + "name": "Relief" + }, + { + "value": "1", + "type": "Number", + "label": "Seed", + "description": "Each seed gives a different terrain with the same relief.", + "group": "Shape", + "name": "Seed" + }, + { + "value": "", + "type": "Resource", + "label": "Heightmap image", + "description": "Optional. Replaces the relief: black is the lowest, white the highest (the object depth).", + "group": "Shape", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "HeightmapImage" + }, + { + "value": "", + "type": "MultilineString", + "label": "Edits", + "description": "Changes of the relief or heightmap, applied in order, as a JSON list, for example [{\"tool\": \"raise\", \"x\": 0.3, \"y\": 0.4, \"radius\": 0.15, \"height\": 0.4}, {\"tool\": \"paint\", \"x\": 0.1, \"y\": 0.9, \"toX\": 0.8, \"toY\": 0.2, \"radius\": 0.02, \"layer\": 2}]. Tools: raise (height added to the ground, negative to lower), flatten (z: the ground is brought to this height, fully at the center, cutting what is above it and filling what is below; with toX and toY, toZ gives the height at the end of the line, for a road going up), smooth (strength), paint (layer, from 1 to 4, and strength), tint (color, like \"120;140;60\", multiplying the layers, white removing the tint, and strength), wetness (amount, from 0 for dry to 1 for wet, and strength), hole (no ground in the radius, for the entrances of tunnels and caves) and fill (the holes in the radius are filled with the layers of the ground around them). x, y and radius are fractions (0 to 1) of the terrain size (x is (scene X - terrain X) / terrain width). height and z are fractions of its depth: the ground at z is at Z = terrain Z + z * terrain depth. Add toX and toY to change the ground along a line (paths, rivers, ridges). Shown in the scene editor.", + "group": "Shape", + "advanced": true, + "name": "Edits" + }, + { + "value": "256", + "type": "Choice", + "label": "Resolution", + "description": "The number of cells on each side of the terrain. Higher is more detailed but slower.", + "group": "Shape", + "choices": [ + { + "label": "64 x 64", + "value": "64" + }, + { + "label": "128 x 128", + "value": "128" + }, + { + "label": "256 x 256", + "value": "256" + }, + { + "label": "512 x 512", + "value": "512" + }, + { + "label": "1024 x 1024", + "value": "1024" + } + ], + "name": "Resolution" + }, + { + "value": "96;160;64", + "type": "Color", + "label": "Layer 1 color (grass)", + "group": "Layers", + "name": "Layer1Color" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 1 texture", + "description": "Optional. Tinted by the layer color.", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer1Texture" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 1 normal map", + "description": "Optional. The bumps of the texture, lit by the lights (an OpenGL normal map, like the textures of ambientCG).", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer1NormalMap" + }, + { + "value": "150;112;72", + "type": "Color", + "label": "Layer 2 color (dirt)", + "group": "Layers", + "name": "Layer2Color" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 2 texture", + "description": "Optional. Tinted by the layer color.", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer2Texture" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 2 normal map", + "description": "Optional. The bumps of the texture, lit by the lights (an OpenGL normal map, like the textures of ambientCG).", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer2NormalMap" + }, + { + "value": "124;118;112", + "type": "Color", + "label": "Layer 3 color (rock)", + "group": "Layers", + "name": "Layer3Color" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 3 texture", + "description": "Optional. Tinted by the layer color.", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer3Texture" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 3 normal map", + "description": "Optional. The bumps of the texture, lit by the lights (an OpenGL normal map, like the textures of ambientCG).", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer3NormalMap" + }, + { + "value": "224;204;148", + "type": "Color", + "label": "Layer 4 color (sand)", + "group": "Layers", + "name": "Layer4Color" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 4 texture", + "description": "Optional. Tinted by the layer color.", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer4Texture" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 4 normal map", + "description": "Optional. The bumps of the texture, lit by the lights (an OpenGL normal map, like the textures of ambientCG).", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer4NormalMap" + }, + { + "value": "1", + "type": "Number", + "label": "Normal map strength", + "description": "How much the normal maps bump the ground: 0 for none, 1 as they are, more to exaggerate them.", + "group": "Layers", + "name": "NormalMapStrength" + }, + { + "value": "256", + "type": "Number", + "unit": "Pixel", + "label": "Texture size", + "description": "The size, in the scene, of one repetition of the layer textures.", + "group": "Layers", + "name": "TextureSize" + }, + { + "value": "0.5", + "type": "Number", + "label": "Height blending", + "description": "Where layers meet, the bright parts of textures (stones, tufts) cover the dark ones (gaps, sand) instead of fading into each other: 0 for none, 1 for the most.", + "group": "Layers", + "name": "HeightBlending" + }, + { + "value": "0.5", + "type": "Number", + "label": "Texture variation", + "description": "Makes the repetition of textures less visible, with patches of bigger and rotated textures and colors slowly varying: 0 for none, 1 for the most.", + "group": "Layers", + "name": "TextureVariation" + }, + { + "value": "false", + "type": "Boolean", + "label": "Wet shore", + "description": "Make the ground wet (darker and shiny) under a Z position, like the sand of a beach near the water.", + "group": "Wetness", + "name": "WetShore" + }, + { + "value": "0", + "type": "Number", + "unit": "Pixel", + "label": "Wet below Z", + "description": "The ground is wet under this Z position (for example, the Z of a 3D water object).", + "group": "Wetness", + "name": "WetShoreZ" + }, + { + "value": "60", + "type": "Number", + "unit": "Pixel", + "label": "Wet shore fade", + "description": "The ground becomes dry over this height above \"Wet below Z\".", + "group": "Wetness", + "name": "WetShoreFade" + }, + { + "value": "3", + "type": "Choice", + "label": "Layer on steep slopes", + "description": "This layer is shown where the ground is steep, whatever is painted.", + "group": "Layers", + "choices": [ + { + "label": "None", + "value": "0" + }, + { + "label": "Layer 1", + "value": "1" + }, + { + "label": "Layer 2", + "value": "2" + }, + { + "label": "Layer 3", + "value": "3" + }, + { + "label": "Layer 4", + "value": "4" + } + ], + "name": "CliffLayer" + }, + { + "value": "45", + "type": "Number", + "unit": "DegreeAngle", + "label": "Steep slope angle", + "group": "Layers", + "name": "CliffAngle" + }, + { + "value": "true", + "type": "Boolean", + "label": "Cast shadows", + "group": "Rendering", + "name": "CastShadow" + }, + { + "value": "true", + "type": "Boolean", + "label": "Receive shadows", + "group": "Rendering", + "name": "ReceiveShadow" + }, + { + "value": "", + "type": "String", + "label": "Sculpted and painted data", + "description": "Written by the sculpt and paint tools of the scene editor, in a compact format not meant to be read or written by hand: use Edits instead. Clear it to go back to the relief, heightmap and edits.", + "advanced": true, + "name": "SculptData" + } + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Shape", + "children": [ + { + "propertyName": "Relief" + }, + { + "propertyName": "Seed" + }, + { + "propertyName": "HeightmapImage" + }, + { + "propertyName": "Edits" + }, + { + "propertyName": "Resolution" + } + ] + }, + { + "folderName": "Layers", + "children": [ + { + "propertyName": "Layer1Color" + }, + { + "propertyName": "Layer1Texture" + }, + { + "propertyName": "Layer1NormalMap" + }, + { + "propertyName": "Layer2Color" + }, + { + "propertyName": "Layer2Texture" + }, + { + "propertyName": "Layer2NormalMap" + }, + { + "propertyName": "Layer3Color" + }, + { + "propertyName": "Layer3Texture" + }, + { + "propertyName": "Layer3NormalMap" + }, + { + "propertyName": "Layer4Color" + }, + { + "propertyName": "Layer4Texture" + }, + { + "propertyName": "Layer4NormalMap" + }, + { + "propertyName": "NormalMapStrength" + }, + { + "propertyName": "TextureSize" + }, + { + "propertyName": "HeightBlending" + }, + { + "propertyName": "TextureVariation" + }, + { + "propertyName": "CliffLayer" + }, + { + "propertyName": "CliffAngle" + } + ] + }, + { + "folderName": "Wetness", + "children": [ + { + "propertyName": "WetShore" + }, + { + "propertyName": "WetShoreZ" + }, + { + "propertyName": "WetShoreFade" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "CastShadow" + }, + { + "propertyName": "ReceiveShadow" + } + ] + }, + { + "propertyName": "SculptData" + } + ] + }, + "variants": [] + }, + { + "areaMaxX": 1000, + "areaMaxY": 1000, + "areaMaxZ": 40, + "areaMinX": 0, + "areaMinY": 0, + "areaMinZ": 0, + "defaultName": "Grass", + "description": "A field of grass blades swaying in the wind and pushed aside by characters. They grow in the area of the object, on the highest 3D terrain at each point (one field can cover several terrains), or at the Z position of the object where there is no terrain.", + "fullName": "Terrain grass", + "helpPath": "", + "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGQ9Ik0zIDIxYzEtNSAxLTkgMC0xMyAyIDMgMyA3IDMgMTN6bTQgMGMwLTYgMS0xMSA0LTE2LTEgNS0xIDEwIDAgMTZ6bTUgMGMwLTUgMi05IDUtMTItMiA0LTIgOC0xIDEyem01IDBjMS00IDItNiA0LTgtMSAzLTEgNS0xIDh6Ii8+PC9zdmc+", + "is3D": true, + "isUsingLegacyInstancesRenderer": true, + "name": "TerrainGrass", + "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/732ef90b7fcf5dd9171fe95d0cf262e09159b487304915a4693baa893d9ce16c_grass.svg", + "objects": [ + { + "assetStoreId": "", + "name": "AreaPlaceholder", + "type": "Scene3D::Cube3DObject", + "variables": [], + "effects": [], + "behaviors": [], + "content": { + "width": 1000, + "height": 1000, + "depth": 40, + "enableTextureTransparency": false, + "facesOrientation": "Y", + "backFaceUpThroughWhichAxisRotation": "X", + "materialType": "Basic", + "tint": "255;255;255", + "frontFaceResourceName": "", + "frontFaceVisible": false, + "frontFaceResourceRepeat": false, + "backFaceResourceName": "", + "backFaceVisible": false, + "backFaceResourceRepeat": false, + "leftFaceResourceName": "", + "leftFaceVisible": false, + "leftFaceResourceRepeat": false, + "rightFaceResourceName": "", + "rightFaceVisible": false, + "rightFaceResourceRepeat": false, + "topFaceResourceName": "", + "topFaceVisible": false, + "topFaceResourceRepeat": false, + "bottomFaceResourceName": "", + "bottomFaceVisible": false, + "bottomFaceResourceRepeat": false + } + } + ], + "objectsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "objectName": "AreaPlaceholder" + } + ] + }, + "objectsGroups": [], + "layers": [ + { + "ambientLightColorB": 200, + "ambientLightColorG": 200, + "ambientLightColorR": 200, + "camera2DPlaneMaxDrawingDistance": 5000, + "camera3DFarPlaneDistance": 10000, + "camera3DFieldOfView": 45, + "camera3DNearPlaneDistance": 3, + "cameraType": "", + "followBaseLayerCamera": false, + "isLightingLayer": false, + "isLocked": false, + "name": "", + "renderingType": "", + "visibility": true, + "cameras": [ + { + "defaultSize": true, + "defaultViewport": true, + "height": 0, + "viewportBottom": 1, + "viewportLeft": 0, + "viewportRight": 1, + "viewportTop": 0, + "width": 0 + } + ], + "effects": [] + } + ], + "instances": [ + { + "angle": 0, + "customSize": true, + "depth": 40, + "height": 1000, + "keepRatio": true, + "layer": "", + "name": "AreaPlaceholder", + "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-001000001000", + "width": 1000, + "x": 0, + "y": 0, + "zOrder": 1, + "numberProperties": [], + "stringProperties": [], + "initialVariables": [] + } + ], + "editionSettings": {}, + "eventsFunctions": [ + { + "fullName": "", + "functionType": "Action", + "name": "onCreated", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Terrain3D::DefineHelperClasses" + }, + "parameters": [ + "", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Terrain3D::DefineGrassHelperClasses" + }, + "parameters": [ + "", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "for (const child of object.getChildrenContainer().getAdhocListOfAllInstances()) {", + " child.deleteFromScene();", + "}", + "if (object.__terrainGrass) object.__terrainGrass.dispose();", + "object.__terrainGrass = new gdjs.__terrainGrassExtension.Grass(object);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "onDestroy", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrainGrass.dispose();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "doStepPostEvents", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const game = runtimeScene.getGame();", + "// Time doesn't pass in the scene editor: real time is used to show the wind.", + "const isInGameEdition = game.isInGameEdition && game.isInGameEdition();", + "const elapsedTime = isInGameEdition ? 1 / 60 : object.getElapsedTime() / 1000;", + "const time = isInGameEdition", + " ? performance.now() / 1000", + " : object.getRuntimeScene().getTimeManager().getTimeFromStart() / 1000;", + "const layer = object.getInstanceContainer().getLayer(object.getLayer());", + "object.__terrainGrass.update(elapsedTime, time, layer.getRenderer().getThreeCamera());" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "onHotReloading", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrainGrass.updateFromProperties();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" + } + ], + "objectGroups": [] + } + ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + } + ] + }, + "propertyDescriptors": [ + { + "value": "60", + "type": "Number", + "label": "Density", + "description": "The number of blades in a square of 100 x 100 pixels.", + "group": "Blades", + "name": "Density" + }, + { + "value": "36", + "type": "Number", + "unit": "Pixel", + "label": "Blade height", + "description": "The average height of blades (each blade is between 60% and 140% of it).", + "group": "Blades", + "name": "BladeHeight" + }, + { + "value": "5", + "type": "Number", + "unit": "Pixel", + "label": "Blade width", + "group": "Blades", + "name": "BladeWidth" + }, + { + "value": "52;104;36", + "type": "Color", + "label": "Base color", + "group": "Blades", + "name": "BaseColor" + }, + { + "value": "150;196;82", + "type": "Color", + "label": "Tip color", + "group": "Blades", + "name": "TipColor" + }, + { + "value": "1", + "type": "Number", + "label": "Seed", + "description": "Each seed places blades differently.", + "group": "Blades", + "name": "Seed" + }, + { + "value": "1", + "type": "Choice", + "label": "Grows on terrain layer", + "description": "On a 3D terrain, blades only grow where this layer is painted. A new terrain has layer 1 painted everywhere.", + "group": "Ground", + "choices": [ + { + "label": "Any", + "value": "0" + }, + { + "label": "Layer 1", + "value": "1" + }, + { + "label": "Layer 2", + "value": "2" + }, + { + "label": "Layer 3", + "value": "3" + }, + { + "label": "Layer 4", + "value": "4" + } + ], + "name": "GroundLayer" + }, + { + "value": "40", + "type": "Number", + "unit": "DegreeAngle", + "label": "Maximum slope", + "description": "No blades grow on steeper ground.", + "group": "Ground", + "name": "MaxSlope" + }, + { + "value": "1", + "type": "Number", + "label": "Wind strength", + "description": "0 for no wind.", + "group": "Wind", + "name": "WindStrength" + }, + { + "value": "2", + "type": "Number", + "label": "Wind speed", + "group": "Wind", + "name": "WindSpeed" + }, + { + "value": "", + "type": "String", + "label": "Object bending the grass", + "description": "The name of an object pushing blades aside when walking through the grass, like the player. The 8 first instances are used.", + "group": "Wind", + "name": "BendingObject" + }, + { + "value": "2500", + "type": "Number", + "unit": "Pixel", + "label": "Fade distance", + "description": "Blades are less and less dense up to this distance from the camera, and hidden after.", + "group": "Rendering", + "name": "FadeDistance" + } + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Blades", + "children": [ + { + "propertyName": "Density" + }, + { + "propertyName": "BladeHeight" + }, + { + "propertyName": "BladeWidth" + }, + { + "propertyName": "BaseColor" + }, + { + "propertyName": "TipColor" + }, + { + "propertyName": "Seed" + } + ] + }, + { + "folderName": "Ground", + "children": [ + { + "propertyName": "GroundLayer" + }, + { + "propertyName": "MaxSlope" + } + ] + }, + { + "folderName": "Wind", + "children": [ + { + "propertyName": "WindStrength" + }, + { + "propertyName": "WindSpeed" + }, + { + "propertyName": "BendingObject" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "FadeDistance" + } + ] + } + ] + }, + "variants": [] + }, + { + "areaMaxX": 2000, + "areaMaxY": 2000, + "areaMaxZ": 200, + "areaMinX": 0, + "areaMinY": 0, + "areaMinZ": 0, + "assetStoreTag": "3d scattered models", + "defaultName": "ScatteredModels", + "description": "Many copies of a 3D model (trees, rocks, bushes, flowers...) placed in the area of the object and drawn all at once, which is much faster than an object for each one. They stand on the highest 3D terrain at each point, or at the Z position of the object where there is no terrain. Paint where they grow with the brushes of the scene editor.", + "fullName": "Scattered models (forests, rocks...)", + "helpPath": "", + "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGQ9Ik0xMCAyMXYtM0gzbDUtNUg1bDUtNUg3bDUtNSA1IDVoLTNsNSA1aC0zbDUgNWgtN3YzeiIvPjwvc3ZnPg==", + "is3D": true, + "isUsingLegacyInstancesRenderer": true, + "name": "ScatteredModels", + "previewIconUrl": "", + "objects": [ + { + "assetStoreId": "", + "name": "AreaPlaceholder", + "type": "Scene3D::Cube3DObject", + "variables": [], + "effects": [], + "behaviors": [], + "content": { + "width": 2000, + "height": 2000, + "depth": 200, + "enableTextureTransparency": false, + "facesOrientation": "Y", + "backFaceUpThroughWhichAxisRotation": "X", + "materialType": "Basic", + "tint": "255;255;255", + "frontFaceResourceName": "", + "frontFaceVisible": false, + "frontFaceResourceRepeat": false, + "backFaceResourceName": "", + "backFaceVisible": false, + "backFaceResourceRepeat": false, + "leftFaceResourceName": "", + "leftFaceVisible": false, + "leftFaceResourceRepeat": false, + "rightFaceResourceName": "", + "rightFaceVisible": false, + "rightFaceResourceRepeat": false, + "topFaceResourceName": "", + "topFaceVisible": false, + "topFaceResourceRepeat": false, + "bottomFaceResourceName": "", + "bottomFaceVisible": false, + "bottomFaceResourceRepeat": false + } + } + ], + "objectsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "objectName": "AreaPlaceholder" + } + ] + }, + "objectsGroups": [], + "layers": [ + { + "ambientLightColorB": 200, + "ambientLightColorG": 200, + "ambientLightColorR": 200, + "camera2DPlaneMaxDrawingDistance": 5000, + "camera3DFarPlaneDistance": 10000, + "camera3DFieldOfView": 45, + "camera3DNearPlaneDistance": 3, + "cameraType": "", + "followBaseLayerCamera": false, + "isLightingLayer": false, + "isLocked": false, + "name": "", + "renderingType": "", + "visibility": true, + "cameras": [ + { + "defaultSize": true, + "defaultViewport": true, + "height": 0, + "viewportBottom": 1, + "viewportLeft": 0, + "viewportRight": 1, + "viewportTop": 0, + "width": 0 + } + ], + "effects": [] + } + ], + "instances": [ + { + "angle": 0, + "customSize": true, + "depth": 200, + "height": 2000, + "keepRatio": true, + "layer": "", + "name": "AreaPlaceholder", + "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-002000002000", + "width": 2000, + "x": 0, + "y": 0, + "zOrder": 1, + "numberProperties": [], + "stringProperties": [], + "initialVariables": [] + } + ], + "editionSettings": {}, + "eventsFunctions": [ + { + "fullName": "", + "functionType": "Action", + "name": "onCreated", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Terrain3D::DefineHelperClasses" + }, + "parameters": [ + "", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Terrain3D::DefineScatterHelperClasses" + }, + "parameters": [ + "", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "for (const child of object.getChildrenContainer().getAdhocListOfAllInstances()) {", + " child.deleteFromScene();", + "}", + "if (object.__terrainScatter) object.__terrainScatter.dispose();", + "object.__terrainScatter = new gdjs.__terrainScatterExtension.Scatter(object);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "onDestroy", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrainScatter.dispose();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "doStepPostEvents", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const game = runtimeScene.getGame();", + "const isInGameEdition = game.isInGameEdition && game.isInGameEdition();", + "const elapsedTime = isInGameEdition ? 1 / 60 : object.getElapsedTime() / 1000;", + "const layer = object.getInstanceContainer().getLayer(object.getLayer());", + "object.__terrainScatter.update(elapsedTime, layer.getRenderer().getThreeCamera());" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "onHotReloading", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrainScatter.onHotReloading();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "description": "Paint the density of copies around a position, up to the density of the object (copies removed with \"Erase copies\" come back). The effect fades out up to the radius.", + "fullName": "Add copies", + "functionType": "Action", + "group": "Paint", + "name": "AddCopies", + "sentence": "Add copies of _PARAM0_ around _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "object.__terrainScatter.addCopies(x, y, radius);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "name": "Radius", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Remove the copies standing in a circle, for example a tree cut by the player (use the position of the nearest copy and a small radius) or a forest cleared for a building. They come back when the scene is restarted.", + "fullName": "Erase copies", + "functionType": "Action", + "group": "Paint", + "name": "EraseCopies", + "sentence": "Erase the copies of _PARAM0_ in a circle at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "object.__terrainScatter.removeCopies(x, y, radius);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "name": "Radius", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "the number of copies placed in the area.", + "fullName": "Number of copies", + "functionType": "ExpressionAndCondition", + "group": "Copies", + "name": "CopyCount", + "sentence": "the number of copies", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "eventsFunctionContext.returnValue = object.__terrainScatter.getCopyCount();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "description": "The X position of the base of the copy nearest to a position (the position itself if there is no copy).", + "fullName": "X position of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyX", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? copy.x : x;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "The Y position of the base of the copy nearest to a position (the position itself if there is no copy).", + "fullName": "Y position of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyY", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? copy.y : y;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "The Z position of the base of the copy nearest to a position (on the ground).", + "fullName": "Z position of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyZ", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? copy.z : object.getZ();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "the distance (on X and Y) from a position to the base of the nearest copy. It's a very big number when there is no copy.", + "fullName": "Distance to the nearest copy", + "functionType": "ExpressionAndCondition", + "group": "Copies", + "name": "NearestCopyDistance", + "sentence": "the distance from _PARAM1_; _PARAM2_ to the nearest copy", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? Math.hypot(copy.x - x, copy.y - y) : Number.MAX_VALUE;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + } + ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + }, + { + "folderName": "Paint", + "children": [ + { + "functionName": "AddCopies" + }, + { + "functionName": "EraseCopies" + } + ] + }, + { + "folderName": "Copies", + "children": [ + { + "functionName": "CopyCount" + }, + { + "functionName": "NearestCopyX" + }, + { + "functionName": "NearestCopyY" + }, + { + "functionName": "NearestCopyZ" + }, + { + "functionName": "NearestCopyDistance" + } + ] + } + ] + }, + "propertyDescriptors": [ + { + "value": "", + "type": "Resource", + "label": "3D model", + "description": "The model copied everywhere (a .glb file). Models standing on the Y axis, like most models of the asset store, are stood up along Z.", + "group": "Model", + "extraInformation": [ + "model3D" + ], + "choices": [], + "name": "Model" + }, + { + "value": "300", + "type": "Number", + "unit": "Pixel", + "label": "Height of a copy", + "description": "Copies keep the proportions of the model.", + "group": "Model", + "name": "Size" + }, + { + "value": "0.8", + "type": "Number", + "label": "Smallest random scale", + "description": "Each copy is scaled at random between the smallest and the largest scale.", + "group": "Model", + "name": "MinScale" + }, + { + "value": "1.2", + "type": "Number", + "label": "Largest random scale", + "group": "Model", + "name": "MaxScale" + }, + { + "value": "true", + "type": "Boolean", + "label": "Random rotation", + "description": "Turn each copy around the Z axis at random.", + "group": "Model", + "name": "RandomRotation" + }, + { + "value": "255;255;255", + "type": "Color", + "label": "Random tint", + "description": "Each copy is tinted between white and this color, at random. White for no tint.", + "group": "Model", + "name": "RandomTint" + }, + { + "value": "0.3", + "type": "Number", + "label": "Density", + "description": "The number of copies in a square of 100 x 100 pixels where the density is fully painted (0.3 is about one copy every 180 pixels). Copies never overlap their neighbors much: they are placed at random in a grid.", + "group": "Placement", + "name": "Density" + }, + { + "value": "1", + "type": "Number", + "label": "Seed", + "description": "Each seed places copies differently.", + "group": "Placement", + "name": "Seed" + }, + { + "value": "Everywhere", + "type": "Choice", + "label": "Copies grow", + "description": "Everywhere in the area, or only where they are painted with the \"Add copies\" brush (or the edits).", + "group": "Placement", + "choices": [ + { + "label": "Everywhere (erase them with the brush)", + "value": "Everywhere" + }, + { + "label": "Only where painted", + "value": "Painted" + } + ], + "name": "Coverage" + }, + { + "value": "1", + "type": "Choice", + "label": "Grows on terrain layer", + "description": "On a 3D terrain, copies only grow where this layer is painted, away from its borders (so that trees don't stand on roads painted with another layer). A new terrain has layer 1 painted everywhere.", + "group": "Ground", + "choices": [ + { + "label": "Any", + "value": "0" + }, + { + "label": "Layer 1", + "value": "1" + }, + { + "label": "Layer 2", + "value": "2" + }, + { + "label": "Layer 3", + "value": "3" + }, + { + "label": "Layer 4", + "value": "4" + } + ], + "name": "GroundLayer" + }, + { + "value": "30", + "type": "Number", + "unit": "DegreeAngle", + "label": "Maximum slope", + "description": "No copies grow on steeper ground.", + "group": "Ground", + "name": "MaxSlope" + }, + { + "value": "-100000", + "type": "Number", + "unit": "Pixel", + "label": "Minimum ground Z", + "description": "No copies grow where the ground is lower (for example, under the water level).", + "group": "Ground", + "name": "MinGroundZ" + }, + { + "value": "100000", + "type": "Number", + "unit": "Pixel", + "label": "Maximum ground Z", + "description": "No copies grow where the ground is higher (for example, above the tree line).", + "group": "Ground", + "name": "MaxGroundZ" + }, + { + "value": "false", + "type": "Boolean", + "label": "Trunk collisions", + "description": "Give each copy a static collision shape around its trunk, for the 3D physics behavior (add it to this object, with the \"Static\" type) and navigation meshes. Without them, these behaviors have no effect on this object.", + "group": "Collisions", + "name": "TrunkCollisions" + }, + { + "value": "30", + "type": "Number", + "unit": "Pixel", + "label": "Trunk diameter", + "description": "The diameter of the collision shape of a copy at the height given in \"Height of a copy\" (it is scaled with the copy).", + "group": "Collisions", + "name": "TrunkDiameter" + }, + { + "value": "8000", + "type": "Number", + "unit": "Pixel", + "label": "Draw distance", + "description": "Copies shrink and are hidden at this distance from the camera.", + "group": "Rendering", + "name": "DrawDistance" + }, + { + "value": "true", + "type": "Boolean", + "label": "Cast shadows", + "group": "Rendering", + "name": "CastShadow" + }, + { + "value": "true", + "type": "Boolean", + "label": "Receive shadows", + "group": "Rendering", + "name": "ReceiveShadow" + }, + { + "value": "", + "type": "MultilineString", + "label": "Edits", + "description": "Where copies are added or erased, applied in order, as a JSON list, for example [{\"tool\": \"add\", \"x\": 0.3, \"y\": 0.4, \"radius\": 0.15}, {\"tool\": \"erase\", \"x\": 0.1, \"y\": 0.9, \"toX\": 0.8, \"toY\": 0.2, \"radius\": 0.02}]. Tools: add (density: from 0 to 1, 1 by default, how dense copies are compared to the Density property) and erase. strength (from 0 to 1) makes a stroke lighter. x, y and radius are fractions (0 to 1) of the object size (x is (scene X - object X) / object width). Add toX and toY to paint along a line (a row of trees, a path through a forest). With \"Copies grow: only where painted\", only the added areas have copies. Shown in the scene editor.", + "group": "Placement", + "advanced": true, + "name": "Edits" + }, + { + "value": "", + "type": "String", + "label": "Painted data", + "description": "Written by the brushes of the scene editor, in a compact format not meant to be read or written by hand: use Edits instead. Clear it to go back to the coverage and the edits.", + "advanced": true, + "name": "PaintData" + } + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Model", + "children": [ + { + "propertyName": "Model" + }, + { + "propertyName": "Size" + }, + { + "propertyName": "MinScale" + }, + { + "propertyName": "MaxScale" + }, + { + "propertyName": "RandomRotation" + }, + { + "propertyName": "RandomTint" + } + ] + }, + { + "folderName": "Placement", + "children": [ + { + "propertyName": "Density" + }, + { + "propertyName": "Seed" + }, + { + "propertyName": "Coverage" + }, + { + "propertyName": "Edits" + } + ] + }, + { + "folderName": "Ground", + "children": [ + { + "propertyName": "GroundLayer" + }, + { + "propertyName": "MaxSlope" + }, + { + "propertyName": "MinGroundZ" + }, + { + "propertyName": "MaxGroundZ" + } + ] + }, + { + "folderName": "Collisions", + "children": [ + { + "propertyName": "TrunkCollisions" + }, + { + "propertyName": "TrunkDiameter" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "DrawDistance" + }, + { + "propertyName": "CastShadow" + }, + { + "propertyName": "ReceiveShadow" + } + ] + }, + { + "propertyName": "PaintData" + } + ] + }, + "variants": [] + } + ] } ], "externalLayouts": [] diff --git a/examples/starting-first-person-shooter/assets/StartingWall.png b/examples/starting-first-person-shooter/assets/StartingWall.png index b3a097887..06278d6e5 100644 Binary files a/examples/starting-first-person-shooter/assets/StartingWall.png and b/examples/starting-first-person-shooter/assets/StartingWall.png differ diff --git a/examples/starting-first-person-shooter/starting-first-person-shooter.json b/examples/starting-first-person-shooter/starting-first-person-shooter.json index 151b68d67..018f097fc 100644 --- a/examples/starting-first-person-shooter/starting-first-person-shooter.json +++ b/examples/starting-first-person-shooter/starting-first-person-shooter.json @@ -1,7 +1,7 @@ { "firstLayout": "", "gdVersion": { - "build": 283, + "build": 284, "major": 5, "minor": 6, "revision": 0 @@ -266,6 +266,14 @@ "name": "assets/Skybox_Right.png", "smoothed": true, "userAdded": false + }, + { + "file": "assets/StartingGround.png", + "kind": "image", + "metadata": "", + "name": "assets/StartingGround.png", + "smoothed": true, + "userAdded": false } ] }, @@ -282,12 +290,13 @@ "mangledName": "Game_32Scene", "name": "Game Scene", "r": 255, + "renderer3DWorldScale": 100, "standardSortMethod": true, "stopSoundsOnStartup": true, "title": "", "v": 255, "uiSettings": { - "grid": false, + "grid": true, "gridType": "rectangular", "gridWidth": 64, "gridHeight": 64, @@ -297,7 +306,7 @@ "gridOffsetZ": 0, "gridColor": 10401023, "gridAlpha": 0.8, - "snap": false, + "snap": true, "zoomFactor": 0.5468750000000299, "windowMask": false, "selectedLayer": "", @@ -307,9 +316,6 @@ { "name": "BulletObstacle", "objects": [ - { - "name": "Ground" - }, { "name": "Obstacle" }, @@ -318,6 +324,9 @@ }, { "name": "Target" + }, + { + "name": "Terrain" } ] } @@ -331,24 +340,6 @@ } ], "instances": [ - { - "angle": 0, - "customSize": true, - "depth": 30, - "height": 1728, - "layer": "", - "locked": true, - "name": "Ground", - "persistentUuid": "6a14e5ab-5c31-4692-8cd5-c50937fd60c6", - "width": 1920, - "x": -320, - "y": -576, - "z": -30, - "zOrder": 1, - "numberProperties": [], - "stringProperties": [], - "initialVariables": [] - }, { "angle": 0, "customSize": true, @@ -769,77 +760,25 @@ "numberProperties": [], "stringProperties": [], "initialVariables": [] + }, + { + "angle": 0, + "customSize": true, + "height": 1728, + "layer": "", + "locked": true, + "name": "Terrain", + "persistentUuid": "51d1abbc-ccd6-4289-afc1-3bf9da787228", + "width": 1920, + "x": -320, + "y": -576, + "zOrder": 0, + "numberProperties": [], + "stringProperties": [], + "initialVariables": [] } ], "objects": [ - { - "assetStoreId": "", - "name": "Ground", - "persistentUuid": "58b72fb1-240e-4ae2-8305-96701ccdf706", - "type": "Scene3D::Cube3DObject", - "variables": [], - "effects": [], - "behaviors": [ - { - "name": "Physics3D", - "type": "Physics3D::Physics3DBehavior", - "object3D": "Object3D", - "bodyType": "Static", - "bullet": false, - "fixedRotation": false, - "shape": "Box", - "shapeOrientation": "Z", - "shapeDimensionA": 0, - "shapeDimensionB": 0, - "shapeDimensionC": 0, - "density": 1, - "friction": 0.3, - "restitution": 0.1, - "linearDamping": 0.1, - "angularDamping": 0.1, - "gravityScale": 1, - "layers": 17, - "masks": 17, - "shapeOffsetX": 0, - "shapeOffsetY": 0, - "shapeOffsetZ": 0, - "massCenterOffsetX": 0, - "massCenterOffsetY": 0, - "massCenterOffsetZ": 0, - "massOverride": 0 - } - ], - "content": { - "width": 1000, - "height": 1000, - "depth": 40, - "enableTextureTransparency": false, - "facesOrientation": "Y", - "frontFaceResourceName": "assets\\StartingGround.png", - "backFaceResourceName": "", - "backFaceUpThroughWhichAxisRotation": "X", - "leftFaceResourceName": "assets\\StartingWall.png", - "rightFaceResourceName": "assets\\StartingWall.png", - "topFaceResourceName": "assets\\StartingWall.png", - "bottomFaceResourceName": "assets\\StartingWall.png", - "frontFaceVisible": true, - "backFaceVisible": false, - "leftFaceVisible": true, - "rightFaceVisible": true, - "topFaceVisible": true, - "bottomFaceVisible": true, - "frontFaceResourceRepeat": true, - "backFaceResourceRepeat": false, - "leftFaceResourceRepeat": false, - "rightFaceResourceRepeat": false, - "topFaceResourceRepeat": false, - "bottomFaceResourceRepeat": false, - "materialType": "StandardWithoutMetalness", - "isCastingShadow": true, - "isReceivingShadow": true, - "tileScale": 0.2 - } - }, { "assetStoreId": "", "name": "Obstacle", @@ -1622,6 +1561,59 @@ ] } } + }, + { + "assetStoreId": "", + "name": "Terrain", + "persistentUuid": "e43f3bea-760c-4b25-9854-328ce305e472", + "type": "Terrain3D::Terrain3D", + "variant": "", + "variables": [], + "effects": [], + "behaviors": [ + { + "name": "Physics3D", + "type": "Physics3D::Physics3DBehavior", + "object3D": "Object3D", + "bodyType": "Static", + "bullet": false, + "fixedRotation": false, + "shape": "Box", + "meshShapeResourceName": "", + "shapeOrientation": "Z", + "shapeDimensionA": 0, + "shapeDimensionB": 0, + "shapeDimensionC": 0, + "shapeOffsetX": 0, + "shapeOffsetY": 0, + "shapeOffsetZ": 0, + "massCenterOffsetX": 0, + "massCenterOffsetY": 0, + "massCenterOffsetZ": 0, + "massOverride": 0, + "density": 1, + "friction": 0.3, + "restitution": 0.1, + "linearDamping": 0.1, + "angularDamping": 0.1, + "gravityScale": 1, + "layers": 17, + "masks": 17 + } + ], + "content": { + "SculptData": "2;256;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AQABAQIBAgIAAQEAAQEAAQIBAAICAQEEAQEAVQIAAQIAAQIBAgIDAgIAAQIAARAAAQEAAQECAQABAQIAAQECAQEFBAACAgABAQADAQABAQACAQAFAgAEAQIAAQICAgEDAgIAAQECBgABAwABCAMAAQMCBAMGAwACAQEMAQMHAAICAAEBGhkEAwIABAECAQYDAQAEAQIABAEGAwABAgMCAAECAwIAAgEAAgQFAgQAAQEDAgAEAgMEAAEDAgIAAQUCAggHAAIBAgEAAgQBAQEAVwICAAEBAgIAAgQFAgIAAQ4AAQEAAwEAAQQDAAIFAQICAAECAAEBAQQDAQEAFgIEAQADAgEGAgcCCAEBAAEBBgECAAECBQACDAEJAAEDAgACAR4ZAgUCAgEBAAEBAgADAgECAQQAAQUEAQAEAQABBAMCAAEBAAMBBAEBBgMAAgIAAQMEAQEEAwIBAgABAgMAAQQDAgAFAwIAAgEAXAQAAwEGAwQBAAEEDgECAQABAwQCBQQHAQIGAwIBAQADAwAdAgIAAgEGBgcAAQYBAAEBAAECAgABAQABBAUAAQoDBQEAAwIAAQEEAQABAwIAAQIBAAMBAgACAgMAAQIBAgABAQEEAAQBAgMEAQIAAgEAAwECAgABAQACAQABBAMAAQIAAQMCAgEABgIAAQEBAF8EAgACAQABAgACAgwAAgECAgMAAQEFAgIAAwEAAgEBACICAgABAgQFAgYFBAEAAgIAAQIDAAIEAgMBAwABAgACHBsBAgACAgMEAAIDAAECAAEBAgACAgMAAQICAQACAwIBBgMEAwQFAgACAQABAgEEAQABAQABBgMBAgIDAAECAAEBAgECAAEBAAMBAGICAAIEAgEBAgACDgABBQQBAQQHAQQAAQIAAQMCAQEBACgECAUAAQYDAAEDCAMAAQQDCAUDDAEFAAEDAQIBAhgVAwQBAgEBAAICAAEBAgEAAQIBAAICAQIBAAICAwIBBAMAAQIAAgECAAECAgEDAgEEAwABAgEBBAEABwQFAGcCAgECAAECAAEBAgoBBAABAwIFAAECAAIBAAIDAC0GAQABCAUGBwQCAQYJBAEAAQwCDRAFBQUCBAEBBAwCBAkJAQQCBgcBAgMCAw4PAgECAQICAAIDBgABAQMCAQABAgABAQABAgECAgMCAAEBAgEAAQQDBgMCBQIBAGkCAAECAQQBAAEEAQgEAQABAQMAAQIMDQIAAQMBAC8EAgICAgYJBBQbCAABDg8IFAEkPygBKzoHKwMOHQEEJBcGAw02AysIEhECAgMCEBIjHAgFHx4GHxgBFRgbHBUOERYVEA0KDxQDDQwJCA0KAQMAAQQFAGkEAQIAAgIAAQIMAwIFAQQAAgUIAwMAMgIBBgUIAwABBQoUGQUKBgkDAgQWChkAARliRwUAAQoFEBEFEAsRDhYGIQwLNCMFEgcOCwcGHCMBAAEGAgMAAQIFAQACAQIBBAIKDwIKCwACBAMAAQQAAQMAawIAAQIBAAMIAgUBBAABAQABAwA3AgYHEgABBxQRHgMIDwABFDE+OQNAOzw3QhMaDQgpKAMREhsHDgsOCAUrJgQjCxIRAAICAQABIB8AARoZFhUWERATEA8MCwIBAAEEAwgHAgEAAQIBAGwEAQECAgECCgUBBAQFAgEDAPIBBAACAQIAAQQBBAIFAgABAQEA8gECAgIAAQEAAQIEAgMAAgMA9QECAAICAAEGAAEDAgABAwD2AQIAAQECAgYBAAIDAPgBBAEAAgICAgUA+gEEAAEDBAEBAPoBAgACAgQAAQMA+wECAAICAQD8AQIBAgIBAPwBAgIBAP8BAgCBAgIA/wECAQD/AQIAgQICAIACAgCBBAIAjBoBAIIGAQCABAECAP8BAgCCBAEA/wEBAgD/AQIDAIACAQD/AQMEAP8BAQD/AQEAggICAP0BAQEEAQCAAgEA/gECAQUA/gEBAwgA/AEBAAEDBgD9AQIDAgEBAPwBAQICAwD9AQMEAwYA/AEBBAMCAAECAPsBAgMCAgABBwD7AQECAgIHBgD7AQICAwUEAQD8AQEDBgEBAP0BBAkEBAD7AQEAAQEIAwD8AQMAAgICAgD6AQUEAAEEAwABAQD6AQIBAAEDAgEBAPoBAgIDAAEDAAEEAPoBAwABBAABAQYDAPoBAgMDAggBAwD6AQMAAQYCBwEGAPoBAgQDBwYBAPsBBAUCAgECAwD6AQECAQYCBQEA+gEDBAQDBwgGAPoBBAEFAAEIAQD8AQMEAgEBAPsBAwYAAQEAAgMA/gEBAAEEAPoBBAEDAAECBAEA+gEDAAEEAgIFAQD9AQEBAgD+AQEBAgQDAgIA+AEBAwIEAAIHAgYA9wEBAAEEAAMCAAEBAAECAPYBAQQAAgIBAQECAwQA9gEEAAEBAgEAAgECAQMGAPUBAgECAAEBBAMAAQECAAEDBAD0AQIAAQIDBAMEAwIAAQIBAAECAPQBAgECAQIAAQQDAQIAAQEAAQIA8gECAAIBAgEAAQIBAAEBAgD4AQIDAgEAAQECAwYBAPQBAgEBAgAEAwQAAQMEAAEBAgDzAQIAAQEAAwQDAAEBAgD2AQIDBAABAgABAgEAAQIDAgD0AQEAAQIAAQICAgABAQIBAPcBAQICAQEA+wECAAIDAgIBAgIAAQECAQD1AQIAAQIBAgACAgEEAQEA9AEEBQICAgMAAQIBAgABAQIAAQEA8gEBBgABAQECAAECAAIBAAEBAgDzAQICAwABAgEAAQMAAQYBAAECAQD1AQICAQECAAIFBAIBAPYBAgMCAgIAAwEBAgD3AQIAAgIBAgABAQIBAPYBAgADBAEAAQEA+wECAAICAQABAgEA+AECAQQCAAEBAgEA+QECAAEBAAICAQD7AQIDAgABAQD9AQIAgAIBAIACAgCAAgIAgAICAP8BAgMAgAICAIEEAgCDCAIA5hoDAAICAQABAgIA+QEBAAMCAgEBAAECAAEDBAD0AQQAAgMBAwACAgABAgIBAAICAPUBAgABAQABAwEBAgABBgABAQIA8AEBAgECAQQCAQACAQACBwABAgQAAQIBAgDtAQMCAgIFAAECBAMCAAECAgEDAwABBAQBAOwBAwABBAUEBgEAAQEEAAEBAQABAgABAwABBQABCADrAQcCBAEEAQUEAAICAQYDAgECAgABAQMFAgYCAOgBAwIBAQABAQYCAAIBAgMEAwQCAQACAgMBAwIGAOgBAgEAAwEBAAEBAgQAAQMCAAEBBAEEAAECAQMDAQYCAOYBBAACAQACAQABAgEFAAEEAAEBAAICAAECAAECAAEBAQMGAAEBAgDjAQECAAEEAQAFAgEBAAQBAQABAgABAgIDAQMCBgECAOYBAgABAQAEAgEABAIBAQACAgABAgEDAgMGAQIA4QEBAAECAwICAAYCAQACAQICAQAFAgEBAgcIAQIA4gEBBAEAAQIABAECAgABAQABAQABBAMABAICAwEGCQgBAOABAgACAwADAgABAgECAQACAgEAAgECAAMCAQADAwQHCADfAQIAAQEAAQIBAAIDAgQEAQIAAgICAAECAAMBAAEBAAICAAIBAAEHAN8BCgABAgACAgEAAQIBBQECAQACAQACAQDinQEBAIACAQD/AQUA/wEDAAEBAP0BAQMAAQMA/AEBAwACAgECAPkBAwABAwQAAgECAPkBAwECAQABAQMEAPcBAQMCAQIBAwQDAQYA9QEBAQEAAQIDBAEBAwABBAD1AQUAAgIDBAMBAAEDAAECAgDyAQEFBAACAQABAwMEBQUEAQQA8QEDAQQAAQEBAAEHAgADAwEBAgYA7wEDAgABAwEDAAIEAQIBAAEJBAICAgDtAQEBAAEBAAEBAQIAAQUGAQEAAQIFAAQGAOwBAQMBAAEBAQQBAgIHAgIBAwcCAAEDBgICAOgBAQEDAAEBAgABAQYDAgcCBAEBAQEJAAEEAgMCBAIA5gEBAwEAAgIAAQEEAwEDBgEAAgUAAQoA6aECBAACAQIAAQEBAPkBAgADAQECAgABAQABBAMA9AEDAAIEAgQAAgEAAQEBAgACAQD1AQEAAQIAAQQCAgEAAQUAAQIBAPABAgECAQIDAQIAAgIAAggAAQEDAAEBAgEA7QEEAQEBBgABAQMEAQABAQECBAQAAQMDAgDsAQQAAQMGAwUCAAECAwABAgIAAQEAAQQAAQYAAQcA6wEIAQMCAwIGAwACAQIFBAECAQEAAQIEBgEFAQDoAQQBAgIAAQIFAQACAgEEAwQDAQIAAgEEAgQBBQDoAQECAAMCAgABAgEDAAEEAQABAgMCAwABAQIEBAIFAQDmAQMAAgIAAgIAAQECBgABAwABAgACAQABAQABAQABAgIEBQABAgEA4wECAQABAwIABQECAgAEAgIAAQEAAQEBBAIEAQUCAQDmAQEAAQIABAECAAQBAgIAAgEAAQECBAEEBQIBAOEBAgABAQQBAQAGAQIAAgIBAQIABQECAgEIBwIBAOIBAgMCAAEBAAQCAQEAAQIAAQIAAQMEAAQBAQQCBQoHAgDgAQEAAgQAAwEAAQECAQIAAgECAAICAQADAQIAAwQDCAcA3wEBAAECAAEBAgACBAEDAwIBAAIBAQABAQADAgABAgACAQACAgABCADfAQkAAQEAAgECAAEBAgYCAQIAAgIAAgIA9L8BqgERBw8JCQkHCQcJAwUFBQUDBQACAQEBAQEBAQABAQABAQEABAIAAQIAAgIBAgABAgEAAQQEAQIGBAYCAgoECAIGAgoKBAgGAgIODgABBAMDAQEBIAUCAAIHAQcCAQUBBQABAQUBAAIDAAEDAwEDAQEAAQMDAgMAAQMAAQEBAwIFAgUCAwMCAwABBQACAwABAQABBQABAQMBAAEBAwABAgEHAQACAwEBAgABAwIDAQBVAgIEBAYGCggICggKDhATBgUGAQIAAwICAQABAgACBAQBAQACAQADAgECAAICAAYBAAIDBAECAAEEAAEDAwQAAQUAAgQCCQYBBAUEAQUGAQABBAABCwABDAIBAgABAgIlIAECAAECAQQDAgEBBgIDAQQAAQEAAQEEAAEBAAECAAQBAgECAgMCAgMCAQIBAgIDBAACAwIEBQIAAQIBAgABAgECAQEAAQIAAQIAAQECAAIBAgIDAgIAVQEAAQEAAQECAQEEAQEAAQEAAQ8AAQIAAQIBAgABAgEAAQIBAgIGAwACAQABAgADAgABAgACAgAFAQAEAgEAAQEBAQIEAQEAAQIBBQABBAABBwQAAQQBAwQFBAACAgILAgQIAAIBAAECGRoDBAEABAIBAgUEAgAEAgEABAIFBAABAQQBAAEBBAEAAgIAAgMGAQMAAQIEAQAEAQQDAAEEAQEAAQYBAQcIAAICAQIAAgMCAgIAVwEBAAECAQEAAgMGAQEAAQ0AAQIAAwIAAQMEAAIGAgEBAAEBAAECAgMEAgIAFgEDAgADAQIFAQgBBwICAAECBQIBAAEBBgACCwIKAAEEAQACAh0aAQYBAQICAAECAQADAQIBAgMAAQYDAgAEAgABAwQBAAECAAMCAwICBQQAAgEAAQQDAgIDBAECAQABAQQAAQMEAQAFBAEAAgIAXAMAAwIFBAMCAAEDDQIBAgABBAMBBgMIAgEFBAECAgADBAAdAQEAAgIFBQgAAQUCAAECAAEBAQABAgABAwYAAQkEBgIAAwEAAQIDAgABBAEAAQECAAMCAQACAQQAAQECAQABAgIDAAQCAQQDAgEAAgIAAwIBAQABAgACAgABAwQAAQEAAQQBAQIABgEAAQICAF8DAQACAgABAQACAQsAAgIBAQQAAQIGAQEAAwIAAgICACIBAQABAQMGAQUGAwIAAgEAAQEEAAIDAQQCBAABAQACGxwCAQACAQQDAAIEAAEBAAECAQACAQQAAQEBAgACBAECBQQDBAMGAQACAgABAQIDAgABAgABBQQCAQEEAAEBAAECAQIBAAECAAMCAGIBAAIDAQICAQACDQABBgMCAgMIAgMAAQEAAQQBAgICACgDBwYAAQUEAAEEBwQAAQMEBwYECwIGAAEEAgECARcWBAMCAQICAAIBAAECAQIAAQECAAIBAgECAAIBBAECAwQAAQEAAgIBAAEBAQIEAQIDBAABAQICAwIABwMGAGcBAQIBAAEBAAECAQkCAwABBAEGAAEBAAICAAIEAC0FAgABBwYFCAMBAgUKAwIAAQsBDg8GBgYBAwICAwsBAwoKAgMBBQgCAQQBBA0QAQIBAgEBAAIEBQABAgQBAgABAQABAgABAQIBAQQBAAECAQIAAQMEBQQBBgECAGkBAAEBAgMCAAEDAgcDAgABAgQAAQELDgEAAQQCAC8DAQEBAQUKAxMcBwABDRAHEwIjQCcCLDkILAQNHgIDIxgFBA41BCwHERIBAQQBDxEkGwcGIB0FIBcCFhccGxYNEhUWDw4JEBMEDgsKBw4JAgQAAQMGAGkDAgEAAgEAAQELBAEGAgMAAgYHBAQAMgECBQYHBAABBgkTGgYJBQoEAQMVCRoAARphSAYAAQkGDxIGDwwSDRUFIgsMMyQGEQgNDAgFGyQCAAEFAQQAAQEGAgACAgECAwEJEAEJDAACAwQAAQMAAQQAawEAAQECAAMHAQYCAwABAgABBAA3AQUIEQABCBMSHQQHEAABEzI9OgQ/PDs4QRQZDgcqJwQSERwIDQwNBwYsJQMkDBESAAIBAgABHyAAARkaFRYVEg8UDxALDAECAAEDBAcIAQIAAQECAGwDAgIBAQIBCQYCAwMGAQIEAPIBAwACAgEAAQMCAwEGAQABAgIA8gEBAQEAAQIAAQEDAQQAAgQA9QEBAAIBAAEFAAEEAQABBAD2AQEAAQIBAQUCAAIEAPgBAwIAAgEBAQYA+gEDAAEEAwICAPoBAQACAQMAAQQA+wEBAAIBAgD8AQECAQECAPwBAQECAP8BAQCBAgEA/wEBAgD/AQEAgQIBAIACAQCBBAEAjBoCAIIGAgCABAIBAP8BAQCCBAIA/wECAQD/AQEEAIACAgD/AQQDAP8BAgD/AQIAggIBAP0BAgIDAgCAAgIA/gEBAgYA/gECBAcA/AECAAEEBQD9AQEEAQICAPwBAgEBBAD9AQQDBAUA/AECAwQBAAEBAPsBAQQBAQABCAD7AQIBAQEIBQD7AQEBBAYDAgD8AQIEBQICAP0BAwoDAwD7AQIAAQIHBAD8AQQAAgEBAQD6AQYDAAEDBAABAgD6AQECAAEEAQICAPoBAQEEAAEEAAEDAPoBBAABAwABAgUEAPoBAQQEAQcCBAD6AQQAAQUBCAIFAPoBAQMECAUCAPsBAwYBAQIBBAD6AQIBAgUBBgIA+gEEAwMECAcFAPoBAwIGAAEHAgD8AQQDAQICAPsBBAUAAQIAAgQA/gECAAEDAPoBAwIEAAEBAwIA+gEEAAEDAQEGAgD9AQICAQD+AQICAQMEAQEA+AECBAEDAAIIAQUA9wECAAEDAAMBAAECAAEBAPYBAgMAAgECAgIBBAMA9gEDAAECAQIAAgIBAgQFAPUBAQIBAAECAwQAAQIBAAEEAwD0AQEAAQEEAwQDBAEAAQECAAEBAPQBAQIBAgEAAQMEAgEAAQIAAQEA8gEBAAICAQIAAQECAAECAQD4AQEEAQIAAQIBBAUCAPQBAQICAQAEBAMAAQQDAAECAQDzAQEAAQIAAwMEAAECAQD2AQEEAwABAQABAQIAAQEEAQD0AQIAAQEAAQEBAQABAgECAPcBAgEBAgIA+wEBAAIEAQECAQEAAQIBAgD1AQEAAQECAQACAQIDAgIA9AEDBgEBAQQAAQECAQABAgEAAQIA8gECBQABAgIBAAEBAAICAAECAQDzAQEBBAABAQIAAQQAAQUCAAEBAgD1AQEBAgIBAAIGAwECAPYBAQQBAQEAAwICAQD3AQEAAgECAQABAgECAPYBAQADAwIAAQIA+wEBAAIBAgABAQIA+AEBAgMBAAECAQIA+QEBAAECAAIBAgD7AQEEAQABAgD9AQEAgAICAIACAQCAAgEAgAIBAP8BAQQAgAIBAIEEAQCDCAEA7NoBAgCAAgIA/wEGAP8BBAABAgD9AQIEAAEEAPwBAgQAAgECAQD5AQQAAQQDAAICAQD5AQQCAQIAAQIEAwD3AQIEAQIBAgQDBAIFAPUBAgICAAEBBAMCAgQAAQMA9QEGAAIBBAMEAgABBAABAQEA8gECBgMAAgIAAQQEAwYGAwIDAPEBBAIDAAECAgABCAEAAwQCAgEFAO8BBAEAAQQCBAACAwIBAgABCgMBAQEA7QECAgABAgABAgIBAAEGBQICAAEBBgAEBQDsAQIEAgABAgIDAgEBCAEBAgQIAQABBAUBAQDoAQICBAABAgEAAQIFBAEIAQMCAgICCgABAwEEAQMBAOYBAgQCAAIBAAECAwQCBAUCAAIGAAEJAIGEBACBhAQAgYQEAIGEBACBhAQ=", + "Layer1Texture": "assets/StartingGround.png", + "Layer1Color": "255;255;255", + "HeightBlending": 0.5, + "TextureVariation": 0, + "TextureSize": 128, + "CliffLayer": "3", + "Layer2Texture": "", + "Layer3Texture": "", + "CliffAngle": 45, + "CastShadow": false + } } ], "objectsFolderStructure": { @@ -1646,7 +1638,7 @@ "objectName": "Obstacle" }, { - "objectName": "Ground" + "objectName": "Terrain" }, { "objectName": "ControlsToggle" @@ -7793,7 +7785,6 @@ "name": "PlatformerMultitouchMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -8047,7 +8038,6 @@ "name": "Platformer3DMultitouchMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -8222,7 +8212,6 @@ "name": "Shooter3DMultitouchMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -8386,7 +8375,6 @@ "name": "FirstPersonMultitouchMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -9591,7 +9579,6 @@ "name": "PhysicsCar3DMultitouchMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -9804,7 +9791,6 @@ "name": "TopDownMultitouchMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -39820,6 +39806,7802 @@ }, "eventsBasedBehaviors": [], "eventsBasedObjects": [] + }, + { + "author": "", + "category": "General", + "dimension": "3D", + "extensionNamespace": "", + "fullName": "3D terrain", + "gdevelopVersion": ">=5.5.222", + "helpPath": "", + "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLXRlcnJhaW4iIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTQsNkwxMC4yNSwxMUwxMy4xLDE0LjhMMTEuNSwxNkM5LjgxLDEzLjc1IDcsMTAgNywxMEwxLDE4SDIzTDE0LDZaIiAvPjwvc3ZnPg==", + "name": "Terrain3D", + "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", + "shortDescription": "A ground with hills and valleys that can be sculpted and painted in the editor, with collisions.", + "version": "0.1.0", + "description": [ + "A 3D terrain object for open worlds, adventure or racing games: hills, mountains, islands or valleys, with painted layers of grass, dirt, rock or sand.", + "", + "- **Sculpt and paint in the scene editor**: select a terrain to show the tools (raise, lower, smooth, flatten and paint 4 layers).", + "- **Start from a relief** (hills, mountains, island) or **from a heightmap image**.", + "- **Steep slopes automatically show rock** (or any layer).", + "- **Natural borders between layers**: stones and tufts of a texture cover the gaps of the other where layers meet (height blending), and the repetition of textures is made less visible (texture variation).", + "- **Physics**: add the **3D physics** behavior to the terrain. Characters and objects collide with the ground exactly. The terrain is always static.", + "- **Pathfinding**: add the **floor/obstacle for pathfinding (navmesh based)** behavior to the terrain, so that characters with the navmesh pathfinding behavior walk on its hills and around its cliffs (in recent GDevelop versions).", + "- **Shape it with the Edits property**: a list of raise, flatten, smooth and paint strokes written in JSON, shown in the scene editor. It's readable and can be changed by hand or by an AI agent, like the relief, the heightmap and the other properties. Actions only change the terrain during the game.", + "- **Change it during the game** with actions (raise, lower, flatten, smooth, paint, along a line for paths and rivers) and read the ground height, slope or layer with expressions.", + "- **Grass**: add a **terrain grass** object over the terrain. Thousands of blades sway in the wind, grow where a layer (like grass) is painted and where the ground is not too steep, and are pushed aside by characters. Sculpt or paint the terrain and the grass follows.", + "- **Forests, rocks and bushes**: add a **scattered models** object over the terrain. It places hundreds or thousands of copies of a 3D model (trees, rocks, bushes, flowers...) on the ground and draws them all at once, which is much faster than an object for each. Copies grow where a layer is painted, away from steep slopes, and follow the ground when it's sculpted. Add or erase them with the brushes of the scene editor or with the Edits property, give trees trunk collisions, and cut them down with actions. Ready-made forests, rocks and bushes are in the asset store.", + "- **Tunnels and caves**: make holes in the ground (and fill them again) with the brushes, the Edits property or actions, and put a 3D model of the tunnel or the cave (with a mesh collision shape) under the ground. Nothing is drawn in holes, and characters, cars and rays go through them.", + "- **Made for large worlds**: the terrain is split in chunks that are hidden when off-screen and simplified when far away, and collisions are updated only where the ground changes." + ], + "origin": { + "identifier": "Terrain3D", + "name": "gdevelop-extension-store" + }, + "tags": [ + "3d", + "terrain", + "ground", + "heightmap", + "landscape", + "open world", + "grass", + "foliage", + "forest", + "trees", + "tunnel", + "cave" + ], + "authorIds": [], + "dependencies": [], + "globalVariables": [], + "sceneVariables": [], + "eventsFunctions": [ + { + "description": "Define helper classes JavaScript code.", + "fullName": "Define helper classes", + "functionType": "Action", + "name": "DefineHelperClasses", + "private": true, + "sentence": "Define helper classes JavaScript code", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "if (gdjs.__terrain3DExtension) {", + " return;", + "}", + "", + "// Tested by the gameplay tests of the \"3D island terrain\" example (GDevelop-examples repository).", + "", + "const groundsByScene = new WeakMap();", + "/**", + " * The grounds of a scene (the terrains), followed by the terrain grass.", + " *", + " * A ground has: `getVersion()` (changing when the ground changes),", + " * `containsPoint(x, y)`, `getHeightAt(x, y)` (Z position of the ground),", + " * `getSlopeAt(x, y)` (in degrees), `getLayerWeightAt(x, y, layer)` (how", + " * much a painted layer, from 1 to 4, is there, from 0 to 1) and optionally", + " * `getTintAt(x, y)` (the painted tint, as red, green and blue from 0 to 255).", + " * @param {gdjs.RuntimeScene} runtimeScene", + " * @returns {Set}", + " */", + "const getGrounds = (runtimeScene) => {", + " let grounds = groundsByScene.get(runtimeScene);", + " if (!grounds) {", + " grounds = new Set();", + " groundsByScene.set(runtimeScene, grounds);", + " }", + " return grounds;", + "};", + "", + "/**", + " * @param {number} seed", + " * @returns {() => number} Random numbers between 0 (included) and 1 (excluded), the same for the same seed.", + " */", + "const createRandom = (seed) => {", + " let state = seed >>> 0 || 1;", + " return () => {", + " state ^= state << 13;", + " state ^= state >>> 17;", + " state ^= state << 5;", + " return (state >>> 0) / 4294967296;", + " };", + "};", + "", + "/**", + " * Find the highest ground at a position: grounds can overlap (like an island over a sea floor).", + " * @param {Iterable} grounds", + " * @param {{ground: object | null, z: number}} result Set to the ground (null if there is none) and its Z position.", + " */", + "const findHighestGround = (grounds, x, y, result) => {", + " result.ground = null;", + " for (const ground of grounds) {", + " if (!ground.containsPoint(x, y)) continue;", + " const z = ground.getHeightAt(x, y);", + " if (!result.ground || z > result.z) {", + " result.ground = ground;", + " result.z = z;", + " }", + " }", + " return result;", + "};", + "", + "// The grounds or the objects placed on them can change every frame (while", + "// sculpting or moving them in the editor): things are placed again at most this often.", + "const PLACEMENT_DELAY = 0.3;", + "", + "/**", + " * Tells when things placed on the grounds of a scene (grass blades, scattered", + " * models) must be placed again: after the grounds or the given properties", + " * changed, at most every `PLACEMENT_DELAY` seconds.", + " */", + "class GroundPlacement {", + " /** @param {gdjs.RuntimeScene} runtimeScene */", + " constructor(runtimeScene) {", + " this.grounds = getGrounds(runtimeScene);", + " this.placedFrom = '';", + " this.timeSincePlacement = 0;", + " }", + "", + " /**", + " * @param {number} elapsedSeconds", + " * @param {string} propertiesKey Changes when the things must be placed again.", + " */", + " shouldPlaceAgain(elapsedSeconds, propertiesKey) {", + " this.timeSincePlacement += elapsedSeconds;", + " if (this.placedFrom && this.timeSincePlacement < PLACEMENT_DELAY) return false;", + " let placementKey = propertiesKey;", + " for (const ground of this.grounds) placementKey += '|' + ground.getVersion();", + " if (placementKey === this.placedFrom) return false;", + " this.placedFrom = placementKey;", + " this.timeSincePlacement = 0;", + " return true;", + " }", + "", + " placeAgain() {", + " this.placedFrom = '';", + " }", + "}", + "", + "/**", + " * Replace parts of the shader code of a Three.js material, with an error if", + " * a part is missing (for example after an update of Three.js).", + " * @param {string} shaderCode", + " * @param {Array<[string, string]>} replacements The searched code and its replacement.", + " * @param {string} materialName", + " */", + "const patchShaderCode = (shaderCode, replacements, materialName) => {", + " for (const [searchedCode, newCode] of replacements) {", + " if (!shaderCode.includes(searchedCode)) {", + " console.error(`${materialName}: \"${searchedCode}\" was not found in the shader, which won't look as expected.`);", + " }", + " shaderCode = shaderCode.replace(searchedCode, newCode);", + " }", + " return shaderCode;", + "};", + "", + "// Size of the object inner area. Instances are scaled from it, so these only", + "// define the default size of a new terrain.", + "const AREA_SIZE = 4096;", + "const AREA_DEPTH = 600;", + "", + "// Terrains have at most 8 x 8 chunks (so at most 64 draw calls), of at least", + "// 32 x 32 cells.", + "const MIN_CHUNK_CELLS = 32;", + "const MAX_CHUNKS_PER_SIDE = 8;", + "/** @param {number} resolution */", + "const getChunkCells = (resolution) => Math.max(MIN_CHUNK_CELLS, resolution / MAX_CHUNKS_PER_SIDE);", + "// Grid steps of each level of detail, and the distance (in chunk sizes)", + "// from which each one is used.", + "const LOD_STEPS = [1, 2, 4];", + "const LOD_DISTANCES = [0, 3, 6];", + "", + "const LAYER_COUNT = 4;", + "const RESOLUTIONS = [64, 128, 256, 512, 1024];", + "const DEFAULT_RESOLUTION = 256;", + "// Each undo step is a copy of the data: fewer are kept for big terrains.", + "const UNDO_STEPS_SAMPLES = 20 * 257 * 257;", + "", + "const clamp01 = (value) => (value < 0 ? 0 : value > 1 ? 1 : value);", + "const clampInteger = (value, min, max) =>", + " value < min ? min : value > max ? max : Math.floor(value);", + "", + "/**", + " * A rectangle of grid samples, inclusive. Empty when `minI > maxI`.", + " */", + "class SampleRectangle {", + " constructor() {", + " this.clear();", + " }", + "", + " clear() {", + " this.minI = Number.MAX_SAFE_INTEGER;", + " this.minJ = Number.MAX_SAFE_INTEGER;", + " this.maxI = -1;", + " this.maxJ = -1;", + " }", + "", + " isEmpty() {", + " return this.minI > this.maxI;", + " }", + "", + " /** @param {SampleRectangle} other */", + " add(other) {", + " if (other.isEmpty()) return;", + " this.minI = Math.min(this.minI, other.minI);", + " this.minJ = Math.min(this.minJ, other.minJ);", + " this.maxI = Math.max(this.maxI, other.maxI);", + " this.maxJ = Math.max(this.maxJ, other.maxJ);", + " }", + "", + " set(minI, minJ, maxI, maxJ) {", + " this.minI = minI;", + " this.minJ = minJ;", + " this.maxI = maxI;", + " this.maxJ = maxJ;", + " return this;", + " }", + "}", + "", + "/**", + " * The heights (between 0 and 1) and the painted layers weights of a square", + " * grid of `size * size` samples.", + " */", + "class TerrainData {", + " /** @param {number} resolution The number of cells on each side. */", + " constructor(resolution) {", + " this.resolution = resolution;", + " this.size = resolution + 1;", + " this.heights = new Float32Array(this.size * this.size);", + " /** 4 weights (0-255) per sample, one for each layer. */", + " this.splat = new Uint8Array(this.size * this.size * LAYER_COUNT);", + " this.fillLayer(0);", + " /**", + " * 4 bytes per sample: a tint multiplying the layers (red, green and", + " * blue, white for none) and the wetness (0 for dry).", + " */", + " this.colors = new Uint8Array(this.size * this.size * 4);", + " this.clearColors();", + " }", + "", + " clearColors() {", + " this.colors.fill(255);", + " for (let index = 3; index < this.colors.length; index += 4) {", + " this.colors[index] = 0;", + " }", + " }", + "", + " /** @param {number} layer */", + " fillLayer(layer) {", + " this.splat.fill(0);", + " for (let index = layer; index < this.splat.length; index += LAYER_COUNT) {", + " this.splat[index] = 255;", + " }", + " }", + "", + " getHeight(i, j) {", + " const last = this.resolution;", + " const clampedI = i < 0 ? 0 : i > last ? last : i;", + " const clampedJ = j < 0 ? 0 : j > last ? last : j;", + " return this.heights[clampedJ * this.size + clampedI];", + " }", + "", + " /**", + " * @param {number} gridX", + " * @param {number} gridY", + " * @returns {number} The height interpolated on the triangles that are drawn.", + " */", + " sampleHeight(gridX, gridY) {", + " const x = Math.min(Math.max(gridX, 0), this.resolution);", + " const y = Math.min(Math.max(gridY, 0), this.resolution);", + " const i = Math.min(Math.floor(x), this.resolution - 1);", + " const j = Math.min(Math.floor(y), this.resolution - 1);", + " const fx = x - i;", + " const fy = y - j;", + " const topRight = this.getHeight(i + 1, j);", + " const bottomLeft = this.getHeight(i, j + 1);", + " if (fx + fy <= 1) {", + " const topLeft = this.getHeight(i, j);", + " return topLeft + fx * (topRight - topLeft) + fy * (bottomLeft - topLeft);", + " }", + " const bottomRight = this.getHeight(i + 1, j + 1);", + " return (", + " bottomRight +", + " (1 - fx) * (bottomLeft - bottomRight) +", + " (1 - fy) * (topRight - bottomRight)", + " );", + " }", + "", + " /** @returns {number} The weight (0-1) of a layer, interpolated between samples. */", + " sampleLayerWeight(gridX, gridY, layer) {", + " const x = Math.min(Math.max(gridX, 0), this.resolution);", + " const y = Math.min(Math.max(gridY, 0), this.resolution);", + " const i = Math.min(Math.floor(x), this.resolution - 1);", + " const j = Math.min(Math.floor(y), this.resolution - 1);", + " const fx = x - i;", + " const fy = y - j;", + " const weight = (i, j) => this.splat[(j * this.size + i) * LAYER_COUNT + layer];", + " const top = weight(i, j) + fx * (weight(i + 1, j) - weight(i, j));", + " const bottom = weight(i, j + 1) + fx * (weight(i + 1, j + 1) - weight(i, j + 1));", + " return (top + fy * (bottom - top)) / 255;", + " }", + "", + " /**", + " * @param {number} channel 0, 1 and 2 for the tint, 3 for the wetness.", + " * @returns {number} The channel (0-1), interpolated between samples.", + " */", + " sampleColor(gridX, gridY, channel) {", + " const x = Math.min(Math.max(gridX, 0), this.resolution);", + " const y = Math.min(Math.max(gridY, 0), this.resolution);", + " const i = Math.min(Math.floor(x), this.resolution - 1);", + " const j = Math.min(Math.floor(y), this.resolution - 1);", + " const fx = x - i;", + " const fy = y - j;", + " const value = (i, j) => this.colors[(j * this.size + i) * 4 + channel];", + " const top = value(i, j) + fx * (value(i + 1, j) - value(i, j));", + " const bottom = value(i, j + 1) + fx * (value(i + 1, j + 1) - value(i, j + 1));", + " return (top + fy * (bottom - top)) / 255;", + " }", + "", + " /**", + " * @param {number} index The index of a sample.", + " * @returns {boolean} true if no layer is painted on the sample: it's a hole.", + " */", + " isHole(index) {", + " const offset = index * LAYER_COUNT;", + " return (this.splat[offset] | this.splat[offset + 1] | this.splat[offset + 2] | this.splat[offset + 3]) === 0;", + " }", + "", + " /**", + " * @returns {number} The sum of the layer weights (0-1), interpolated between samples: there is a", + " * hole where it's under 0.5, like what is drawn.", + " */", + " sampleSolidity(gridX, gridY) {", + " let solidity = 0;", + " for (let layer = 0; layer < LAYER_COUNT; layer++) solidity += this.sampleLayerWeight(gridX, gridY, layer);", + " return solidity;", + " }", + "", + " /** @returns {number} The index (from 0) of the layer painted the most. */", + " getDominantLayer(gridX, gridY) {", + " const i = clampInteger(Math.round(gridX), 0, this.resolution);", + " const j = clampInteger(Math.round(gridY), 0, this.resolution);", + " const offset = (j * this.size + i) * LAYER_COUNT;", + " let dominantLayer = 0;", + " for (let layer = 1; layer < LAYER_COUNT; layer++) {", + " if (this.splat[offset + layer] > this.splat[offset + dominantLayer]) {", + " dominantLayer = layer;", + " }", + " }", + " return dominantLayer;", + " }", + "", + " /** @param {TerrainData} other */", + " copyFrom(other) {", + " this.heights.set(other.heights);", + " this.splat.set(other.splat);", + " this.colors.set(other.colors);", + " }", + "", + " /**", + " * @param {number} resolution", + " * @returns {TerrainData} A copy of this data with another resolution.", + " */", + " resampled(resolution) {", + " const data = new TerrainData(resolution);", + " const ratio = this.resolution / resolution;", + " for (let j = 0; j < data.size; j++) {", + " for (let i = 0; i < data.size; i++) {", + " const index = j * data.size + i;", + " data.heights[index] = this.sampleHeight(i * ratio, j * ratio);", + " const sourceOffset =", + " (Math.round(j * ratio) * this.size + Math.round(i * ratio)) * LAYER_COUNT;", + " for (let layer = 0; layer < LAYER_COUNT; layer++) {", + " data.splat[index * LAYER_COUNT + layer] = this.splat[sourceOffset + layer];", + " }", + " for (let channel = 0; channel < 4; channel++) {", + " data.colors[index * 4 + channel] = this.colors[sourceOffset + channel];", + " }", + " }", + " }", + " return data;", + " }", + "}", + "", + "/**", + " * Brushes change the samples around a segment, with an effect going smoothly", + " * from full on the segment to none at `radius`. A point is a segment of", + " * length 0. Positions are in scene units relative to the terrain origin.", + " */", + "const brushes = {", + " /**", + " * @param {TerrainData} data", + " * @param {{cellWidth: number, cellHeight: number, ax: number, ay: number, bx: number, by: number, radius: number}} stroke", + " * @param {(sampleIndex: number, weight: number, t: number) => void} apply Called with the weight of", + " * the brush and the position (from 0 to 1) of the nearest point of the segment.", + " * @param {SampleRectangle} changedSamples", + " */", + " forEachSample(data, stroke, apply, changedSamples) {", + " const { cellWidth, cellHeight, ax, ay, bx, by, radius } = stroke;", + " const last = data.resolution;", + " const minI = clampInteger(Math.floor((Math.min(ax, bx) - radius) / cellWidth), 0, last);", + " const maxI = clampInteger(Math.ceil((Math.max(ax, bx) + radius) / cellWidth), 0, last);", + " const minJ = clampInteger(Math.floor((Math.min(ay, by) - radius) / cellHeight), 0, last);", + " const maxJ = clampInteger(Math.ceil((Math.max(ay, by) + radius) / cellHeight), 0, last);", + " changedSamples.set(minI, minJ, maxI, maxJ);", + " if (radius <= 0) return;", + "", + " const abX = bx - ax;", + " const abY = by - ay;", + " const abLengthSquared = abX * abX + abY * abY;", + " const radiusSquared = radius * radius;", + " for (let j = minJ; j <= maxJ; j++) {", + " const y = j * cellHeight;", + " for (let i = minI; i <= maxI; i++) {", + " const x = i * cellWidth;", + " const t =", + " abLengthSquared > 0", + " ? clamp01(((x - ax) * abX + (y - ay) * abY) / abLengthSquared)", + " : 0;", + " const dx = x - (ax + t * abX);", + " const dy = y - (ay + t * abY);", + " const distanceSquared = dx * dx + dy * dy;", + " if (distanceSquared >= radiusSquared) continue;", + " const falloff = 1 - distanceSquared / radiusSquared;", + " apply(j * data.size + i, falloff * falloff, t);", + " }", + " }", + " },", + "", + " raise(data, stroke, amount, changedSamples) {", + " const { heights } = data;", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index, weight) => {", + " heights[index] = clamp01(heights[index] + amount * weight);", + " },", + " changedSamples", + " );", + " },", + "", + " /**", + " * @param {number} targetHeight", + " * @param {number} [endTargetHeight] The target at the end of the segment, for a slope (like a road", + " * going up). The same target as at its start by default.", + " */", + " flatten(data, stroke, targetHeight, strength, changedSamples, endTargetHeight = targetHeight) {", + " const { heights } = data;", + " const startTarget = clamp01(targetHeight);", + " const endTarget = clamp01(endTargetHeight);", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index, weight, t) => {", + " const target = startTarget + (endTarget - startTarget) * t;", + " heights[index] += (target - heights[index]) * clamp01(weight * strength);", + " },", + " changedSamples", + " );", + " },", + "", + " smooth(data, stroke, strength, changedSamples) {", + " const { heights, size } = data;", + " const changes = [];", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index, weight) => {", + " const i = index % size;", + " const j = (index - i) / size;", + " let sum = 0;", + " for (let dj = -1; dj <= 1; dj++) {", + " for (let di = -1; di <= 1; di++) {", + " sum += data.getHeight(i + di, j + dj);", + " }", + " }", + " changes.push(index, heights[index] + (sum / 9 - heights[index]) * clamp01(weight * strength));", + " },", + " changedSamples", + " );", + " // Written after all averages are computed, so that the result", + " // doesn't depend on the order in which samples are visited.", + " for (let index = 0; index < changes.length; index += 2) {", + " heights[changes[index]] = changes[index + 1];", + " }", + " },", + "", + " paint(data, stroke, layer, strength, changedSamples) {", + " const { splat } = data;", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index, weight) => {", + " const amount = clamp01(weight * strength);", + " const offset = index * LAYER_COUNT;", + " for (let otherLayer = 0; otherLayer < LAYER_COUNT; otherLayer++) {", + " const target = otherLayer === layer ? 255 : 0;", + " const value = splat[offset + otherLayer];", + " // Rounded towards the target, so that light strokes still paint.", + " const change = (target - value) * amount;", + " splat[offset + otherLayer] = value + (change > 0 ? Math.ceil(change) : Math.floor(change));", + " }", + " },", + " changedSamples", + " );", + " },", + "", + " /** Remove all the layers, making a hole (`fill` or painting a layer fills it again). */", + " hole(data, stroke, changedSamples) {", + " const { splat } = data;", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index) => {", + " splat.fill(0, index * LAYER_COUNT, (index + 1) * LAYER_COUNT);", + " },", + " changedSamples", + " );", + " },", + "", + " /**", + " * Fill the holes in the radius with the layers of the ground around them:", + " * each pass fills the holes next to the ground with the average of their", + " * neighbors, from the edges of the holes inward.", + " */", + " fill(data, stroke, changedSamples) {", + " const { splat, size, resolution } = data;", + " /** @type {number[]} */", + " let holes = [];", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index) => {", + " if (data.isHole(index)) holes.push(index);", + " },", + " changedSamples", + " );", + " const filledWeights = new Float32Array(holes.length * LAYER_COUNT);", + " while (holes.length > 0) {", + " /** @type {number[]} */", + " const remainingHoles = [];", + " /** @type {number[]} */", + " const filledHoles = [];", + " for (const index of holes) {", + " const i = index % size;", + " const j = (index - i) / size;", + " const weights = filledWeights.subarray(filledHoles.length * LAYER_COUNT, (filledHoles.length + 1) * LAYER_COUNT);", + " weights.fill(0);", + " for (let neighborJ = Math.max(j - 1, 0); neighborJ <= Math.min(j + 1, resolution); neighborJ++) {", + " for (let neighborI = Math.max(i - 1, 0); neighborI <= Math.min(i + 1, resolution); neighborI++) {", + " const neighbor = neighborJ * size + neighborI;", + " if (data.isHole(neighbor)) continue;", + " for (let layer = 0; layer < LAYER_COUNT; layer++) weights[layer] += splat[neighbor * LAYER_COUNT + layer];", + " }", + " }", + " if (weights[0] + weights[1] + weights[2] + weights[3] > 0) filledHoles.push(index);", + " else remainingHoles.push(index);", + " }", + " if (filledHoles.length === 0) {", + " // No ground around (a hole bigger than the brush): the first layer is used.", + " for (const index of remainingHoles) splat[index * LAYER_COUNT] = 255;", + " break;", + " }", + " // Filled after the pass, so that the holes are filled from the edges inward.", + " filledHoles.forEach((index, filledIndex) => {", + " const weights = filledWeights.subarray(filledIndex * LAYER_COUNT, (filledIndex + 1) * LAYER_COUNT);", + " const total = weights[0] + weights[1] + weights[2] + weights[3];", + " for (let layer = 0; layer < LAYER_COUNT; layer++) {", + " splat[index * LAYER_COUNT + layer] = Math.round((weights[layer] / total) * 255);", + " }", + " });", + " holes = remainingHoles;", + " }", + " },", + "", + " /** @param {number[]} color The red, green and blue (0-255) of the tint: white removes it. */", + " tint(data, stroke, color, strength, changedSamples) {", + " brushes.paintColors(data, stroke, 0, color, strength, changedSamples);", + " },", + "", + " /** @param {number} wetness From 0 (dry) to 1. */", + " wet(data, stroke, wetness, strength, changedSamples) {", + " brushes.paintColors(data, stroke, 3, [Math.round(clamp01(wetness) * 255)], strength, changedSamples);", + " },", + "", + " /**", + " * Bring channels of the colors (see `TerrainData.colors`) towards targets.", + " * @param {number} firstChannel", + " * @param {number[]} targets From 0 to 255, for the channels from the first one.", + " */", + " paintColors(data, stroke, firstChannel, targets, strength, changedSamples) {", + " const { colors } = data;", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index, weight) => {", + " const amount = clamp01(weight * strength);", + " for (let channel = 0; channel < targets.length; channel++) {", + " const offset = index * 4 + firstChannel + channel;", + " // Rounded towards the target, so that light strokes still paint.", + " const change = (targets[channel] - colors[offset]) * amount;", + " colors[offset] += change > 0 ? Math.ceil(change) : Math.floor(change);", + " }", + " },", + " changedSamples", + " );", + " },", + "};", + "", + "const relief = (() => {", + " const hash = (x, y, seed) => {", + " let h = Math.imul(x, 374761393) + Math.imul(y, 668265263) + Math.imul(seed, 982451653);", + " h = Math.imul(h ^ (h >>> 13), 1274126177);", + " return ((h ^ (h >>> 16)) >>> 0) / 4294967296;", + " };", + " const valueNoise = (x, y, seed) => {", + " const x0 = Math.floor(x);", + " const y0 = Math.floor(y);", + " const fx = x - x0;", + " const fy = y - y0;", + " const sx = fx * fx * (3 - 2 * fx);", + " const sy = fy * fy * (3 - 2 * fy);", + " const top = hash(x0, y0, seed) + sx * (hash(x0 + 1, y0, seed) - hash(x0, y0, seed));", + " const bottom =", + " hash(x0, y0 + 1, seed) + sx * (hash(x0 + 1, y0 + 1, seed) - hash(x0, y0 + 1, seed));", + " return top + sy * (bottom - top);", + " };", + " /** Fractal noise between 0 and 1. */", + " const fractalNoise = (x, y, seed) => {", + " let value = 0;", + " let amplitude = 0.5;", + " let frequency = 1;", + " for (let octave = 0; octave < 5; octave++) {", + " value += amplitude * valueNoise(x * frequency, y * frequency, seed + octave);", + " amplitude *= 0.5;", + " frequency *= 2;", + " }", + " return value / 0.96875;", + " };", + " const smoothstep = (edge0, edge1, x) => {", + " const t = clamp01((x - edge0) / (edge1 - edge0));", + " return t * t * (3 - 2 * t);", + " };", + " const reliefHeights = {", + " Flat: () => 0,", + " Hills: (u, v, seed) => 0.05 + 0.5 * Math.pow(fractalNoise(u * 3, v * 3, seed), 2),", + " Mountains: (u, v, seed) => {", + " const ridge = 1 - Math.abs(2 * fractalNoise(u * 2.5, v * 2.5, seed) - 1);", + " return 0.9 * Math.pow(ridge, 2.5);", + " },", + " Island: (u, v, seed) => {", + " const distanceToCenter = 2 * Math.hypot(u - 0.5, v - 0.5);", + " const coast = 0.2 * (fractalNoise(u * 4, v * 4, seed + 7) - 0.5);", + " const land = smoothstep(0.95, 0.55, distanceToCenter + coast);", + " return land * (0.12 + 0.6 * Math.pow(fractalNoise(u * 3, v * 3, seed), 1.5));", + " },", + " };", + " const beachLayer = 3;", + "", + " return {", + " /**", + " * @param {TerrainData} data", + " * @param {string} reliefName Flat, Hills, Mountains or Island.", + " * @param {number} seed", + " */", + " generate(data, reliefName, seed) {", + " const getHeight = reliefHeights[reliefName] || reliefHeights.Flat;", + " const integerSeed = Math.floor(seed) | 0;", + " data.fillLayer(0);", + " for (let j = 0; j < data.size; j++) {", + " for (let i = 0; i < data.size; i++) {", + " const index = j * data.size + i;", + " const height = clamp01(", + " getHeight(i / data.resolution, j / data.resolution, integerSeed)", + " );", + " data.heights[index] = height;", + " if (reliefName === 'Island') {", + " const sand = Math.round(255 * smoothstep(0.16, 0.1, height));", + " const offset = index * LAYER_COUNT;", + " data.splat[offset] = 255 - sand;", + " data.splat[offset + beachLayer] = sand;", + " }", + " }", + " }", + " },", + " };", + "})();", + "", + "/**", + " * Edits written in the Edits property: a JSON list of brush strokes, like", + " * `[{\"tool\": \"raise\", \"x\": 0.5, \"y\": 0.5, \"radius\": 0.2, \"height\": 0.3}]`,", + " * applied on the relief or the heightmap. Positions and radius are fractions", + " * of the terrain size, heights and Z positions fractions of its depth: they", + " * are the same for all instances, whatever their size. They are made to be", + " * read and written by people and by AI agents.", + " */", + "const edits = {", + " /**", + " * @param {TerrainData} data", + " * @param {string} text", + " */", + " apply(data, text) {", + " if (!text.trim()) return;", + " let editList;", + " try {", + " editList = JSON.parse(text);", + " } catch (error) {", + " console.warn('The edits of a terrain are not valid JSON: ' + error.message);", + " return;", + " }", + " if (!Array.isArray(editList)) {", + " console.warn('The edits of a terrain must be a JSON list (like [{\"tool\": \"raise\", ...}]).');", + " return;", + " }", + " const numberOr = (value, defaultValue) => (typeof value === 'number' && isFinite(value) ? value : defaultValue);", + " const changedSamples = new SampleRectangle();", + " for (const edit of editList) {", + " if (!edit || typeof edit !== 'object') continue;", + " const x = numberOr(edit.x, 0.5);", + " const y = numberOr(edit.y, 0.5);", + " const stroke = {", + " cellWidth: 1 / data.resolution,", + " cellHeight: 1 / data.resolution,", + " ax: x,", + " ay: y,", + " bx: numberOr(edit.toX, x),", + " by: numberOr(edit.toY, y),", + " radius: numberOr(edit.radius, 0.1),", + " };", + " const strength = clamp01(numberOr(edit.strength, 1));", + " if (edit.tool === 'raise') {", + " brushes.raise(data, stroke, numberOr(edit.height, 0.1), changedSamples);", + " } else if (edit.tool === 'flatten') {", + " const z = numberOr(edit.z, 0);", + " brushes.flatten(data, stroke, z, strength, changedSamples, numberOr(edit.toZ, z));", + " } else if (edit.tool === 'smooth') {", + " // Smoothing the same fraction of the terrain at any resolution.", + " const passes = Math.max(Math.round(((1 + 9 * strength) * data.resolution) / 256), 1);", + " for (let pass = 0; pass < passes; pass++) brushes.smooth(data, stroke, 1, changedSamples);", + " } else if (edit.tool === 'paint') {", + " const layer = clampInteger(numberOr(edit.layer, 1) - 1, 0, LAYER_COUNT - 1);", + " brushes.paint(data, stroke, layer, strength, changedSamples);", + " } else if (edit.tool === 'tint') {", + " const color = gdjs.rgbOrHexToRGBColor(typeof edit.color === 'string' ? edit.color : '255;255;255');", + " brushes.tint(data, stroke, color, strength, changedSamples);", + " } else if (edit.tool === 'wetness') {", + " brushes.wet(data, stroke, numberOr(edit.amount, 1), strength, changedSamples);", + " } else if (edit.tool === 'hole') {", + " brushes.hole(data, stroke, changedSamples);", + " } else if (edit.tool === 'fill') {", + " brushes.fill(data, stroke, changedSamples);", + " } else {", + " console.warn('Unknown tool in the edits of a terrain: \"' + edit.tool + '\" (use raise, flatten, smooth, paint, tint, wetness, hole or fill).');", + " }", + " }", + " },", + "};", + "", + "/**", + " * @param {gdjs.RuntimeGame} game", + " * @param {string} imageResourceName", + " * @returns {CanvasImageSource | null} The image, or null if it's not loaded (yet).", + " */", + "const getLoadedImage = (game, imageResourceName) => {", + " const imageManager = game.getImageManager();", + " if (typeof imageManager.getImageSource === 'function') return imageManager.getImageSource(imageResourceName);", + " // Older GDevelop versions: a 192 x 192 placeholder is given for images that are not loaded.", + " const texture = imageManager.getPIXITexture(imageResourceName);", + " return texture && texture !== imageManager.getInvalidPIXITexture() ? texture.baseTexture.getDrawableSource() : null;", + "};", + "", + "/**", + " * Loads heights from an image: black is the lowest, white the highest.", + " * Heights have 16 bits of precision when the red channel holds the most", + " * significant byte and the green one the least significant (a gray image", + " * works as expected too).", + " * @param {TerrainData} data", + " * @param {gdjs.RuntimeGame} game", + " * @param {string} imageResourceName", + " * @returns {boolean} true if the image could be read.", + " */", + "const loadHeightsFromImage = (data, game, imageResourceName) => {", + " const source = getLoadedImage(game, imageResourceName);", + " if (!source) return false;", + "", + " const canvas = document.createElement('canvas');", + " canvas.width = source.width;", + " canvas.height = source.height;", + " const context = canvas.getContext('2d', { willReadFrequently: true });", + " if (!context) return false;", + " context.drawImage(source, 0, 0);", + " const pixels = context.getImageData(0, 0, source.width, source.height).data;", + " // Pixels are decoded before being interpolated, to keep the 16 bits.", + " const pixelHeight = (x, y) => {", + " const offset = (y * source.width + x) * 4;", + " return (256 * pixels[offset] + pixels[offset + 1]) / 65535;", + " };", + " for (let j = 0; j < data.size; j++) {", + " const y = (j / data.resolution) * (source.height - 1);", + " const y0 = Math.floor(y);", + " const y1 = Math.min(y0 + 1, source.height - 1);", + " for (let i = 0; i < data.size; i++) {", + " const x = (i / data.resolution) * (source.width - 1);", + " const x0 = Math.floor(x);", + " const x1 = Math.min(x0 + 1, source.width - 1);", + " const top = pixelHeight(x0, y0) + (x - x0) * (pixelHeight(x1, y0) - pixelHeight(x0, y0));", + " const bottom = pixelHeight(x0, y1) + (x - x0) * (pixelHeight(x1, y1) - pixelHeight(x0, y1));", + " data.heights[j * data.size + i] = top + (y - y0) * (bottom - top);", + " }", + " }", + " return true;", + "};", + "", + "/**", + " * Stores the data in a string. Only the differences with the base data (from", + " * the relief or the heightmap) are stored: heights on 16 bits, layer weights", + " * on 8 bits, then (since format 2) the tint and wetness on 8 bits. Each value is predicted from its neighbors and the", + " * prediction errors are written as variable length integers, with runs of", + " * zeros collapsed. Untouched or smoothly changed areas take almost no space.", + " */", + "const codec = (() => {", + " const formatVersion = '2';", + " // Format 1 has no tint and wetness.", + " const readableFormatVersions = ['1', '2'];", + "", + " class ByteWriter {", + " constructor() {", + " this.bytes = new Uint8Array(1024);", + " this.length = 0;", + " }", + " writeVarUint(value) {", + " if (this.length + 5 > this.bytes.length) {", + " const bytes = new Uint8Array(this.bytes.length * 2);", + " bytes.set(this.bytes);", + " this.bytes = bytes;", + " }", + " while (value >= 0x80) {", + " this.bytes[this.length++] = (value & 0x7f) | 0x80;", + " value >>>= 7;", + " }", + " this.bytes[this.length++] = value;", + " }", + " }", + "", + " class ByteReader {", + " /** @param {Uint8Array} bytes */", + " constructor(bytes) {", + " this.bytes = bytes;", + " this.position = 0;", + " }", + " readVarUint() {", + " let value = 0;", + " let shift = 0;", + " let byte;", + " do {", + " if (this.position >= this.bytes.length) throw new Error('Truncated terrain data.');", + " byte = this.bytes[this.position++];", + " value += (byte & 0x7f) * Math.pow(2, shift);", + " shift += 7;", + " } while (byte & 0x80);", + " return value;", + " }", + " }", + "", + " /** Predicts a value from the left, top and top-left ones. */", + " const predict = (values, size, index) => {", + " const i = index % size;", + " if (index < size) return i === 0 ? 0 : values[index - 1];", + " if (i === 0) return values[index - size];", + " return values[index - 1] + values[index - size] - values[index - size - 1];", + " };", + "", + " /**", + " * @param {ByteWriter} writer", + " * @param {Int32Array} values A grid of `size * size` values.", + " */", + " const writeGrid = (writer, values, size) => {", + " let zeroRun = 0;", + " for (let index = 0; index < values.length; index++) {", + " const error = values[index] - predict(values, size, index);", + " if (error === 0) {", + " zeroRun++;", + " continue;", + " }", + " if (zeroRun > 0) {", + " writer.writeVarUint(0);", + " writer.writeVarUint(zeroRun);", + " zeroRun = 0;", + " }", + " // Zigzag: small negative and positive numbers get small codes.", + " writer.writeVarUint(error > 0 ? error * 2 : -error * 2 - 1);", + " }", + " if (zeroRun > 0) {", + " writer.writeVarUint(0);", + " writer.writeVarUint(zeroRun);", + " }", + " };", + "", + " /** @param {ByteReader} reader */", + " const readGrid = (reader, size) => {", + " const values = new Int32Array(size * size);", + " let zeroRun = 0;", + " for (let index = 0; index < values.length; index++) {", + " let error = 0;", + " if (zeroRun > 0) {", + " zeroRun--;", + " } else {", + " const code = reader.readVarUint();", + " if (code === 0) {", + " zeroRun = reader.readVarUint() - 1;", + " } else {", + " error = code % 2 === 0 ? code / 2 : -(code + 1) / 2;", + " }", + " }", + " values[index] = predict(values, size, index) + error;", + " }", + " return values;", + " };", + "", + " const toBase64 = (bytes, length) => {", + " let binary = '';", + " for (let offset = 0; offset < length; offset += 0x8000) {", + " binary += String.fromCharCode.apply(null, bytes.subarray(offset, Math.min(offset + 0x8000, length)));", + " }", + " return btoa(binary);", + " };", + "", + " const fromBase64 = (base64) => {", + " const binary = atob(base64);", + " const bytes = new Uint8Array(binary.length);", + " for (let index = 0; index < binary.length; index++) {", + " bytes[index] = binary.charCodeAt(index);", + " }", + " return bytes;", + " };", + "", + " return {", + " /**", + " * @param {TerrainData} data", + " * @param {TerrainData} baseData The data without changes, with the same resolution.", + " */", + " encode(data, baseData) {", + " const writer = new ByteWriter();", + " const values = new Int32Array(data.heights.length);", + " for (let index = 0; index < values.length; index++) {", + " values[index] = Math.round((data.heights[index] - baseData.heights[index]) * 65535);", + " }", + " writeGrid(writer, values, data.size);", + " for (let layer = 0; layer < LAYER_COUNT; layer++) {", + " for (let index = 0; index < values.length; index++) {", + " const offset = index * LAYER_COUNT + layer;", + " values[index] = data.splat[offset] - baseData.splat[offset];", + " }", + " writeGrid(writer, values, data.size);", + " }", + " // Not painted, these grids are zeros: a few bytes each.", + " for (let channel = 0; channel < 4; channel++) {", + " for (let index = 0; index < values.length; index++) {", + " const offset = index * 4 + channel;", + " values[index] = data.colors[offset] - baseData.colors[offset];", + " }", + " writeGrid(writer, values, data.size);", + " }", + " return formatVersion + ';' + data.resolution + ';' + toBase64(writer.bytes, writer.length);", + " },", + "", + " /**", + " * Store grids of integers in a string, like the terrain data. Used by other objects (the scattered models).", + " * @param {Int32Array[]} grids Grids of `size * size` values.", + " * @param {number} size", + " */", + " encodeGrids(grids, size) {", + " const writer = new ByteWriter();", + " for (const grid of grids) writeGrid(writer, grid, size);", + " return toBase64(writer.bytes, writer.length);", + " },", + "", + " /**", + " * @param {string} base64 A string from `encodeGrids`.", + " * @param {number} size", + " * @param {number} count The number of grids.", + " * @returns {Int32Array[]} The grids (an error is thrown if the string is not valid).", + " */", + " decodeGrids(base64, size, count) {", + " const reader = new ByteReader(fromBase64(base64));", + " return Array.from({ length: count }, () => readGrid(reader, size));", + " },", + "", + " /**", + " * @param {string} text", + " * @param {(resolution: number) => TerrainData} createBaseData", + " * @returns {TerrainData | null} null if the text is empty or not valid.", + " */", + " decode(text, createBaseData) {", + " if (!text) return null;", + " const parts = text.split(';');", + " const resolution = parseInt(parts[1], 10);", + " if (readableFormatVersions.indexOf(parts[0]) === -1 || RESOLUTIONS.indexOf(resolution) === -1) {", + " return null;", + " }", + " try {", + " const reader = new ByteReader(fromBase64(parts[2]));", + " const data = createBaseData(resolution);", + " const heightChanges = readGrid(reader, data.size);", + " for (let index = 0; index < heightChanges.length; index++) {", + " data.heights[index] = clamp01(data.heights[index] + heightChanges[index] / 65535);", + " }", + " for (let layer = 0; layer < LAYER_COUNT; layer++) {", + " const weightChanges = readGrid(reader, data.size);", + " for (let index = 0; index < weightChanges.length; index++) {", + " const offset = index * LAYER_COUNT + layer;", + " // The base may have changed since the data was saved.", + " data.splat[offset] = Math.min(Math.max(data.splat[offset] + weightChanges[index], 0), 255);", + " }", + " }", + " if (parts[0] !== '1') {", + " for (let channel = 0; channel < 4; channel++) {", + " const colorChanges = readGrid(reader, data.size);", + " for (let index = 0; index < colorChanges.length; index++) {", + " const offset = index * 4 + channel;", + " data.colors[offset] = Math.min(Math.max(data.colors[offset] + colorChanges[index], 0), 255);", + " }", + " }", + " }", + " return data;", + " } catch (error) {", + " console.error('Terrain data could not be read:', error);", + " return null;", + " }", + " },", + " };", + "})();", + "", + "const lodIndices = new Map();", + "/**", + " * The triangles of a chunk, shared by all chunks of the same size. Chunk", + " * borders have a skirt going down, hiding the gaps between chunks drawn", + " * with different levels of detail.", + " * @param {number} chunkCells", + " * @param {number} step", + " */", + "const getLodIndex = (chunkCells, step) => {", + " const key = chunkCells + ';' + step;", + " const existingIndex = lodIndices.get(key);", + " if (existingIndex) return existingIndex;", + "", + " const sideVertices = chunkCells + 1;", + " const gridVertices = sideVertices * sideVertices;", + " const indices = [];", + " const grid = (i, j) => j * sideVertices + i;", + " for (let j = 0; j < chunkCells; j += step) {", + " for (let i = 0; i < chunkCells; i += step) {", + " const topLeft = grid(i, j);", + " const topRight = grid(i + step, j);", + " const bottomLeft = grid(i, j + step);", + " const bottomRight = grid(i + step, j + step);", + " indices.push(topLeft, topRight, bottomLeft, topRight, bottomRight, bottomLeft);", + " }", + " }", + " const borders = [", + " (k) => grid(k, 0),", + " (k) => grid(k, chunkCells),", + " (k) => grid(0, k),", + " (k) => grid(chunkCells, k),", + " ];", + " borders.forEach((borderVertex, border) => {", + " const skirtVertex = (k) => gridVertices + border * sideVertices + k;", + " for (let k = 0; k < chunkCells; k += step) {", + " const a = borderVertex(k);", + " const b = borderVertex(k + step);", + " const skirtA = skirtVertex(k);", + " const skirtB = skirtVertex(k + step);", + " // Both faces, as a skirt can be seen from either side.", + " indices.push(a, b, skirtB, a, skirtB, skirtA, a, skirtB, b, a, skirtA, skirtB);", + " }", + " });", + " const index = new THREE.BufferAttribute(new Uint16Array(indices), 1);", + " lodIndices.set(key, index);", + " return index;", + "};", + "", + "const vertexShaderDeclarations = `", + "varying vec2 vTerrainLocalXY;", + "varying float vTerrainLocalZ;", + "uniform float terrainWorldScale;", + "varying vec2 vTerrainWorldXY;", + "varying float vTerrainUp;", + "`;", + "const vertexShaderCode = `", + "vTerrainLocalXY = position.xy;", + "vTerrainLocalZ = position.z;", + "vTerrainWorldXY = (modelMatrix * vec4(transformed, 1.0)).xy * terrainWorldScale;", + "vTerrainUp = normalize(transpose(inverse(mat3(modelMatrix))) * objectNormal).z;", + "`;", + "const fragmentShaderDeclarations = `", + "uniform sampler2D terrainSplatMap;", + "uniform vec2 terrainSplatUvScaleAndOffset;", + "uniform vec3 terrainLayerColors[4];", + "uniform sampler2D terrainLayerMap0;", + "uniform sampler2D terrainLayerMap1;", + "uniform sampler2D terrainLayerMap2;", + "uniform sampler2D terrainLayerMap3;", + "uniform sampler2D terrainLayerNormalMap0;", + "uniform sampler2D terrainLayerNormalMap1;", + "uniform sampler2D terrainLayerNormalMap2;", + "uniform sampler2D terrainLayerNormalMap3;", + "uniform float terrainHasNormalMaps;", + "uniform float terrainNormalMapStrength;", + "uniform float terrainTextureSize;", + "uniform float terrainHeightBlending;", + "uniform float terrainTextureVariation;", + "uniform vec4 terrainCliffLayer;", + "uniform vec2 terrainCliffCosines;", + "uniform vec4 terrainBrush;", + "uniform sampler2D terrainColorMap;", + "uniform vec3 terrainWetShore;", + "varying vec2 vTerrainLocalXY;", + "varying float vTerrainLocalZ;", + "varying vec2 vTerrainWorldXY;", + "varying float vTerrainUp;", + "", + "float terrainHash(vec2 p) {", + " return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);", + "}", + "// The rotation of the copies of textures used to make repetition less visible.", + "const mat2 terrainCopyRotation = mat2(0.8, -0.6, 0.6, 0.8);", + "/** A bump of a normal map (from -1 to 1), mixed with its rotated copy in patches. */", + "vec3 terrainSampleNormal(sampler2D normalMap, vec2 uv, vec2 uv2, float patches) {", + " vec3 bump = texture2D(normalMap, uv).xyz * 2.0 - 1.0;", + " if (patches <= 0.0) return bump;", + " vec3 copyBump = texture2D(normalMap, uv2).xyz * 2.0 - 1.0;", + " // The bumps of the copy are rotated back with the inverse rotation.", + " copyBump.xy = transpose(terrainCopyRotation) * copyBump.xy;", + " return mix(bump, copyBump, patches);", + "}", + "float terrainNoise(vec2 p) {", + " vec2 i = floor(p);", + " vec2 f = fract(p);", + " vec2 u = f * f * (3.0 - 2.0 * f);", + " return mix(", + " mix(terrainHash(i), terrainHash(i + vec2(1.0, 0.0)), u.x),", + " mix(terrainHash(i + vec2(0.0, 1.0)), terrainHash(i + vec2(1.0, 1.0)), u.x),", + " u.y", + " );", + "}", + "`;", + "// Holes are samples without any painted layer: the interpolated sum of the", + "// weights gives smooth edges. Only compiled for terrains having holes, as", + "// discard makes drawing slower on some GPUs.", + "const holeShaderCode = `", + "#ifdef TERRAIN_HOLES", + "if (dot(texture2D(terrainSplatMap, vTerrainLocalXY * terrainSplatUvScaleAndOffset.x + terrainSplatUvScaleAndOffset.y), vec4(1.0)) < 0.5) discard;", + "#endif", + "`;", + "// For the shadows: the depth of holes is not drawn.", + "const holeDepthShaderDeclarations = `", + "uniform sampler2D terrainSplatMap;", + "uniform vec2 terrainSplatUvScaleAndOffset;", + "varying vec2 vTerrainLocalXY;", + "`;", + "", + "const fragmentShaderCode = holeShaderCode + `", + "// Borders between layers are made irregular and sharper, to look painted.", + "vec2 terrainNoisePosition = vTerrainWorldXY / 24.0;", + "vec4 terrainJitter = vec4(", + " terrainNoise(terrainNoisePosition),", + " terrainNoise(terrainNoisePosition + 19.1),", + " terrainNoise(terrainNoisePosition + 37.7),", + " terrainNoise(terrainNoisePosition + 53.3)", + ");", + "// Layer weights are in a texture, so that painted details stay the same", + "// whatever the level of detail of the ground.", + "vec2 terrainSplatUv = vTerrainLocalXY * terrainSplatUvScaleAndOffset.x + terrainSplatUvScaleAndOffset.y;", + "vec4 terrainWeights = texture2D(terrainSplatMap, terrainSplatUv) * (0.7 + 0.6 * terrainJitter);", + "terrainWeights = terrainWeights * terrainWeights;", + "terrainWeights /= max(dot(terrainWeights, vec4(1.0)), 0.00001);", + "float terrainCliff = 1.0 - smoothstep(terrainCliffCosines.x, terrainCliffCosines.y, vTerrainUp);", + "terrainWeights = mix(terrainWeights, terrainCliffLayer, terrainCliff * dot(terrainCliffLayer, vec4(1.0)));", + "", + "vec2 terrainUv = vTerrainWorldXY / terrainTextureSize;", + "vec3 terrainTexture0 = texture2D(terrainLayerMap0, terrainUv).rgb;", + "vec3 terrainTexture1 = texture2D(terrainLayerMap1, terrainUv).rgb;", + "vec3 terrainTexture2 = texture2D(terrainLayerMap2, terrainUv).rgb;", + "vec3 terrainTexture3 = texture2D(terrainLayerMap3, terrainUv).rgb;", + "", + "// Less visible repetition: in irregular patches, textures are replaced by a", + "// rotated and shifted copy of themselves, and colors slowly vary.", + "float terrainPatches = 0.0;", + "vec2 terrainUv2 = terrainUv;", + "float terrainColorVariation = 1.0;", + "if (terrainTextureVariation > 0.0) {", + " // Up to half of the ground is covered by the copies, each patch showing", + " // only one of them (mixing them would make them blurry).", + " float terrainPatchThreshold = 1.0 - 0.5 * terrainTextureVariation;", + " terrainPatches = smoothstep(terrainPatchThreshold - 0.05, terrainPatchThreshold + 0.05, terrainNoise(terrainUv * 0.23 + 7.3));", + " terrainUv2 = terrainCopyRotation * terrainUv + vec2(0.31, 0.73);", + " terrainTexture0 = mix(terrainTexture0, texture2D(terrainLayerMap0, terrainUv2).rgb, terrainPatches);", + " terrainTexture1 = mix(terrainTexture1, texture2D(terrainLayerMap1, terrainUv2).rgb, terrainPatches);", + " terrainTexture2 = mix(terrainTexture2, texture2D(terrainLayerMap2, terrainUv2).rgb, terrainPatches);", + " terrainTexture3 = mix(terrainTexture3, texture2D(terrainLayerMap3, terrainUv2).rgb, terrainPatches);", + " terrainColorVariation += terrainTextureVariation * 0.3 *", + " (terrainNoise(terrainUv * 0.11) + 0.5 * terrainNoise(terrainUv * 0.37 + 3.1) - 0.75);", + "}", + "", + "// Height-based blending: the brightness of a texture, compared to its", + "// average, is used as its height. Where layers meet, the high parts (stones,", + "// tufts) cover the low parts (sand, gaps) instead of fading into each other.", + "if (terrainHeightBlending > 0.0) {", + " const vec3 terrainLuminance = vec3(0.299, 0.587, 0.114);", + " vec4 terrainHeights = vec4(", + " dot(terrainTexture0 - textureLod(terrainLayerMap0, vec2(0.5), 16.0).rgb, terrainLuminance),", + " dot(terrainTexture1 - textureLod(terrainLayerMap1, vec2(0.5), 16.0).rgb, terrainLuminance),", + " dot(terrainTexture2 - textureLod(terrainLayerMap2, vec2(0.5), 16.0).rgb, terrainLuminance),", + " dot(terrainTexture3 - textureLod(terrainLayerMap3, vec2(0.5), 16.0).rgb, terrainLuminance)", + " );", + " vec4 terrainHeightWeights = terrainWeights + terrainHeights;", + " float terrainTop = max(max(terrainHeightWeights.x, terrainHeightWeights.y), max(terrainHeightWeights.z, terrainHeightWeights.w));", + " // Only painted layers (and the cliff layer) can show.", + " vec4 terrainHeightBlend = max(terrainHeightWeights - terrainTop + 0.1, 0.0) * step(0.004, terrainWeights);", + " terrainHeightBlend /= max(dot(terrainHeightBlend, vec4(1.0)), 0.00001);", + " terrainWeights = mix(terrainWeights, terrainHeightBlend, terrainHeightBlending);", + "}", + "", + "vec3 terrainColor = terrainColorVariation * (", + " terrainWeights.x * terrainLayerColors[0] * terrainTexture0 +", + " terrainWeights.y * terrainLayerColors[1] * terrainTexture1 +", + " terrainWeights.z * terrainLayerColors[2] * terrainTexture2 +", + " terrainWeights.w * terrainLayerColors[3] * terrainTexture3);", + "", + "// The painted tint multiplies all layers. Wet ground is darker (and", + "// shinier, see the roughness).", + "vec4 terrainTintAndWetness = texture2D(terrainColorMap, terrainSplatUv);", + "float terrainWetness = max(", + " terrainTintAndWetness.a,", + " terrainWetShore.z * (1.0 - smoothstep(terrainWetShore.x, terrainWetShore.x + terrainWetShore.y, vTerrainLocalZ))", + ");", + "terrainColor *= terrainTintAndWetness.rgb * (1.0 - 0.4 * terrainWetness);", + "diffuseColor.rgb *= terrainColor;", + "", + "if (terrainBrush.w > 0.5) {", + " float brushDistance = distance(vTerrainWorldXY, terrainBrush.xy);", + " float ringWidth = max(terrainBrush.z * 0.04, 1.5);", + " float ring = 1.0 - smoothstep(0.0, ringWidth, abs(brushDistance - terrainBrush.z));", + " float disc = brushDistance < terrainBrush.z ? 0.12 : 0.0;", + " diffuseColor.rgb = mix(diffuseColor.rgb, vec3(1.0), max(ring * 0.85, disc));", + "}", + "`;", + "", + "const fragmentShaderRoughnessCode = `", + "roughnessFactor = mix(roughnessFactor, 0.3, terrainWetness);", + "`;", + "", + "const fragmentShaderNormalCode = `", + "if (terrainHasNormalMaps > 0.5) {", + " vec3 terrainMapNormal =", + " terrainWeights.x * terrainSampleNormal(terrainLayerNormalMap0, terrainUv, terrainUv2, terrainPatches) +", + " terrainWeights.y * terrainSampleNormal(terrainLayerNormalMap1, terrainUv, terrainUv2, terrainPatches) +", + " terrainWeights.z * terrainSampleNormal(terrainLayerNormalMap2, terrainUv, terrainUv2, terrainPatches) +", + " terrainWeights.w * terrainSampleNormal(terrainLayerNormalMap3, terrainUv, terrainUv2, terrainPatches);", + " terrainMapNormal.xy *= terrainNormalMapStrength;", + " // The X and Y of textures follow the X and Y of the 3D world, projected on", + " // the ground (the camera space of the normal is used).", + " vec3 terrainTangent = (viewMatrix * vec4(1.0, 0.0, 0.0, 0.0)).xyz;", + " terrainTangent -= normal * dot(normal, terrainTangent);", + " // A ground facing the X axis (a rotated terrain) uses the Y axis instead.", + " if (dot(terrainTangent, terrainTangent) < 0.000001) {", + " terrainTangent = (viewMatrix * vec4(0.0, 1.0, 0.0, 0.0)).xyz;", + " terrainTangent -= normal * dot(normal, terrainTangent);", + " }", + " terrainTangent = normalize(terrainTangent);", + " vec3 terrainBitangent = cross(normal, terrainTangent);", + " normal = normalize(mat3(terrainTangent, terrainBitangent, normal) * terrainMapNormal);", + "}", + "`;", + "", + "let whiteTexture = null;", + "const getWhiteTexture = () => {", + " if (!whiteTexture) {", + " whiteTexture = new THREE.DataTexture(new Uint8Array([255, 255, 255, 255]), 1, 1);", + " whiteTexture.needsUpdate = true;", + " }", + " return whiteTexture;", + "};", + "", + "let flatNormalTexture = null;", + "const getFlatNormalTexture = () => {", + " if (!flatNormalTexture) {", + " flatNormalTexture = new THREE.DataTexture(new Uint8Array([128, 128, 255, 255]), 1, 1);", + " flatNormalTexture.needsUpdate = true;", + " }", + " return flatNormalTexture;", + "};", + "", + "const layerTextures = new WeakMap();", + "const layerNormalMaps = new WeakMap();", + "/**", + " * Textures of the image manager have no mipmaps: they would shimmer when", + " * repeated in the distance.", + " * @param {gdjs.RuntimeGame} game", + " * @param {string} resourceName", + " * @param {boolean} isNormalMap Normal maps are directions, not colors.", + " */", + "const getLayerTexture = (game, resourceName, isNormalMap) => {", + " if (!resourceName) return isNormalMap ? getFlatNormalTexture() : getWhiteTexture();", + " const imageTexture = game.getImageManager().getThreeTexture(resourceName);", + " const textures = isNormalMap ? layerNormalMaps : layerTextures;", + " let texture = textures.get(imageTexture);", + " if (!texture) {", + " texture = imageTexture.clone();", + " if (isNormalMap) texture.colorSpace = THREE.NoColorSpace;", + " texture.wrapS = THREE.RepeatWrapping;", + " texture.wrapT = THREE.RepeatWrapping;", + " texture.minFilter = THREE.LinearMipmapLinearFilter;", + " texture.generateMipmaps = true;", + " texture.anisotropy = 4;", + " texture.needsUpdate = true;", + " textures.set(imageTexture, texture);", + " }", + " return texture;", + "};", + "", + "/**", + " * A texture of 4 bytes per sample of a grid of `size * size` samples (layer", + " * weights, tint and wetness...), interpolated between samples.", + " * @param {Uint8Array} bytes", + " * @param {number} size", + " */", + "const createGridTexture = (bytes, size) => {", + " const texture = new THREE.DataTexture(bytes, size, size);", + " texture.magFilter = THREE.LinearFilter;", + " texture.minFilter = THREE.LinearFilter;", + " texture.userData.isUploaded = false;", + " texture.onUpdate = () => {", + " texture.userData.isUploaded = true;", + " };", + " return texture;", + "};", + "", + "/**", + " * Send the rows of the changed samples to the GPU, or the whole texture", + " * before its first upload (and in older GDevelop versions).", + " * @param {THREE.DataTexture} texture", + " * @param {SampleRectangle} samples", + " */", + "const uploadGridTextureRows = (texture, samples) => {", + " if (texture.userData.isUploaded && typeof texture.addUpdateRange === 'function') {", + " const size = texture.image.width;", + " for (let j = samples.minJ; j <= samples.maxJ; j++) {", + " texture.addUpdateRange((j * size + samples.minI) * 4, (samples.maxI - samples.minI + 1) * 4);", + " }", + " }", + " texture.needsUpdate = true;", + "};", + "", + "class TerrainRenderer {", + " /** @param {Terrain} terrain */", + " constructor(terrain) {", + " this.terrain = terrain;", + " this.group = new THREE.Group();", + " terrain.object.get3DRendererObject().add(this.group);", + " /** @type {Array<{mesh: THREE.Mesh, geometry: THREE.BufferGeometry, firstI: number, firstJ: number, center: THREE.Vector3, lod: number}>} */", + " this.chunks = [];", + " this.chunksPerSide = 0;", + " this.chunkCells = 0;", + " this.chunkSideVertices = 0;", + " this.chunkGridVertices = 0;", + " this.chunkVertices = 0;", + " /** @type {THREE.DataTexture | null} */", + " this.splatTexture = null;", + " /** @type {THREE.DataTexture | null} */", + " this.colorTexture = null;", + " this.uniforms = {", + " terrainSplatMap: { value: getWhiteTexture() },", + " terrainSplatUvScaleAndOffset: { value: new THREE.Vector2() },", + " terrainLayerColors: { value: [new THREE.Color(), new THREE.Color(), new THREE.Color(), new THREE.Color()] },", + " terrainLayerMap0: { value: getWhiteTexture() },", + " terrainLayerMap1: { value: getWhiteTexture() },", + " terrainLayerMap2: { value: getWhiteTexture() },", + " terrainLayerMap3: { value: getWhiteTexture() },", + " terrainLayerNormalMap0: { value: getFlatNormalTexture() },", + " terrainLayerNormalMap1: { value: getFlatNormalTexture() },", + " terrainLayerNormalMap2: { value: getFlatNormalTexture() },", + " terrainLayerNormalMap3: { value: getFlatNormalTexture() },", + " terrainHasNormalMaps: { value: 0 },", + " terrainNormalMapStrength: { value: 1 },", + " terrainTextureSize: { value: 256 },", + " terrainHeightBlending: { value: 0 },", + " terrainTextureVariation: { value: 0 },", + " terrainCliffLayer: { value: new THREE.Vector4() },", + " terrainCliffCosines: { value: new THREE.Vector2() },", + " terrainBrush: { value: new THREE.Vector4() },", + " terrainColorMap: { value: getWhiteTexture() },", + " // Z (in the terrain group) under which the ground is wet, fade height, and 1 if it's used.", + " terrainWetShore: { value: new THREE.Vector3() },", + " // Scene units by 3D world unit.", + " terrainWorldScale: { value: 1 },", + " };", + " this.material = new THREE.MeshStandardMaterial({ roughness: 1, metalness: 0 });", + " this.material.onBeforeCompile = (shader) => {", + " Object.assign(shader.uniforms, this.uniforms);", + " shader.vertexShader = patchShaderCode(shader.vertexShader, [", + " ['#include ', '#include \\n' + vertexShaderDeclarations],", + " ['#include ', '#include \\n' + vertexShaderCode],", + " ], 'Terrain3D');", + " shader.fragmentShader = patchShaderCode(shader.fragmentShader, [", + " ['#include ', '#include \\n' + fragmentShaderDeclarations],", + " ['#include ', fragmentShaderCode],", + " ['#include ', '#include \\n' + fragmentShaderRoughnessCode],", + " ['#include ', '#include \\n' + fragmentShaderNormalCode],", + " ], 'Terrain3D');", + " };", + " this.material.customProgramCacheKey = () => (this.hasHoles ? 'Terrain3DWithHoles' : 'Terrain3D');", + " this.hasHoles = false;", + " // Without them, holes would cast shadows.", + " this.depthMaterial = this._createHoleDepthMaterial(new THREE.MeshDepthMaterial());", + " this.distanceMaterial = this._createHoleDepthMaterial(new THREE.MeshDistanceMaterial());", + " this._cameraPosition = new THREE.Vector3();", + " this._chunkPosition = new THREE.Vector3();", + " this._scale = new THREE.Vector3();", + " }", + "", + " /**", + " * @param {THREE.MeshDepthMaterial | THREE.MeshDistanceMaterial} material", + " * @returns The material, drawing nothing in holes.", + " */", + " _createHoleDepthMaterial(material) {", + " material.defines = { TERRAIN_HOLES: '' };", + " material.onBeforeCompile = (shader) => {", + " Object.assign(shader.uniforms, this.uniforms);", + " shader.vertexShader = patchShaderCode(shader.vertexShader, [", + " ['#include ', '#include \\nvarying vec2 vTerrainLocalXY;'],", + " ['#include ', '#include \\nvTerrainLocalXY = position.xy;'],", + " ], 'Terrain3D shadows');", + " shader.fragmentShader = patchShaderCode(shader.fragmentShader, [", + " ['#include ', '#include \\n' + holeDepthShaderDeclarations],", + " ['#include ', holeShaderCode + '#include '],", + " ], 'Terrain3D shadows');", + " };", + " material.customProgramCacheKey = () => 'Terrain3DHoles';", + " return material;", + " }", + "", + " /**", + " * Use the shaders drawing holes, or the faster ones without them.", + " * @param {boolean} hasHoles", + " */", + " setHasHoles(hasHoles) {", + " if (hasHoles === this.hasHoles) return;", + " this.hasHoles = hasHoles;", + " // The material has its own defines (STANDARD).", + " if (hasHoles) this.material.defines.TERRAIN_HOLES = '';", + " else delete this.material.defines.TERRAIN_HOLES;", + " this.material.needsUpdate = true;", + " for (const chunk of this.chunks) this._setChunkShadowMaterials(chunk.mesh);", + " }", + "", + " /** @param {THREE.Mesh} mesh */", + " _setChunkShadowMaterials(mesh) {", + " mesh.customDepthMaterial = this.hasHoles ? this.depthMaterial : undefined;", + " mesh.customDistanceMaterial = this.hasHoles ? this.distanceMaterial : undefined;", + " }", + "", + " /** Recreate all the chunks, for example after the resolution changed. */", + " rebuild() {", + " this.disposeChunks();", + " const { data } = this.terrain;", + " this.chunkCells = getChunkCells(data.resolution);", + " this.chunkSideVertices = this.chunkCells + 1;", + " this.chunkGridVertices = this.chunkSideVertices * this.chunkSideVertices;", + " this.chunkVertices = this.chunkGridVertices + 4 * this.chunkSideVertices;", + " this.chunksPerSide = data.resolution / this.chunkCells;", + " const cellSize = AREA_SIZE / data.resolution;", + " // Sample centers are at the texel centers.", + " this.splatTexture = createGridTexture(data.splat, data.size);", + " this.uniforms.terrainSplatMap.value = this.splatTexture;", + " this.colorTexture = createGridTexture(data.colors, data.size);", + " // Tints are colors chosen like the layer colors.", + " this.colorTexture.colorSpace = THREE.SRGBColorSpace;", + " this.uniforms.terrainColorMap.value = this.colorTexture;", + " this.uniforms.terrainSplatUvScaleAndOffset.value.set(1 / (cellSize * data.size), 0.5 / data.size);", + " for (let chunkJ = 0; chunkJ < this.chunksPerSide; chunkJ++) {", + " for (let chunkI = 0; chunkI < this.chunksPerSide; chunkI++) {", + " const firstI = chunkI * this.chunkCells;", + " const firstJ = chunkJ * this.chunkCells;", + " const positions = new Float32Array(this.chunkVertices * 3);", + " for (let j = 0; j < this.chunkSideVertices; j++) {", + " for (let i = 0; i < this.chunkSideVertices; i++) {", + " const vertex = j * this.chunkSideVertices + i;", + " positions[vertex * 3] = (firstI + i) * cellSize;", + " positions[vertex * 3 + 1] = (firstJ + j) * cellSize;", + " }", + " }", + " // Skirt vertices are below border vertices.", + " this._forEachSkirtVertex((skirtVertex, borderVertex) => {", + " positions[skirtVertex * 3] = positions[borderVertex * 3];", + " positions[skirtVertex * 3 + 1] = positions[borderVertex * 3 + 1];", + " });", + " const geometry = new THREE.BufferGeometry();", + " geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));", + " geometry.setAttribute('normal', new THREE.BufferAttribute(new Float32Array(this.chunkVertices * 3), 3));", + " geometry.setIndex(getLodIndex(this.chunkCells, LOD_STEPS[0]));", + " geometry.boundingBox = new THREE.Box3();", + " geometry.boundingSphere = new THREE.Sphere();", + " const mesh = new THREE.Mesh(geometry, this.material);", + " mesh.matrixAutoUpdate = false;", + " // Rays hit the ground at its full resolution, whatever the level of", + " // detail drawn, and go through holes.", + " const chunkCells = this.chunkCells;", + " const terrainData = () => this.terrain.data;", + " const localPoint = new THREE.Vector3();", + " mesh.raycast = function (raycaster, intersects) {", + " const drawnIndex = geometry.index;", + " const firstIntersection = intersects.length;", + " geometry.setIndex(getLodIndex(chunkCells, LOD_STEPS[0]));", + " THREE.Mesh.prototype.raycast.call(this, raycaster, intersects);", + " geometry.setIndex(drawnIndex);", + " const data = terrainData();", + " const cellSize = AREA_SIZE / data.resolution;", + " for (let index = intersects.length - 1; index >= firstIntersection; index--) {", + " this.worldToLocal(localPoint.copy(intersects[index].point));", + " if (data.sampleSolidity(localPoint.x / cellSize, localPoint.y / cellSize) < 0.5) {", + " intersects.splice(index, 1);", + " }", + " }", + " };", + " this._setChunkShadowMaterials(mesh);", + " this.group.add(mesh);", + " this.chunks.push({", + " mesh,", + " geometry,", + " firstI,", + " firstJ,", + " center: new THREE.Vector3(", + " (firstI + this.chunkCells / 2) * cellSize,", + " (firstJ + this.chunkCells / 2) * cellSize,", + " 0", + " ),", + " lod: 0,", + " });", + " }", + " }", + " this.updateSamples(new SampleRectangle().set(0, 0, data.resolution, data.resolution), true);", + " this.updateMaterial();", + " }", + "", + " _forEachSkirtVertex(callback) {", + " for (let k = 0; k < this.chunkSideVertices; k++) {", + " callback(this.chunkGridVertices + k, k);", + " callback(this.chunkGridVertices + this.chunkSideVertices + k, this.chunkCells * this.chunkSideVertices + k);", + " callback(this.chunkGridVertices + 2 * this.chunkSideVertices + k, k * this.chunkSideVertices);", + " callback(", + " this.chunkGridVertices + 3 * this.chunkSideVertices + k,", + " k * this.chunkSideVertices + this.chunkCells", + " );", + " }", + " }", + "", + " /**", + " * Skirts go down only to the lowest point a coarser neighbor chunk can", + " * draw along the border, so that they can't be seen through holes.", + " * @param {Float32Array} positions", + " */", + " _updateSkirtHeights(positions) {", + " const coarsestStep = LOD_STEPS[LOD_STEPS.length - 1];", + " const borderVertices = [", + " (k) => k,", + " (k) => this.chunkCells * this.chunkSideVertices + k,", + " (k) => k * this.chunkSideVertices,", + " (k) => k * this.chunkSideVertices + this.chunkCells,", + " ];", + " borderVertices.forEach((borderVertex, border) => {", + " for (let k = 0; k < this.chunkSideVertices; k++) {", + " let minZ = Infinity;", + " const from = Math.max(k - coarsestStep, 0);", + " const to = Math.min(k + coarsestStep, this.chunkCells);", + " for (let other = from; other <= to; other++) {", + " minZ = Math.min(minZ, positions[borderVertex(other) * 3 + 2]);", + " }", + " const skirtVertex = this.chunkGridVertices + border * this.chunkSideVertices + k;", + " positions[skirtVertex * 3 + 2] = minZ;", + " }", + " });", + " }", + "", + " /**", + " * Update the vertices of the chunks sharing the given samples.", + " * @param {SampleRectangle} samples", + " * @param {boolean} hasPaintChanged", + " */", + " updateSamples(samples, hasPaintChanged) {", + " if (samples.isEmpty()) return;", + " const { data } = this.terrain;", + " const cellSize = AREA_SIZE / data.resolution;", + " const slopeFactor = AREA_DEPTH / cellSize;", + " // Normals of samples next to changed ones change too.", + " const minI = Math.max(samples.minI - 1, 0);", + " const minJ = Math.max(samples.minJ - 1, 0);", + " const maxI = Math.min(samples.maxI + 1, data.resolution);", + " const maxJ = Math.min(samples.maxJ + 1, data.resolution);", + " const firstChunkI = Math.max(Math.ceil(minI / this.chunkCells) - 1, 0);", + " const firstChunkJ = Math.max(Math.ceil(minJ / this.chunkCells) - 1, 0);", + " const lastChunkI = Math.min(Math.floor(maxI / this.chunkCells), this.chunksPerSide - 1);", + " const lastChunkJ = Math.min(Math.floor(maxJ / this.chunkCells), this.chunksPerSide - 1);", + "", + " for (let chunkJ = firstChunkJ; chunkJ <= lastChunkJ; chunkJ++) {", + " for (let chunkI = firstChunkI; chunkI <= lastChunkI; chunkI++) {", + " const chunk = this.chunks[chunkJ * this.chunksPerSide + chunkI];", + " const positions = chunk.geometry.attributes.position.array;", + " const normals = chunk.geometry.attributes.normal.array;", + " const fromI = Math.max(minI - chunk.firstI, 0);", + " const toI = Math.min(maxI - chunk.firstI, this.chunkCells);", + " const fromJ = Math.max(minJ - chunk.firstJ, 0);", + " const toJ = Math.min(maxJ - chunk.firstJ, this.chunkCells);", + " for (let j = fromJ; j <= toJ; j++) {", + " const gridJ = chunk.firstJ + j;", + " for (let i = fromI; i <= toI; i++) {", + " const gridI = chunk.firstI + i;", + " const vertex = j * this.chunkSideVertices + i;", + " const sample = gridJ * data.size + gridI;", + " positions[vertex * 3 + 2] = data.heights[sample] * AREA_DEPTH;", + "", + " const leftI = Math.max(gridI - 1, 0);", + " const rightI = Math.min(gridI + 1, data.resolution);", + " const upJ = Math.max(gridJ - 1, 0);", + " const downJ = Math.min(gridJ + 1, data.resolution);", + " const nx =", + " ((data.getHeight(leftI, gridJ) - data.getHeight(rightI, gridJ)) * slopeFactor) /", + " (rightI - leftI);", + " const ny =", + " ((data.getHeight(gridI, upJ) - data.getHeight(gridI, downJ)) * slopeFactor) /", + " (downJ - upJ);", + " const inverseLength = 1 / Math.sqrt(nx * nx + ny * ny + 1);", + " normals[vertex * 3] = nx * inverseLength;", + " normals[vertex * 3 + 1] = ny * inverseLength;", + " normals[vertex * 3 + 2] = inverseLength;", + " }", + " }", + " this._forEachSkirtVertex((skirtVertex, borderVertex) => {", + " for (let axis = 0; axis < 3; axis++) {", + " normals[skirtVertex * 3 + axis] = normals[borderVertex * 3 + axis];", + " }", + " });", + " this._updateSkirtHeights(positions);", + " let maxZ = 0;", + " for (let vertex = 0; vertex < this.chunkGridVertices; vertex++) {", + " maxZ = Math.max(maxZ, positions[vertex * 3 + 2]);", + " }", + " const chunkSize = this.chunkCells * cellSize;", + " chunk.geometry.boundingBox.min.set(chunk.firstI * cellSize, chunk.firstJ * cellSize, 0);", + " chunk.geometry.boundingBox.max.set(", + " chunk.firstI * cellSize + chunkSize,", + " chunk.firstJ * cellSize + chunkSize,", + " maxZ", + " );", + " chunk.geometry.boundingBox.getBoundingSphere(chunk.geometry.boundingSphere);", + " chunk.center.z = maxZ / 2;", + " chunk.geometry.attributes.position.needsUpdate = true;", + " chunk.geometry.attributes.normal.needsUpdate = true;", + " }", + " }", + " if (hasPaintChanged) {", + " // The data may have been replaced by one with the same resolution.", + " this.splatTexture.image.data = data.splat;", + " uploadGridTextureRows(this.splatTexture, samples);", + " this.colorTexture.image.data = data.colors;", + " uploadGridTextureRows(this.colorTexture, samples);", + " }", + " }", + "", + " updateMaterial() {", + " const { object } = this.terrain;", + " const game = object.getRuntimeScene().getGame();", + " const colors = [object._getLayer1Color(), object._getLayer2Color(), object._getLayer3Color(), object._getLayer4Color()];", + " const textures = [", + " object._getLayer1Texture(),", + " object._getLayer2Texture(),", + " object._getLayer3Texture(),", + " object._getLayer4Texture(),", + " ];", + " const normalMaps = [", + " object._getLayer1NormalMap(),", + " object._getLayer2NormalMap(),", + " object._getLayer3NormalMap(),", + " object._getLayer4NormalMap(),", + " ];", + " for (let layer = 0; layer < LAYER_COUNT; layer++) {", + " this.uniforms.terrainLayerColors.value[layer].set(gdjs.rgbOrHexStringToNumber(colors[layer]));", + " this.uniforms['terrainLayerMap' + layer].value = getLayerTexture(game, textures[layer], false);", + " this.uniforms['terrainLayerNormalMap' + layer].value = getLayerTexture(game, normalMaps[layer], true);", + " }", + " this.uniforms.terrainHasNormalMaps.value = normalMaps.some((normalMap) => !!normalMap) ? 1 : 0;", + " this.uniforms.terrainNormalMapStrength.value = Math.max(object._getNormalMapStrength(), 0);", + " this.uniforms.terrainTextureSize.value = Math.max(object._getTextureSize(), 1);", + " this.uniforms.terrainHeightBlending.value = clamp01(object._getHeightBlending());", + " this.uniforms.terrainTextureVariation.value = clamp01(object._getTextureVariation());", + "", + " const cliffLayer = parseInt(object._getCliffLayer(), 10);", + " const cliffAngle = gdjs.toRad(object._getCliffAngle());", + " const cliffBlendAngle = gdjs.toRad(4);", + " this.uniforms.terrainCliffLayer.value.set(", + " cliffLayer === 1 ? 1 : 0,", + " cliffLayer === 2 ? 1 : 0,", + " cliffLayer === 3 ? 1 : 0,", + " cliffLayer === 4 ? 1 : 0", + " );", + " this.uniforms.terrainCliffCosines.value.set(", + " Math.cos(cliffAngle + cliffBlendAngle),", + " Math.cos(cliffAngle - cliffBlendAngle)", + " );", + "", + " const castShadow = object._getCastShadow();", + " const receiveShadow = object._getReceiveShadow();", + " for (const chunk of this.chunks) {", + " chunk.mesh.castShadow = castShadow;", + " chunk.mesh.receiveShadow = receiveShadow;", + " }", + " }", + "", + " /**", + " * Show the brush circle of the editor.", + " * @param {THREE.Vector3 | null} localPosition", + " * @param {number} radius In scene units.", + " */", + " setBrush(localPosition, radius) {", + " const brush = this.uniforms.terrainBrush.value;", + " if (!localPosition) {", + " brush.w = 0;", + " return;", + " }", + " this.group.updateWorldMatrix(true, false);", + " const worldPosition = this._chunkPosition.copy(localPosition).applyMatrix4(this.group.matrixWorld);", + " const worldScale = this.uniforms.terrainWorldScale.value;", + " brush.set(worldPosition.x * worldScale, worldPosition.y * worldScale, radius, 1);", + " }", + "", + " /** @param {THREE.Camera | null} camera */", + " updateLevelsOfDetail(camera) {", + " if (!camera || !this.group.visible) return;", + " const matrixWorld = this.group.matrixWorld;", + " this._cameraPosition.setFromMatrixPosition(camera.matrixWorld);", + " this._scale.setFromMatrixScale(matrixWorld);", + " const cellSize = AREA_SIZE / this.terrain.data.resolution;", + " const chunkWorldSize = this.chunkCells * cellSize * Math.max(this._scale.x, this._scale.y);", + " for (const chunk of this.chunks) {", + " const distance =", + " this._chunkPosition.copy(chunk.center).applyMatrix4(matrixWorld).distanceTo(this._cameraPosition) /", + " chunkWorldSize;", + " let lod = LOD_DISTANCES.length - 1;", + " while (lod > 0 && distance < LOD_DISTANCES[lod]) lod--;", + " if (lod !== chunk.lod) {", + " chunk.lod = lod;", + " chunk.geometry.setIndex(getLodIndex(this.chunkCells, LOD_STEPS[lod]));", + " }", + " }", + " }", + "", + " disposeChunks() {", + " for (const chunk of this.chunks) {", + " chunk.mesh.removeFromParent();", + " // The index is shared by all chunks: it must stay on the GPU.", + " chunk.geometry.setIndex(null);", + " chunk.geometry.dispose();", + " }", + " this.chunks.length = 0;", + " if (this.splatTexture) this.splatTexture.dispose();", + " this.splatTexture = null;", + " if (this.colorTexture) this.colorTexture.dispose();", + " this.colorTexture = null;", + " }", + "", + " dispose() {", + " this.disposeChunks();", + " this.material.dispose();", + " this.depthMaterial.dispose();", + " this.distanceMaterial.dispose();", + " this.group.removeFromParent();", + " }", + "}", + "", + "/**", + " * Makes a Physics3D behavior use a height field shape following the terrain.", + " * Changes of heights are applied to the shape without recreating it.", + " * @implements {gdjs.Physics3DRuntimeBehavior.BodyUpdater}", + " */", + "class TerrainBodyUpdater {", + " /**", + " * @param {Terrain} terrain", + " * @param {gdjs.Physics3DRuntimeBehavior} physics", + " */", + " constructor(terrain, physics) {", + " this.terrain = terrain;", + " this.physics = physics;", + " this.defaultBodyUpdater = physics.bodyUpdater;", + " this.changedSamples = new SampleRectangle();", + " /** @type {Jolt.HeightFieldShape | null} */", + " this.heightField = null;", + " this.heightScale = 1;", + " this.heightsBufferPointer = 0;", + " this.heightsBufferLength = 0;", + " this.bodyX = 0;", + " this.bodyY = 0;", + " this.bodyZ = 0;", + " this.bodyHeight = 0;", + " physics.bodyType = 'Static';", + " physics.bodyUpdater = this;", + " }", + "", + " createShape() {", + " const { data, object } = this.terrain;", + " const worldInvScale = this.physics._sharedData.worldInvScale;", + " const { size } = data;", + " this.heightScale = object.getDepth() * worldInvScale;", + "", + " // Changes of heights (SetHeights) break collisions unless the sample", + " // count is a power of 2: extra samples have no collision.", + " const sampleCount = Math.pow(2, Math.ceil(Math.log2(size)));", + " const settings = new Jolt.HeightFieldShapeSettings();", + " settings.mScale.Set(", + " (object.getWidth() / data.resolution) * worldInvScale,", + " 1,", + " (object.getHeight() / data.resolution) * worldInvScale", + " );", + " settings.mSampleCount = sampleCount;", + " settings.mBlockSize = 4;", + " // Allows later changes of heights on the whole depth of the object.", + " settings.mMinHeightValue = 0;", + " settings.mMaxHeightValue = this.heightScale;", + " settings.mHeightSamples.resize(sampleCount * sampleCount);", + " const samples = new Float32Array(", + " Jolt.HEAPF32.buffer,", + " Jolt.getPointer(settings.mHeightSamples.data()),", + " sampleCount * sampleCount", + " );", + " const noCollision = Jolt.HeightFieldShapeConstantValues.prototype.cNoCollisionValue;", + " samples.fill(noCollision);", + " // Height fields go along X and Z: rows are flipped so that, once", + " // rotated to have heights along Z, rows go along Y.", + " for (let j = 0; j < size; j++) {", + " const row = (size - 1 - j) * sampleCount;", + " for (let i = 0; i < size; i++) {", + " samples[row + i] = this._getSampleHeight(j * size + i, noCollision);", + " }", + " }", + " const shape = settings.Create().Get();", + " Jolt.destroy(settings);", + " this.heightField = Jolt.castObject(shape, Jolt.HeightFieldShape);", + " this.changedSamples.clear();", + " return shape;", + " }", + "", + " /** @returns {number} The height of a sample in the shape, or no collision in a hole. */", + " _getSampleHeight(index, noCollision) {", + " const height = this.terrain.surfaceHeights[index];", + " return Number.isNaN(height) ? noCollision : height * this.heightScale;", + " }", + "", + " _getBodyPosition() {", + " const { object } = this.terrain;", + " const worldInvScale = this.physics._sharedData.worldInvScale;", + " this.bodyX = object.getX();", + " this.bodyY = object.getY();", + " this.bodyZ = object.getZ();", + " // The origin of the height field is at the bottom of the terrain (see createShape).", + " this.bodyHeight = object.getHeight();", + " return this.physics._sharedData.getRVec3(", + " object.getX() * worldInvScale,", + " (object.getY() + object.getHeight()) * worldInvScale,", + " object.getZ() * worldInvScale", + " );", + " }", + "", + " _getBodyRotation() {", + " // A quarter turn around X: heights go along Z.", + " return this.physics._sharedData.getQuat(Math.SQRT1_2, 0, 0, Math.SQRT1_2);", + " }", + "", + " createAndAddBody() {", + " const { physics } = this;", + " const { bodyInterface } = physics._sharedData;", + " const shape = this.createShape();", + " const settings = new Jolt.BodyCreationSettings(", + " shape,", + " this._getBodyPosition(),", + " this._getBodyRotation(),", + " Jolt.EMotionType_Static,", + " physics.getBodyLayer()", + " );", + " settings.mFriction = physics.friction;", + " settings.mRestitution = physics.restitution;", + " const body = bodyInterface.CreateBody(settings);", + " Jolt.destroy(settings);", + " bodyInterface.AddBody(body.GetID(), Jolt.EActivation_DontActivate);", + " return body;", + " }", + "", + " updateObjectFromBody() {", + " // The terrain is static: the body never moves it.", + " }", + "", + " updateBodyFromObject() {", + " const { physics } = this;", + " const body = physics._body;", + " if (!body) return;", + " const { object } = this.terrain;", + " const { bodyInterface } = physics._sharedData;", + " if (", + " object.getX() !== this.bodyX ||", + " object.getY() !== this.bodyY ||", + " object.getZ() !== this.bodyZ ||", + " object.getHeight() !== this.bodyHeight", + " ) {", + " bodyInterface.SetPositionAndRotation(", + " body.GetID(),", + " this._getBodyPosition(),", + " this._getBodyRotation(),", + " Jolt.EActivation_DontActivate", + " );", + " }", + " if (!this.changedSamples.isEmpty()) {", + " this._applyChangedHeights(body);", + " }", + " }", + "", + " _applyChangedHeights(body) {", + " const { data } = this.terrain;", + " const { size } = data;", + " const { minI, minJ, maxI, maxJ } = this.changedSamples;", + " // Jolt only changes blocks of samples. Rows are flipped and extra", + " // samples have no collision (see createShape).", + " const blockSize = this.heightField.GetBlockSize();", + " const sampleCount = this.heightField.GetSampleCount();", + " const alignDown = (value) => Math.floor(value / blockSize) * blockSize;", + " const alignUp = (value) => Math.min(Math.ceil(value / blockSize) * blockSize, sampleCount);", + " const firstX = alignDown(minI);", + " const firstRow = alignDown(size - 1 - maxJ);", + " const width = alignUp(maxI + 1) - firstX;", + " const height = alignUp(size - minJ) - firstRow;", + " if (width * height > this.heightsBufferLength) {", + " if (this.heightsBufferPointer) Jolt._webidl_free(this.heightsBufferPointer);", + " this.heightsBufferLength = width * height;", + " this.heightsBufferPointer = Jolt._webidl_malloc(this.heightsBufferLength * 4);", + " }", + " const heights = new Float32Array(Jolt.HEAPF32.buffer, this.heightsBufferPointer, width * height);", + " const noCollision = Jolt.HeightFieldShapeConstantValues.prototype.cNoCollisionValue;", + " for (let row = 0; row < height; row++) {", + " const j = size - 1 - (firstRow + row);", + " for (let column = 0; column < width; column++) {", + " const i = firstX + column;", + " heights[row * width + column] =", + " i < size && j >= 0 ? this._getSampleHeight(j * size + i, noCollision) : noCollision;", + " }", + " }", + " const sharedData = this.physics._sharedData;", + " this.heightField.SetHeights(", + " firstX,", + " firstRow,", + " width,", + " height,", + " this.heightsBufferPointer,", + " width,", + " sharedData.jolt.GetTempAllocator()", + " );", + " sharedData.bodyInterface.NotifyShapeChanged(", + " body.GetID(),", + " sharedData.getVec3(0, 0, 0),", + " false,", + " Jolt.EActivation_DontActivate", + " );", + " this._wakeUpBodiesOnChangedSamples();", + " this.changedSamples.clear();", + " }", + "", + " /** Bodies sleeping on the changed area would otherwise float or sink. */", + " _wakeUpBodiesOnChangedSamples() {", + " const { object, data } = this.terrain;", + " const worldInvScale = this.physics._sharedData.worldInvScale;", + " const cellWidth = object.getWidth() / data.resolution;", + " const cellHeight = object.getHeight() / data.resolution;", + " const { minI, minJ, maxI, maxJ } = this.changedSamples;", + " const min = new Jolt.Vec3(", + " (object.getX() + (minI - 1) * cellWidth) * worldInvScale,", + " (object.getY() + (minJ - 1) * cellHeight) * worldInvScale,", + " (object.getZ() - 1) * worldInvScale", + " );", + " const max = new Jolt.Vec3(", + " (object.getX() + (maxI + 1) * cellWidth) * worldInvScale,", + " (object.getY() + (maxJ + 1) * cellHeight) * worldInvScale,", + " (object.getZ() + object.getDepth() + 1) * worldInvScale", + " );", + " const box = new Jolt.AABox(min, max);", + " const broadPhaseLayerFilter = new Jolt.BroadPhaseLayerFilter();", + " const objectLayerFilter = new Jolt.ObjectLayerFilter();", + " this.physics._sharedData.bodyInterface.ActivateBodiesInAABox(box, broadPhaseLayerFilter, objectLayerFilter);", + " Jolt.destroy(min);", + " Jolt.destroy(max);", + " Jolt.destroy(box);", + " Jolt.destroy(broadPhaseLayerFilter);", + " Jolt.destroy(objectLayerFilter);", + " }", + "", + " recreateShape() {", + " const body = this.physics._body;", + " if (!body) return;", + " this.physics._sharedData.bodyInterface.SetShape(", + " body.GetID(),", + " this.createShape(),", + " false,", + " Jolt.EActivation_DontActivate", + " );", + " const { resolution } = this.terrain.data;", + " this.changedSamples.set(0, 0, resolution, resolution);", + " this._wakeUpBodiesOnChangedSamples();", + " this.changedSamples.clear();", + " }", + "", + " destroyBody() {", + " this.defaultBodyUpdater.destroyBody();", + " this.heightField = null;", + " }", + "", + " dispose() {", + " if (this.heightsBufferPointer) Jolt._webidl_free(this.heightsBufferPointer);", + " this.heightsBufferPointer = 0;", + " this.heightsBufferLength = 0;", + " }", + "}", + "", + "/** @param {TerrainData} data */", + "const copyTerrainData = (data) => {", + " const copy = new TerrainData(data.resolution);", + " copy.copyFrom(data);", + " return copy;", + "};", + "", + "// Generating a relief or reading an image is long for big terrains, and done", + "// for every instance and every hot-reload: the results are kept.", + "const baseDataCache = new Map();", + "const MAX_CACHED_BASE_DATA = 8;", + "// Changes of the shape of a terrain kept for consumers of its surface that", + "// missed some versions: older changes update the whole surface.", + "const MAX_SURFACE_CHANGES = 64;", + "// Unique among all terrains: a terrain replaced by another one never has its version.", + "let lastGroundVersion = 0;", + "", + "/**", + " * The terrain of a Terrain3D object: its data, how it's drawn and its", + " * surface (for collisions and navigation meshes). Positions are in the scene.", + " */", + "class Terrain {", + " /** @param {gdjs.CustomRuntimeObject3D} object */", + " constructor(object) {", + " this.object = object;", + " /** @type {TerrainData} */", + " this.data = new TerrainData(DEFAULT_RESOLUTION);", + " /** The data before any sculpting or painting, from the relief or heightmap. */", + " this.baseData = this.data;", + " this.renderer = new TerrainRenderer(this);", + " /** @type {TerrainBodyUpdater[]} */", + " this.bodyUpdaters = [];", + " this.changedSamples = new SampleRectangle();", + " this.hasPaintChanged = false;", + " /** Increased each time the ground changes, for objects following it (like grass). */", + " this.version = 0;", + " /** Increased each time the shape of the ground changes (not its paint or position). */", + " this.surfaceVersion = 0;", + " /**", + " * The last changes of the shape of the ground, for `getChangedArea`.", + " * @type {Array<{version: number, samples: SampleRectangle}>}", + " */", + " this.surfaceChanges = [];", + " /** The changes since this version are all in `surfaceChanges`. */", + " this.surfaceChangesStartVersion = 0;", + " this._stroke = { cellWidth: 1, cellHeight: 1, ax: 0, ay: 0, bx: 0, by: 0, radius: 0 };", + " this._strokeChangedSamples = new SampleRectangle();", + " /** The properties the data was loaded from, to reload it only if they change. */", + " this.loadedFrom = '';", + " /** The heightmap image being loaded: the relief is used until it is. */", + " this.loadingHeightmapImage = '';", + " this.isDisposed = false;", + " /** The last value of the sculpt data property read or saved. */", + " this.sculptData = '';", + " /**", + " * Values saved by the editor tools: the editor sends them back with a", + " * hot-reload, possibly after newer ones were saved.", + " * @type {string[]}", + " */", + " this.savedSculptData = [];", + " /** The heights of the surface: the heights of the data, and NaN in holes. */", + " this.surfaceHeights = new Float32Array(0);", + " this.loadFromProperties();", + " this._transform = this._getTransform();", + " getGrounds(object.getRuntimeScene()).add(this);", + " /**", + " * The shape of the ground, for the physics engine and navigation meshes.", + " * @type {gdjs.Surface}", + " */", + " this.surface = {", + " getVersion: () => this.surfaceVersion,", + " getChangedArea: (sinceVersion) => this.getChangedArea(sinceVersion),", + " getTriangles: () => null,", + " getHeightField: () => ({ columns: this.data.size, rows: this.data.size, heights: this.surfaceHeights }),", + " };", + " if (typeof object.setSurface === 'function') {", + " object.setSurface(this.surface);", + " } else if (gdjs.Physics3DRuntimeBehavior) {", + " // Older GDevelop versions don't use surfaces.", + " for (const behavior of object._behaviors) {", + " if (behavior instanceof gdjs.Physics3DRuntimeBehavior) {", + " this.bodyUpdaters.push(new TerrainBodyUpdater(this, behavior));", + " }", + " }", + " }", + " }", + "", + " _getSourceProperties() {", + " const { object } = this;", + " return [", + " object._getResolution(),", + " object._getRelief(),", + " object._getSeed(),", + " object._getHeightmapImage(),", + " object._getEdits(),", + " ].join('|');", + " }", + "", + " loadFromProperties() {", + " const { object } = this;", + " const resolution = parseInt(object._getResolution(), 10);", + " const validResolution = RESOLUTIONS.indexOf(resolution) !== -1 ? resolution : DEFAULT_RESOLUTION;", + " this.sculptData = object._getSculptData();", + " this.loadedFrom = this._getSourceProperties();", + "", + " this.baseData = this._getBaseData(validResolution);", + " let data = codec.decode(this.sculptData, (resolution) => copyTerrainData(this._getBaseData(resolution)));", + " if (data && data.resolution !== validResolution) {", + " data = data.resampled(validResolution);", + " }", + " this.setData(data || copyTerrainData(this.baseData));", + " }", + "", + " /**", + " * @param {number} resolution", + " * @returns {TerrainData} The data from the relief or the heightmap, shared: it must not be changed.", + " */", + " _getBaseData(resolution) {", + " const { object } = this;", + " const key = [resolution, object._getRelief(), object._getSeed(), object._getHeightmapImage(), object._getEdits()].join('|');", + " const cachedData = baseDataCache.get(key);", + " if (cachedData) return cachedData;", + "", + " const data = new TerrainData(resolution);", + " const heightmapImage = object._getHeightmapImage();", + " const isHeightmapLoaded =", + " !!heightmapImage && loadHeightsFromImage(data, object.getRuntimeScene().getGame(), heightmapImage);", + " if (!isHeightmapLoaded) {", + " relief.generate(data, object._getRelief(), object._getSeed());", + " if (heightmapImage) this._reloadOnceLoaded(heightmapImage);", + " }", + " edits.apply(data, object._getEdits());", + " // Not kept if the image is missing: it may be loaded later.", + " if (isHeightmapLoaded || !heightmapImage) baseDataCache.set(key, data);", + " if (baseDataCache.size > MAX_CACHED_BASE_DATA) {", + " baseDataCache.delete(baseDataCache.keys().next().value);", + " }", + " return data;", + " }", + "", + " /** @param {TerrainData} data */", + " setData(data) {", + " this.version = ++lastGroundVersion;", + " this.surfaceVersion = this.version;", + " this.surfaceChanges.length = 0;", + " this.surfaceChangesStartVersion = this.version;", + " const hasSameResolution = data.resolution === this.data.resolution && this.renderer.chunks.length > 0;", + " this.data = data;", + " const allSamples = new SampleRectangle().set(0, 0, data.resolution, data.resolution);", + " this.surfaceHeights = new Float32Array(data.heights.length);", + " this.renderer.setHasHoles(false);", + " this._updateSurfaceHeights(allSamples);", + " if (hasSameResolution) {", + " this.renderer.updateSamples(allSamples, true);", + " } else {", + " this.renderer.rebuild();", + " }", + " for (const bodyUpdater of this.bodyUpdaters) {", + " bodyUpdater.recreateShape();", + " }", + " }", + "", + " onHotReloading() {", + " const sculptData = this.object._getSculptData();", + " const savedIndex = this.savedSculptData.indexOf(sculptData);", + " if (savedIndex !== -1 && this._getSourceProperties() === this.loadedFrom) {", + " // The terrain already has this data, or newer changes.", + " this.savedSculptData.splice(0, savedIndex + 1);", + " } else if (sculptData !== this.sculptData || this._getSourceProperties() !== this.loadedFrom) {", + " this.savedSculptData.length = 0;", + " this.loadFromProperties();", + " }", + " this.renderer.updateMaterial();", + " }", + "", + " /** @returns {string} The data to save in the sculpt data property. */", + " save() {", + " this.sculptData = codec.encode(this.data, this.baseData);", + " this.savedSculptData.push(this.sculptData);", + " return this.sculptData;", + " }", + "", + " getCellWidth() {", + " return this.object.getWidth() / this.data.resolution;", + " }", + "", + " getCellHeight() {", + " return this.object.getHeight() / this.data.resolution;", + " }", + "", + " _toGridX(x) {", + " return (x - this.object.getX()) / this.getCellWidth();", + " }", + "", + " _toGridY(y) {", + " return (y - this.object.getY()) / this.getCellHeight();", + " }", + "", + " /** @returns {number} The Z position of the ground. */", + " getHeightAt(x, y) {", + " return this.object.getZ() + this.data.sampleHeight(this._toGridX(x), this._toGridY(y)) * this.object.getDepth();", + " }", + "", + " /** @returns {number} The angle of the ground with the horizontal, in degrees. */", + " getSlopeAt(x, y) {", + " const gridX = this._toGridX(x);", + " const gridY = this._toGridY(y);", + " const { data } = this;", + " const depth = this.object.getDepth();", + " const slopeX = ((data.sampleHeight(gridX + 0.5, gridY) - data.sampleHeight(gridX - 0.5, gridY)) * depth) / this.getCellWidth();", + " const slopeY = ((data.sampleHeight(gridX, gridY + 0.5) - data.sampleHeight(gridX, gridY - 0.5)) * depth) / this.getCellHeight();", + " return gdjs.toDegrees(Math.atan(Math.hypot(slopeX, slopeY)));", + " }", + "", + " /** @returns {number} The layer painted the most, from 1 to 4. */", + " getLayerAt(x, y) {", + " return this.data.getDominantLayer(this._toGridX(x), this._toGridY(y)) + 1;", + " }", + "", + " /**", + " * @param {number} layer From 1 to 4.", + " * @returns {number} How much the layer is painted, from 0 to 1.", + " */", + " getLayerWeightAt(x, y, layer) {", + " const layerIndex = clampInteger(layer - 1, 0, LAYER_COUNT - 1);", + " return this.data.sampleLayerWeight(this._toGridX(x), this._toGridY(y), layerIndex);", + " }", + "", + " /** @returns {number[]} The painted tint: red, green and blue, from 0 to 255 (white when there is none). */", + " getTintAt(x, y) {", + " const gridX = this._toGridX(x);", + " const gridY = this._toGridY(y);", + " return [0, 1, 2].map((channel) => Math.round(this.data.sampleColor(gridX, gridY, channel) * 255));", + " }", + "", + " /** @returns {number} How wet the ground is, from 0 (dry) to 1, painted or near the water (see the wet shore). */", + " getWetnessAt(x, y) {", + " const paintedWetness = this.data.sampleColor(this._toGridX(x), this._toGridY(y), 3);", + " const { object } = this;", + " if (!object._getWetShore()) return paintedWetness;", + " const fade = Math.max(object._getWetShoreFade(), 0.001);", + " const shoreWetness = clamp01((object._getWetShoreZ() + fade - this.getHeightAt(x, y)) / fade);", + " // Like the smoothstep of the shader.", + " return Math.max(paintedWetness, shoreWetness * shoreWetness * (3 - 2 * shoreWetness));", + " }", + "", + " _isInArea(x, y) {", + " const { object } = this;", + " return (", + " x >= object.getX() &&", + " x <= object.getX() + object.getWidth() &&", + " y >= object.getY() &&", + " y <= object.getY() + object.getHeight()", + " );", + " }", + "", + " /** @returns {boolean} true if the position is above or under the terrain, and not in a hole. */", + " containsPoint(x, y) {", + " return this._isInArea(x, y) && this.data.sampleSolidity(this._toGridX(x), this._toGridY(y)) >= 0.5;", + " }", + "", + " /** @returns {boolean} true if there is a hole in the terrain at the position, like what is drawn. */", + " isHoleAt(x, y) {", + " return this._isInArea(x, y) && this.data.sampleSolidity(this._toGridX(x), this._toGridY(y)) < 0.5;", + " }", + "", + " _getStroke(x1, y1, x2, y2, radius) {", + " const stroke = this._stroke;", + " stroke.cellWidth = this.getCellWidth();", + " stroke.cellHeight = this.getCellHeight();", + " stroke.ax = x1 - this.object.getX();", + " stroke.ay = y1 - this.object.getY();", + " stroke.bx = x2 - this.object.getX();", + " stroke.by = y2 - this.object.getY();", + " stroke.radius = Math.max(radius, 0);", + " return stroke;", + " }", + "", + " /**", + " * @param {SampleRectangle} samples", + " * @param {boolean} haveHeightsChanged false when only the paint changed.", + " */", + " _onSamplesChanged(samples, haveHeightsChanged) {", + " if (samples.isEmpty()) return;", + " this.version = ++lastGroundVersion;", + " this.changedSamples.add(samples);", + " // Painting changes the surface when it makes holes or fills them.", + " const hasSurfaceChanged = this._updateSurfaceHeights(samples);", + " if (!haveHeightsChanged && !hasSurfaceChanged) return;", + " this.surfaceVersion = this.version;", + " this.surfaceChanges.push({ version: this.version, samples: new SampleRectangle().set(samples.minI, samples.minJ, samples.maxI, samples.maxJ) });", + " if (this.surfaceChanges.length > MAX_SURFACE_CHANGES) {", + " this.surfaceChangesStartVersion = this.surfaceChanges.shift().version;", + " }", + " for (const bodyUpdater of this.bodyUpdaters) {", + " bodyUpdater.changedSamples.add(samples);", + " }", + " }", + "", + " /**", + " * @param {SampleRectangle} samples", + " * @returns {boolean} true if heights of the surface changed, or holes were made or filled.", + " */", + " _updateSurfaceHeights(samples) {", + " const { data, surfaceHeights } = this;", + " let hasChanged = false;", + " let hasHoles = false;", + " for (let j = samples.minJ; j <= samples.maxJ; j++) {", + " for (let i = samples.minI; i <= samples.maxI; i++) {", + " const index = j * data.size + i;", + " const isHole = data.isHole(index);", + " const height = isHole ? NaN : data.heights[index];", + " const previousHeight = surfaceHeights[index];", + " if (isHole) {", + " hasHoles = true;", + " if (Number.isNaN(previousHeight)) continue;", + " } else if (height === previousHeight) {", + " continue;", + " }", + " surfaceHeights[index] = height;", + " hasChanged = true;", + " }", + " }", + " // Filled holes keep the shaders drawing holes until the data is loaded again.", + " if (hasHoles) this.renderer.setHasHoles(true);", + " return hasChanged;", + " }", + "", + " /** @param {number} height In scene units, negative to lower the ground. */", + " raise(x1, y1, x2, y2, radius, height) {", + " const samples = this._strokeChangedSamples;", + " brushes.raise(this.data, this._getStroke(x1, y1, x2, y2, radius), height / this.object.getDepth(), samples);", + " this._onSamplesChanged(samples, true);", + " }", + "", + " /** @param {number} z The Z position to flatten the ground to. */", + " /** @param {number} [toZ] The Z position at the end of the segment, for a slope. The same as z by default. */", + " flatten(x1, y1, x2, y2, radius, z, strength, toZ = z) {", + " const samples = this._strokeChangedSamples;", + " const toHeight = (value) => (value - this.object.getZ()) / this.object.getDepth();", + " brushes.flatten(this.data, this._getStroke(x1, y1, x2, y2, radius), toHeight(z), strength, samples, toHeight(toZ));", + " this._onSamplesChanged(samples, true);", + " }", + "", + " smooth(x1, y1, x2, y2, radius, strength) {", + " const samples = this._strokeChangedSamples;", + " brushes.smooth(this.data, this._getStroke(x1, y1, x2, y2, radius), strength, samples);", + " this._onSamplesChanged(samples, true);", + " }", + "", + " /** @param {number} layer From 1 to 4. */", + " paint(x1, y1, x2, y2, radius, layer, strength) {", + " const layerIndex = clampInteger(layer - 1, 0, LAYER_COUNT - 1);", + " const samples = this._strokeChangedSamples;", + " brushes.paint(this.data, this._getStroke(x1, y1, x2, y2, radius), layerIndex, strength, samples);", + " this.hasPaintChanged = true;", + " this._onSamplesChanged(samples, false);", + " }", + "", + " /** @param {number[]} color The red, green and blue (0-255) of the tint: white removes it. */", + " tint(x1, y1, x2, y2, radius, color, strength) {", + " const samples = this._strokeChangedSamples;", + " brushes.tint(this.data, this._getStroke(x1, y1, x2, y2, radius), color, strength, samples);", + " this.hasPaintChanged = true;", + " this._onSamplesChanged(samples, false);", + " }", + "", + " /** @param {number} wetness From 0 (dry) to 1. */", + " wet(x1, y1, x2, y2, radius, wetness, strength) {", + " const samples = this._strokeChangedSamples;", + " brushes.wet(this.data, this._getStroke(x1, y1, x2, y2, radius), wetness, strength, samples);", + " this.hasPaintChanged = true;", + " this._onSamplesChanged(samples, false);", + " }", + "", + " /** Make a hole around a segment (`fillHoles` fills it again). */", + " makeHole(x1, y1, x2, y2, radius) {", + " const samples = this._strokeChangedSamples;", + " brushes.hole(this.data, this._getStroke(x1, y1, x2, y2, radius), samples);", + " this.hasPaintChanged = true;", + " this._onSamplesChanged(samples, false);", + " }", + "", + " /** Fill the holes around a segment with the layers of the ground around them. */", + " fillHoles(x1, y1, x2, y2, radius) {", + " const samples = this._strokeChangedSamples;", + " brushes.fill(this.data, this._getStroke(x1, y1, x2, y2, radius), samples);", + " this.hasPaintChanged = true;", + " this._onSamplesChanged(samples, false);", + " }", + "", + " /**", + " * Make the entrance of a tunnel or a cave: a hole, with the ground in", + " * front of it flattened to the floor of the tunnel.", + " * @param {number} angle The direction going into the ground, in degrees.", + " * @param {number} floorZ", + " * @param {number} length The length of the flat ground in front of the entrance.", + " */", + " carveEntrance(x, y, radius, angle, floorZ, length) {", + " const directionX = Math.cos(gdjs.toRad(angle));", + " const directionY = Math.sin(gdjs.toRad(angle));", + " this.flatten(x - directionX * length, y - directionY * length, x, y, radius * 1.5, floorZ, 1);", + " this.makeHole(x, y, x + directionX * radius, y + directionY * radius, radius);", + " }", + "", + " generate(reliefName, seed) {", + " this.baseData = new TerrainData(this.data.resolution);", + " relief.generate(this.baseData, reliefName, seed);", + " this.setData(copyTerrainData(this.baseData));", + " }", + "", + " loadHeightmap(imageResourceName) {", + " const baseData = new TerrainData(this.data.resolution);", + " if (loadHeightsFromImage(baseData, this.object.getRuntimeScene().getGame(), imageResourceName)) {", + " baseData.splat.set(this.data.splat);", + " baseData.colors.set(this.data.colors);", + " this.baseData = baseData;", + " this.setData(copyTerrainData(baseData));", + " }", + " }", + "", + " /** @param {gdjs.RuntimeObject[]} objects */", + " placeOnGround(objects) {", + " for (const object of objects) {", + " if (object === this.object || !gdjs.Base3DHandler.is3D(object)) continue;", + " const groundZ = this.getHeightAt(object.getCenterXInScene(), object.getCenterYInScene());", + " // The origin of the object is not always at its bottom.", + " object.setZ(groundZ + object.getZ() - object.getUnrotatedAABBMinZ());", + " }", + " }", + "", + " /**", + " * @returns {THREE.Vector3 | null} The point of the ground under the cursor,", + " * in the terrain group coordinates. Holes are hit too, so that brushes can fill them.", + " */", + " raycastFromCursor(game, raycaster, ndc) {", + " const layer = this.object.getInstanceContainer().getLayer(this.object.getLayer());", + " const camera = layer.getRenderer().getThreeCamera();", + " if (!camera) return null;", + " const inputManager = game.getInputManager();", + " ndc.set(", + " (inputManager.getCursorX() / game.getGameResolutionWidth()) * 2 - 1,", + " -(inputManager.getCursorY() / game.getGameResolutionHeight()) * 2 + 1", + " );", + " raycaster.setFromCamera(ndc, camera);", + " this.renderer.group.updateWorldMatrix(true, false);", + " const ray = raycaster.ray.clone().applyMatrix4(this.renderer.group.matrixWorld.clone().invert());", + " ray.direction.normalize();", + "", + " const box = new THREE.Box3(new THREE.Vector3(0, 0, 0), new THREE.Vector3(AREA_SIZE, AREA_SIZE, AREA_DEPTH));", + " const entry = box.containsPoint(ray.origin) ? ray.origin.clone() : ray.intersectBox(box, new THREE.Vector3());", + " if (!entry) return null;", + " const cellSize = AREA_SIZE / this.data.resolution;", + " const groundZAt = (point) => this.data.sampleHeight(point.x / cellSize, point.y / cellSize) * AREA_DEPTH;", + " const step = cellSize / 2;", + " const point = entry.clone();", + " const previousPoint = entry.clone();", + " for (let distance = 0; distance < AREA_SIZE * 2; distance += step) {", + " if (point.z <= groundZAt(point)) {", + " // Refine between the last point above the ground and this one.", + " for (let iteration = 0; iteration < 8; iteration++) {", + " const middle = previousPoint.clone().lerp(point, 0.5);", + " if (middle.z <= groundZAt(middle)) point.copy(middle);", + " else previousPoint.copy(middle);", + " }", + " return point;", + " }", + " if (!box.containsPoint(point)) return null;", + " previousPoint.copy(point);", + " point.addScaledVector(ray.direction, step);", + " }", + " return null;", + " }", + "", + " /**", + " * @param {THREE.Vector3} localPosition In the terrain group coordinates (like from `raycastFromCursor`).", + " * @returns {{x: number, y: number, z: number}} The position in the scene.", + " */", + " toScenePosition(localPosition) {", + " const { object } = this;", + " return {", + " x: object.getX() + (localPosition.x / AREA_SIZE) * object.getWidth(),", + " y: object.getY() + (localPosition.y / AREA_SIZE) * object.getHeight(),", + " z: object.getZ() + (localPosition.z / AREA_DEPTH) * object.getDepth(),", + " };", + " }", + "", + " /** Draw the changes of the ground (collisions are updated by the physics engine). */", + " applyChanges() {", + " this.renderer.updateSamples(this.changedSamples, this.hasPaintChanged);", + " this.changedSamples.clear();", + " this.hasPaintChanged = false;", + " }", + "", + " /** @returns {number} A number changing each time the ground changes (see `getGrounds`). */", + " getVersion() {", + " return this.version;", + " }", + "", + " /**", + " * @param {number} sinceVersion", + " * @returns {gdjs.SurfaceArea | null} The part of the ground whose shape changed since the version (see `gdjs.Surface`).", + " */", + " getChangedArea(sinceVersion) {", + " if (sinceVersion < this.surfaceChangesStartVersion) return null;", + " const samples = new SampleRectangle();", + " for (const change of this.surfaceChanges) {", + " if (change.version > sinceVersion) samples.add(change.samples);", + " }", + " if (samples.isEmpty()) return { minX: 0, minY: 0, maxX: 0, maxY: 0 };", + " const { resolution } = this.data;", + " return {", + " minX: samples.minI / resolution,", + " minY: samples.minJ / resolution,", + " maxX: samples.maxI / resolution,", + " maxY: samples.maxJ / resolution,", + " };", + " }", + "", + " _getTransform() {", + " const { object } = this;", + " return [object.getX(), object.getY(), object.getZ(), object.getWidth(), object.getHeight(), object.getDepth()].join(',');", + " }", + "", + " /** @param {string} imageResourceName A heightmap image that is not loaded yet. */", + " _reloadOnceLoaded(imageResourceName) {", + " if (this.loadingHeightmapImage === imageResourceName) return;", + " this.loadingHeightmapImage = imageResourceName;", + " const game = this.object.getRuntimeScene().getGame();", + " game.getImageManager()", + " .loadResource(imageResourceName)", + " .then(() => {", + " if (this.isDisposed || this.loadingHeightmapImage !== imageResourceName) return;", + " this.loadingHeightmapImage = '';", + " if (getLoadedImage(game, imageResourceName)) this.loadFromProperties();", + " })", + " .catch(() => {});", + " }", + "", + " update() {", + " const transform = this._getTransform();", + " if (transform !== this._transform) {", + " this._transform = transform;", + " this.version = ++lastGroundVersion;", + " }", + " this.applyChanges();", + " this.renderer.uniforms.terrainWorldScale.value = getWorldScale(this.object);", + " this._updateWetShore();", + " const layer = this.object.getInstanceContainer().getLayer(this.object.getLayer());", + " this.renderer.updateLevelsOfDetail(layer.getRenderer().getThreeCamera());", + " }", + "", + " _updateWetShore() {", + " const { object } = this;", + " // In the coordinates of the terrain group, like the vertices.", + " const depthScale = AREA_DEPTH / Math.max(object.getDepth(), 0.0001);", + " this.renderer.uniforms.terrainWetShore.value.set(", + " (object._getWetShoreZ() - object.getZ()) * depthScale,", + " Math.max(object._getWetShoreFade(), 0.001) * depthScale,", + " object._getWetShore() ? 1 : 0", + " );", + " }", + "", + " dispose() {", + " this.isDisposed = true;", + " getGrounds(this.object.getRuntimeScene()).delete(this);", + " if (typeof this.object.setSurface === 'function' && this.object.getSurface() === this.surface) {", + " this.object.setSurface(null);", + " }", + " this.renderer.dispose();", + " for (const bodyUpdater of this.bodyUpdaters) {", + " bodyUpdater.dispose();", + " }", + " }", + "}", + "", + "/**", + " * @param {gdjs.RuntimeObject} object", + " * @returns {number} The number of scene units by 3D world unit (1 in GDevelop versions before the world scale).", + " */", + "const getWorldScale = (object) => {", + " const scene = object.getRuntimeScene();", + " return typeof scene.getRenderer3DWorldScale === 'function' ? scene.getRenderer3DWorldScale() : 1;", + "};", + "", + "const averageColors = new WeakMap();", + "let white = null;", + "/**", + " * @param {THREE.Texture} texture", + " * @returns {THREE.Color} The average color of the texture (white when it's not loaded).", + " */", + "const getAverageColor = (texture) => {", + " const cachedColor = averageColors.get(texture);", + " if (cachedColor) return cachedColor;", + " const image = texture.image;", + " const isDrawable =", + " typeof document !== 'undefined' &&", + " ((image instanceof HTMLImageElement && image.complete && image.naturalWidth > 0) ||", + " image instanceof HTMLCanvasElement ||", + " (typeof ImageBitmap !== 'undefined' && image instanceof ImageBitmap));", + " if (!isDrawable) return white || (white = new THREE.Color(1, 1, 1));", + " const size = 8;", + " const canvas = document.createElement('canvas');", + " canvas.width = size;", + " canvas.height = size;", + " const context = canvas.getContext('2d');", + " context.drawImage(image, 0, 0, size, size);", + " const pixels = context.getImageData(0, 0, size, size).data;", + " let red = 0;", + " let green = 0;", + " let blue = 0;", + " for (let index = 0; index < pixels.length; index += 4) {", + " red += pixels[index];", + " green += pixels[index + 1];", + " blue += pixels[index + 2];", + " }", + " const count = size * size * 255;", + " const color = new THREE.Color().setRGB(red / count, green / count, blue / count, THREE.SRGBColorSpace);", + " averageColors.set(texture, color);", + " return color;", + "};", + "", + "const svgIcon = (content) =>", + " 'data:image/svg+xml,' +", + " encodeURIComponent('' + content + '');", + "const strokes = 'fill=\"none\" stroke=\"#000\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"';", + "const toolIcons = {", + " Raise: svgIcon(''),", + " Lower: svgIcon(''),", + " Smooth: svgIcon(''),", + " Flatten: svgIcon(''),", + " Size: svgIcon(''),", + " Strength: svgIcon(''),", + " Undo: svgIcon(''),", + " Hole: svgIcon(''),", + " FillHoles: svgIcon(''),", + " Wetness: svgIcon(''),", + "};", + "// Tints for the color brush when the editor has no color picker (older GDevelop versions).", + "const tintPresets = ['#e2c879', '#a5c45a', '#8a6a48', '#c98a62', '#9a9a92', '#ffffff'];", + "", + "/**", + " * The sculpt and paint tools shown in the scene editor when a terrain is", + " * selected. Without the needed editor functions (older GDevelop versions),", + " * nothing is shown: the terrain can still be changed with events.", + " */", + "class TerrainEditorTools {", + " constructor() {", + " /** '' when no brush is used. */", + " this.toolName = '';", + " this.sizePercent = 30;", + " this.strengthPercent = 50;", + " /** @type {Terrain | null} */", + " this.terrain = null;", + " /** @type {gdjs.InGameEditor | null} */", + " this.editor = null;", + " this.isStroking = false;", + " this.flattenHeight = 0;", + " /** The color of the \"Tint\" brush, as \"#rrggbb\". */", + " this.tintColor = tintPresets[0];", + " this.wetnessPercent = 100;", + " this.lastStrokeTime = 0;", + " /** @type {{data: TerrainData, baseData: TerrainData}[]} */", + " this.undoSteps = [];", + " this.raycaster = new THREE.Raycaster();", + " this.ndc = new THREE.Vector2();", + " this._swatchColor = new THREE.Color();", + " }", + "", + " /** @param {gdjs.InGameEditor} editor */", + " update(editor) {", + " if (typeof editor.showToolbar !== 'function') {", + " // Older GDevelop versions: the terrain can only be changed with events.", + " return;", + " }", + " this.editor = editor;", + " const selectedObjects = editor.getSelectedObjects();", + " const terrain =", + " selectedObjects.length === 1 && selectedObjects[0].__terrain3D ? selectedObjects[0].__terrain3D : null;", + " if (terrain !== this.terrain) {", + " this._endStroke();", + " if (this.terrain) this.terrain.renderer.setBrush(null, 0);", + " this.terrain = terrain;", + " this.undoSteps.length = 0;", + " }", + " if (terrain) editor.showToolbar('Terrain3D', this._getToolbarItems(terrain));", + " const game = editor.getRuntimeGame();", + " if (!terrain || !this.toolName) {", + " if (terrain) terrain.renderer.setBrush(null, 0);", + " return;", + " }", + "", + " const inputManager = game.getInputManager();", + " if (inputManager.wasKeyJustPressed(gdjs.evtTools.input.keysNameToCode.Escape)) {", + " this._endStroke();", + " this.toolName = '';", + " terrain.renderer.setBrush(null, 0);", + " return;", + " }", + " editor.captureLeftMouseButton();", + " const hit = terrain.raycastFromCursor(game, this.raycaster, this.ndc);", + " const radius = this._getRadius();", + " terrain.renderer.setBrush(hit, radius);", + "", + " // Older GDevelop versions don't have it.", + " const isPressedForTool =", + " typeof editor.isCapturedLeftMouseButtonPressed === 'function'", + " ? editor.isCapturedLeftMouseButtonPressed()", + " : inputManager.isMouseButtonPressed(0);", + " // The button is released outside the canvas when the mouse is on a toolbar.", + " const isPressed = isPressedForTool && inputManager.isMouseInsideCanvas();", + " if (isPressed && hit && !this.isStroking) {", + " this._startStroke(terrain, hit);", + " }", + " if (this.isStroking && isPressed && hit) {", + " this._applyBrush(terrain, hit, radius);", + " }", + " if (!isPressed) {", + " this._endStroke();", + " }", + " }", + "", + " _getRadius() {", + " const { object } = this.terrain;", + " const maxRadius = Math.max(object.getWidth(), object.getHeight()) / 4;", + " return Math.max(maxRadius * Math.pow(this.sizePercent / 100, 2), this.terrain.getCellWidth());", + " }", + "", + " _startStroke(terrain, hit) {", + " this.isStroking = true;", + " this.lastStrokeTime = performance.now();", + " this.flattenHeight = terrain.toScenePosition(hit).z;", + " this.undoSteps.push({ data: copyTerrainData(terrain.data), baseData: terrain.baseData });", + " const maxUndoSteps = Math.max(Math.floor(UNDO_STEPS_SAMPLES / terrain.data.heights.length), 2);", + " while (this.undoSteps.length > maxUndoSteps) this.undoSteps.shift();", + " }", + "", + " _endStroke() {", + " if (!this.isStroking) return;", + " this.isStroking = false;", + " this._save();", + " }", + "", + " _applyBrush(terrain, hit, radius) {", + " const now = performance.now();", + " const elapsedSeconds = Math.min((now - this.lastStrokeTime) / 1000, 0.1);", + " this.lastStrokeTime = now;", + " const { x, y } = terrain.toScenePosition(hit);", + " const amount = (this.strengthPercent / 100) * elapsedSeconds;", + " const depth = terrain.object.getDepth();", + " if (this.toolName === 'Raise') terrain.raise(x, y, x, y, radius, amount * depth * 0.5);", + " else if (this.toolName === 'Lower') terrain.raise(x, y, x, y, radius, -amount * depth * 0.5);", + " else if (this.toolName === 'Smooth') terrain.smooth(x, y, x, y, radius, amount * 10);", + " else if (this.toolName === 'Flatten') terrain.flatten(x, y, x, y, radius, this.flattenHeight, amount * 10);", + " else if (this.toolName.startsWith('Paint')) {", + " terrain.paint(x, y, x, y, radius, parseInt(this.toolName.slice(5), 10), amount * 8);", + " } else if (this.toolName === 'Hole') {", + " terrain.makeHole(x, y, x, y, radius);", + " } else if (this.toolName === 'FillHoles') {", + " terrain.fillHoles(x, y, x, y, radius);", + " } else if (this.toolName === 'Tint') {", + " terrain.tint(x, y, x, y, radius, gdjs.rgbOrHexToRGBColor(this.tintColor), amount * 8);", + " } else if (this.toolName === 'Wetness') {", + " terrain.wet(x, y, x, y, radius, this.wetnessPercent / 100, amount * 8);", + " }", + " // Drawn now, as the terrain was updated earlier in this frame.", + " terrain.applyChanges();", + " }", + "", + " _save() {", + " const { terrain, editor } = this;", + " if (!terrain || !editor) return;", + " editor.updateObjectProperties(terrain.object.getName(), { SculptData: terrain.save() });", + " }", + "", + " _undo() {", + " const undoStep = this.undoSteps.pop();", + " // The relief, the edits or the resolution may have changed since the stroke.", + " if (!this.terrain || !undoStep || undoStep.baseData !== this.terrain.baseData) {", + " this.undoSteps.length = 0;", + " return;", + " }", + " this.terrain.setData(undoStep.data);", + " this._save();", + " }", + "", + " /** @returns {Array} The settings of the tint or wetness brush, when it's used. */", + " _getColorToolItems() {", + " const { editor } = this;", + " if (this.toolName === 'Tint') {", + " if (editor && typeof editor.isToolbarItemTypeSupported === 'function' && editor.isToolbarItemTypeSupported('color')) {", + " return [", + " {", + " type: 'color',", + " id: 'TintColor',", + " tooltip: 'Color of the tint',", + " value: this.tintColor,", + " onChange: (color) => (this.tintColor = color),", + " },", + " ];", + " }", + " return tintPresets.map((color) => ({", + " type: 'button',", + " id: 'TintPreset' + color,", + " tooltip: color === '#ffffff' ? 'No tint' : 'Tint with this color',", + " color,", + " isActive: this.tintColor === color,", + " onClick: () => (this.tintColor = color),", + " }));", + " }", + " if (this.toolName === 'Wetness') {", + " return [", + " {", + " type: 'slider',", + " id: 'WetnessAmount',", + " tooltip: 'Wetness',", + " iconUrl: toolIcons.Wetness,", + " min: 0,", + " max: 100,", + " value: this.wetnessPercent,", + " onChange: (value) => (this.wetnessPercent = value),", + " },", + " ];", + " }", + " return [];", + " }", + "", + " /**", + " * @param {Terrain} terrain", + " * @returns {Array} The toolbar for the current state (the editor only", + " * updates what changed).", + " */", + " _getToolbarItems(terrain) {", + " const { object } = terrain;", + " const toolButton = (toolName, tooltip, color) => ({", + " type: 'button',", + " id: toolName,", + " tooltip,", + " iconUrl: toolIcons[toolName],", + " color,", + " isActive: this.toolName === toolName,", + " onClick: () => (this.toolName = this.toolName === toolName ? '' : toolName),", + " });", + " const layerColors = [object._getLayer1Color(), object._getLayer2Color(), object._getLayer3Color(), object._getLayer4Color()];", + " // Layers are shown with their color, tinting their texture.", + " const swatchColors = layerColors.map((color, index) =>", + " this._swatchColor", + " .set(gdjs.rgbOrHexStringToNumber(color))", + " .multiply(getAverageColor(terrain.renderer.uniforms['terrainLayerMap' + index].value))", + " .getHexString()", + " );", + " return [", + " toolButton('Raise', 'Raise the ground (drag on the terrain)'),", + " toolButton('Lower', 'Lower the ground'),", + " toolButton('Smooth', 'Smooth the ground'),", + " toolButton('Flatten', 'Flatten the ground to the height where the drag starts'),", + " { type: 'divider', id: 'PaintDivider' },", + " ...swatchColors.map((swatchColor, index) =>", + " toolButton('Paint' + (index + 1), 'Paint layer ' + (index + 1), '#' + swatchColor)", + " ),", + " toolButton('Hole', 'Make a hole, for the entrance of a tunnel or a cave'),", + " toolButton('FillHoles', 'Fill holes, with the layers of the ground around them'),", + " { type: 'divider', id: 'ColorDivider' },", + " toolButton('Tint', 'Tint the ground (white removes the tint)', this.tintColor),", + " toolButton('Wetness', 'Make the ground wet, or dry with a wetness of 0'),", + " ...this._getColorToolItems(),", + " { type: 'divider', id: 'BrushDivider' },", + " {", + " type: 'slider',", + " id: 'Size',", + " tooltip: 'Brush size',", + " iconUrl: toolIcons.Size,", + " min: 1,", + " max: 100,", + " value: this.sizePercent,", + " onChange: (value) => (this.sizePercent = value),", + " },", + " {", + " type: 'slider',", + " id: 'Strength',", + " tooltip: 'Brush strength',", + " iconUrl: toolIcons.Strength,", + " min: 5,", + " max: 100,", + " value: this.strengthPercent,", + " onChange: (value) => (this.strengthPercent = value),", + " },", + " { type: 'button', id: 'Undo', tooltip: 'Undo the last stroke on this terrain', iconUrl: toolIcons.Undo, onClick: () => this._undo() },", + " ];", + " }", + "}", + "", + "const editorTools = new TerrainEditorTools();", + "if (gdjs.registerInGameEditorPostStepCallback) {", + " gdjs.registerInGameEditorPostStepCallback((editor) => editorTools.update(editor));", + "}", + "", + "gdjs.__terrain3DExtension = {", + " Terrain,", + " // Used by the terrain grass and the scattered models.", + " getWorldScale,", + " getGrounds,", + " patchShaderCode,", + " createRandom,", + " findHighestGround,", + " GroundPlacement,", + " svgIcon,", + " toolIcons,", + " // Exposed for tests.", + " TerrainBodyUpdater,", + " TerrainData,", + " SampleRectangle,", + " brushes,", + " edits,", + " codec,", + " relief,", + "};" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [], + "objectGroups": [] + }, + { + "description": "Define grass helper classes JavaScript code.", + "fullName": "Define grass helper classes", + "functionType": "Action", + "name": "DefineGrassHelperClasses", + "private": true, + "sentence": "Define grass helper classes JavaScript code", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "if (gdjs.__terrainGrassExtension) {", + " return;", + "}", + "// Blades grow on the grounds of the scene: the 3D terrains (see `getGrounds`", + "// in the terrain helper).", + "", + "// Size of the object inner area: instances are scaled from it.", + "const AREA_SIZE = 1000;", + "const AREA_DEPTH = 40;", + "const CHUNK_SIZE = 500;", + "const MAX_BENDING_OBJECTS = 8;", + "", + "/**", + " * A blade: a thin, tapered strip. Its width and height are 1: blades are", + " * scaled by the shader.", + " */", + "const createBladeGeometry = () => {", + " const halfWidths = [0.5, 0.42, 0.28, 0];", + " const heights = [0, 0.35, 0.7, 1];", + " const positions = [];", + " halfWidths.forEach((halfWidth, level) => {", + " positions.push(-halfWidth, 0, heights[level]);", + " if (halfWidth > 0) positions.push(halfWidth, 0, heights[level]);", + " });", + " const indices = [0, 1, 2, 1, 3, 2, 2, 3, 4, 3, 5, 4, 4, 5, 6];", + " const geometry = new THREE.InstancedBufferGeometry();", + " geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));", + " geometry.setAttribute('normal', new THREE.Float32BufferAttribute(new Array(positions.length).fill(0), 3));", + " geometry.setIndex(indices);", + " return geometry;", + "};", + "", + "const vertexShaderDeclarations = `", + "attribute vec4 grassRoot;", + "attribute vec2 grassShape;", + "// The tint of the ground at the root (linear, from 0 to 1).", + "attribute vec3 grassGroundTint;", + "uniform mat3 grassWorldToLocal;", + "uniform float grassWorldScale;", + "uniform float grassBladeWidth;", + "uniform float grassTime;", + "uniform float grassWindStrength;", + "uniform float grassWindSpeed;", + "uniform vec4 grassBenders[${MAX_BENDING_OBJECTS}];", + "uniform vec3 grassBaseColor;", + "uniform vec3 grassTipColor;", + "varying vec3 vGrassColor;", + "`;", + "const vertexShaderPosition = `", + "// Blades are shaped in scene units (with Y flipped like the 3D world), then", + "// moved back in the (scaled) object space.", + "float bladeHeight = position.z;", + "float grassAngle = grassRoot.w;", + "float bladeWidth = position.x * grassBladeWidth;", + "vec3 blade = vec3(", + " bladeWidth * cos(grassAngle),", + " bladeWidth * sin(grassAngle),", + " bladeHeight * grassShape.x", + ");", + "vec3 rootWorld = (modelMatrix * vec4(grassRoot.xyz, 1.0)).xyz * grassWorldScale;", + "// Tips bend more than the base.", + "float bendFactor = bladeHeight * bladeHeight * grassShape.x;", + "float gust = sin(grassTime * grassWindSpeed + rootWorld.x * 0.013 + rootWorld.y * 0.011);", + "float flutter = sin(grassTime * grassWindSpeed * 2.7 + grassShape.y * 40.0) * 0.3;", + "blade.xy += vec2(0.8, 0.5) * (gust + flutter + 0.6) * grassWindStrength * 0.35 * bendFactor;", + "for (int i = 0; i < ${MAX_BENDING_OBJECTS}; i++) {", + " vec4 bender = grassBenders[i];", + " if (bender.z <= 0.0) continue;", + " vec2 away = rootWorld.xy - bender.xy;", + " float distanceToBender = length(away);", + " float push = 1.0 - smoothstep(bender.z * 0.3, bender.z, distanceToBender);", + " if (push > 0.0) {", + " blade.xy += normalize(away + vec2(0.001)) * push * bendFactor * 0.9;", + " blade.z *= 1.0 - push * 0.5;", + " }", + "}", + "vec3 transformed = grassRoot.xyz + grassWorldToLocal * (blade / grassWorldScale);", + "vGrassColor = mix(grassBaseColor, grassTipColor, bladeHeight) * (0.85 + 0.3 * grassShape.y) * grassGroundTint;", + "`;", + "const vertexShaderNormal = `", + "// Blades are lit like the ground below them.", + "transformedNormal = normalize((viewMatrix * vec4(0.0, 0.0, 1.0, 0.0)).xyz);", + "`;", + "const fragmentShaderDeclarations = `", + "varying vec3 vGrassColor;", + "`;", + "const fragmentShaderColor = `", + "diffuseColor.rgb *= vGrassColor;", + "`;", + "// Both faces of blades are lit like the ground (not flipped for back faces).", + "const fragmentShaderNormal = `", + "#include ", + "normal = normalize(vNormal);", + "nonPerturbedNormal = normal;", + "`;", + "", + "const { getWorldScale, patchShaderCode, createRandom, findHighestGround, GroundPlacement } = gdjs.__terrain3DExtension;", + "", + "/**", + " * The grass of a terrain grass object: blades drawn with GPU instancing, in chunks", + " * that are hidden when off-screen and thinned out with the distance. They grow", + " * on the grounds below, like 3D terrains (only where a given layer is painted).", + " */", + "class Grass {", + " /** @param {gdjs.CustomRuntimeObject3D} object */", + " constructor(object) {", + " this.object = object;", + " this.group = new THREE.Group();", + " object.get3DRendererObject().add(this.group);", + " this.bladeGeometry = createBladeGeometry();", + " /** @type {Array<{mesh: THREE.Mesh, blades: number, center: THREE.Vector3}>} */", + " this.chunks = [];", + " this.placement = new GroundPlacement(object.getRuntimeScene());", + " this._groundAtBlade = { ground: null, z: 0 };", + " this.uniforms = {", + " grassWorldToLocal: { value: new THREE.Matrix3() },", + " // Scene units by 3D world unit.", + " grassWorldScale: { value: 1 },", + " grassBladeWidth: { value: 1 },", + " grassTime: { value: 0 },", + " grassWindStrength: { value: 1 },", + " grassWindSpeed: { value: 1 },", + " grassBenders: { value: Array.from({ length: MAX_BENDING_OBJECTS }, () => new THREE.Vector4()) },", + " grassBaseColor: { value: new THREE.Color() },", + " grassTipColor: { value: new THREE.Color() },", + " };", + " this.material = new THREE.MeshStandardMaterial({ roughness: 1, metalness: 0, side: THREE.DoubleSide });", + " this.material.onBeforeCompile = (shader) => {", + " Object.assign(shader.uniforms, this.uniforms);", + " shader.vertexShader = patchShaderCode(shader.vertexShader, [", + " ['#include ', '#include \\n' + vertexShaderDeclarations],", + " ['#include ', vertexShaderPosition],", + " ['#include ', '#include \\n' + vertexShaderNormal],", + " ], 'TerrainGrass');", + " shader.fragmentShader = patchShaderCode(shader.fragmentShader, [", + " ['#include ', '#include \\n' + fragmentShaderDeclarations],", + " ['#include ', '#include \\n' + fragmentShaderColor],", + " ['#include ', fragmentShaderNormal],", + " ], 'TerrainGrass');", + " };", + " this.material.customProgramCacheKey = () => 'TerrainGrass';", + "", + " this._matrix = new THREE.Matrix4();", + " this._cameraPosition = new THREE.Vector3();", + " this._chunkPosition = new THREE.Vector3();", + " this._scale = new THREE.Vector3();", + " this.updateFromProperties();", + " }", + "", + " updateFromProperties() {", + " const { object } = this;", + " this.uniforms.grassBaseColor.value.set(gdjs.rgbOrHexStringToNumber(object._getBaseColor()));", + " this.uniforms.grassTipColor.value.set(gdjs.rgbOrHexStringToNumber(object._getTipColor()));", + " this.uniforms.grassWindStrength.value = object._getWindStrength();", + " this.uniforms.grassWindSpeed.value = object._getWindSpeed();", + " this.uniforms.grassBladeWidth.value = Math.max(object._getBladeWidth(), 0);", + " this.material.needsUpdate = true;", + " this.placement.placeAgain();", + " }", + "", + " _getPlacementKey() {", + " const { object } = this;", + " return [", + " object.getX(), object.getY(), object.getZ(), object.getWidth(), object.getHeight(), object.getDepth(),", + " object._getDensity(), object._getBladeHeight(), object._getGroundLayer(), object._getMaxSlope(),", + " object._getSeed(),", + " ].join('|');", + " }", + "", + " /** Place the blades again, for example after the ground or the object changed. */", + " rebuild() {", + " const grounds = Array.from(this.placement.grounds);", + " for (const chunk of this.chunks) {", + " chunk.mesh.removeFromParent();", + " chunk.mesh.geometry.dispose();", + " }", + " this.chunks.length = 0;", + "", + " const { object } = this;", + " const width = object.getWidth();", + " const height = object.getHeight();", + " const scaleX = width / AREA_SIZE;", + " const scaleY = height / AREA_SIZE;", + " const scaleZ = Math.max(object.getDepth() / AREA_DEPTH, 0.0001);", + " const bladesPerChunk = Math.round((Math.max(object._getDensity(), 0) * CHUNK_SIZE * CHUNK_SIZE) / 10000);", + " const groundLayer = parseInt(object._getGroundLayer(), 10) || 0;", + " const maxSlope = object._getMaxSlope();", + " const chunksX = Math.max(Math.ceil(width / CHUNK_SIZE), 1);", + " const chunksY = Math.max(Math.ceil(height / CHUNK_SIZE), 1);", + " const nextRandom = createRandom(object._getSeed() * 7919 + 1);", + " const bladeHeight = Math.max(object._getBladeHeight(), 0);", + "", + " for (let chunkY = 0; chunkY < chunksY; chunkY++) {", + " for (let chunkX = 0; chunkX < chunksX; chunkX++) {", + " const roots = new Float32Array(bladesPerChunk * 4);", + " const shapes = new Float32Array(bladesPerChunk * 2);", + " const groundTints = new Uint8Array(bladesPerChunk * 3).fill(255);", + " let blades = 0;", + " let minZ = Infinity;", + " let maxZ = -Infinity;", + " for (let attempt = 0; attempt < bladesPerChunk; attempt++) {", + " // In scene units, relative to the object.", + " const x = (chunkX + nextRandom()) * CHUNK_SIZE;", + " const y = (chunkY + nextRandom()) * CHUNK_SIZE;", + " const angle = nextRandom() * Math.PI * 2;", + " const heightFactor = 0.6 + 0.8 * nextRandom();", + " const tint = nextRandom();", + " const growth = nextRandom();", + " if (x > width || y > height) continue;", + " const sceneX = object.getX() + x;", + " const sceneY = object.getY() + y;", + " const { ground, z } = findHighestGround(grounds, sceneX, sceneY, this._groundAtBlade);", + " const groundZ = ground ? z : object.getZ();", + " if (ground) {", + " if (groundLayer > 0 && growth > ground.getLayerWeightAt(sceneX, sceneY, groundLayer)) continue;", + " if (ground.getSlopeAt(sceneX, sceneY) > maxSlope) continue;", + " if (typeof ground.getTintAt === 'function') {", + " const tint = ground.getTintAt(sceneX, sceneY);", + " for (let channel = 0; channel < 3; channel++) {", + " groundTints[blades * 3 + channel] = Math.round(255 * Math.pow(tint[channel] / 255, 2.2));", + " }", + " }", + " }", + " const localZ = (groundZ - object.getZ()) / scaleZ;", + " roots[blades * 4] = x / scaleX;", + " roots[blades * 4 + 1] = y / scaleY;", + " roots[blades * 4 + 2] = localZ;", + " roots[blades * 4 + 3] = angle;", + " shapes[blades * 2] = heightFactor * bladeHeight;", + " shapes[blades * 2 + 1] = tint;", + " minZ = Math.min(minZ, localZ);", + " maxZ = Math.max(maxZ, localZ);", + " blades++;", + " }", + " if (blades === 0) continue;", + "", + " const geometry = new THREE.InstancedBufferGeometry();", + " geometry.index = this.bladeGeometry.index;", + " geometry.setAttribute('position', this.bladeGeometry.attributes.position);", + " geometry.setAttribute('normal', this.bladeGeometry.attributes.normal);", + " geometry.setAttribute('grassRoot', new THREE.InstancedBufferAttribute(roots.subarray(0, blades * 4), 4));", + " geometry.setAttribute('grassShape', new THREE.InstancedBufferAttribute(shapes.subarray(0, blades * 2), 2));", + " geometry.setAttribute('grassGroundTint', new THREE.InstancedBufferAttribute(groundTints.subarray(0, blades * 3), 3, true));", + " geometry.instanceCount = blades;", + " // The furthest tips can go with the wind and the bending objects (see the shader).", + " const maxBladeHeight = bladeHeight * 1.4;", + " const lean =", + " (0.35 * 1.9 * 0.943 * Math.max(object._getWindStrength(), 0) + 0.9) * maxBladeHeight +", + " Math.max(object._getBladeWidth(), 0) / 2;", + " const leanX = lean / scaleX;", + " const leanY = lean / scaleY;", + " geometry.boundingBox = new THREE.Box3(", + " new THREE.Vector3((chunkX * CHUNK_SIZE) / scaleX - leanX, (chunkY * CHUNK_SIZE) / scaleY - leanY, minZ),", + " new THREE.Vector3(", + " Math.min((chunkX + 1) * CHUNK_SIZE, width) / scaleX + leanX,", + " Math.min((chunkY + 1) * CHUNK_SIZE, height) / scaleY + leanY,", + " maxZ + (bladeHeight * 1.4) / scaleZ", + " )", + " );", + " geometry.boundingSphere = geometry.boundingBox.getBoundingSphere(new THREE.Sphere());", + " const mesh = new THREE.Mesh(geometry, this.material);", + " mesh.receiveShadow = true;", + " // Raycasts would only test one blade: the selection plane is used instead.", + " mesh.raycast = () => {};", + " this.group.add(mesh);", + " this.chunks.push({ mesh, blades, center: geometry.boundingSphere.center.clone() });", + " }", + " }", + " }", + "", + " _updateBendingObjects() {", + " const benders = this.uniforms.grassBenders.value;", + " const objectName = this.object._getBendingObject();", + " const bendingObjects = objectName ? this.object.getInstanceContainer().getObjects(objectName) || [] : [];", + " for (let index = 0; index < MAX_BENDING_OBJECTS; index++) {", + " const bendingObject = bendingObjects[index];", + " if (!bendingObject) {", + " benders[index].set(0, 0, 0, 0);", + " continue;", + " }", + " // Blades are pushed in the 3D scene, which is flipped on Y.", + " const radius = Math.max(bendingObject.getWidth(), bendingObject.getHeight()) * 1.2;", + " benders[index].set(bendingObject.getCenterXInScene(), -bendingObject.getCenterYInScene(), radius, 0);", + " }", + " }", + "", + " /**", + " * @param {number} elapsedSeconds", + " * @param {number} time In seconds, the same for all grass objects.", + " */", + " update(elapsedSeconds, time, camera) {", + " if (this.placement.shouldPlaceAgain(elapsedSeconds, this._getPlacementKey())) {", + " this.rebuild();", + " }", + "", + " this.uniforms.grassTime.value = time;", + " this.uniforms.grassWorldScale.value = getWorldScale(this.object);", + " // The object transformation is applied just before rendering, it's needed now.", + " this.object.getRenderer().ensureUpToDate();", + " this.group.updateWorldMatrix(true, false);", + " this.uniforms.grassWorldToLocal.value.setFromMatrix4(this._matrix.copy(this.group.matrixWorld).invert());", + " this._updateBendingObjects();", + " if (!camera) return;", + "", + " // Far chunks are hidden, and thinned out before (in 3D world units).", + " const fadeDistance = Math.max(this.object._getFadeDistance(), 1) / getWorldScale(this.object);", + " this._cameraPosition.setFromMatrixPosition(camera.matrixWorld);", + " for (const chunk of this.chunks) {", + " const distance = this._chunkPosition", + " .copy(chunk.center)", + " .applyMatrix4(this.group.matrixWorld)", + " .distanceTo(this._cameraPosition);", + " const density = gdjs.evtTools.common.clamp((fadeDistance - distance) / (fadeDistance * 0.5), 0, 1);", + " chunk.mesh.visible = density > 0;", + " chunk.mesh.geometry.instanceCount = Math.ceil(chunk.blades * density);", + " }", + " }", + "", + " dispose() {", + " for (const chunk of this.chunks) {", + " chunk.mesh.geometry.dispose();", + " }", + " this.bladeGeometry.dispose();", + " this.material.dispose();", + " this.group.removeFromParent();", + " }", + "}", + "", + "gdjs.__terrainGrassExtension = { Grass };" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [], + "objectGroups": [] + }, + { + "description": "Define scattered models helper classes JavaScript code.", + "fullName": "Define scattered models helper classes", + "functionType": "Action", + "name": "DefineScatterHelperClasses", + "private": true, + "sentence": "Define scattered models helper classes JavaScript code", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "if (gdjs.__terrainScatterExtension) {", + " return;", + "}", + "// Copies grow on the grounds of the scene, like the terrain grass (see", + "// `getGrounds` in the terrain helper).", + "", + "const {", + " getWorldScale,", + " getGrounds,", + " patchShaderCode,", + " createRandom,", + " findHighestGround,", + " GroundPlacement,", + " SampleRectangle,", + " brushes,", + " codec,", + " svgIcon,", + " toolIcons,", + "} = gdjs.__terrain3DExtension;", + "", + "// Size of the object inner area: instances are scaled from it.", + "const AREA_SIZE = 2000;", + "const AREA_DEPTH = 200;", + "// The painted density is stored in a grid of this many cells on each side,", + "// whatever the size of the object.", + "const MASK_RESOLUTION = 256;", + "const MASK_FORMAT_VERSION = '1';", + "// Copies are hidden in chunks: big chunks mean fewer draw calls, small ones", + "// hide more copies out of the screen.", + "const MIN_CHUNK_SIZE = 500;", + "const MAX_CHUNK_SIZE = 2000;", + "// Copies shrink to nothing over this part of the draw distance, instead of popping out.", + "const FADE_PART = 0.15;", + "const MAX_COPIES = 200000;", + "// Prisms of the trunk collisions.", + "const TRUNK_SIDES = 6;", + "", + "const clamp01 = (value) => (value < 0 ? 0 : value > 1 ? 1 : value);", + "", + "/**", + " * @returns {number} A number for a cell of the copies grid, changing with the seed.", + " */", + "const hashCell = (seed, i, j) => {", + " let h = Math.imul(i, 374761393) + Math.imul(j, 668265263) + Math.imul(seed, 982451653);", + " h = Math.imul(h ^ (h >>> 13), 1274126177);", + " return (h ^ (h >>> 16)) >>> 0;", + "};", + "", + "/**", + " * How dense copies are, from 0 to 1, in a grid covering the object: painted", + " * with the editor brushes, the edits or events.", + " */", + "class DensityMask {", + " /** @param {number} fillValue From 0 to 255. */", + " constructor(fillValue) {", + " // `resolution` and `size` let terrain brushes paint it.", + " this.resolution = MASK_RESOLUTION;", + " this.size = MASK_RESOLUTION + 1;", + " this.values = new Uint8Array(this.size * this.size).fill(fillValue);", + " }", + "", + " /**", + " * @param {number} u From 0 to 1, along the object width.", + " * @param {number} v From 0 to 1, along the object height.", + " * @returns {number} The density, from 0 to 1, interpolated between samples.", + " */", + " sample(u, v) {", + " const x = clamp01(u) * this.resolution;", + " const y = clamp01(v) * this.resolution;", + " const i = Math.min(Math.floor(x), this.resolution - 1);", + " const j = Math.min(Math.floor(y), this.resolution - 1);", + " const fx = x - i;", + " const fy = y - j;", + " const value = (i, j) => this.values[j * this.size + i];", + " const top = value(i, j) + fx * (value(i + 1, j) - value(i, j));", + " const bottom = value(i, j + 1) + fx * (value(i + 1, j + 1) - value(i, j + 1));", + " return (top + fy * (bottom - top)) / 255;", + " }", + "", + " /**", + " * Bring the density towards a target around a segment: fully up to 70%", + " * of the radius, then fading out (forests have dense centers and soft edges).", + " * @param {{cellWidth: number, cellHeight: number, ax: number, ay: number, bx: number, by: number, radius: number}} stroke", + " * @param {number} density The target, from 0 to 1.", + " * @param {number} strength From 0 to 1.", + " * @param {SampleRectangle} changedSamples", + " */", + " paint(stroke, density, strength, changedSamples) {", + " const { values } = this;", + " const target = Math.round(clamp01(density) * 255);", + " brushes.forEachSample(", + " this,", + " stroke,", + " (index, falloff) => {", + " // The falloff of terrain brushes is (1 - distance² / radius²)².", + " const weight = clamp01(2 * Math.sqrt(falloff));", + " // Rounded towards the target, so that light strokes still paint.", + " const change = (target - values[index]) * clamp01(weight * strength);", + " values[index] += change > 0 ? Math.ceil(change) : Math.floor(change);", + " },", + " changedSamples", + " );", + " }", + "", + " /** @returns {DensityMask} */", + " clone() {", + " const mask = new DensityMask(0);", + " mask.values.set(this.values);", + " return mask;", + " }", + "}", + "", + "/**", + " * Edits written in the Edits property: a JSON list of brush strokes, like", + " * `[{\"tool\": \"add\", \"x\": 0.5, \"y\": 0.5, \"radius\": 0.2}]`. Positions and", + " * radius are fractions of the object size, like the terrain edits.", + " */", + "const applyEdits = (mask, text) => {", + " if (!text.trim()) return;", + " let editList;", + " try {", + " editList = JSON.parse(text);", + " } catch (error) {", + " console.warn('The edits of scattered models are not valid JSON: ' + error.message);", + " return;", + " }", + " if (!Array.isArray(editList)) {", + " console.warn('The edits of scattered models must be a JSON list (like [{\"tool\": \"add\", ...}]).');", + " return;", + " }", + " const numberOr = (value, defaultValue) => (typeof value === 'number' && isFinite(value) ? value : defaultValue);", + " const changedSamples = new SampleRectangle();", + " for (const edit of editList) {", + " if (!edit || typeof edit !== 'object') continue;", + " const x = numberOr(edit.x, 0.5);", + " const y = numberOr(edit.y, 0.5);", + " const stroke = {", + " cellWidth: 1 / mask.resolution,", + " cellHeight: 1 / mask.resolution,", + " ax: x,", + " ay: y,", + " bx: numberOr(edit.toX, x),", + " by: numberOr(edit.toY, y),", + " radius: numberOr(edit.radius, 0.1),", + " };", + " const strength = clamp01(numberOr(edit.strength, 1));", + " if (edit.tool === 'add') {", + " mask.paint(stroke, numberOr(edit.density, 1), strength, changedSamples);", + " } else if (edit.tool === 'erase') {", + " mask.paint(stroke, 0, strength, changedSamples);", + " } else {", + " console.warn('Unknown tool in the edits of scattered models: \"' + edit.tool + '\" (use add or erase).');", + " }", + " }", + "};", + "", + "/**", + " * The density painted in the editor, stored as its difference with the", + " * density from the properties (the coverage and the edits), like the terrain", + " * sculpt data.", + " */", + "const maskCodec = {", + " /**", + " * @param {DensityMask} mask", + " * @param {DensityMask} baseMask", + " */", + " encode(mask, baseMask) {", + " const changes = new Int32Array(mask.values.length);", + " let hasChanges = false;", + " for (let index = 0; index < changes.length; index++) {", + " changes[index] = mask.values[index] - baseMask.values[index];", + " if (changes[index] !== 0) hasChanges = true;", + " }", + " if (!hasChanges) return '';", + " return MASK_FORMAT_VERSION + ';' + MASK_RESOLUTION + ';' + codec.encodeGrids([changes], mask.size);", + " },", + "", + " /**", + " * @param {string} text", + " * @param {DensityMask} baseMask", + " * @returns {DensityMask | null} null if the text is empty or not valid.", + " */", + " decode(text, baseMask) {", + " if (!text) return null;", + " const parts = text.split(';');", + " if (parts[0] !== MASK_FORMAT_VERSION || parseInt(parts[1], 10) !== MASK_RESOLUTION) return null;", + " try {", + " const mask = baseMask.clone();", + " const [changes] = codec.decodeGrids(parts[2], mask.size, 1);", + " for (let index = 0; index < changes.length; index++) {", + " // The base may have changed since the data was saved.", + " mask.values[index] = Math.min(Math.max(mask.values[index] + changes[index], 0), 255);", + " }", + " return mask;", + " } catch (error) {", + " console.error('The painted data of scattered models could not be read:', error);", + " return null;", + " }", + " },", + "};", + "", + "const fadeShaderDeclarations = `", + "uniform float scatterDrawDistance;", + "`;", + "// Instance matrices are applied after this: copies shrink towards their base.", + "const fadeShaderCode = `", + "#ifdef USE_INSTANCING", + "vec3 scatterCopyPosition = (modelMatrix * instanceMatrix * vec4(0.0, 0.0, 0.0, 1.0)).xyz;", + "transformed *= clamp(", + " (scatterDrawDistance - distance(scatterCopyPosition, cameraPosition)) / (scatterDrawDistance * ${FADE_PART}),", + " 0.0,", + " 1.0", + ");", + "#endif", + "`;", + "", + "const flipTriangles = (geometry) => {", + " const index = geometry.index;", + " for (let offset = 0; offset + 2 < index.count; offset += 3) {", + " const second = index.getX(offset + 1);", + " index.setX(offset + 1, index.getX(offset + 2));", + " index.setX(offset + 2, second);", + " }", + "};", + "", + "/**", + " * The parts of a model, ready to be drawn with instancing: each glTF", + " * primitive with its material. Geometries are moved so that the model base", + " * is at the origin, its height is 1 and it stands along Z (like 3D model", + " * objects with a 90° rotation around X).", + " * @param {THREE.Object3D} modelScene", + " * @param {object} uniforms Uniforms added to the materials.", + " * @returns {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}}", + " */", + "const createModelParts = (modelScene, uniforms) => {", + " modelScene.updateMatrixWorld(true);", + " const rootInverse = modelScene.matrixWorld.clone().invert();", + " const standUp = new THREE.Matrix4().makeRotationX(Math.PI / 2);", + " /** @type {Array<{mesh: THREE.Mesh, matrix: THREE.Matrix4}>} */", + " const meshes = [];", + " const box = new THREE.Box3();", + " modelScene.traverse((node) => {", + " if (!node.isMesh || !node.geometry || !node.geometry.attributes.position) return;", + " const matrix = standUp.clone().multiply(rootInverse).multiply(node.matrixWorld);", + " if (!node.geometry.boundingBox) node.geometry.computeBoundingBox();", + " box.union(node.geometry.boundingBox.clone().applyMatrix4(matrix));", + " meshes.push({ mesh: node, matrix });", + " });", + " if (box.isEmpty()) return { parts: [], width: 0, depth: 0 };", + "", + " const height = Math.max(box.max.z - box.min.z, 0.000001);", + " // The Y axis of the scene goes down: the model is flipped, like 3D model objects.", + " const normalization = new THREE.Matrix4()", + " .makeScale(1 / height, -1 / height, 1 / height)", + " .multiply(", + " new THREE.Matrix4().makeTranslation(", + " -(box.min.x + box.max.x) / 2,", + " -(box.min.y + box.max.y) / 2,", + " -box.min.z", + " )", + " );", + " const parts = meshes.map(({ mesh, matrix }) => {", + " let geometry = mesh.geometry.clone();", + " if (!geometry.index) {", + " const indices = Array.from({ length: geometry.attributes.position.count }, (_, index) => index);", + " geometry.setIndex(indices);", + " }", + " geometry.applyMatrix4(normalization.clone().multiply(matrix));", + " // The flip turned the faces inside out.", + " flipTriangles(geometry);", + " geometry.computeBoundingSphere();", + " geometry.computeBoundingBox();", + " const patchMaterial = (originalMaterial) => {", + " const material = originalMaterial.clone();", + " // Like 3D model objects (\"Standard without metalness\"), which look better without an environment map.", + " if (material.metalness) material.metalness = 0;", + " material.onBeforeCompile = (shader) => {", + " Object.assign(shader.uniforms, uniforms);", + " shader.vertexShader = patchShaderCode(shader.vertexShader, [", + " ['#include ', '#include \\n' + fadeShaderDeclarations],", + " ['#include ', '#include \\n' + fadeShaderCode],", + " ], 'ScatteredModels');", + " };", + " material.customProgramCacheKey = () => 'ScatteredModels';", + " return material;", + " };", + " const material = Array.isArray(mesh.material) ? mesh.material.map(patchMaterial) : patchMaterial(mesh.material);", + " return { geometry, material };", + " });", + " return {", + " parts,", + " width: (box.max.x - box.min.x) / height,", + " depth: (box.max.y - box.min.y) / height,", + " };", + "};", + "", + "/**", + " * A copy of the model: its base position in the scene, its scale (the", + " * height of the copy) and the key of its cell in the grid of copies.", + " * @typedef {{x: number, y: number, z: number, angle: number, height: number, key: number}} Copy", + " */", + "", + "/**", + " * The scattered models of an object: copies of a 3D model placed on the", + " * grounds of the scene, drawn with GPU instancing in chunks that are hidden", + " * when far or off-screen.", + " */", + "class Scatter {", + " /** @param {gdjs.CustomRuntimeObject3D} object */", + " constructor(object) {", + " this.object = object;", + " this.group = new THREE.Group();", + " object.get3DRendererObject().add(this.group);", + " /** @type {Array<{firstCellI: number, firstCellJ: number, meshes: THREE.InstancedMesh[], copies: Copy[], center: THREE.Vector3, radius: number}>} */", + " this.chunks = [];", + " /**", + " * The grid of copies: one copy at most in each cell, cells grouped in chunks.", + " * @type {{spacing: number, cellsPerChunk: number, cellsX: number, cellsY: number} | null}", + " */", + " this.grid = null;", + " this.placement = new GroundPlacement(object.getRuntimeScene());", + " this.uniforms = {", + " // In 3D world units.", + " scatterDrawDistance: { value: 1 },", + " };", + " /** @type {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}} */", + " this.model = { parts: [], width: 0, depth: 0 };", + " this.modelResourceName = '';", + " this.loadingModelResourceName = '';", + " /**", + " * Copies removed by events (for example, cut trees), by key.", + " * @type {Map}", + " */", + " this.removedCopies = new Map();", + " /** Increased each time copies change, for the collisions. */", + " this.version = 0;", + " this.isDisposed = false;", + " /** The last value of the painted data property read or saved. */", + " this.paintData = '';", + " /**", + " * Values saved by the editor tools: the editor sends them back with a", + " * hot-reload, possibly after newer ones were saved.", + " * @type {string[]}", + " */", + " this.savedPaintData = [];", + " this.maskSource = '';", + " /** @type {DensityMask} */", + " this.baseMask = new DensityMask(255);", + " /** @type {DensityMask} */", + " this.mask = this.baseMask;", + " this.changedSamples = new SampleRectangle();", + " this._stroke = { cellWidth: 1, cellHeight: 1, ax: 0, ay: 0, bx: 0, by: 0, radius: 0 };", + " this._groundAtCopy = { ground: null, z: 0 };", + " this._matrix = new THREE.Matrix4();", + " this._raycastSphere = new THREE.Sphere();", + " this._raycastRay = new THREE.Ray();", + " this._raycastBox = new THREE.Box3();", + " this._raycastInverseMatrix = new THREE.Matrix4();", + " this._raycastScale = new THREE.Vector3();", + " /** Placed on a copy to test the triangles of the model. */", + " this._raycastMesh = new THREE.Mesh();", + " /** @type {THREE.Intersection[]} */", + " this._raycastHits = [];", + " this._color = new THREE.Color();", + " this._tint = new THREE.Color();", + " this._cameraPosition = new THREE.Vector3();", + " this._chunkPosition = new THREE.Vector3();", + " /**", + " * Trunks, for the 3D physics behavior and navigation meshes. Always given,", + " * empty without trunk collisions: otherwise these behaviors would use the", + " * object box, covering the whole area.", + " * @type {gdjs.Surface}", + " */", + " this.surface = {", + " getVersion: () => this.version,", + " getChangedArea: () => null,", + " getTriangles: () => this._getTrunkTriangles(),", + " getHeightField: () => null,", + " };", + " /** The trunk collision properties, changing the surface. */", + " this.trunkKey = '';", + " this.loadMaskFromProperties();", + " this.updateFromProperties();", + " // Before the behaviors create their shapes.", + " this._updateSurface();", + " }", + "", + " _getMaskSource() {", + " return this.object._getCoverage() + '|' + this.object._getEdits();", + " }", + "", + " loadMaskFromProperties() {", + " const { object } = this;", + " this.maskSource = this._getMaskSource();", + " this.paintData = object._getPaintData();", + " this.baseMask = new DensityMask(object._getCoverage() === 'Painted' ? 0 : 255);", + " applyEdits(this.baseMask, object._getEdits());", + " this.mask = maskCodec.decode(this.paintData, this.baseMask) || this.baseMask.clone();", + " this.placement.placeAgain();", + " }", + "", + " onHotReloading() {", + " const paintData = this.object._getPaintData();", + " const savedIndex = this.savedPaintData.indexOf(paintData);", + " if (savedIndex !== -1 && this._getMaskSource() === this.maskSource) {", + " // The object already has this data, or newer changes.", + " this.savedPaintData.splice(0, savedIndex + 1);", + " } else if (paintData !== this.paintData || this._getMaskSource() !== this.maskSource) {", + " this.savedPaintData.length = 0;", + " this.loadMaskFromProperties();", + " }", + " this.updateFromProperties();", + " }", + "", + " /** @returns {string} The data to save in the painted data property. */", + " save() {", + " this.paintData = maskCodec.encode(this.mask, this.baseMask);", + " this.savedPaintData.push(this.paintData);", + " return this.paintData;", + " }", + "", + " updateFromProperties() {", + " const { object } = this;", + " const modelResourceName = object._getModel();", + " if (modelResourceName !== this.modelResourceName) {", + " this.modelResourceName = modelResourceName;", + " this._loadModel();", + " }", + " this.placement.placeAgain();", + " }", + "", + " _disposeModel() {", + " for (const part of this.model.parts) {", + " part.geometry.dispose();", + " for (const material of Array.isArray(part.material) ? part.material : [part.material]) {", + " material.dispose();", + " }", + " }", + " this.model = { parts: [], width: 0, depth: 0 };", + " }", + "", + " _loadModel() {", + " this._disposeChunks(this.chunks);", + " this._disposeModel();", + " const resourceName = this.modelResourceName;", + " if (!resourceName) return;", + " const game = this.object.getRuntimeScene().getGame();", + " const modelManager = game.getModel3DManager();", + " const gltf = modelManager.getModel(resourceName);", + " if (gltf === modelManager._invalidModel) {", + " // Not loaded yet, for example a model just chosen in the editor.", + " if (this.loadingModelResourceName === resourceName) return;", + " this.loadingModelResourceName = resourceName;", + " game.getResourceLoader()", + " .loadResource(resourceName)", + " .then(() => {", + " if (this.isDisposed || this.modelResourceName !== resourceName) return;", + " this.loadingModelResourceName = '';", + " if (modelManager.getModel(resourceName) !== modelManager._invalidModel) {", + " this._loadModel();", + " this.placement.placeAgain();", + " }", + " })", + " .catch(() => {});", + " return;", + " }", + " this.model = createModelParts(gltf.scene, this.uniforms);", + " }", + "", + " _getPlacementKey() {", + " const { object } = this;", + " return [", + " object.getX(), object.getY(), object.getZ(), object.getWidth(), object.getHeight(), object.getDepth(),", + " object._getDensity(), object._getSize(), object._getMinScale(), object._getMaxScale(),", + " object._getRandomRotation(), object._getRandomTint(), object._getGroundLayer(), object._getMaxSlope(),", + " object._getMinGroundZ(), object._getMaxGroundZ(), object._getSeed(), object._getDrawDistance(),", + " object._getCastShadow(), object._getReceiveShadow(), this.model.parts.length,", + " ].join('|');", + " }", + "", + " _getChunkSize() {", + " return Math.min(Math.max(this.object._getDrawDistance() / 4, MIN_CHUNK_SIZE), MAX_CHUNK_SIZE);", + " }", + "", + " /** @returns {number} The distance between copies (one copy per cell of this size at most). */", + " _getSpacing() {", + " const { object } = this;", + " const density = Math.max(object._getDensity(), 0);", + " if (density <= 0) return 0;", + " const spacing = 100 / Math.sqrt(density);", + " // Too many cells would freeze the game: the density is lowered.", + " const cellCount = (object.getWidth() / spacing) * (object.getHeight() / spacing);", + " return cellCount > MAX_COPIES ? spacing * Math.sqrt(cellCount / MAX_COPIES) : spacing;", + " }", + "", + " /**", + " * Find the copy of a cell of the grid, if there is one.", + " * @returns {Copy | null}", + " */", + " _placeCopy(cellI, cellJ, spacing, grounds) {", + " const { object } = this;", + " const width = object.getWidth();", + " const height = object.getHeight();", + " const nextRandom = createRandom(hashCell(Math.floor(object._getSeed()), cellI, cellJ));", + " // Always drawn in the same order, so that a copy stays the same when properties change.", + " const x = (cellI + nextRandom()) * spacing;", + " const y = (cellJ + nextRandom()) * spacing;", + " const presence = nextRandom();", + " const growth = nextRandom();", + " const scaleFactor = nextRandom();", + " const angle = nextRandom() * Math.PI * 2;", + " if (x > width || y > height) return null;", + " if (presence >= this.mask.sample(x / width, y / height)) return null;", + " const key = cellJ * 65536 + cellI;", + " if (this.removedCopies.has(key)) return null;", + "", + " const sceneX = object.getX() + x;", + " const sceneY = object.getY() + y;", + " const { ground, z } = findHighestGround(grounds, sceneX, sceneY, this._groundAtCopy);", + " const groundZ = ground ? z : object.getZ();", + " if (groundZ < object._getMinGroundZ() || groundZ > object._getMaxGroundZ()) return null;", + " const copyHeight =", + " Math.max(object._getSize(), 0) *", + " (object._getMinScale() + (object._getMaxScale() - object._getMinScale()) * scaleFactor);", + " if (ground) {", + " if (ground.getSlopeAt(sceneX, sceneY) > object._getMaxSlope()) return null;", + " const groundLayer = parseInt(object._getGroundLayer(), 10) || 0;", + " if (groundLayer > 0) {", + " if (growth > ground.getLayerWeightAt(sceneX, sceneY, groundLayer)) return null;", + " // Copies don't stand at the border of their layer, so that trees don't cover roads.", + " const margin = (copyHeight * Math.max(this.model.width, this.model.depth)) / 4;", + " for (const [offsetX, offsetY] of [[margin, 0], [-margin, 0], [0, margin], [0, -margin]]) {", + " if (ground.getLayerWeightAt(sceneX + offsetX, sceneY + offsetY, groundLayer) < 0.5) return null;", + " }", + " }", + " }", + " return {", + " x: sceneX,", + " y: sceneY,", + " z: groundZ,", + " angle: object._getRandomRotation() ? angle : 0,", + " height: copyHeight,", + " key,", + " };", + " }", + "", + " _disposeChunks(chunks) {", + " for (const chunk of chunks) {", + " for (const mesh of chunk.meshes) {", + " mesh.removeFromParent();", + " // Geometries and materials are the ones of the model.", + " mesh.dispose();", + " }", + " }", + " if (chunks === this.chunks) this.chunks = [];", + " }", + "", + " /** Place the copies again, for example after the ground or the object changed. */", + " rebuild() {", + " this._disposeChunks(this.chunks);", + " this.version++;", + " const { object } = this;", + " const spacing = this._getSpacing();", + " if (spacing <= 0 || this.model.parts.length === 0) {", + " this.grid = null;", + " return;", + " }", + " const cellsPerChunk = Math.max(Math.round(this._getChunkSize() / spacing), 1);", + " const grid = {", + " spacing,", + " cellsPerChunk,", + " cellsX: Math.ceil(object.getWidth() / spacing),", + " cellsY: Math.ceil(object.getHeight() / spacing),", + " };", + " this.grid = grid;", + " for (let firstCellJ = 0; firstCellJ < grid.cellsY; firstCellJ += cellsPerChunk) {", + " for (let firstCellI = 0; firstCellI < grid.cellsX; firstCellI += cellsPerChunk) {", + " this._createChunk(firstCellI, firstCellJ);", + " }", + " }", + " }", + "", + " /**", + " * Place again the copies of the chunks touching a rectangle of the scene,", + " * for example after painting.", + " */", + " _rebuildArea(minX, minY, maxX, maxY) {", + " const { grid, object } = this;", + " if (!grid) return;", + " const chunkSize = grid.spacing * grid.cellsPerChunk;", + " const firstChunkI = Math.max(Math.floor((minX - object.getX()) / chunkSize), 0);", + " const firstChunkJ = Math.max(Math.floor((minY - object.getY()) / chunkSize), 0);", + " const lastChunkI = Math.min(Math.floor((maxX - object.getX()) / chunkSize), Math.ceil(grid.cellsX / grid.cellsPerChunk) - 1);", + " const lastChunkJ = Math.min(Math.floor((maxY - object.getY()) / chunkSize), Math.ceil(grid.cellsY / grid.cellsPerChunk) - 1);", + " if (firstChunkI > lastChunkI || firstChunkJ > lastChunkJ) return;", + " const isInArea = (chunk) => {", + " const chunkI = chunk.firstCellI / grid.cellsPerChunk;", + " const chunkJ = chunk.firstCellJ / grid.cellsPerChunk;", + " return chunkI >= firstChunkI && chunkI <= lastChunkI && chunkJ >= firstChunkJ && chunkJ <= lastChunkJ;", + " };", + " this._disposeChunks(this.chunks.filter(isInArea));", + " this.chunks = this.chunks.filter((chunk) => !isInArea(chunk));", + " for (let chunkJ = firstChunkJ; chunkJ <= lastChunkJ; chunkJ++) {", + " for (let chunkI = firstChunkI; chunkI <= lastChunkI; chunkI++) {", + " this._createChunk(chunkI * grid.cellsPerChunk, chunkJ * grid.cellsPerChunk);", + " }", + " }", + " this.version++;", + " }", + "", + " _createChunk(firstCellI, firstCellJ) {", + " const { grid } = this;", + " const grounds = Array.from(this.placement.grounds);", + " /** @type {Copy[]} */", + " const copies = [];", + " for (let cellJ = firstCellJ; cellJ < Math.min(firstCellJ + grid.cellsPerChunk, grid.cellsY); cellJ++) {", + " for (let cellI = firstCellI; cellI < Math.min(firstCellI + grid.cellsPerChunk, grid.cellsX); cellI++) {", + " const copy = this._placeCopy(cellI, cellJ, grid.spacing, grounds);", + " if (copy) copies.push(copy);", + " }", + " }", + " if (copies.length === 0) return;", + "", + " const { object } = this;", + " // Copies are placed in scene units, and the object group is scaled", + " // from its inner area: they are scaled back to keep their proportions.", + " const scaleX = Math.max(object.getWidth() / AREA_SIZE, 0.0001);", + " const scaleY = Math.max(object.getHeight() / AREA_SIZE, 0.0001);", + " const scaleZ = Math.max(object.getDepth() / AREA_DEPTH, 0.0001);", + " const matrices = new Float32Array(copies.length * 16);", + " const tintColor = gdjs.rgbOrHexStringToNumber(object._getRandomTint());", + " const hasTint = tintColor !== 0xffffff;", + " const colors = hasTint ? new Float32Array(copies.length * 3) : null;", + " this._tint.setHex(tintColor);", + " const min = new THREE.Vector3(Infinity, Infinity, Infinity);", + " const max = new THREE.Vector3(-Infinity, -Infinity, -Infinity);", + " copies.forEach((copy, index) => {", + " const cos = Math.cos(copy.angle) * copy.height;", + " const sin = Math.sin(copy.angle) * copy.height;", + " const localX = (copy.x - object.getX()) / scaleX;", + " const localY = (copy.y - object.getY()) / scaleY;", + " const localZ = (copy.z - object.getZ()) / scaleZ;", + " this._matrix.set(", + " cos / scaleX, -sin / scaleX, 0, localX,", + " sin / scaleY, cos / scaleY, 0, localY,", + " 0, 0, copy.height / scaleZ, localZ,", + " 0, 0, 0, 1", + " );", + " this._matrix.toArray(matrices, index * 16);", + " if (colors) {", + " // The tint of each copy is between white and the tint color.", + " this._color.setRGB(1, 1, 1).lerp(this._tint, createRandom(copy.key + 1)());", + " this._color.toArray(colors, index * 3);", + " }", + " const reach = (copy.height * Math.max(this.model.width, this.model.depth)) / 2;", + " min.min(this._chunkPosition.set(copy.x - reach, copy.y - reach, copy.z));", + " max.max(this._chunkPosition.set(copy.x + reach, copy.y + reach, copy.z + copy.height));", + " });", + " const instanceMatrix = new THREE.InstancedBufferAttribute(matrices, 16);", + " const instanceColor = colors ? new THREE.InstancedBufferAttribute(colors, 3) : null;", + " const castShadow = object._getCastShadow();", + " const receiveShadow = object._getReceiveShadow();", + " const meshes = this.model.parts.map((part) => {", + " const mesh = new THREE.InstancedMesh(part.geometry, part.material, 0);", + " // All the parts of the model share the positions of the copies.", + " mesh.instanceMatrix = instanceMatrix;", + " mesh.instanceColor = instanceColor;", + " mesh.count = copies.length;", + " mesh.computeBoundingSphere();", + " mesh.castShadow = castShadow;", + " mesh.receiveShadow = receiveShadow;", + " // The first mesh tests all the parts of the copies.", + " mesh.raycast = () => {};", + " this.group.add(mesh);", + " return mesh;", + " });", + " const center = new THREE.Vector3(", + " ((min.x + max.x) / 2 - object.getX()) / scaleX,", + " ((min.y + max.y) / 2 - object.getY()) / scaleY,", + " ((min.z + max.z) / 2 - object.getZ()) / scaleZ", + " );", + " const chunk = { firstCellI, firstCellJ, meshes, copies, center, radius: min.distanceTo(max) / 2 };", + " // Used by the scene editor to select the object when a copy is clicked.", + " meshes[0].raycast = (raycaster, intersects) => this._raycastChunk(chunk, raycaster, intersects);", + " this.chunks.push(chunk);", + " }", + "", + " /**", + " * Add the hits of a ray on the copies of a chunk, like they are drawn. There", + " * can be thousands of copies: only the ones whose box is crossed by the ray", + " * are tested with the triangles of the model.", + " * @param {{meshes: THREE.InstancedMesh[], copies: Copy[], center: THREE.Vector3, radius: number}} chunk", + " * @param {THREE.Raycaster} raycaster", + " * @param {THREE.Intersection[]} intersects", + " */", + " _raycastChunk(chunk, raycaster, intersects) {", + " const { object, group } = this;", + " this._raycastSphere.center.copy(chunk.center).applyMatrix4(group.matrixWorld);", + " this._raycastSphere.radius = chunk.radius / getWorldScale(object);", + " if (!raycaster.ray.intersectsSphere(this._raycastSphere)) return;", + "", + " // The ray in the scene coordinates, like the copies.", + " const scale = this._raycastScale.set(", + " Math.max(object.getWidth() / AREA_SIZE, 0.0001),", + " Math.max(object.getHeight() / AREA_SIZE, 0.0001),", + " Math.max(object.getDepth() / AREA_DEPTH, 0.0001)", + " );", + " const ray = this._raycastRay.copy(raycaster.ray).applyMatrix4(this._raycastInverseMatrix.copy(group.matrixWorld).invert());", + " ray.origin.multiply(scale).add(this._chunkPosition.set(object.getX(), object.getY(), object.getZ()));", + " ray.direction.multiply(scale);", + "", + " const reachPerHeight = Math.max(this.model.width, this.model.depth) / 2;", + " const instanceMatrices = chunk.meshes[0].instanceMatrix.array;", + " chunk.copies.forEach((copy, index) => {", + " const reach = copy.height * reachPerHeight;", + " this._raycastBox.min.set(copy.x - reach, copy.y - reach, copy.z);", + " this._raycastBox.max.set(copy.x + reach, copy.y + reach, copy.z + copy.height);", + " if (!ray.intersectsBox(this._raycastBox)) return;", + " this._raycastMesh.matrixWorld.fromArray(instanceMatrices, index * 16).premultiply(group.matrixWorld);", + " for (const mesh of chunk.meshes) {", + " this._raycastMesh.geometry = mesh.geometry;", + " this._raycastMesh.material = mesh.material;", + " this._raycastHits.length = 0;", + " this._raycastMesh.raycast(raycaster, this._raycastHits);", + " for (const hit of this._raycastHits) {", + " hit.object = mesh;", + " hit.instanceId = index;", + " intersects.push(hit);", + " }", + " }", + " });", + " }", + "", + " /** @param {THREE.Camera | null} camera */", + " _hideFarChunks(camera) {", + " if (!camera) return;", + " const worldScale = getWorldScale(this.object);", + " const drawDistance = Math.max(this.object._getDrawDistance(), 1) / worldScale;", + " this.uniforms.scatterDrawDistance.value = drawDistance;", + " this._cameraPosition.setFromMatrixPosition(camera.matrixWorld);", + " for (const chunk of this.chunks) {", + " const distance = this._chunkPosition", + " .copy(chunk.center)", + " .applyMatrix4(this.group.matrixWorld)", + " .distanceTo(this._cameraPosition);", + " const isVisible = distance - chunk.radius / worldScale < drawDistance;", + " for (const mesh of chunk.meshes) mesh.visible = isVisible;", + " }", + " }", + "", + " /**", + " * @param {number} elapsedSeconds", + " * @param {THREE.Camera | null} camera", + " */", + " update(elapsedSeconds, camera) {", + " if (this.placement.shouldPlaceAgain(elapsedSeconds, this._getPlacementKey())) {", + " this.rebuild();", + " }", + " if (!this.changedSamples.isEmpty()) {", + " const { object } = this;", + " const sampleWidth = object.getWidth() / MASK_RESOLUTION;", + " const sampleHeight = object.getHeight() / MASK_RESOLUTION;", + " const { minI, minJ, maxI, maxJ } = this.changedSamples;", + " // Copies between samples use the density of the samples around them.", + " this._rebuildArea(", + " object.getX() + (minI - 1) * sampleWidth,", + " object.getY() + (minJ - 1) * sampleHeight,", + " object.getX() + (maxI + 1) * sampleWidth,", + " object.getY() + (maxJ + 1) * sampleHeight", + " );", + " this.changedSamples.clear();", + " }", + " // The object transformation is applied just before rendering, it's needed now.", + " this.object.getRenderer().ensureUpToDate();", + " this.group.updateWorldMatrix(true, false);", + " this._hideFarChunks(camera);", + " this._updateSurface();", + " }", + "", + " _updateSurface() {", + " const { object } = this;", + " if (typeof object.setSurface !== 'function') return;", + " const trunkKey = object._getTrunkCollisions() + '|' + object._getTrunkDiameter();", + " if (trunkKey !== this.trunkKey) {", + " this.trunkKey = trunkKey;", + " this.version++;", + " }", + " if (object.getSurface() !== this.surface) object.setSurface(this.surface);", + " }", + "", + " /** @returns {Copy[]} */", + " getCopies() {", + " const copies = [];", + " for (const chunk of this.chunks) copies.push(...chunk.copies);", + " return copies;", + " }", + "", + " /**", + " * @returns {Copy | null} The copy whose base is the nearest to a position (on X and Y), if any.", + " */", + " findNearestCopy(x, y) {", + " let nearestCopy = null;", + " let nearestDistanceSquared = Infinity;", + " for (const chunk of this.chunks) {", + " for (const copy of chunk.copies) {", + " const distanceSquared = (copy.x - x) * (copy.x - x) + (copy.y - y) * (copy.y - y);", + " if (distanceSquared < nearestDistanceSquared) {", + " nearestDistanceSquared = distanceSquared;", + " nearestCopy = copy;", + " }", + " }", + " }", + " return nearestCopy;", + " }", + "", + " getCopyCount() {", + " let count = 0;", + " for (const chunk of this.chunks) count += chunk.copies.length;", + " return count;", + " }", + "", + " /** Remove the copies whose base is in a circle (they come back when the scene is restarted). */", + " removeCopies(x, y, radius) {", + " let hasRemovedCopies = false;", + " for (const chunk of this.chunks) {", + " for (const copy of chunk.copies) {", + " if ((copy.x - x) * (copy.x - x) + (copy.y - y) * (copy.y - y) <= radius * radius) {", + " this.removedCopies.set(copy.key, copy);", + " hasRemovedCopies = true;", + " }", + " }", + " }", + " if (hasRemovedCopies) this._rebuildArea(x - radius, y - radius, x + radius, y + radius);", + " }", + "", + " /** Paint the full density around a position, and bring back the copies removed there. */", + " addCopies(x, y, radius) {", + " for (const [key, copy] of this.removedCopies) {", + " if ((copy.x - x) * (copy.x - x) + (copy.y - y) * (copy.y - y) <= radius * radius) {", + " this.removedCopies.delete(key);", + " }", + " }", + " this.paint(x, y, x, y, radius, 1, 1);", + " }", + "", + " _getStroke(x1, y1, x2, y2, radius) {", + " const { object } = this;", + " const stroke = this._stroke;", + " stroke.cellWidth = object.getWidth() / MASK_RESOLUTION;", + " stroke.cellHeight = object.getHeight() / MASK_RESOLUTION;", + " stroke.ax = x1 - object.getX();", + " stroke.ay = y1 - object.getY();", + " stroke.bx = x2 - object.getX();", + " stroke.by = y2 - object.getY();", + " stroke.radius = Math.max(radius, 0);", + " return stroke;", + " }", + "", + " /**", + " * Paint the density around a segment (copies are placed again at the next frame).", + " * @param {number} density From 0 (erase) to 1 (as dense as the Density property).", + " * @param {number} strength From 0 to 1.", + " */", + " paint(x1, y1, x2, y2, radius, density, strength) {", + " const changedSamples = new SampleRectangle();", + " this.mask.paint(this._getStroke(x1, y1, x2, y2, radius), density, strength, changedSamples);", + " this.changedSamples.add(changedSamples);", + " }", + "", + " /** @param {DensityMask} mask */", + " setMask(mask) {", + " this.mask = mask;", + " this.rebuild();", + " }", + "", + " /**", + " * Prisms around the trunks, from 0 to 1 in the object box (see `gdjs.Surface`).", + " * @returns {gdjs.SurfaceTriangles}", + " */", + " _getTrunkTriangles() {", + " const { object } = this;", + " const copies = object._getTrunkCollisions() ? this.getCopies() : [];", + " const verticesPerCopy = TRUNK_SIDES * 2 + 1;", + " const positions = new Float32Array(copies.length * verticesPerCopy * 3);", + " const indices = new Uint32Array(copies.length * TRUNK_SIDES * 3 * 3);", + " const width = object.getWidth() || 1;", + " const height = object.getHeight() || 1;", + " const depth = object.getDepth() || 1;", + " let indexOffset = 0;", + " copies.forEach((copy, copyIndex) => {", + " const radius = (Math.max(object._getTrunkDiameter(), 0) / 2) * (copy.height / Math.max(object._getSize(), 1));", + " const top = copy.z + copy.height * 0.8;", + " const firstVertex = copyIndex * verticesPerCopy;", + " const setVertex = (vertex, x, y, z) => {", + " positions[vertex * 3] = (x - object.getX()) / width;", + " positions[vertex * 3 + 1] = (y - object.getY()) / height;", + " positions[vertex * 3 + 2] = (z - object.getZ()) / depth;", + " };", + " for (let side = 0; side < TRUNK_SIDES; side++) {", + " const angle = ((side + 0.5) / TRUNK_SIDES) * Math.PI * 2;", + " const x = copy.x + Math.cos(angle) * radius;", + " const y = copy.y + Math.sin(angle) * radius;", + " // Starting a bit under the ground, which can be sloped.", + " setVertex(firstVertex + side * 2, x, y, copy.z - radius);", + " setVertex(firstVertex + side * 2 + 1, x, y, top);", + " }", + " const center = firstVertex + TRUNK_SIDES * 2;", + " setVertex(center, copy.x, copy.y, top);", + " for (let side = 0; side < TRUNK_SIDES; side++) {", + " const bottom = firstVertex + side * 2;", + " const nextBottom = firstVertex + ((side + 1) % TRUNK_SIDES) * 2;", + " // Triangles face out of the prism (vertices at odd indices are at the top).", + " indices.set([bottom, nextBottom, bottom + 1, nextBottom, nextBottom + 1, bottom + 1], indexOffset);", + " indices.set([center, bottom + 1, nextBottom + 1], indexOffset + 6);", + " indexOffset += 9;", + " }", + " });", + " if (copies.length === 0) {", + " // Without triangles, the physics engine and navigation meshes would use", + " // the object box: a tiny triangle under the object is used instead.", + " return { positions: new Float32Array([0, 0, -1, 0.0001, 0, -1, 0, 0.0001, -1]), indices: new Uint32Array([0, 2, 1]) };", + " }", + " return { positions, indices };", + " }", + "", + " dispose() {", + " this.isDisposed = true;", + " if (typeof this.object.setSurface === 'function' && this.object.getSurface() === this.surface) {", + " this.object.setSurface(null);", + " }", + " this._disposeChunks(this.chunks);", + " this._disposeModel();", + " this.group.removeFromParent();", + " }", + "}", + "", + "const scatterToolIcons = {", + " Add: svgIcon(", + " ''", + " ),", + " Erase: svgIcon(", + " ''", + " ),", + "};", + "", + "/**", + " * The brushes shown in the scene editor when scattered models are selected,", + " * to add or erase copies. Without the needed editor functions (older GDevelop", + " * versions), nothing is shown: copies can still be painted with events.", + " */", + "class ScatterEditorTools {", + " constructor() {", + " /** '' when no brush is used. */", + " this.toolName = '';", + " this.sizePercent = 30;", + " this.strengthPercent = 50;", + " /** @type {Scatter | null} */", + " this.scatter = null;", + " /** @type {gdjs.InGameEditor | null} */", + " this.editor = null;", + " this.isStroking = false;", + " this.lastStrokeTime = 0;", + " /** @type {Array<{mask: DensityMask, baseMask: DensityMask}>} */", + " this.undoSteps = [];", + " this.raycaster = new THREE.Raycaster();", + " this.ndc = new THREE.Vector2();", + " this._groundAtCursor = { ground: null, z: 0 };", + " /** A circle following the ground under the cursor. */", + " this.brushRing = new THREE.LineLoop(", + " new THREE.BufferGeometry().setAttribute('position', new THREE.BufferAttribute(new Float32Array(64 * 3), 3)),", + " new THREE.LineBasicMaterial({ color: 0xffffff, depthTest: false, transparent: true })", + " );", + " this.brushRing.renderOrder = 1000;", + " this.brushRing.frustumCulled = false;", + " this.brushRing.raycast = () => {};", + " }", + "", + " /** @param {gdjs.InGameEditor} editor */", + " update(editor) {", + " if (typeof editor.showToolbar !== 'function') return;", + " this.editor = editor;", + " const selectedObjects = editor.getSelectedObjects();", + " const scatter =", + " selectedObjects.length === 1 && selectedObjects[0].__terrainScatter ? selectedObjects[0].__terrainScatter : null;", + " if (scatter !== this.scatter) {", + " this._endStroke();", + " this.scatter = scatter;", + " this.undoSteps.length = 0;", + " }", + " if (scatter) editor.showToolbar('ScatteredModels', this._getToolbarItems());", + " const game = editor.getRuntimeGame();", + " if (!scatter || !this.toolName) {", + " this.brushRing.removeFromParent();", + " return;", + " }", + " const inputManager = game.getInputManager();", + " if (inputManager.wasKeyJustPressed(gdjs.evtTools.input.keysNameToCode.Escape)) {", + " this._endStroke();", + " this.toolName = '';", + " this.brushRing.removeFromParent();", + " return;", + " }", + " editor.captureLeftMouseButton();", + " const hit = this._getPositionUnderCursor(scatter, game);", + " const radius = this._getRadius();", + " this._showBrushRing(scatter, hit, radius);", + "", + " const isPressedForTool =", + " typeof editor.isCapturedLeftMouseButtonPressed === 'function'", + " ? editor.isCapturedLeftMouseButtonPressed()", + " : inputManager.isMouseButtonPressed(0);", + " // The button is released outside the canvas when the mouse is on a toolbar.", + " const isPressed = isPressedForTool && inputManager.isMouseInsideCanvas();", + " if (isPressed && hit && !this.isStroking) {", + " this._startStroke(scatter);", + " }", + " if (this.isStroking && isPressed && hit) {", + " this._applyBrush(scatter, hit, radius);", + " }", + " if (!isPressed) {", + " this._endStroke();", + " }", + " }", + "", + " _getRadius() {", + " const { object } = this.scatter;", + " const maxRadius = Math.max(object.getWidth(), object.getHeight()) / 4;", + " return Math.max(maxRadius * Math.pow(this.sizePercent / 100, 2), 10);", + " }", + "", + " /**", + " * @returns {{x: number, y: number} | null} The position of the scene under the cursor: on the", + " * highest ground hit, or on the plane at the object Z.", + " */", + " _getPositionUnderCursor(scatter, game) {", + " let hit = null;", + " for (const ground of getGrounds(scatter.object.getRuntimeScene())) {", + " if (typeof ground.raycastFromCursor !== 'function') continue;", + " const localHit = ground.raycastFromCursor(game, this.raycaster, this.ndc);", + " if (!localHit) continue;", + " const sceneHit = ground.toScenePosition(localHit);", + " if (!hit || sceneHit.z > hit.z) hit = sceneHit;", + " }", + " if (hit) return hit;", + "", + " const { object } = scatter;", + " const layer = object.getInstanceContainer().getLayer(object.getLayer());", + " const camera = layer.getRenderer().getThreeCamera();", + " if (!camera) return null;", + " const inputManager = game.getInputManager();", + " this.ndc.set(", + " (inputManager.getCursorX() / game.getGameResolutionWidth()) * 2 - 1,", + " -(inputManager.getCursorY() / game.getGameResolutionHeight()) * 2 + 1", + " );", + " this.raycaster.setFromCamera(this.ndc, camera);", + " scatter.group.updateWorldMatrix(true, false);", + " const ray = this.raycaster.ray.clone().applyMatrix4(scatter.group.matrixWorld.clone().invert());", + " const point = ray.intersectPlane(new THREE.Plane(new THREE.Vector3(0, 0, 1), 0), new THREE.Vector3());", + " if (!point) return null;", + " return {", + " x: object.getX() + (point.x / AREA_SIZE) * object.getWidth(),", + " y: object.getY() + (point.y / AREA_SIZE) * object.getHeight(),", + " };", + " }", + "", + " _showBrushRing(scatter, hit, radius) {", + " if (!hit) {", + " this.brushRing.removeFromParent();", + " return;", + " }", + " const { object } = scatter;", + " if (this.brushRing.parent !== scatter.group) scatter.group.add(this.brushRing);", + " const grounds = getGrounds(object.getRuntimeScene());", + " const positions = this.brushRing.geometry.attributes.position;", + " for (let index = 0; index < positions.count; index++) {", + " const angle = (index / positions.count) * Math.PI * 2;", + " const x = hit.x + Math.cos(angle) * radius;", + " const y = hit.y + Math.sin(angle) * radius;", + " const { ground, z } = findHighestGround(grounds, x, y, this._groundAtCursor);", + " positions.setXYZ(", + " index,", + " ((x - object.getX()) / object.getWidth()) * AREA_SIZE,", + " ((y - object.getY()) / object.getHeight()) * AREA_SIZE,", + " (((ground ? z : object.getZ()) + 2 - object.getZ()) / Math.max(object.getDepth(), 0.0001)) * AREA_DEPTH", + " );", + " }", + " positions.needsUpdate = true;", + " }", + "", + " _startStroke(scatter) {", + " this.isStroking = true;", + " this.lastStrokeTime = performance.now();", + " this.undoSteps.push({ mask: scatter.mask.clone(), baseMask: scatter.baseMask });", + " while (this.undoSteps.length > 30) this.undoSteps.shift();", + " }", + "", + " _endStroke() {", + " if (!this.isStroking) return;", + " this.isStroking = false;", + " this._save();", + " }", + "", + " _applyBrush(scatter, hit, radius) {", + " const now = performance.now();", + " const elapsedSeconds = Math.min((now - this.lastStrokeTime) / 1000, 0.1);", + " this.lastStrokeTime = now;", + " const strength = (this.strengthPercent / 100) * elapsedSeconds * 8;", + " scatter.paint(hit.x, hit.y, hit.x, hit.y, radius, this.toolName === 'Add' ? 1 : 0, strength);", + " }", + "", + " _save() {", + " const { scatter, editor } = this;", + " if (!scatter || !editor) return;", + " editor.updateObjectProperties(scatter.object.getName(), { PaintData: scatter.save() });", + " }", + "", + " _undo() {", + " const undoStep = this.undoSteps.pop();", + " // The coverage or the edits may have changed since the stroke.", + " if (!this.scatter || !undoStep || undoStep.baseMask !== this.scatter.baseMask) {", + " this.undoSteps.length = 0;", + " return;", + " }", + " this.scatter.setMask(undoStep.mask);", + " this._save();", + " }", + "", + " /** @returns {Array} */", + " _getToolbarItems() {", + " const toolButton = (toolName, tooltip) => ({", + " type: 'button',", + " id: toolName,", + " tooltip,", + " iconUrl: scatterToolIcons[toolName],", + " isActive: this.toolName === toolName,", + " onClick: () => (this.toolName = this.toolName === toolName ? '' : toolName),", + " });", + " return [", + " toolButton('Add', 'Add copies (drag on the ground)'),", + " toolButton('Erase', 'Erase copies'),", + " { type: 'divider', id: 'BrushDivider' },", + " {", + " type: 'slider',", + " id: 'Size',", + " tooltip: 'Brush size',", + " iconUrl: toolIcons.Size,", + " min: 1,", + " max: 100,", + " value: this.sizePercent,", + " onChange: (value) => (this.sizePercent = value),", + " },", + " {", + " type: 'slider',", + " id: 'Strength',", + " tooltip: 'Brush strength',", + " iconUrl: toolIcons.Strength,", + " min: 5,", + " max: 100,", + " value: this.strengthPercent,", + " onChange: (value) => (this.strengthPercent = value),", + " },", + " {", + " type: 'button',", + " id: 'Undo',", + " tooltip: 'Undo the last stroke on these scattered models',", + " iconUrl: toolIcons.Undo,", + " onClick: () => this._undo(),", + " },", + " ];", + " }", + "}", + "", + "const editorTools = new ScatterEditorTools();", + "if (gdjs.registerInGameEditorPostStepCallback) {", + " gdjs.registerInGameEditorPostStepCallback((editor) => editorTools.update(editor));", + "}", + "", + "gdjs.__terrainScatterExtension = {", + " Scatter,", + " // Exposed for tests.", + " DensityMask,", + " maskCodec,", + " applyEdits,", + "};", + "" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [], + "objectGroups": [] + } + ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "DefineHelperClasses" + }, + { + "functionName": "DefineGrassHelperClasses" + }, + { + "functionName": "DefineScatterHelperClasses" + } + ] + }, + "eventsBasedBehaviors": [], + "eventsBasedObjects": [ + { + "areaMaxX": 4096, + "areaMaxY": 4096, + "areaMaxZ": 600, + "areaMinX": 0, + "areaMinY": 0, + "areaMinZ": 0, + "assetStoreTag": "3d terrains", + "defaultName": "Terrain", + "description": "A ground with hills, mountains or valleys that can be sculpted and painted in the scene editor, or changed with events. Add the 3D physics behavior to it to make objects walk or roll on it.", + "fullName": "3D terrain", + "helpPath": "", + "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLXRlcnJhaW4iIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTQsNkwxMC4yNSwxMUwxMy4xLDE0LjhMMTEuNSwxNkM5LjgxLDEzLjc1IDcsMTAgNywxMEwxLDE4SDIzTDE0LDZaIiAvPjwvc3ZnPg==", + "is3D": true, + "isUsingLegacyInstancesRenderer": true, + "name": "Terrain3D", + "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", + "objects": [ + { + "assetStoreId": "", + "name": "AreaPlaceholder", + "type": "Scene3D::Cube3DObject", + "variables": [], + "effects": [], + "behaviors": [], + "content": { + "width": 4096, + "height": 4096, + "depth": 600, + "enableTextureTransparency": false, + "facesOrientation": "Y", + "backFaceUpThroughWhichAxisRotation": "X", + "materialType": "Basic", + "tint": "255;255;255", + "frontFaceResourceName": "", + "frontFaceVisible": false, + "frontFaceResourceRepeat": false, + "backFaceResourceName": "", + "backFaceVisible": false, + "backFaceResourceRepeat": false, + "leftFaceResourceName": "", + "leftFaceVisible": false, + "leftFaceResourceRepeat": false, + "rightFaceResourceName": "", + "rightFaceVisible": false, + "rightFaceResourceRepeat": false, + "topFaceResourceName": "", + "topFaceVisible": false, + "topFaceResourceRepeat": false, + "bottomFaceResourceName": "", + "bottomFaceVisible": false, + "bottomFaceResourceRepeat": false + } + } + ], + "objectsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "objectName": "AreaPlaceholder" + } + ] + }, + "objectsGroups": [], + "layers": [ + { + "ambientLightColorB": 200, + "ambientLightColorG": 200, + "ambientLightColorR": 200, + "camera2DPlaneMaxDrawingDistance": 5000, + "camera3DFarPlaneDistance": 10000, + "camera3DFieldOfView": 45, + "camera3DNearPlaneDistance": 3, + "cameraType": "", + "followBaseLayerCamera": false, + "isLightingLayer": false, + "isLocked": false, + "name": "", + "renderingType": "", + "visibility": true, + "cameras": [ + { + "defaultSize": true, + "defaultViewport": true, + "height": 0, + "viewportBottom": 1, + "viewportLeft": 0, + "viewportRight": 1, + "viewportTop": 0, + "width": 0 + } + ], + "effects": [] + } + ], + "instances": [ + { + "angle": 0, + "customSize": true, + "depth": 600, + "height": 4096, + "keepRatio": true, + "layer": "", + "name": "AreaPlaceholder", + "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-004096004096", + "width": 4096, + "x": 0, + "y": 0, + "zOrder": 1, + "numberProperties": [], + "stringProperties": [], + "initialVariables": [] + } + ], + "editionSettings": {}, + "eventsFunctions": [ + { + "fullName": "", + "functionType": "Action", + "name": "onCreated", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Terrain3D::DefineHelperClasses" + }, + "parameters": [ + "", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "for (const child of object.getChildrenContainer().getAdhocListOfAllInstances()) {", + " child.deleteFromScene();", + "}", + "if (object.__terrain3D) object.__terrain3D.dispose();", + "object.__terrain3D = new gdjs.__terrain3DExtension.Terrain(object);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "onDestroy", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrain3D.dispose();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "doStepPostEvents", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrain3D.update();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "onHotReloading", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrain3D.onHotReloading();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "description": "Raise the ground around a position (or lower it with a negative height). It's raised the most at the center and less and less up to the radius.", + "fullName": "Raise or lower the ground", + "functionType": "Action", + "group": "Sculpt", + "name": "RaiseGround", + "sentence": "Raise the ground of _PARAM0_ by _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const height = eventsFunctionContext.getArgument(\"Height\");", + "object.__terrain3D.raise(x, y, x, y, radius, height);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Height to add (negative to lower the ground)", + "name": "Height", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Raise the ground along a line, for example for a ridge, or lower it with a negative height to dig a river bed or a valley.", + "fullName": "Raise or lower the ground along a line", + "functionType": "Action", + "group": "Sculpt", + "name": "RaiseGroundAlongLine", + "sentence": "Raise the ground of _PARAM0_ by _PARAM6_ from _PARAM1_; _PARAM2_ to _PARAM3_; _PARAM4_ with a radius of _PARAM5_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x1 = eventsFunctionContext.getArgument(\"X1\");", + "const y1 = eventsFunctionContext.getArgument(\"Y1\");", + "const x2 = eventsFunctionContext.getArgument(\"X2\");", + "const y2 = eventsFunctionContext.getArgument(\"Y2\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const height = eventsFunctionContext.getArgument(\"Height\");", + "object.__terrain3D.raise(x1, y1, x2, y2, radius, height);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the start", + "name": "X1", + "type": "expression" + }, + { + "description": "Y position of the start", + "name": "Y1", + "type": "expression" + }, + { + "description": "X position of the end", + "name": "X2", + "type": "expression" + }, + { + "description": "Y position of the end", + "name": "Y2", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance from the line.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Height to add (negative to lower the ground)", + "name": "Height", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Flatten the ground around a position to a given height, for example to place a building.", + "fullName": "Flatten the ground", + "functionType": "Action", + "group": "Sculpt", + "name": "FlattenGround", + "sentence": "Flatten the ground of _PARAM0_ to a Z of _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const z = eventsFunctionContext.getArgument(\"Z\");", + "object.__terrain3D.flatten(x, y, x, y, radius, z, 1);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Z position of the flat ground", + "name": "Z", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Smooth the ground around a position, softening bumps and cliffs.", + "fullName": "Smooth the ground", + "functionType": "Action", + "group": "Sculpt", + "name": "SmoothGround", + "sentence": "Smooth the ground of _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_ (strength: _PARAM4_)", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const strength = eventsFunctionContext.getArgument(\"Strength\");", + "object.__terrain3D.smooth(x, y, x, y, radius, strength);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Strength (between 0 and 1)", + "name": "Strength", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Paint a layer (like grass, dirt, rock or sand) on the ground around a position.", + "fullName": "Paint a layer", + "functionType": "Action", + "group": "Paint", + "name": "PaintLayer", + "sentence": "Paint layer _PARAM4_ on _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const layer = eventsFunctionContext.getArgument(\"Layer\");", + "object.__terrain3D.paint(x, y, x, y, radius, layer, 1);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Layer (1 to 4)", + "name": "Layer", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Paint a layer on the ground along a line, for example for a path or a road.", + "fullName": "Paint a layer along a line", + "functionType": "Action", + "group": "Paint", + "name": "PaintLayerAlongLine", + "sentence": "Paint layer _PARAM6_ on _PARAM0_ from _PARAM1_; _PARAM2_ to _PARAM3_; _PARAM4_ with a radius of _PARAM5_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x1 = eventsFunctionContext.getArgument(\"X1\");", + "const y1 = eventsFunctionContext.getArgument(\"Y1\");", + "const x2 = eventsFunctionContext.getArgument(\"X2\");", + "const y2 = eventsFunctionContext.getArgument(\"Y2\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const layer = eventsFunctionContext.getArgument(\"Layer\");", + "object.__terrain3D.paint(x1, y1, x2, y2, radius, layer, 1);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the start", + "name": "X1", + "type": "expression" + }, + { + "description": "Y position of the start", + "name": "Y1", + "type": "expression" + }, + { + "description": "X position of the end", + "name": "X2", + "type": "expression" + }, + { + "description": "Y position of the end", + "name": "Y2", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance from the line.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Layer (1 to 4)", + "name": "Layer", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Tint the ground around a position with a color multiplying its layers, like dry grass or darker mud. White removes the tint.", + "fullName": "Tint the ground", + "functionType": "Action", + "group": "Paint", + "name": "TintGround", + "sentence": "Tint the ground of _PARAM0_ with _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const color = gdjs.rgbOrHexToRGBColor(eventsFunctionContext.getArgument(\"Color\"));", + "object.__terrain3D.tint(x, y, x, y, radius, color, 1);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Color", + "name": "Color", + "type": "color" + } + ], + "objectGroups": [] + }, + { + "description": "Make the ground around a position wet (darker and shiny, like after rain or near water) or dry.", + "fullName": "Change the wetness of the ground", + "functionType": "Action", + "group": "Paint", + "name": "WetGround", + "sentence": "Change the wetness of the ground of _PARAM0_ to _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const wetness = eventsFunctionContext.getArgument(\"Wetness\");", + "object.__terrain3D.wet(x, y, x, y, radius, wetness, 1);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Wetness (from 0 for dry to 1)", + "name": "Wetness", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "the wetness of the ground at a position, from 0 (dry) to 1, painted or near the water (wet shore).", + "fullName": "Ground wetness", + "functionType": "ExpressionAndCondition", + "group": "Paint", + "name": "GroundWetness", + "sentence": "the wetness of the ground at _PARAM1_; _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "eventsFunctionContext.returnValue = object.__terrain3D.getWetnessAt(x, y);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "The tint painted on the ground at a position, like \"255;255;255\" (white when there is none).", + "fullName": "Ground tint", + "functionType": "StringExpression", + "group": "Paint", + "name": "GroundTint", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "eventsFunctionContext.returnValue = object.__terrain3D.getTintAt(x, y).join(\";\");" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "string" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Remove the ground in a circle, for the entrance of a tunnel, a cave or a well: there is nothing drawn and no collision there. Fill it again with the \"Fill holes\" action. A height field can't go under itself: put a 3D model (with a collision shape) for the tunnel or the cave under the ground. The physics engine removes the triangles touching a hole, so the hole in the collisions can be up to a cell larger than the one drawn: make the model go a bit further than the hole, or put rocks around the entrance.", + "fullName": "Make a hole", + "functionType": "Action", + "group": "Holes", + "name": "MakeHole", + "sentence": "Make a hole in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "object.__terrain3D.makeHole(x, y, x, y, radius);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "name": "Radius", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Fill the holes in a circle with the layers of the ground around them: the ground is drawn and has collisions again.", + "fullName": "Fill holes", + "functionType": "Action", + "group": "Holes", + "name": "FillHoles", + "sentence": "Fill the holes in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "object.__terrain3D.fillHoles(x, y, x, y, radius);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "name": "Radius", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Make the entrance of a tunnel or a cave on a slope: a hole, with the ground in front of it flattened to the floor of the tunnel.", + "fullName": "Carve an entrance", + "functionType": "Action", + "group": "Holes", + "name": "CarveEntrance", + "sentence": "Carve an entrance in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_, going in at _PARAM4_° with its floor at Z _PARAM5_ and _PARAM6_ pixels of flat ground in front", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const angle = eventsFunctionContext.getArgument(\"Angle\");", + "const floorZ = eventsFunctionContext.getArgument(\"FloorZ\");", + "const length = eventsFunctionContext.getArgument(\"Length\");", + "object.__terrain3D.carveEntrance(x, y, radius, angle, floorZ, length);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "name": "Radius", + "type": "expression" + }, + { + "description": "Direction going into the ground (in degrees)", + "name": "Angle", + "type": "expression" + }, + { + "description": "Z position of the floor", + "name": "FloorZ", + "type": "expression" + }, + { + "description": "Length of the flat ground in front of the entrance (in pixels)", + "name": "Length", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Check if there is a hole in the ground at a position.", + "fullName": "Hole", + "functionType": "Condition", + "group": "Holes", + "name": "IsHole", + "sentence": "There is a hole in _PARAM0_ at _PARAM1_; _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "eventsFunctionContext.returnValue = object.__terrain3D.isHoleAt(x, y);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "The Z position of the ground at a position, or the given value where there is a hole.", + "fullName": "Ground Z position, with holes", + "functionType": "Expression", + "group": "Holes", + "name": "GroundZWithHoles", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const terrain = object.__terrain3D;", + "eventsFunctionContext.returnValue = terrain.isHoleAt(x, y) ? eventsFunctionContext.getArgument(\"ZInHoles\") : terrain.getHeightAt(x, y);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + }, + { + "description": "Value in holes", + "longDescription": "For example, a Z position far under the ground.", + "name": "ZInHoles", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Replace the ground with a generated relief. Painted layers are reset.", + "fullName": "Generate a relief", + "functionType": "Action", + "group": "Generation", + "name": "GenerateRelief", + "sentence": "Generate a relief of _PARAM1_ on _PARAM0_ with seed _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const relief = eventsFunctionContext.getArgument(\"Relief\");", + "const seed = eventsFunctionContext.getArgument(\"Seed\");", + "object.__terrain3D.generate(relief, seed);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "Relief", + "name": "Relief", + "supplementaryInformation": "[\"Flat\", \"Hills\", \"Mountains\", \"Island\"]", + "type": "stringWithSelector" + }, + { + "description": "Seed", + "longDescription": "Each seed gives a different terrain with the same relief.", + "name": "Seed", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Replace the ground with the heights of an image. Painted layers are kept.", + "fullName": "Load a heightmap", + "functionType": "Action", + "group": "Generation", + "name": "LoadHeightmap", + "sentence": "Load the heights of _PARAM0_ from _PARAM1_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const image = eventsFunctionContext.getArgument(\"Image\");", + "object.__terrain3D.loadHeightmap(image);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "Heightmap image", + "longDescription": "Black is the lowest, white the highest. For more precision, store the height on 16 bits in the red (most significant byte) and green channels.", + "name": "Image", + "type": "imageResource" + } + ], + "objectGroups": [] + }, + { + "description": "Move objects up or down so that their bottom is on the ground, under their center. Useful to place trees, rocks or characters on a terrain.", + "fullName": "Put objects on the ground", + "functionType": "Action", + "group": "Position", + "name": "PutOnGround", + "sentence": "Put _PARAM1_ on the ground of _PARAM0_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrain3D.placeOnGround(eventsFunctionContext.getObjects(\"Objects\"));" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "Objects to put on the ground", + "name": "Objects", + "type": "objectList" + } + ], + "objectGroups": [] + }, + { + "description": "the Z position of the ground at a position of the scene.", + "fullName": "Ground Z position", + "functionType": "ExpressionAndCondition", + "group": "Position", + "name": "GroundZ", + "sentence": "the ground Z position at _PARAM1_; _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "eventsFunctionContext.returnValue = object.__terrain3D.getHeightAt(x, y);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "the slope of the ground (angle with the horizontal, in degrees) at a position of the scene.", + "fullName": "Ground slope", + "functionType": "ExpressionAndCondition", + "group": "Position", + "name": "GroundSlope", + "sentence": "the ground slope at _PARAM1_; _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "eventsFunctionContext.returnValue = object.__terrain3D.getSlopeAt(x, y);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "the layer (from 1 to 4) painted the most at a position of the scene. Useful to play footstep sounds or to slow down characters in water or mud.", + "fullName": "Ground layer", + "functionType": "ExpressionAndCondition", + "group": "Paint", + "name": "GroundLayer", + "sentence": "the ground layer at _PARAM1_; _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "eventsFunctionContext.returnValue = object.__terrain3D.getLayerAt(x, y);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + } + ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + }, + { + "folderName": "Sculpt", + "children": [ + { + "functionName": "RaiseGround" + }, + { + "functionName": "RaiseGroundAlongLine" + }, + { + "functionName": "FlattenGround" + }, + { + "functionName": "SmoothGround" + } + ] + }, + { + "folderName": "Paint", + "children": [ + { + "functionName": "PaintLayer" + }, + { + "functionName": "PaintLayerAlongLine" + }, + { + "functionName": "TintGround" + }, + { + "functionName": "WetGround" + }, + { + "functionName": "GroundWetness" + }, + { + "functionName": "GroundTint" + }, + { + "functionName": "GroundLayer" + } + ] + }, + { + "folderName": "Holes", + "children": [ + { + "functionName": "MakeHole" + }, + { + "functionName": "FillHoles" + }, + { + "functionName": "CarveEntrance" + }, + { + "functionName": "IsHole" + }, + { + "functionName": "GroundZWithHoles" + } + ] + }, + { + "folderName": "Generation", + "children": [ + { + "functionName": "GenerateRelief" + }, + { + "functionName": "LoadHeightmap" + } + ] + }, + { + "folderName": "Position", + "children": [ + { + "functionName": "PutOnGround" + }, + { + "functionName": "GroundZ" + }, + { + "functionName": "GroundSlope" + } + ] + } + ] + }, + "propertyDescriptors": [ + { + "value": "Hills", + "type": "Choice", + "label": "Relief", + "description": "The initial shape, before sculpting.", + "group": "Shape", + "choices": [ + { + "label": "Flat", + "value": "Flat" + }, + { + "label": "Hills", + "value": "Hills" + }, + { + "label": "Mountains", + "value": "Mountains" + }, + { + "label": "Island", + "value": "Island" + } + ], + "name": "Relief" + }, + { + "value": "1", + "type": "Number", + "label": "Seed", + "description": "Each seed gives a different terrain with the same relief.", + "group": "Shape", + "name": "Seed" + }, + { + "value": "", + "type": "Resource", + "label": "Heightmap image", + "description": "Optional. Replaces the relief: black is the lowest, white the highest (the object depth).", + "group": "Shape", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "HeightmapImage" + }, + { + "value": "", + "type": "MultilineString", + "label": "Edits", + "description": "Changes of the relief or heightmap, applied in order, as a JSON list, for example [{\"tool\": \"raise\", \"x\": 0.3, \"y\": 0.4, \"radius\": 0.15, \"height\": 0.4}, {\"tool\": \"paint\", \"x\": 0.1, \"y\": 0.9, \"toX\": 0.8, \"toY\": 0.2, \"radius\": 0.02, \"layer\": 2}]. Tools: raise (height added to the ground, negative to lower), flatten (z: the ground is brought to this height, fully at the center, cutting what is above it and filling what is below; with toX and toY, toZ gives the height at the end of the line, for a road going up), smooth (strength), paint (layer, from 1 to 4, and strength), tint (color, like \"120;140;60\", multiplying the layers, white removing the tint, and strength), wetness (amount, from 0 for dry to 1 for wet, and strength), hole (no ground in the radius, for the entrances of tunnels and caves) and fill (the holes in the radius are filled with the layers of the ground around them). x, y and radius are fractions (0 to 1) of the terrain size (x is (scene X - terrain X) / terrain width). height and z are fractions of its depth: the ground at z is at Z = terrain Z + z * terrain depth. Add toX and toY to change the ground along a line (paths, rivers, ridges). Shown in the scene editor.", + "group": "Shape", + "advanced": true, + "name": "Edits" + }, + { + "value": "256", + "type": "Choice", + "label": "Resolution", + "description": "The number of cells on each side of the terrain. Higher is more detailed but slower.", + "group": "Shape", + "choices": [ + { + "label": "64 x 64", + "value": "64" + }, + { + "label": "128 x 128", + "value": "128" + }, + { + "label": "256 x 256", + "value": "256" + }, + { + "label": "512 x 512", + "value": "512" + }, + { + "label": "1024 x 1024", + "value": "1024" + } + ], + "name": "Resolution" + }, + { + "value": "96;160;64", + "type": "Color", + "label": "Layer 1 color (grass)", + "group": "Layers", + "name": "Layer1Color" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 1 texture", + "description": "Optional. Tinted by the layer color.", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer1Texture" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 1 normal map", + "description": "Optional. The bumps of the texture, lit by the lights (an OpenGL normal map, like the textures of ambientCG).", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer1NormalMap" + }, + { + "value": "150;112;72", + "type": "Color", + "label": "Layer 2 color (dirt)", + "group": "Layers", + "name": "Layer2Color" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 2 texture", + "description": "Optional. Tinted by the layer color.", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer2Texture" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 2 normal map", + "description": "Optional. The bumps of the texture, lit by the lights (an OpenGL normal map, like the textures of ambientCG).", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer2NormalMap" + }, + { + "value": "124;118;112", + "type": "Color", + "label": "Layer 3 color (rock)", + "group": "Layers", + "name": "Layer3Color" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 3 texture", + "description": "Optional. Tinted by the layer color.", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer3Texture" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 3 normal map", + "description": "Optional. The bumps of the texture, lit by the lights (an OpenGL normal map, like the textures of ambientCG).", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer3NormalMap" + }, + { + "value": "224;204;148", + "type": "Color", + "label": "Layer 4 color (sand)", + "group": "Layers", + "name": "Layer4Color" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 4 texture", + "description": "Optional. Tinted by the layer color.", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer4Texture" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 4 normal map", + "description": "Optional. The bumps of the texture, lit by the lights (an OpenGL normal map, like the textures of ambientCG).", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer4NormalMap" + }, + { + "value": "1", + "type": "Number", + "label": "Normal map strength", + "description": "How much the normal maps bump the ground: 0 for none, 1 as they are, more to exaggerate them.", + "group": "Layers", + "name": "NormalMapStrength" + }, + { + "value": "256", + "type": "Number", + "unit": "Pixel", + "label": "Texture size", + "description": "The size, in the scene, of one repetition of the layer textures.", + "group": "Layers", + "name": "TextureSize" + }, + { + "value": "0.5", + "type": "Number", + "label": "Height blending", + "description": "Where layers meet, the bright parts of textures (stones, tufts) cover the dark ones (gaps, sand) instead of fading into each other: 0 for none, 1 for the most.", + "group": "Layers", + "name": "HeightBlending" + }, + { + "value": "0.5", + "type": "Number", + "label": "Texture variation", + "description": "Makes the repetition of textures less visible, with patches of bigger and rotated textures and colors slowly varying: 0 for none, 1 for the most.", + "group": "Layers", + "name": "TextureVariation" + }, + { + "value": "false", + "type": "Boolean", + "label": "Wet shore", + "description": "Make the ground wet (darker and shiny) under a Z position, like the sand of a beach near the water.", + "group": "Wetness", + "name": "WetShore" + }, + { + "value": "0", + "type": "Number", + "unit": "Pixel", + "label": "Wet below Z", + "description": "The ground is wet under this Z position (for example, the Z of a 3D water object).", + "group": "Wetness", + "name": "WetShoreZ" + }, + { + "value": "60", + "type": "Number", + "unit": "Pixel", + "label": "Wet shore fade", + "description": "The ground becomes dry over this height above \"Wet below Z\".", + "group": "Wetness", + "name": "WetShoreFade" + }, + { + "value": "3", + "type": "Choice", + "label": "Layer on steep slopes", + "description": "This layer is shown where the ground is steep, whatever is painted.", + "group": "Layers", + "choices": [ + { + "label": "None", + "value": "0" + }, + { + "label": "Layer 1", + "value": "1" + }, + { + "label": "Layer 2", + "value": "2" + }, + { + "label": "Layer 3", + "value": "3" + }, + { + "label": "Layer 4", + "value": "4" + } + ], + "name": "CliffLayer" + }, + { + "value": "45", + "type": "Number", + "unit": "DegreeAngle", + "label": "Steep slope angle", + "group": "Layers", + "name": "CliffAngle" + }, + { + "value": "true", + "type": "Boolean", + "label": "Cast shadows", + "group": "Rendering", + "name": "CastShadow" + }, + { + "value": "true", + "type": "Boolean", + "label": "Receive shadows", + "group": "Rendering", + "name": "ReceiveShadow" + }, + { + "value": "", + "type": "String", + "label": "Sculpted and painted data", + "description": "Written by the sculpt and paint tools of the scene editor, in a compact format not meant to be read or written by hand: use Edits instead. Clear it to go back to the relief, heightmap and edits.", + "advanced": true, + "name": "SculptData" + } + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Shape", + "children": [ + { + "propertyName": "Relief" + }, + { + "propertyName": "Seed" + }, + { + "propertyName": "HeightmapImage" + }, + { + "propertyName": "Edits" + }, + { + "propertyName": "Resolution" + } + ] + }, + { + "folderName": "Layers", + "children": [ + { + "propertyName": "Layer1Color" + }, + { + "propertyName": "Layer1Texture" + }, + { + "propertyName": "Layer1NormalMap" + }, + { + "propertyName": "Layer2Color" + }, + { + "propertyName": "Layer2Texture" + }, + { + "propertyName": "Layer2NormalMap" + }, + { + "propertyName": "Layer3Color" + }, + { + "propertyName": "Layer3Texture" + }, + { + "propertyName": "Layer3NormalMap" + }, + { + "propertyName": "Layer4Color" + }, + { + "propertyName": "Layer4Texture" + }, + { + "propertyName": "Layer4NormalMap" + }, + { + "propertyName": "NormalMapStrength" + }, + { + "propertyName": "TextureSize" + }, + { + "propertyName": "HeightBlending" + }, + { + "propertyName": "TextureVariation" + }, + { + "propertyName": "CliffLayer" + }, + { + "propertyName": "CliffAngle" + } + ] + }, + { + "folderName": "Wetness", + "children": [ + { + "propertyName": "WetShore" + }, + { + "propertyName": "WetShoreZ" + }, + { + "propertyName": "WetShoreFade" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "CastShadow" + }, + { + "propertyName": "ReceiveShadow" + } + ] + }, + { + "propertyName": "SculptData" + } + ] + }, + "variants": [] + }, + { + "areaMaxX": 1000, + "areaMaxY": 1000, + "areaMaxZ": 40, + "areaMinX": 0, + "areaMinY": 0, + "areaMinZ": 0, + "defaultName": "Grass", + "description": "A field of grass blades swaying in the wind and pushed aside by characters. They grow in the area of the object, on the highest 3D terrain at each point (one field can cover several terrains), or at the Z position of the object where there is no terrain.", + "fullName": "Terrain grass", + "helpPath": "", + "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGQ9Ik0zIDIxYzEtNSAxLTkgMC0xMyAyIDMgMyA3IDMgMTN6bTQgMGMwLTYgMS0xMSA0LTE2LTEgNS0xIDEwIDAgMTZ6bTUgMGMwLTUgMi05IDUtMTItMiA0LTIgOC0xIDEyem01IDBjMS00IDItNiA0LTgtMSAzLTEgNS0xIDh6Ii8+PC9zdmc+", + "is3D": true, + "isUsingLegacyInstancesRenderer": true, + "name": "TerrainGrass", + "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/732ef90b7fcf5dd9171fe95d0cf262e09159b487304915a4693baa893d9ce16c_grass.svg", + "objects": [ + { + "assetStoreId": "", + "name": "AreaPlaceholder", + "type": "Scene3D::Cube3DObject", + "variables": [], + "effects": [], + "behaviors": [], + "content": { + "width": 1000, + "height": 1000, + "depth": 40, + "enableTextureTransparency": false, + "facesOrientation": "Y", + "backFaceUpThroughWhichAxisRotation": "X", + "materialType": "Basic", + "tint": "255;255;255", + "frontFaceResourceName": "", + "frontFaceVisible": false, + "frontFaceResourceRepeat": false, + "backFaceResourceName": "", + "backFaceVisible": false, + "backFaceResourceRepeat": false, + "leftFaceResourceName": "", + "leftFaceVisible": false, + "leftFaceResourceRepeat": false, + "rightFaceResourceName": "", + "rightFaceVisible": false, + "rightFaceResourceRepeat": false, + "topFaceResourceName": "", + "topFaceVisible": false, + "topFaceResourceRepeat": false, + "bottomFaceResourceName": "", + "bottomFaceVisible": false, + "bottomFaceResourceRepeat": false + } + } + ], + "objectsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "objectName": "AreaPlaceholder" + } + ] + }, + "objectsGroups": [], + "layers": [ + { + "ambientLightColorB": 200, + "ambientLightColorG": 200, + "ambientLightColorR": 200, + "camera2DPlaneMaxDrawingDistance": 5000, + "camera3DFarPlaneDistance": 10000, + "camera3DFieldOfView": 45, + "camera3DNearPlaneDistance": 3, + "cameraType": "", + "followBaseLayerCamera": false, + "isLightingLayer": false, + "isLocked": false, + "name": "", + "renderingType": "", + "visibility": true, + "cameras": [ + { + "defaultSize": true, + "defaultViewport": true, + "height": 0, + "viewportBottom": 1, + "viewportLeft": 0, + "viewportRight": 1, + "viewportTop": 0, + "width": 0 + } + ], + "effects": [] + } + ], + "instances": [ + { + "angle": 0, + "customSize": true, + "depth": 40, + "height": 1000, + "keepRatio": true, + "layer": "", + "name": "AreaPlaceholder", + "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-001000001000", + "width": 1000, + "x": 0, + "y": 0, + "zOrder": 1, + "numberProperties": [], + "stringProperties": [], + "initialVariables": [] + } + ], + "editionSettings": {}, + "eventsFunctions": [ + { + "fullName": "", + "functionType": "Action", + "name": "onCreated", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Terrain3D::DefineHelperClasses" + }, + "parameters": [ + "", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Terrain3D::DefineGrassHelperClasses" + }, + "parameters": [ + "", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "for (const child of object.getChildrenContainer().getAdhocListOfAllInstances()) {", + " child.deleteFromScene();", + "}", + "if (object.__terrainGrass) object.__terrainGrass.dispose();", + "object.__terrainGrass = new gdjs.__terrainGrassExtension.Grass(object);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "onDestroy", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrainGrass.dispose();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "doStepPostEvents", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const game = runtimeScene.getGame();", + "// Time doesn't pass in the scene editor: real time is used to show the wind.", + "const isInGameEdition = game.isInGameEdition && game.isInGameEdition();", + "const elapsedTime = isInGameEdition ? 1 / 60 : object.getElapsedTime() / 1000;", + "const time = isInGameEdition", + " ? performance.now() / 1000", + " : object.getRuntimeScene().getTimeManager().getTimeFromStart() / 1000;", + "const layer = object.getInstanceContainer().getLayer(object.getLayer());", + "object.__terrainGrass.update(elapsedTime, time, layer.getRenderer().getThreeCamera());" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "onHotReloading", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrainGrass.updateFromProperties();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" + } + ], + "objectGroups": [] + } + ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + } + ] + }, + "propertyDescriptors": [ + { + "value": "60", + "type": "Number", + "label": "Density", + "description": "The number of blades in a square of 100 x 100 pixels.", + "group": "Blades", + "name": "Density" + }, + { + "value": "36", + "type": "Number", + "unit": "Pixel", + "label": "Blade height", + "description": "The average height of blades (each blade is between 60% and 140% of it).", + "group": "Blades", + "name": "BladeHeight" + }, + { + "value": "5", + "type": "Number", + "unit": "Pixel", + "label": "Blade width", + "group": "Blades", + "name": "BladeWidth" + }, + { + "value": "52;104;36", + "type": "Color", + "label": "Base color", + "group": "Blades", + "name": "BaseColor" + }, + { + "value": "150;196;82", + "type": "Color", + "label": "Tip color", + "group": "Blades", + "name": "TipColor" + }, + { + "value": "1", + "type": "Number", + "label": "Seed", + "description": "Each seed places blades differently.", + "group": "Blades", + "name": "Seed" + }, + { + "value": "1", + "type": "Choice", + "label": "Grows on terrain layer", + "description": "On a 3D terrain, blades only grow where this layer is painted. A new terrain has layer 1 painted everywhere.", + "group": "Ground", + "choices": [ + { + "label": "Any", + "value": "0" + }, + { + "label": "Layer 1", + "value": "1" + }, + { + "label": "Layer 2", + "value": "2" + }, + { + "label": "Layer 3", + "value": "3" + }, + { + "label": "Layer 4", + "value": "4" + } + ], + "name": "GroundLayer" + }, + { + "value": "40", + "type": "Number", + "unit": "DegreeAngle", + "label": "Maximum slope", + "description": "No blades grow on steeper ground.", + "group": "Ground", + "name": "MaxSlope" + }, + { + "value": "1", + "type": "Number", + "label": "Wind strength", + "description": "0 for no wind.", + "group": "Wind", + "name": "WindStrength" + }, + { + "value": "2", + "type": "Number", + "label": "Wind speed", + "group": "Wind", + "name": "WindSpeed" + }, + { + "value": "", + "type": "String", + "label": "Object bending the grass", + "description": "The name of an object pushing blades aside when walking through the grass, like the player. The 8 first instances are used.", + "group": "Wind", + "name": "BendingObject" + }, + { + "value": "2500", + "type": "Number", + "unit": "Pixel", + "label": "Fade distance", + "description": "Blades are less and less dense up to this distance from the camera, and hidden after.", + "group": "Rendering", + "name": "FadeDistance" + } + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Blades", + "children": [ + { + "propertyName": "Density" + }, + { + "propertyName": "BladeHeight" + }, + { + "propertyName": "BladeWidth" + }, + { + "propertyName": "BaseColor" + }, + { + "propertyName": "TipColor" + }, + { + "propertyName": "Seed" + } + ] + }, + { + "folderName": "Ground", + "children": [ + { + "propertyName": "GroundLayer" + }, + { + "propertyName": "MaxSlope" + } + ] + }, + { + "folderName": "Wind", + "children": [ + { + "propertyName": "WindStrength" + }, + { + "propertyName": "WindSpeed" + }, + { + "propertyName": "BendingObject" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "FadeDistance" + } + ] + } + ] + }, + "variants": [] + }, + { + "areaMaxX": 2000, + "areaMaxY": 2000, + "areaMaxZ": 200, + "areaMinX": 0, + "areaMinY": 0, + "areaMinZ": 0, + "assetStoreTag": "3d scattered models", + "defaultName": "ScatteredModels", + "description": "Many copies of a 3D model (trees, rocks, bushes, flowers...) placed in the area of the object and drawn all at once, which is much faster than an object for each one. They stand on the highest 3D terrain at each point, or at the Z position of the object where there is no terrain. Paint where they grow with the brushes of the scene editor.", + "fullName": "Scattered models (forests, rocks...)", + "helpPath": "", + "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGQ9Ik0xMCAyMXYtM0gzbDUtNUg1bDUtNUg3bDUtNSA1IDVoLTNsNSA1aC0zbDUgNWgtN3YzeiIvPjwvc3ZnPg==", + "is3D": true, + "isUsingLegacyInstancesRenderer": true, + "name": "ScatteredModels", + "previewIconUrl": "", + "objects": [ + { + "assetStoreId": "", + "name": "AreaPlaceholder", + "type": "Scene3D::Cube3DObject", + "variables": [], + "effects": [], + "behaviors": [], + "content": { + "width": 2000, + "height": 2000, + "depth": 200, + "enableTextureTransparency": false, + "facesOrientation": "Y", + "backFaceUpThroughWhichAxisRotation": "X", + "materialType": "Basic", + "tint": "255;255;255", + "frontFaceResourceName": "", + "frontFaceVisible": false, + "frontFaceResourceRepeat": false, + "backFaceResourceName": "", + "backFaceVisible": false, + "backFaceResourceRepeat": false, + "leftFaceResourceName": "", + "leftFaceVisible": false, + "leftFaceResourceRepeat": false, + "rightFaceResourceName": "", + "rightFaceVisible": false, + "rightFaceResourceRepeat": false, + "topFaceResourceName": "", + "topFaceVisible": false, + "topFaceResourceRepeat": false, + "bottomFaceResourceName": "", + "bottomFaceVisible": false, + "bottomFaceResourceRepeat": false + } + } + ], + "objectsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "objectName": "AreaPlaceholder" + } + ] + }, + "objectsGroups": [], + "layers": [ + { + "ambientLightColorB": 200, + "ambientLightColorG": 200, + "ambientLightColorR": 200, + "camera2DPlaneMaxDrawingDistance": 5000, + "camera3DFarPlaneDistance": 10000, + "camera3DFieldOfView": 45, + "camera3DNearPlaneDistance": 3, + "cameraType": "", + "followBaseLayerCamera": false, + "isLightingLayer": false, + "isLocked": false, + "name": "", + "renderingType": "", + "visibility": true, + "cameras": [ + { + "defaultSize": true, + "defaultViewport": true, + "height": 0, + "viewportBottom": 1, + "viewportLeft": 0, + "viewportRight": 1, + "viewportTop": 0, + "width": 0 + } + ], + "effects": [] + } + ], + "instances": [ + { + "angle": 0, + "customSize": true, + "depth": 200, + "height": 2000, + "keepRatio": true, + "layer": "", + "name": "AreaPlaceholder", + "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-002000002000", + "width": 2000, + "x": 0, + "y": 0, + "zOrder": 1, + "numberProperties": [], + "stringProperties": [], + "initialVariables": [] + } + ], + "editionSettings": {}, + "eventsFunctions": [ + { + "fullName": "", + "functionType": "Action", + "name": "onCreated", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Terrain3D::DefineHelperClasses" + }, + "parameters": [ + "", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Terrain3D::DefineScatterHelperClasses" + }, + "parameters": [ + "", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "for (const child of object.getChildrenContainer().getAdhocListOfAllInstances()) {", + " child.deleteFromScene();", + "}", + "if (object.__terrainScatter) object.__terrainScatter.dispose();", + "object.__terrainScatter = new gdjs.__terrainScatterExtension.Scatter(object);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "onDestroy", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrainScatter.dispose();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "doStepPostEvents", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const game = runtimeScene.getGame();", + "const isInGameEdition = game.isInGameEdition && game.isInGameEdition();", + "const elapsedTime = isInGameEdition ? 1 / 60 : object.getElapsedTime() / 1000;", + "const layer = object.getInstanceContainer().getLayer(object.getLayer());", + "object.__terrainScatter.update(elapsedTime, layer.getRenderer().getThreeCamera());" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "onHotReloading", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrainScatter.onHotReloading();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "description": "Paint the density of copies around a position, up to the density of the object (copies removed with \"Erase copies\" come back). The effect fades out up to the radius.", + "fullName": "Add copies", + "functionType": "Action", + "group": "Paint", + "name": "AddCopies", + "sentence": "Add copies of _PARAM0_ around _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "object.__terrainScatter.addCopies(x, y, radius);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "name": "Radius", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Remove the copies standing in a circle, for example a tree cut by the player (use the position of the nearest copy and a small radius) or a forest cleared for a building. They come back when the scene is restarted.", + "fullName": "Erase copies", + "functionType": "Action", + "group": "Paint", + "name": "EraseCopies", + "sentence": "Erase the copies of _PARAM0_ in a circle at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "object.__terrainScatter.removeCopies(x, y, radius);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "name": "Radius", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "the number of copies placed in the area.", + "fullName": "Number of copies", + "functionType": "ExpressionAndCondition", + "group": "Copies", + "name": "CopyCount", + "sentence": "the number of copies", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "eventsFunctionContext.returnValue = object.__terrainScatter.getCopyCount();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "description": "The X position of the base of the copy nearest to a position (the position itself if there is no copy).", + "fullName": "X position of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyX", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? copy.x : x;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "The Y position of the base of the copy nearest to a position (the position itself if there is no copy).", + "fullName": "Y position of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyY", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? copy.y : y;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "The Z position of the base of the copy nearest to a position (on the ground).", + "fullName": "Z position of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyZ", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? copy.z : object.getZ();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "the distance (on X and Y) from a position to the base of the nearest copy. It's a very big number when there is no copy.", + "fullName": "Distance to the nearest copy", + "functionType": "ExpressionAndCondition", + "group": "Copies", + "name": "NearestCopyDistance", + "sentence": "the distance from _PARAM1_; _PARAM2_ to the nearest copy", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? Math.hypot(copy.x - x, copy.y - y) : Number.MAX_VALUE;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + } + ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + }, + { + "folderName": "Paint", + "children": [ + { + "functionName": "AddCopies" + }, + { + "functionName": "EraseCopies" + } + ] + }, + { + "folderName": "Copies", + "children": [ + { + "functionName": "CopyCount" + }, + { + "functionName": "NearestCopyX" + }, + { + "functionName": "NearestCopyY" + }, + { + "functionName": "NearestCopyZ" + }, + { + "functionName": "NearestCopyDistance" + } + ] + } + ] + }, + "propertyDescriptors": [ + { + "value": "", + "type": "Resource", + "label": "3D model", + "description": "The model copied everywhere (a .glb file). Models standing on the Y axis, like most models of the asset store, are stood up along Z.", + "group": "Model", + "extraInformation": [ + "model3D" + ], + "choices": [], + "name": "Model" + }, + { + "value": "300", + "type": "Number", + "unit": "Pixel", + "label": "Height of a copy", + "description": "Copies keep the proportions of the model.", + "group": "Model", + "name": "Size" + }, + { + "value": "0.8", + "type": "Number", + "label": "Smallest random scale", + "description": "Each copy is scaled at random between the smallest and the largest scale.", + "group": "Model", + "name": "MinScale" + }, + { + "value": "1.2", + "type": "Number", + "label": "Largest random scale", + "group": "Model", + "name": "MaxScale" + }, + { + "value": "true", + "type": "Boolean", + "label": "Random rotation", + "description": "Turn each copy around the Z axis at random.", + "group": "Model", + "name": "RandomRotation" + }, + { + "value": "255;255;255", + "type": "Color", + "label": "Random tint", + "description": "Each copy is tinted between white and this color, at random. White for no tint.", + "group": "Model", + "name": "RandomTint" + }, + { + "value": "0.3", + "type": "Number", + "label": "Density", + "description": "The number of copies in a square of 100 x 100 pixels where the density is fully painted (0.3 is about one copy every 180 pixels). Copies never overlap their neighbors much: they are placed at random in a grid.", + "group": "Placement", + "name": "Density" + }, + { + "value": "1", + "type": "Number", + "label": "Seed", + "description": "Each seed places copies differently.", + "group": "Placement", + "name": "Seed" + }, + { + "value": "Everywhere", + "type": "Choice", + "label": "Copies grow", + "description": "Everywhere in the area, or only where they are painted with the \"Add copies\" brush (or the edits).", + "group": "Placement", + "choices": [ + { + "label": "Everywhere (erase them with the brush)", + "value": "Everywhere" + }, + { + "label": "Only where painted", + "value": "Painted" + } + ], + "name": "Coverage" + }, + { + "value": "1", + "type": "Choice", + "label": "Grows on terrain layer", + "description": "On a 3D terrain, copies only grow where this layer is painted, away from its borders (so that trees don't stand on roads painted with another layer). A new terrain has layer 1 painted everywhere.", + "group": "Ground", + "choices": [ + { + "label": "Any", + "value": "0" + }, + { + "label": "Layer 1", + "value": "1" + }, + { + "label": "Layer 2", + "value": "2" + }, + { + "label": "Layer 3", + "value": "3" + }, + { + "label": "Layer 4", + "value": "4" + } + ], + "name": "GroundLayer" + }, + { + "value": "30", + "type": "Number", + "unit": "DegreeAngle", + "label": "Maximum slope", + "description": "No copies grow on steeper ground.", + "group": "Ground", + "name": "MaxSlope" + }, + { + "value": "-100000", + "type": "Number", + "unit": "Pixel", + "label": "Minimum ground Z", + "description": "No copies grow where the ground is lower (for example, under the water level).", + "group": "Ground", + "name": "MinGroundZ" + }, + { + "value": "100000", + "type": "Number", + "unit": "Pixel", + "label": "Maximum ground Z", + "description": "No copies grow where the ground is higher (for example, above the tree line).", + "group": "Ground", + "name": "MaxGroundZ" + }, + { + "value": "false", + "type": "Boolean", + "label": "Trunk collisions", + "description": "Give each copy a static collision shape around its trunk, for the 3D physics behavior (add it to this object, with the \"Static\" type) and navigation meshes. Without them, these behaviors have no effect on this object.", + "group": "Collisions", + "name": "TrunkCollisions" + }, + { + "value": "30", + "type": "Number", + "unit": "Pixel", + "label": "Trunk diameter", + "description": "The diameter of the collision shape of a copy at the height given in \"Height of a copy\" (it is scaled with the copy).", + "group": "Collisions", + "name": "TrunkDiameter" + }, + { + "value": "8000", + "type": "Number", + "unit": "Pixel", + "label": "Draw distance", + "description": "Copies shrink and are hidden at this distance from the camera.", + "group": "Rendering", + "name": "DrawDistance" + }, + { + "value": "true", + "type": "Boolean", + "label": "Cast shadows", + "group": "Rendering", + "name": "CastShadow" + }, + { + "value": "true", + "type": "Boolean", + "label": "Receive shadows", + "group": "Rendering", + "name": "ReceiveShadow" + }, + { + "value": "", + "type": "MultilineString", + "label": "Edits", + "description": "Where copies are added or erased, applied in order, as a JSON list, for example [{\"tool\": \"add\", \"x\": 0.3, \"y\": 0.4, \"radius\": 0.15}, {\"tool\": \"erase\", \"x\": 0.1, \"y\": 0.9, \"toX\": 0.8, \"toY\": 0.2, \"radius\": 0.02}]. Tools: add (density: from 0 to 1, 1 by default, how dense copies are compared to the Density property) and erase. strength (from 0 to 1) makes a stroke lighter. x, y and radius are fractions (0 to 1) of the object size (x is (scene X - object X) / object width). Add toX and toY to paint along a line (a row of trees, a path through a forest). With \"Copies grow: only where painted\", only the added areas have copies. Shown in the scene editor.", + "group": "Placement", + "advanced": true, + "name": "Edits" + }, + { + "value": "", + "type": "String", + "label": "Painted data", + "description": "Written by the brushes of the scene editor, in a compact format not meant to be read or written by hand: use Edits instead. Clear it to go back to the coverage and the edits.", + "advanced": true, + "name": "PaintData" + } + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Model", + "children": [ + { + "propertyName": "Model" + }, + { + "propertyName": "Size" + }, + { + "propertyName": "MinScale" + }, + { + "propertyName": "MaxScale" + }, + { + "propertyName": "RandomRotation" + }, + { + "propertyName": "RandomTint" + } + ] + }, + { + "folderName": "Placement", + "children": [ + { + "propertyName": "Density" + }, + { + "propertyName": "Seed" + }, + { + "propertyName": "Coverage" + }, + { + "propertyName": "Edits" + } + ] + }, + { + "folderName": "Ground", + "children": [ + { + "propertyName": "GroundLayer" + }, + { + "propertyName": "MaxSlope" + }, + { + "propertyName": "MinGroundZ" + }, + { + "propertyName": "MaxGroundZ" + } + ] + }, + { + "folderName": "Collisions", + "children": [ + { + "propertyName": "TrunkCollisions" + }, + { + "propertyName": "TrunkDiameter" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "DrawDistance" + }, + { + "propertyName": "CastShadow" + }, + { + "propertyName": "ReceiveShadow" + } + ] + }, + { + "propertyName": "PaintData" + } + ] + }, + "variants": [] + } + ] } ], "externalLayouts": [] diff --git a/examples/starting-first-person-survival-crafting/assets/StartingWall.png b/examples/starting-first-person-survival-crafting/assets/StartingWall.png index b3a097887..06278d6e5 100644 Binary files a/examples/starting-first-person-survival-crafting/assets/StartingWall.png and b/examples/starting-first-person-survival-crafting/assets/StartingWall.png differ diff --git a/examples/starting-first-person-survival-crafting/starting-first-person-survival-crafting.json b/examples/starting-first-person-survival-crafting/starting-first-person-survival-crafting.json index f4301eb9f..5337fed24 100644 --- a/examples/starting-first-person-survival-crafting/starting-first-person-survival-crafting.json +++ b/examples/starting-first-person-survival-crafting/starting-first-person-survival-crafting.json @@ -1,7 +1,7 @@ { "firstLayout": "", "gdVersion": { - "build": 283, + "build": 284, "major": 5, "minor": 6, "revision": 0 @@ -299,6 +299,14 @@ "name": "assets/Skybox_Right.png", "smoothed": true, "userAdded": false + }, + { + "file": "assets/StartingGround.png", + "kind": "image", + "metadata": "", + "name": "assets/StartingGround.png", + "smoothed": true, + "userAdded": false } ] }, @@ -315,12 +323,13 @@ "mangledName": "Game_32Scene", "name": "Game Scene", "r": 255, + "renderer3DWorldScale": 100, "standardSortMethod": true, "stopSoundsOnStartup": true, "title": "", "v": 255, "uiSettings": { - "grid": false, + "grid": true, "gridType": "rectangular", "gridWidth": 64, "gridHeight": 64, @@ -330,7 +339,7 @@ "gridOffsetZ": 0, "gridColor": 10401023, "gridAlpha": 0.8, - "snap": false, + "snap": true, "zoomFactor": 0.3325387091250948, "windowMask": false, "selectedLayer": "", @@ -340,9 +349,6 @@ { "name": "RaycastSurface", "objects": [ - { - "name": "Ground" - }, { "name": "Obstacle" } @@ -398,24 +404,6 @@ } ], "instances": [ - { - "angle": 0, - "customSize": true, - "depth": 30, - "height": 1728, - "layer": "", - "locked": true, - "name": "Ground", - "persistentUuid": "6a14e5ab-5c31-4692-8cd5-c50937fd60c6", - "width": 1920, - "x": -320, - "y": -576, - "z": -30, - "zOrder": 1, - "numberProperties": [], - "stringProperties": [], - "initialVariables": [] - }, { "angle": 0, "customSize": false, @@ -939,77 +927,25 @@ "numberProperties": [], "stringProperties": [], "initialVariables": [] + }, + { + "angle": 0, + "customSize": true, + "height": 1728, + "layer": "", + "locked": true, + "name": "Terrain", + "persistentUuid": "eb975506-4c90-4966-b856-f3be5a8d1756", + "width": 1920, + "x": -320, + "y": -576, + "zOrder": 0, + "numberProperties": [], + "stringProperties": [], + "initialVariables": [] } ], "objects": [ - { - "assetStoreId": "", - "name": "Ground", - "persistentUuid": "d2ce8f3c-d5cd-4b8f-8565-ebe9846d9bb4", - "type": "Scene3D::Cube3DObject", - "variables": [], - "effects": [], - "behaviors": [ - { - "name": "Physics3D", - "type": "Physics3D::Physics3DBehavior", - "object3D": "Object3D", - "bodyType": "Static", - "bullet": false, - "fixedRotation": false, - "shape": "Box", - "shapeOrientation": "Z", - "shapeDimensionA": 0, - "shapeDimensionB": 0, - "shapeDimensionC": 0, - "density": 1, - "friction": 0.3, - "restitution": 0.1, - "linearDamping": 0.1, - "angularDamping": 0.1, - "gravityScale": 1, - "layers": 17, - "masks": 17, - "shapeOffsetX": 0, - "shapeOffsetY": 0, - "shapeOffsetZ": 0, - "massCenterOffsetX": 0, - "massCenterOffsetY": 0, - "massCenterOffsetZ": 0, - "massOverride": 0 - } - ], - "content": { - "width": 1000, - "height": 1000, - "depth": 40, - "enableTextureTransparency": false, - "facesOrientation": "Y", - "frontFaceResourceName": "assets\\StartingGround.png", - "backFaceResourceName": "", - "backFaceUpThroughWhichAxisRotation": "X", - "leftFaceResourceName": "assets\\StartingWall.png", - "rightFaceResourceName": "assets\\StartingWall.png", - "topFaceResourceName": "assets\\StartingWall.png", - "bottomFaceResourceName": "assets\\StartingWall.png", - "frontFaceVisible": true, - "backFaceVisible": false, - "leftFaceVisible": true, - "rightFaceVisible": true, - "topFaceVisible": true, - "bottomFaceVisible": true, - "frontFaceResourceRepeat": true, - "backFaceResourceRepeat": false, - "leftFaceResourceRepeat": false, - "rightFaceResourceRepeat": false, - "topFaceResourceRepeat": false, - "bottomFaceResourceRepeat": false, - "materialType": "StandardWithoutMetalness", - "isCastingShadow": false, - "isReceivingShadow": true, - "tileScale": 0.2 - } - }, { "assetStoreId": "", "name": "Obstacle", @@ -1798,20 +1734,24 @@ { "assetStoreId": "", "name": "Log", + "persistentUuid": "49707369-d857-40c7-91e1-f171a385ab93", "type": "Scene3D::Model3DObject", "variables": [ { "name": "Held", + "persistentUuid": "67be6af0-b673-4140-ba50-7de9e8327bcc", "type": "boolean", "value": false }, { "name": "PickUp", + "persistentUuid": "75b21ad5-96b9-42c1-8082-f4194178d307", "type": "boolean", "value": false }, { "name": "JustPlaced", + "persistentUuid": "9337774d-c735-4119-ad68-b8490c906c3f", "type": "boolean", "value": false } @@ -1868,20 +1808,24 @@ { "assetStoreId": "", "name": "Rock", + "persistentUuid": "f7969747-69b6-46c9-89ac-76dfdfe8a627", "type": "Scene3D::Model3DObject", "variables": [ { "name": "Held", + "persistentUuid": "888725ea-847e-4040-a8bd-c033c5e71eed", "type": "boolean", "value": false }, { "name": "PickUp", + "persistentUuid": "be57c652-b560-4878-838e-23cde7b3b7c0", "type": "boolean", "value": false }, { "name": "JustPlaced", + "persistentUuid": "e3fb67b2-7c8c-495d-ab1e-075953bb5b15", "type": "boolean", "value": false } @@ -2204,6 +2148,59 @@ "width": 128, "animations": [] } + }, + { + "assetStoreId": "", + "name": "Terrain", + "persistentUuid": "f1b0b8a4-c751-480d-ac4d-8a349aaa7633", + "type": "Terrain3D::Terrain3D", + "variant": "", + "variables": [], + "effects": [], + "behaviors": [ + { + "name": "Physics3D", + "type": "Physics3D::Physics3DBehavior", + "object3D": "Object3D", + "bodyType": "Static", + "bullet": false, + "fixedRotation": false, + "shape": "Box", + "meshShapeResourceName": "", + "shapeOrientation": "Z", + "shapeDimensionA": 0, + "shapeDimensionB": 0, + "shapeDimensionC": 0, + "shapeOffsetX": 0, + "shapeOffsetY": 0, + "shapeOffsetZ": 0, + "massCenterOffsetX": 0, + "massCenterOffsetY": 0, + "massCenterOffsetZ": 0, + "massOverride": 0, + "density": 1, + "friction": 0.3, + "restitution": 0.1, + "linearDamping": 0.1, + "angularDamping": 0.1, + "gravityScale": 1, + "layers": 17, + "masks": 17 + } + ], + "content": { + "SculptData": "2;256;8ekBFx8QgAGmAsYDwgOuA5oDiAOCA/oDygTCBOQDugK2AZ4BkAGQAZwBpAGIAYIBmgHKAZgCpgPYA5ID3gFH2wOvBu0HhwjlBocEnQFc3gHiAXBLqQGVAQn4Ae4DzATUBIgE7gKkATQBBECsAawBkAF6cG7AAZwCnALAAQ7BAesB2QGTARuQAboCpAPAA5gDsALIAZYBZj4cAAEIIlKUAewBrAK4AoYCogEGrQHJAskDrwT9BLEFxwWtBdkEywONArUBhwFbLQNN6wHZAo8DkwPTAucBnwF7gQGfAUcWUF5IPlpqcnBOBgZM4AGeA8YC0AGcAawBggLuAqYDrAP+AqICxAHSAeYB9gH+AYwC+gKsA/wC7AEB0QGrAtUCywKLAqcBXykHBg3bAaMDmwTFBJcE+wOdBJsE9QOrA+0CuwP7A6EEqwSbBM8E+QT/BNcEiQSFA/kBhQEjJKwB4gOMBYwF8gPgAeQBsALMAsAChgK6AXxCDCFtgQLbAvUCywLtAZMCvQKrAtsBTwwcRo4B8gHaAooDjgPwAq4C2gHOAsoDgAT2A64DsAP8A4QEyAPQAq4BLjN/twHbAaECxQKxAuEBWVMGEBQOBAUHBQMBAg4YGhgIBAYGBgIFFx8hFwEKEBQQBgoYHiIYBA0ZHRMDBAQEAAEFDQ8TDQkCAQABAwEFBQUDAwACAgYIChAWHBgSAAEHExELBQQCAgIAAQQMEg4GDSc7OS8TAQYICAoOEhYUDgQHDQ0PCQABDBISDgoWIh4YAg8ZHRULAw0PDQcIGCAgFgQaLDQsFgENFRMLBxkjKSMTHyUnGwsQMDw8JgoJFRcPAwQICAgEChQaFhIGDhIUFAgEBQMHAAEMGiAWBhk3R0EvDwEAAQQCBgcbIx8TBig2OCgKAwcJCQABChYeGgoFCwsNCQENExMPBwsfJyERBB4oJhwEDx0hGwvJARIoLCQGFRURCQUCJD5ANhoKDBAMCA03UU81BxoqLiISGjxQTjwSIT1BLwsICggBDyMvLScRAQABAwMNDxEPCwUBBAgOFh4qPkQ+KAYZKSkhBwYGBgQAAQgeJCAGH2WRAZUBdTUCChQcHiAyODQkCA8fJSUZAAEaLCwiGD5OTDQMJT9HORUPHyUhDxA8UE4wCjpufmw4ASEvLR0RP19lVTtPZV9LHS50lgGQAWQaFS01KQcKEhQUDhY0PDwkEiQyMiweBgUNCwMePkYyCEONAa8BqQF1JQcAAQYKDBNBU00vEF6GAYgBYhgJFRcRBxo8RDoaDxsjHRUJHS8zJw0dS19TLRJEXl5EECdJUUUZ7QEULDQkBCUfFxMHBC5MVEIgDhYSEggNRWFjQwciODguFiJMZmZQGCNFTTcJBAoEBRcxPz0xGwIBBQ0RGRcXDw0DBhAaIjA+VlxSNAgfMTUnCQoICAYECCAqHAABM4UBuwHDAZsBRQMUICgsNkZQSDIMFSczMSUFIDQ6LCZMZGBCCitNVUcdDScxJxEWTGJePAhIiAGaAYYBSAIjNzMfGVF3gwF3U2+HAYMBYyc8lgHCAboBggEkETM7LQkMGB4aECA+UEgyGDA8RjwmEAEJCwEiREoyA1u3AeMB1QGbATUNAQYOEBVNZ2E3FnKmAagBeCANFRsVCSBEVEIeFScrJxsLIzs9Lw0jXXNlNxRYeHZUEjFXZVEjwQEOJCQWCSspIRcLBCxIUEAkEBIYEAoLPVlVOwUkMjYuFiJMYGhQIhM1PS0LBgIDDyUzQ0EzFQEBCxMXHx8bFwsFChgkMjpMXmRSNgYbKS0fBwoMCgYEBBYYChE9iwG5AcEBoQFHAAESKDQ8QFRaUDYOESk1MykJGCw0LihQXlo+DCdFTT8ZDSMnIQ8YRlhSMgZAeI4BfEgMFScpGRVPeYcBf2F5jwGJAWUrOpQBvgG2AYIBMgEhLSMFDBgaHBQiPkpGMBwuQkY+LhgKAwECGjIwGBtnuwHdAdUBmwE9EwUIDhAPQVdVMxJqmAGYAWwcBxMXEQMYPkQ4EhcnKycbCx81NScLG1FlWS8WUm5uTBIpTVtNH0UGBgILHTMvJxsPAiAyNi4cEBQSDgoFIS8xHQABGCYkHhQcOEpMRCoICxMTAwMJEx0nNzc1JxMCCQ8XHyMjHRkRAwwiMj5ITlpUSCoIDxcXDQIMCggIAgIBCxcpSXGPAZEBdTsDGig6REhWVEoyDg0dKSsnEwQWICQqOkI6KAoTJS0jDQkNEQ8DEig2LBYHIE5cVDgaBAULCxE9W3FvY3F7b1MdKGiCAYQBZjocBgMHAwoSGBYSHCw0MiQYLDQ6OC4kGBQKCAoGBRs9Z5cBrQGhAX05GwkAAQwQAAEbLSkbDD5YWEASAAEJBwkAARAeIBYAARshIx8VCREbGxEAAQkpMy8XEjRGQDIMFSsvKxGQAQsZJS81Nz05KxsBBgoMDhAQDgwKBggQGBoWDAURDwkAARQkLjY2MDAqIBQEDRslLS8nHxMHAQIPGyUnJyMhGREDDig6RExORDgqFgABAgQKCgoMDhIMCAkpQ1FVTUEzKx8XCQQaKDhMTEo8KhAGAAEFDxcdGQ8BEiQQAAEFBQYOEBAOAgYKDhAOCgoEAAEJFRMLAAEOHigoIBgIBRkrPU1ZV0c5Iw8EDBYgMkBAQDIiCAgMEhYUEhIQFBQWGiAiKCw0PjouFg8zTV1jZ3V5b1k3FQABEhQUEhQUEAgDDQ8PBwIOGBYWCgMVHyEdFxsXEw0FCBYWFhIOEBIQDggBCQUFAgwQDg4E3AIJHys1Nz1ZYVk3CQ0ZGQsCEAoGBgQUTG5wUhRDdX1jHxw4SExCNEJGPCoIEycxNzETFioqHgEfMzk5JSEdFw8FDB4sOEJEKhYIAAEBCBAUEg4UJCYiEhFbhwGXAYUBVQkuRj4QHy0rExZCTEhAKhQoOj4qBB8vKxkIFilTWTsBME5WQhwMICgkGAYKCAQJG09za0UDLjIyJhQAAQwKAyNJNRcBBgYjWWNJDT5ibGBAEA4cIiAcDAYEBAwUCggKFihcfIQBbDAdWX2HAXtxlwGrAaUBgQFDCDxOSCQWJi4qFhFNcWtJBy5MWEogGUldWz0bFxcVDQEsUl5QKBosMi4gATlTVTkDJj5GOhiYAwkdKS8xO2N1a0UNFysrHwEOBgQAAQQcbpoBnAFwGmGrAbsBjwE1IkRYWkQ0SFBIMAoVKTU1LQE0TkouASU9RTknGRcTCQMIEh4sNDYYAQ0NAwoUFhYOFiwyLBoXcaUBtwGdAVUcZogBciYtTVM/CTpERDoqFDxeYk4aGy0zHwQUR3t/WQNKcHZiKBAmMCwYBggKAgUfc6UBqQF3GSo2NCgWCCAqHgM7GwogIhI7jwGnAYcBMzxsfHRMEhQkKigcDgABAQEKEAIDAQokapwBogGEATwhZYsBlQGBAW+lAcUBwwGZAUsWVm5cKBAsNi4eGXObAZ0BbRFAbnxiLiVjfXVPFxcXExEAAUR0hAFsNB44Qj4oCVN7eVMLNFhkUCLEAgsVISUnL1dtY0MNGSsvHwMKBgIAAQIecpwBngFwGF+pAbkBkwEzIkJWUkIuQExELAoRIy0tIwo+UlAwASE3OTUZFQ8LCQEEChQaKCYICxUTBQgUFhIMFCguKhYTZ58BrQGTAUsmdpABeCYtVWFLGSY2NjAkFD5kblgmDSEjFwwWP3V5UQJOdH5gKA4kKiYUAwQEAAENIXGrAbEBgwElHiouIhIKJjIqCiMNGiwqFjuTAa8BlQFBMmJybEgSEiIqKB4KBAMAAQoSAgcDBCJikgGaAX44IVt/hwFzY5sBuwG/AZsBTRJOZFIaBiAsKhYZb6EBnwFtE0JuemQuJV99c0kTExMTCwFEeIIBbjQgOEI6KAVReXtVCTZaYlIiXAMNExcXGztHQy0HDx0fFQABCgQAAQQEGFJ0cFYWPXN/YyEeNj5AMCAyNDIgCAsVGRsRDC5AOiQEFR8nGxEHBwUBAQIEBhAUFgENEw8FBgwODAQOGiAaEA1JbXlpNRpSZlAWK0VNQRsSHCIeGBIyUlhMLAoDBQQYIhs9RSkSQl5cRhwKFBoSBAcJBw0PIVmBAYUBZycMFBYUCAYeKCIOFQABGCIiDillf20xHkBQRjIKEBogIBoQBgQIDhIIAgIKGkZkbFQmEz9bYVVNdZEBkwF9TQskMCYAAQsIEhQKEVFzcVELLlJWTB4VRVNRMQsNDQ0JAy5YYlAoGiwyLiIBNVFTOQMqQkg6GIMCAAEDAwEBAwUHBQEEAQsHBQIGBgYGBAocJCYeEgABCQkHBhQYHBgYEhASDgwEAgACAgIEDgwOCgYEAgIAAQQEBAQEBAMBAwMAAwEFAwEDBQUDAQEEBgQEAw8bHR8VBwYGAgsXGx0ZEQUBAQYGDhgiKCwqKCosMDQwIhoYHCQoJB4WBgACAwULFxkdHR8dJy0vKRsPCQUBAAEBBgoMBgEBAAECAgIDERMTBwQQEBAIBgQIDhISEhIQEhQWFhYWFhYYGhYUCgUXHyktLTdBRUtBOTUrKysjGw8LAwkTFxcPAQoQFBAKAAEDCQkFAQMHCQkFCBQaFhIOFBQYEg4AAQUFAAEKFBgYEgivAwABCAgIDBZKZGBIDj9pc1slBAYICAoKEBQWEgwuSE5GJhAUFhYUDgUREw8AARIcIB4QET1HPx8KJDI0LBYcJigcCg0nLysdARAWHBABJT1DOyELBwcBAQQUHhoMCQ8PDxERAAEgMi4WDSMnJRMEDR0bBxxOanZwVjg2NjIyKAoTHR0LBg4QBAklS11dSSEFCAoCFQ8GEhIKAAEMFBgWEClTYVEhHkZWTiYBAQYICAELEREHEBYKBgIMHkJSVEIcDSkxKw8BCREXHRsAARAMCTmFAbUBvQGfAWElCQQOBAYaKCYcAAEhOz8xDypgdmg4ByMvLSMPFR8dEwcIChASEBYuPEI+MjY8NigQ6QMEChASECx+rgGqAXgSb78B1QGnAUUBBAgKCAwWHBgQCEBwfmg4FBYYHBoQDyUnGwMaMDQsGCdngwFxPRA8UlZEJDA+PjIUG0FRSS0FGiwqIAABPWt5ZzkVEQkFAQgkNDIiAw0LCw8TFEpkXDQTM0M/JQEjRUUnGmaUAagBmgFwODQyNi4qBy0/NRcMHB4WBzFviwGHAWUnCB4iEA8RGCgoFgQWJigqFkOPAaUBiwE5MHiSAYABQAkCCgwKAhclJQ8SHAQFBwImXHx8YCYfSVlLHwABBw8VHRMiQj4aMbMBiQKfAu8BhwEjCCAmGAYyRkYuAUVrc10jSKgByAG0AV4TP1FPOxUjOT0vEwABCg4QEhxIZnJsWGBmYEYa8wMGDBQYGDKUAcABwAGCARR72QHrAcMBVQMCBAgKDBYYFgwEQnaEAXQ4FBgcHB4SDyUrHwEeMDoyHCl1jwF/QxBCXGRQLDZISDYWHUlXUzUJHCwwHgABRXeJAXtJIRcPCwMKKjo8JgMLCQsJERZWcGo4DzlLQykDJ09LKR5wqAG2AaYBcjQyNjAyKAk1Qz0ZDiAeEgU3d5cBkwFtJwoiJhAVExYsLBYGHCg0MB5FkwGzAZkBQTaEAZwBiAFCDQMGDggAARclIw8WJAgFBQIiYoYBggFmJiFPXU0fAwULFRkNLEpMKCu7AZ8CtwKLApUBKQwmKBoMNExMMAVPfYcBaylQuAHgAcABZBtJX11BGStDRzkdBQQMDhAiWHyMAYYBcHqCAXJUIMsDBhAUGB4yfqYBngFuEGWvAccBowFLCwMEBAYKEBIMAgUwXmpaMBIaHB4gFgUZHRUAARoqMCoWHVtvZzUQOlRYSC42Qj4wEBc5TUUxCw4iIBgBO2l7b0spIRkNBwYiMDAcAwkJCQsPEEJaUiwRLzs5IQIbMTEPKnKaAaQBkgFkMjAwMC4oAR8xKRMIEBQEDzVlgQF9XyUEEhACIRsIGhwQBBgqMC4iKW2JAXMzKGZ6ZCoTCQABAggAAQ8ZEQABIigYBgYGIFBqbFIiETdFOxUAAQUNDxcNHjo4FimfAfEBiwLrAY0BLwcUHBAGKDo4IAtHb3VbJUCWAbABlgFIHUVXUz0VKTs/Nx8JAQIKECBagAGUAZIBfoYBhAF2VCDxAgYOFhoaJkZWTDYMKU9ZTykNCwcBBAgEAQMJCwggKCYYFBYcIB4cDgYAAgIOFhgYEgUdKSETDCQyNjQmKiwoHAwNHSUpHREDAAEGBAEhO0tLPTEpIRcLBAwQDgYHCwkJDQ0DFBwYCA0bGxcJCAoIEiREYm5wYEguLCwwKiYWAgUHAwEDDRMdLT9JRzUdEREVHy0pGQsDAAECEhwkKCQEFyMjDw4gJBYCGxUPBQUBAggSHC4yKCQYFhQsNDYqHAYHDw0HAgUHDxERAQQAAQsrX4kBmQGNAWk/JxUNBwEKDgwBEy07PS8RFjhAMA4fMzc1JxEXIyclFxEJAwQIHkxyhAGMAYQBggF6ZkYaqwIGEhoeHhQPJSUbAi5MTjgMDQUDBAQDIzU5LxslMSsdAxgoMDAsHA4EAAIGIDQ8MhwKCAYGBggWHiQmIhYMBAADAgEDDREPCwkHAwkVHSkvOT87LRcCEBgYDgUtSU9DJQMKEhAEBwIKEhISJDhGUFJGNiwkJCYyNjQwKCYoJhgKCRUfISchHRcVFREdIykxMzk5NScTAwIKDhYiPExOPhoVP09LOxsXDQsDAhgwOkI8NjIsJhgMBQkHAhImNDYqFg8tPzsxEwUAAQUPIykpLTU/PSsfEQkRN09TQR8QNkA2FiVdc21PHwMIDgYHEx0hHRsTDwkDCBIsRFhqgAF+cFo6Fq0CCBogJiICUX+BAVUBdsIB0gGmAToJBA4MDAtPc3dZJ0FlaU0XIkJOTDgYBxcbEwREeIABbjIGAgABAQIKGiQoKCAGDRUVCQoQFAwAAQ8JBwMAAQEDCRMZKT9VV0knCDBGQjICW6EBswGXAUcONkhCJAABFiYqJBgqPk5STjIMBQMKJDZAQDYiLjYyJhAHFxsjHx0NBQEFDxMZHyUtPUtJOSMFCw0FCCBYhAGQAXQyMXudAZEBXx0JAwQCBChGWlZCLCwkHBQAAR0vLRkKPlpiUiQlZYUBhwFXDxYqKhIVDwMDES0rDwQMCBt3pQGtAYMBK1SgAb4BmgE+WdMBhQLvAZcBGSZKTDYCHy8zLyETEwkDBAgIDCZIdHpsVjwUwwIMGCQkIgdvpwGnAXMFmAH6AYwC1AFMAQoUFBARXYkBjQFnJU97f2MfJExcVjwSEyUrHQZWkAGiAYIBPAQBAwMAAQgYJCgmGAEZIxsLChocFAQJBQAEAQEFER87WV9PKwo+VlpABGu/Ad0BuQFTGE5kXDIIIjI4LhooQEpMRh4HHxsFIDZAPDIgLDY2KA4FERkbHRMDBgYECw0PFR0jOUtNQSEJFRcRARhilgGgAYYBODmPAbMBqQFrGQUGCAgELFBeWDomIBwUDgMtPz0lBkBmcFoqK3mfAZsBZw8mPkAmCwEQEgQbHQYYGhAhiwHJAc8BnQEpbNIB7gHCAUxtgQK/AqUCtwEXPmZsSgYlNTs1IxUPCwUCAhURAipgamJOMBTtAggYHiAaB2mdAZ0BaQOQAeoB+gHCAUoCDhYWEA1Td3tZH0NrdVkfIkRSTjYMEScnGwJUhAGSAXY0BgEFAwABCBYeIBwUBRsfHQkMFhwYCAcDAgQCAAECAgABCxMvSU9FJwg6UlI6DF+rAccBpwFLHlBiWjQQIjQ2LhwkNjxANBYTIR8JGiwyNCgYIiwsIg4HDREVEQsAAQoMBgUJCQ8RGS09QTcfCxEZEwkUUIABjAFwMDF5nQGRAV0TAQYKBgYmQlBILhgUEA4IBSk7OyUCNlhcTCIlZYsBiwFbCSQ8PiYDBBgaDBERChweDh17swG7AY0BKWbCAdQBrAFCY+cBoQKLAqsBETpiZEIEITE3Lx8TDwsFAwMbIRESRlRIOigOqQMGDBIUEAM7W1k/AlyQAZgBeDAEEBASDAMpQ0EvDSE9PzMNGCo0MCAMCxUVDwYyUlZIIgICAQACBg4SEhIMBQsVDQUIEBAOCAEAAQIEAgICAgABAwkXJy0nFQYiMDImBjFfbV0jFjZEOioQHiYqIhYcJComIA4HEQ8DEBogHBgOEBoYEgYDBQsJCQMECAgEAwMHBQkPGSUlHxUFDw0PBQguRkw+GhlDVVM1CwABAgQEBBIoKCYWDAgIBAQFGSUjGQEcLjIqEhM5TUs1AxYkIhYBBA4QCAkJBg4QCBFLaW1VGThqeFwiP4sBqwGhAWcRHjQ0JAMZISMhFw0NCQkBBxcZEwYkKCYgFga7AwABBAQEBAQECgYKBgcTEw8CDBAQEg4KDBAODAwIDAoMCgQAAQEAAQQECggGCgYKEhAMBgICAgABAgYICgoIAgMNCQsBBgwQDAgEAAEDBQEAAQICAgICAgQCBAIEBAoKCgwOEhYUEAwEAAEEBgoAAQMFBA4eJCQeFAgMDAoKCAgGBgYAAQEHCwcBAwsJBwMGFBgWDAIJDQ8NCwcFBwUDAwcJBQMCDBASDgYAAQcJBwUDCQsLBQMEAAECAAEDBQMDAQUDAQEBAwUDBQEBAQABBggKCgYCAAEBAQEECAYEAQkRFRULBwEFBQcJBwUFAwsJCQcLCwsLCwkJDRUbGxcLDRETEQ8DDBIUCgcPFxkRBaMCAgACAwUAASAyOCYAAWuzAb0BkQEvHCgqJBoYMEBAMBIgLjQqGgclMy0VDBQcGhIKEBQUDgABBwkHBQMAARQcHBABIzc7KxESKjAyIgwLGRcPAAEGCAQKDBQgJB4SAAEKDhQWGj5eZFouBy09OSETP1tVOQZAXmJKFgISGhoQBAQEBAACGy8zJxENIScfBx5GXlg+CB0vOTUfCwABBggCBxEXEwUOKDY4LhgBBwsPBwkdJScZBQEAAQUNERURCwsHBQYMDAQLEQ8PCwcMIjQ6Mh4KAwcFCBgmKCAKHT9NRy0BEh4cDgkTFxUXEwgqOjYYDRMTExMRLUNFNRUPKTkzJQJMbGAsJWV9d1sjvQEAAQEDCQ8JKkREMAOfAYUCmQLXAUcoNDo0JBpEWlpCFihARj4kCzlJQx8OHiQiGAwUGhgMAxEREQ8LAAEWIiIUBzVPV0UZHDpMRjYUDx0jEwQMCBAQFCQwNCwYBgwWHCIoWIIBjgF2PhFHXVs7I2WLAYkBVwABUnp+XBYFEBwcEgICAgICAydBRzkXESs1JQcwaIQBgAFUDidHT0kxDwEKCAYHFxsXAxw8VFRCIgQLERELCSU3MyUFAQULEyEjGxkREQkIEhACDxkXFREJDjRKVE4yGAQDAAEOKjo8LhIjV2thOwEcKioYCxkfHx0XDkJWTigNFRkXFRE5V1tFFxE3R0s1BmaOAX40RZsBvQG3AYkBNZ8BAwMLDxkNHjpAKgmhAYkCnwLdAUUoPEI2JB5GXFxCGihCSkIoCTNJPx0QICYkHAwQFhQIDRkZFxcRBxIgHA4NOVddRxkeQlJQPh4DGRkRCBASFBocKDg6MBoGEBoiKC5ihgGQAXo8E0lhX0MrbZEBiwFdA1B2dFAODwYUFA4CAgIAAgUlQUM5FxEnKR8COHKKAX5WDidFVU8zGQcECAIHERcLBiRIWlpIJgYFDQ0JCSUxMyMHAw0THSstKR8dFw0ECggBGR0dGRULEDZUXlpCJhYMDBYwPkAwFiNRa2E5Ax4qJhQPHyElHxkKPlJMJg8XFxkVETVTV0ERCzVJSzcFVnpkGl+3AdUBzQGbAT3NAQELERkdHQgcHhYNc78BywGZASsqNjY0IBY4REQ0FCQyPDYmBh0pIw8UHiAeGAoKCgQFEx0fIRkZCwQKDAIRM0lHORMaPExMQCYMAAEHAAEOFBocHiQqNDQqFgQSHCYqMlRuclwuCzNNSzcrV2tpQQI0TEYqBxkHAAEIBAIBAgABAQEbKzEnDwcTEQMUOmBuYkAIHzVDQTMfEQsDAAEFBwEEFixGUEw+IgwGAQEDAxklIxcHBxEfKTE3Ly0lHRMLAwkPHSEhGxMNDDBIWlxKPC4kHiAsNDAoEBM5R0MrARAYFAYRISUjIx0BIjQsEg8VGRURESc3NycFBR81OTMVHCwUI3mxAckBuQGLATfRAgcRGSEpIx0VDQ0NGyEfDQoiJCQeFg4SFhQSEBIcHCIgGA4MCBAWFhYUDAgBDRMbHyMjISEZFRMPERMXHyEjFQUSJDA2NjAqJB4aFh4mKi4qKCAcEgoGEB4kLDI2OjYsGgABEx8lIx8fHxUJCgYHERkjJR0VDwUAAQIAAQIBAQUFCQMBBBIaIi4yOjIoFgINGR8nKyUlHRcNAg4eKDA0Mi4qIBgYGBoSDAABBw0NCQMLGSMvNzs7NzElGxkfHSEjJyUfFw0GJDxMUlRQRkI0KB4aFg4IAAELDw8LAwABAgABCxkjKSslIRMJAwUJFRkZGRMPBwMCBgoKAAEJESEvR1ttf48BmwGRAYEBWR+JBQsjMTcxIwUOEgoDMmJ4ajwUDggICAgHDQ0FChggKCQkHBYOEhIaJCgkHAYrTV9TOR0RCwkRGRkbGRkbIykjHQMAAQMCDiI4Pjo2KiJUhAGUAYIBSgwZLy8bAAEYJC4uLB4MAgECAgcNFxsLMFhkUBhDgQGTAX9JJSchGQsECgoKBgQUIiogDBAkNjw4LhgKAAEDAQUHDxUfM0VJOx8KSGp4ZjoEHSsdATJmeGxCAAEnNzcnCQABAQsXK0NTWU85HRcTFx0pQ1VXRyMgZpIBngGIAVQ2JhocIg4NFRMDBgYEAQEIMERAKA9LbXNlOwsKFBQAASFTaWlPFx5EUEYiHjY6KAABQ50BzwHhAc0BowF/Z0ctD98GETFBQzsdGDQ6JgRcrgHIAagBUgoDBwcAAQITHx0PDCIuMjQqGg4KCBAeNDo2JgZFgQGRAX9NFQEIAgcbHRkbHR0rNTMjCwsfIw8UPEZGQDQohgHOAeIBvgFkB0VnXzsEGCosMiYIDxsZCQIFDRMZAmaoAbIBiAEeb8cB4wG/AV8jJyUZCQYOEg4OBCY+RDgYEDA6QjoiCAsRDwUAAgMNGTtXYVMvFm6eAagBhAE8G1NhTRNOmAG2AaoBYgM7WVk/CwwSDAMfRWFrZUcbEQkLEyVdf4UBbTU2ogHaAeQBrAFQHAEJAAEeAAEfLycLDAgGBgEOWH5+UAVnnwGxAZMBTQMkNi4SKX+vAasBfSE6cH5oLiZWZEoWT8UBjQKXAvUBpwFtQycPA4cHFTFFRzsbIkZILgRmwAHaAbQBUgINEQ8DAhUhHw0OJjY6Ni4cEAgIDiI2PjwoCE2JAZ8BiwFNFQIOCgkdHR0dGx8vNzknDQ0tMRsKOkZKRDYukgHmAfQBxgFmDVVxbUEBGiQsKCABHykjCwABBQcNEQZ6vAHGAZIBIHPTAe8ByQFhHSMfGQsGEBISDAgoRkw8GhIqOjoyHAMVGRUHBAICBxEzWWNVMRR2rAGqAYYBNCdfb1UVTqIBwgGoAWYBO1VZQQ0QFhYGFzlba2NHIQsHBw8lX4kBkwF5Oz6wAe4B6gGsAUQODxcFHAABIzEnBw4QCgQAARRejAGIAVwDaasBuQGdAVMDKjw2Fi+NAb0BvwGJASVAeIYBaiYgVmZSFE/LAY8CmwL1AZsBVy8PAQT5BRErNz01Fxw0OCABTpYBrAGMATwFDREPAQQPExMDFio0OjYsIBIQDhYeMjgyJAQ7cX9zRRUBBAINIR8fHR0dKzUvIwkNIycXCjRAQjwyLIABwAHKAaIBTgk/WVkzAAESGh4cFgcZJR8NAAIFBQsMZpwBoAF0HFehAbkBnQFLExkZEwkEDA4OCAIkNj4yFAwgKioiDgUTGREFAgYCAQ0lQ01FJxRehgGIAWYoHUlTQQ1GggGYAYQBTAQnPT8tCQwUEAQPLUVRUz8dEQsJFSFPc3tnMTKSAcABugGKATgKBw8AASAIERsXBBYSDgoCDExqbEIDV4kBmQGFAUkHGiokDC1znwGfAXUfLlhiShIMNkQyBkmrAd0B6wHHAX1FIQkEAqsDBxcfIyEXBQoGAQ8QMjowDgkLCQECCAgCChIcKi4wLiwkIBocGiAiJB4WCBsvPTsnFxMRExshIyEdHxsfIR8VAwABBwUEFCgsLCwoIkhgYFAsBBEfHRMCBgoKCgYBDQ0NAwICAAMKMEJINhAbO0U7GwcJCwcFAQABAgICAAEKFhgSCAYKEBAKBgcJCwkBAAECAgABAxEbHR0PBCo4PCwaAxETDQgoQEI6JAoDDQ8LAwICAAEDCxkjKSspHRsXGRUdLzs/MxkUQFRYQiQWDhIYJh4SDBAWGhgUCgQCGiQgFA0vRU1HLxcFAwEPHz9PSz0TBhQUBhMXBQMJGz9jeX1tTTEfFQcBVQoYGBIAASFRbWlTJwUMEg4BER8fGwUOFBgaFhQEAQEKHEBcZmJIIAEVGxMCGB4eEgUZHyMnJSUlJScfGw8BBgoIAgQIDBYiOEJCNB4FHyEZAiI4QjgiByc5Oy0VBwEAAgIECA4ODgwGBgQGDhYkJBwQAgIDBQMHDRENCwIEBAICAgEAAQEDAwMAAQEAAgEGCgwKCAABAwkFBQcdIR8LCCQ6PDokBg0VEQcMHCQgGAYLFRkZEQUICgQHITE5OS8dBQwWEgYRIyUdBxgyQEQ+LBwOCgoQGhYSEAQFDQ8NCwEYKCgaCStDSUUxGQ8DBQEPHzU/Pzk9R0tJQSsFDhQIFTE/Py8TVhxEVEYeL6EB3QHhAaMBNRhASjwWH0FPPyEMFhoSBA8zR0MlDGCsAcwBvAGEASgnUVk/BTZaXEYUFR8nKSclKy0pJx8AARwoJhIBDQ0FDCxedGxSHjtveVkXPHaKAX5IEV+NAY8BczUbCwUAAggUGh4aEgoCAAECDCxETEAgCAACBQcJHR0dCQgQEgwKAgIBBQUHBwMEBAIAAQ4YHh4QAgkNDQ0RQ1tVMwJGaHZiNAk1Rz8hDCo6PCoIFScrKRkGIjAsECNJX1tJHxg8TEYaJ11xYTEUSmZsXjIEGScjCQoOEgwEBREPCQIcWoYBigFmGj1xhQF7TxUGEBYKBzlZa2dRT2tzaVMfQHiCAV4SOWFtXSW6ASRWZlwuMb8BiQKLAscBOyhUZFIiIU1fUSkKEg4CDSlbc2dDAAF01AH6AeYBogEuMWNvTQdGbnheIA8dIyklJSstLykfCCY2MBgFExULCDBqhAF+XBpNjwGbAXMhSIoBpAGUAVYReasBuQGXAU0pGw0FAwYaJCgkGAwEAAEECjZSXFAqDAQBAwcLGyMbCRQaGBQOBgMDBQcLCwEAAQQCAhAiJiQWBgcPEQkXVXNpRQJMfIYBbjoTRVlRKwguQEIuChkpMy0dCDA+OhYfT2lpTyEkVGRUJi91jwGBAUcQTnR8ZDIHLT85HQMICAgCBwsJAAESMoABtgG6AYgBKEN/lwGLAVsRDhwcFAVBa395XV19hQF9XxNkqgG8AY4BLC9pe2Ur0AEiTl5SKiunAe8B8QG1ATkkUl5OIhdFUUshBgYFESVBaX91SQJuzgH0AeIBngE4H09bQwNAaHBYIgkVHyEjIScvLSkfBCIwLBYFDxEHCC5ienJQGEN9iQFnHUCAAZQBggFIEWulAbEBmwFbNykdDwkKGCYqKCASCAgEDjBQVkwsDggCAQMLFRkNAAEaIBwYEAQAAQUHCw0NBwEAAQIAARAeJCIaCAMHDQcTS2VjPwNEbHZgLg0/T0kpAiY2NigGEycrJxkIJjowFh1JWV9JGyJKWk4gK2uFAXlHCkZkblosCStBPSULBQEAAQEHAQQSJkKKAbYBsgGEASY7b4cBe08PDBoeEgc7aXt5X2N9hQF7WRdkqgG6AZYBOhlNX1UjoAESLDYsFhlljQGTAW8nDiw4MBoJISkjEwQLHS9BT2dtXTUEWpgBsAGoAYIBQg4THRkCKEBENhYDDRMXGx0jJSclHQkKFBQOAAEDAAEGFCZGTkw0GB0/RzMJLExYSiwNSW+DAXtZRz0tIQsEGCAoJiIcGA4ODCI0PjYiFA4IBAEFBQECFB4iIBgOBgMHCQ8PDwsHBQMAAQwUHBwUDAgAAgMLKT05JQEmPj44HgcfKysdAAEQHBwSBAsTGxcRAhQYFgQXLT05MRMMKjAoEhk/UUstBCY8QjokAAEdJy8jFRENCwkDChwqPE52hgF8WBQnRVFLMQsEDg4ICS9NX2VZWWtvY08ZOHB+bj4MESEhDYABBREVExEJCQsPERUNBQABBAoKEhAQDAABIz1PV1lRQy0REjBASlBOTkxCNiQMCAgGCAQCAgEJDxsrNTMvHxEFAAEGBAwSGBwcHhoaFhYQDgwMCg4MBAIBAAEFFSk3RU1DMSUVCwMFAQYWIhoUEAoIGCImJh4UEAoGAgQaLjQuJhwQCgYAAQUPGRUVEQ8PCwcBBAgMDg4UGBgUCgABAQABAgEBAAEBAgQGBgEJBwkJAQABBAIAAQUJCQsHBwkJCw8RFRUTEQ0LCwcFBwMBAQEDAwQMDhYYEAYJDxkdGxsVDwABGDBATFRKPCgSAxENCQsJBwEEBAABBxctOUFLTUlHQTUnCwoeKjY6ODAkDKQCI1FlXzUFFh4cDgsVFxMHAhosMi4eBS1NXWNZQScRAhgcEhQeNlBqcmRGEgMFBwUCDA4QCAMjWXd7WykKJjQsFhAgKCYiFAgBAQIMDAgICgwHJTErGwABBAIPJUEPIjw6HiNPX08dEgcbIxUGLkxUSioQBgIBAxBWeH5eKAABFRkXCRMrNTUlDw8LCQMBAwABAgQOHDI6NBwEEhoYEAEPEQ8LAAIPExcPAwYODAwAAQ0bHx0PBQsNDw8RExcXEw0VGx0ZCwgeKCQUAQQKEhQWFg4KAxEdHRcVCwMYKj5ISiwMBxUVBwwSFAgBFCIiFgABEx8rNT8/OS8rJycxLRsCLkpYVEAa6gIvcY8Bf00AASAyMiADGR0ZEQIgNkQ6IgEvTV9fTzETAAESGgoHCwIgTHSCAXZQFAMNCwsCDhgYFAQhb5kBmwF3JRxCUkYgEiQsLCIMBw8RBQgEBAICCBM3SUMnBBIWDA0xEFh6bi4teZMBgQE/DB89PysAAT5ibloyDAABBwkFGHKcAZoBcCQRJy8pERM3RT8rDwsJBQUBAQUDAAEGIj5KPiQIGiQgFAMRFxcRAwcTHRkTBQ4SGA4EEyMrIw8HBwsNDwsVGRkTDRcjJR8REDA8OBwBBBASFBIUFg4EDxcXFRENAAEQJDA6QhYPKSkbAAEWIiAQAiAyMiQCDRUhKzMzJSEZGSdBSzsTJk5iXkoexgItc40BgQFLAiY2NCIAARMbGQ8AAR44PjomAylBTU09HwcOFhgEFxkLEj5oeG5MEgULDwUBEBwaFggZZZEBlQFxHyJKVkgiECIoKBgIDRUVCwIEAwEAAQQTO0tFJQIaIBgBIyJqhgFyNi93lwGFAUMAASVDRy8DPGBqWioIBQkLBxhslgGOAWIYFSszJw8VMz89JwsFBwUDAAEFBQUDBh46RD4iBhgiIBIDExcZDwUJExkZEwEMFBQQAhEfJSEPAAEHCQkHCxETFRELEyEjHxEQMEA2HgEGChAODhAUEgYJFxMPDQkDDBYiLiwIHy0vHQIYIiAQBCAwNCICCQ8VHycjGRENER9DS0EdGERUWEIcsgEfT2FZNQIcJigaBA0PDwkCFiouKhoCFykvLSMNBA4UFAEPFQsGLEZSTDQQAQcHBQQOEhYUCg9DX2NHFSA4PjgYChgcGBACDRERDQACAQEBAAEPKzUvHQIWGhgGDx5QYlQmHVNlYTECHzExJQEoQkg6HAIDCQkHEEhiXDwOEyEhHQsNHysnGQcHAwMBAAEBBQMAAQIUKC4oFgYQFBIKAw0TDw8FBQ0REQkBBhAMCgABCRMVEwcAAgMFAwMHDQ0JBQ0XGRMLDCAqKBYDAgYGCgQKDAoEBw0NCwkHAwIMEBYaAxsnJRUAAQ4YFAoCFCAiFAABBQsNERcXDwcJBxMvOTEZDiw4OCwQPQcRFxMJAgwSFA4EAAEHBQEAAQgMEg4KBAIBAQEAAQIICAoIBgEDAQYMGBwaFAgAAQMDAQIGCAgGBgQBBQcDBgwQEgwICAoKCAQDBw8LBwMAAgEAAQEFBwcHBQIECgYIAgoOEhAGAQcNCQMCAQkJBwEGCgwGBAMAAgEBAAEEBgQCAwcFCQUBAQMDBQMBBQUHAwECBAABBAIGCAwIBgECAAECAwMHCQkFAwEDAAEBAQEAAgIAAQIAAQQGBAYGBAQEAgIAAQICAAIDAwcFAQABBAYCAQMFBQMFAQECAAIDAwADAQUHCQcFAQcNEQ8JAwIEAgABBQABAgACAwkFCQkJCQMDAwUHDxMRCQIKEBAOBsEBAQEBBQkIRmZqSg4vTVdLJwsDAAEGBgoYJCguKBYAAQcHAgUZHxsNEDhISjAOKUlRRR0NGRsVCRhQcnxiKgQNFREDFi42LBIVNT8/KwsGEBAIBQABGCIiFgELDwcAARIVO00/Fx5EWFY4FBYcFhABERkfGREIICokFBlRa2dJGQABBgwMBA4iKCQSDSs/NycHDhgcFgwGCgYIAgENERMPCw8REw0HBgwODgwGAwUHAwABHDQ8PiQMBgIAAQIKEBYUDgQFDw0NAwkZJSMbEyMrKyEPAQQODhAqTl5SLAszSUk7GxsjIxkNDCAmJBADCw8PCwcPGxsXDwEGCgwIByU3Mx8EKkBIOBblAQABBQkNDwpwrgGwAXwQVY8BowGLAU8fEQUAAQYMKDxMUk4oBAsPAAELLz0zGxZYfHRUDkmFAZUBez0fMTktEySMAcYB0AGmAUgKFSMhDSROXEgcJVtvZ0cVChgWDAsAASY4NiABERkRBiAjZ39rKzJ2lgGQAWAiKi4qGAABGy0zKxkONkRAHCGFAbUBqwF7KQMQFBIKFjZEOhwZT2NjQwsYJi4oFgoOEAoIAxUbIRkRGR8dFQcGFhoeFAgAAQcJBwIuVGZcOBYIAgECDBwgHhgEDRcbFQUPLT09LyM7S0k3GwABDhYiIlKQAaYBkgFSDVN3fWErLzk3Kw0cPEpCJgMRFRMRBxklKycVAAEOGBgQBztXUzUMUHh8ZCrNAQMHDRMbBHa6AbwBggEOZakBvwGnAWkvHw8HAAEQMlBkamg8GAEDAg01Qz0fGGKCAYABWBBTkwGpAZEBSyk/QTcXLqAB5gHwAb4BWhQPHx8NJlRaSBgtY39zUxsIEhQEFQUkNjggARERCwwqG2WBAXEpOoYBqAGgAW4qMjYuHAABITE1MSEKOEg8ICWRAcEBvwGJATcFCBQSBho6RDoaG1Vxb0sLGi40LhoOEhIQBgMXIyMhFR8lIxkJChggIBwOAgUHBwI0XGxmPhYQBAIGDhoiIBQGERkdFwcRM0VHOytJV1dFHwMSJCo0YqoBvAGmAV4PWYUBiwFvMzNBPysNIkhUTCoBDRUXDwkXKzEpFwISGh4YAz1bVS8WYogBkgFyMH0DDRMbHQtWiAGMAWICW5sBrwGhAW0/LSEPBQ44WnB8fFY0HhAMByk3MRsQTmpmSAxDfY8Bf0crOz0xES6UAcwB1gGwAVwkCAsNAxg8QiwGLVtrZ0kdBQQCCR0TECQkGAABBwcGHDgDO1VLGzZ0kgGKAWIqMjIqGgMZLTEtHwIiMCoSH3OdAaEBfTcRAQYIBBIqNCgQHUdhWz8JGiosKBoQEhIOBgMTHyUhGR8lIxsJChYgIBwSBgEDAQIsTFpQNhwSDAoIDhYYGBABDRcXFQUPLT9FNzFHV1VDIQUSJDY4ZJgBqgGMAU4LS295XzMzOzclAyI+SkAkAAELERENAxUjKSMTAAEOGBoYBic5ORUiWoABfmQmDAUPGR8nHQgeJBQJQWF1dV9NQTEhCw44XHKCAYYBdmBMMh4ECxMVDQYeKCgcCB87SUUvIyclFQEiWnqCAXZUPiweEAYGCgYDEyk7Pz0xGxUXHSEnIxEDAAECAgoUIjI+LhIEAAEIJEBQUDwqJiYeDAETGSEfGw0BBAQAARE5UVdLMSMXDwcBBAoKBgcVJy0tHQABDhYeGhIQEAwICAMNGRsbGx0dGREJAg4UGhYUDgoGAgIUJiwsIhoYGBIOCgwIBgIFCw8PCwUJGysvMy89QT81JQcQJjI8UGRiTigHKTtBPycnJx0RBhoiJh4UAAEDBwcDAwkRFRMLAwgMEBYSBgIKFC5GTEo2FLIBCx0rMzErJR0bFRs9ZXV1ZUElDwECDCpKZHqEAYYBeGJKJA4MCAQAAQEBAQABAgIDDRMZHQsMHiQiGhggJjRIPjIkFgYLISsvKyEVEQsRFyElKS0xO0lNOx8EJD5KTkpedHJgMggDBwESJjQ0KhoBDRUVGRcnP0E3HwMOEAgLJzs9OSsPAQUJDQ8RCwUBAgQWICQgGAITGxkPAwsRFxkZDQcAAQEDAAEEDAwSEg4OCAQCAgIIChQcIiIeGAgHDxMTCw0NEQsHAgQEBxkvNz07NSUHGC46PigKCREPAw4SDAIbJScfDQgaIBwcDgITGx8XAxAaHhYGAgQMEhQWEAwQHjBGTkg0FrACEzNDRz8pFw8JDxthnQGzAacBdy8SMjokBhwySGR+eG5aQCQSEA4KBAABAgQCBgQDCQ8VFQxEYGJGFgUPBRI6LhYGAQETMT8/NRsAAQwKAAEVHSEjJytRfYkBdUEIPFxqZE5+rAG0AZIBSgUtPzMPKkhUSiwEERcbGRk7a3tnOwo8TEYeHUlhXUUdAAECAAEDCQ0BBAgKCiQ+QjoeETtPRysDCxMZGRcAARQWFAQFAgQKDA4KBAQCAAEFAwMEDhwoKiwaBhEhJyEPERkZFQkMKC4gAi9DTU1BKwABIjhCPAQ/Y189AjZSUi4JLTkxIQIoNjoyIgErRUczCSpIVEQcBhIYHCASESMfAypgfnxiJvgCFTdLTUElEQcFCR1vrwHJAbkBfRsuVFQ0BAocNkxqalxKOCAUFhIOCgIGBggEBgABBQsLDx5efnxWFBMlHwIsIAYJCwcVN0VDNRkGGBQIFxsbHyEnU5EBoQGNAVMKRGh2ZkyEAbgBxAGiAU4LP1FHGyRMWlA0AhETGRkXPXeJAXVBDE5mXDITSWNlTx8CBgQAAQMHAgwSEAoqREo+HhdLX1kxBQ0RFRkTCBwkGggHAQABBAYIBgACAQEFCQkAAQoaJi4oHgQVJSslEREZGRcPEjRCNAorRVFRRy0DIDY6MBNpjQGHAVMCSnBwTAABKzk5IQIsQkpCKgABNVFVQQk2YGpYKg4YIiQiEh03MxUmaIwBjgFuLI4DEy9BRTchEQUFDR1nnQG1AacBaxcwUlAsAAECDh44UFBEPC4cFBQWEgoKBgoKBgYAAgEDASJeenRQFhMhHQUiGAABCw8HEy8/PTEZAhASARUbGRkdIUt9kwGFAUkGQF5mWjxwoAGmAYgBRgk1Rz0bIEBKRigCDRMTEw0zY3FnNwxGXFAuCzlNU0EbBAQGBgEDCg4SDgwmOjwwGhM/UU0tAwkPExMPCBogFgIFBwMAAQICAQEDAQEFCQcDBhYgJCQaBhEdJR8LDRMXEwsSMDguCiU/S01DLwsSICYaI2uPAYUBVQJIbG5MCB0rKRkILkJKQC4GJ0VNNQc0WmZYKhIgIigmFhcvLRMgXHx8XibyAgkdJysnGRMRDxMfR2l1aUsVEigoGAUDAhAeLi4sJhwYFhYUEhAKDAwICAQIBggICCJCUk46GAABCwcCGhAEAQUBDSErMScZCwUFCxkZGRkZGS9PWU8vBiY6PDgkRF5kUiwCEyEdBxIoKiIYAQULCQkLGTM/Nx8KJjIsGAUbKSkhDQABBAQCBAQGEBAOCBQeIhgMCR0pJRcDAwkLCwsAAQoKBgEJBwUFAwMFAwUBAAEFBQUBBg4UGBYOCAMNEQkDBQcLCQUIFhoSAyEtOTs7LRkJAQABBSVRX1s5AyxCRjYQBQsLAAEUKDY6NigQCx8hGwABIjxCPCYaHiYkJhoAAQkNAAEaPEpKNhb0AQgUGBQEFzdJTT8nFxENERUVDwkHCwUCDg4QDBYcIiAYEhAMDgwGAQkJAwQGDBIYHhwYFhQWGBgUEBAOFhoaFAgLGyUnJyEjISMdGxkVDw8PCwMBAAECAgQEDAgKBAIAAQQMGCQqJhoKAwkNDQMBAAMCBAoKCgQDCw8PCwMKFhYSCAMDAwECCAoICAYCAQULBQABCAwQDAgCBAQGAQUNExEPCwkHBwsHCwcJAwEBAQkFAwIEBAICBgQMDBAMCg4SFA4IBQsRFxcZGxshIykvLy8vJyUhHxcPBQIOEhYWDAgIDhoiIiAeHCIoMCogCAABAQIGEhgYHB4eJiwsKiIUAgcPCwUjMHaUAYgBVBefAeUB7wG1AT0QOEY+GAIEBgIBASAyMhwAASREUEYkBgIBCQkVO0tFKwUIDBYmOi4IDQ0CGBQOCgYEJEJIOhYZQVVVRTE1NS8pGwkBAgYGFjA+OiAAAQMHCwcHK0dHNQ02boYBfE4GL0dHMQsAAQYGDAwcMDguHA0zQ0EtCSpMXEogBxMTCwISCAEDAQIVJyUdCRIqMjAaBhYiGhIHGyUpHxMHBwcJDw8PBwcFAAEPFRsTBwEPERELBhQiJCIaLkRKPhwHHSUpIxUHAwMPITdJT0tDLx8RCwcFCBIcICAFJzEjBhwWEBIWMHCSAZIBaBolR005DQ4IBgwWLE5eWDgCP2dtVyPhAUasAdoBzAGCARPNAbcCwQLzAUsoYnhsNhIaFhAAAi5CPiIJIlhiVioCCxEXGSldc2dDCQoUIjZUQAoTGQcUEggCBQcqUFpKICVZb3FZP0FFOysZAwoWGhYuUGBWNAINDxMVFUdtdVUbRpgBuAGqAWYBQWNjQxEGCBAQFCpGTEIiD0dfWT8RNmx2YigJFRcPBhQKAgcFARsxNSkLGjZAOiIKHiYmFAUjLS0nFQcFCQsPFQ8NBwMDESEhHQkJFR0dEwIcLDQyIkJcZlQqBx8vMSURAgwKAR09U19jUzsjEwcFAwwcJCooEUFPOwcaDgwKFDaUAcgBxAGQASA5Z3NdIQIDBQIQLF5wZDwLZ50BowF/M7UCSrIB4AHUAYwBB78BqQK9Au8BTS5qiAF4RiAkIBQEAAEmODQUFRhIXE4kAQ8bHyMxZXdtRwcOHjJOZlQeAw0BFAgCBw8PIEZSRhgjWXd3Y0dLSUExFwABFiAiJDpealw2AAENFxsZH093e18dSJgBugGmAWQCQV9hRQ0EDhIWHC5ITkIgD0VdXUUVNmJwWiQHEREHChwOBAABAQABGy8zJQsaMj42IAYYIiAOCR0rLSMRBwkJCxMVEQ0JBQEPHSEbDQcVHR8TAhwuOjQqRmJoViYDHSstIw0CDg4BGTtXZ2VfRy0ZDwcDDh4sMC4LO0s9AxoMCAwUOpQBxgHGAYoBIjlneV8lAwkLAQ4sVGRYLBlzowGrAYcBNZ0CNogBqgGmAXIMdcUB1wGpAT0eVGhmQCoqIBYCCQ4YEAUlASI2LhgBER0jJTFTX1MxBRowSF56akAiFA4SBgcPFxcGJDAmEh9LY2tdSU9LPy8XAhYkLCo6VFhOKAIPFx0hIUdfYUkVOHSIAXhKBClBPzEJCBAUGhwqPDwwGA8xR0c3FRw+RjoWBQUDCBAgGBIKBgIRHyEXBxAkKiQUAg4OCgQJFx8fGw8HCQsPERURDQkFAQsTFREHAw8VEw0CGCo0Mio+UFBAIgMTHR8dDQEEAAEFGTdPX2dfTzstGxEDECAqMjQKGScZBBoUEBIWMnKSAY4BZholS1NHGwUJBQABDiQ4Pi4OKWGHAYcBaSmvARIuPkA6JgwHFx0bAxosMjAoIhwMAxEdJy0xNScXDwABBAABDxkhJScnJRsPBipMZHyEAX5uWkIqDgABDRcbIRcRCwcDDy1BSU1LRT81JxUBDhokKjQ4OCoaAw0bHyMhKSsnGQEWJCokGAwBCQsJAQoSHB4cHhoaEgYHExkfGxULBQADBgwUGCAeJB4eEgIBAwUBAAEGCggIBAEJBwsNCw0PEQ0HCQkPERMREQ0JAwEBAQMDAAICAQACBhIeIiooLCgkHA4EBw0RDw8RDxMXGys9S1VbWVNFNyEHDB4oMjQwKiQgIBwaGhgcIjI2NCYMBxMXFQsBAQABBhIaEgYLGy1DRT8vE7sBDR0dEQg+mAHCAbwBeAYPAQoYJB4GBxETJ2OHAYsBc0E5NykdAAEBER8jJRUKIiYeFEyCAaYBqAGSAXhoWkAmCgMLFRshKScjGQsVL0FLSz8jDwkFESUtIw0WQmJmWC4BIzM5NSUhIRkPAgQFCQcCCAEFCwcBJkJKQiYMAAEFCwkFCwsLERcrOzsrDxAqNjgqIiAkHhQGBgoMCAYBBQMFAwURGRcTCQ0NCQkJBQ8REREPCQUBAQABAgQEBAIGDBISDgoYIiwsKhADDw0DBQ8bGxkTFRsdHRsNCQ0hQWuFAYkBcUUNBhAeKjZcfIIBbjwMCRELBiI8RkIwDhElLycTCRENAwgiQkpAFiVFUU87Fa0BHUVTQRVO5gG4ArICzgEeEwsBChwODycpIzGPAcEBxwGbAUs9OzchBwABFyEnIQsuTFI+HF6oAcgBwAGSAWJSPC4aCAELDxkdJyspHQ0XM0dPSS8DFCAQCT9ZVzkEUIQBjgF8RgUtQ0s/JR8hGxEDAxMbFQkECxUVDwABOGRsViwCDxsZEQcFCQsLEz1VV0MZGjxKRjQcHiAaEgQKDhAKBAEHCwcFBRMbHRMJCQkNBwUHDREREQsDAAEEAgICBgYGAggUHBwUDBwqMC4kAR0tKQ8JGyUpIRUZHx0fGQYkJAwpcaMBswGfAV8RAQgUHjJ4rgG6AZoBTAMnNy0RIlJoZEYUIUNNQx8NGRsVASp2lAGGAUoXT2dlTR85I1VlVSFI9gHQAsgC4gEiExMHAAEOBCEvNSk1kwHNAdEBoQFHNz83IwkDFyElHwM6YGBIHGKmAcABsgF4SjoqHA4GAQUNERkfJyUbDRExP0M9IwwoLCAHQWVpRwlOggGUAXpEAy1BSTkfGR0ZEQABBx0hGw0AAQ0ZGxMAAT5mblgmBRkfHRUFBQUHCRE5V11JHRpAUEYwFBgYFhACCA4ODAQFBwkJBQURGRcPCQUJCQcDBQsNEQ0JAAEGBAQCBAQGBgQKFB4eFAwcJCoqHAcpNS8VCx8nKyMTGRsfHRcWNDogFWWjAbcBpQFjEQcAAQoYKnq0AcIBngFMCTNFOxkgVGxqTBQlR1NHHw0bIRcFLIABqgGaAV4JS2FlTSGcAR1HVUslOMIBiAKAAq4BHBMRDwUECSExMycreaUBpwGDATkvMS8hCwcTHRsZATROUjoYTH6SAYIBVC4gGA4KAgABAwcLDxcZGRMJDyErLykTDiYoGgMzUVU9CThkbloyAR8xMysTERERCwMHFR8ZCwEPExcPATBQVD4aCRMbGREHAwMDBQspRUs5GRgyPDQiDhAQEAgEBAwKBgIDBwUFBQMNDw8PAwMFAwUBBQcJCwkDAgIGBgABAgQIBAQGFBYWFAoQHB4cFAshLSkTBxkjIRsTERcXFxEQLjQiC019kQGDAVEPCQEEDB5ekAGaAX46Cys3MRcYRFhWPhAbN0E1FwcVGRMBIGyKAYQBUAMzS0s7F9QDCx8lIREOSGRiPgINDw8PCQ8bIyEbHTtNTz0hGyEZGQsLDxEPDQMUICAaCiA2ODIiDgwIBgABAgIBAAEBAwcLBwcBBQsPEQ0HCg4QCgABEyEhGQUWJCYkEgMJDxEPAwMFBQMAAQULDQ0HAwMLCQcBEB4cFAYFCw0NBwMDAQEDARMZGxkHBhQWFAoGBAgGBAABAgIEAgEAAQEDAwABAQMHBQEBAAEBAwABAQABAQMDAQEBBAABAgABAgQCBAQECgwMBgQKCgoIBAMPExEJBQsPEQ0JDQsNDQsEDhIKByM1PzslCwMFAAEECCg6QDYaAxETEwUOHiomHgwHERURAwABAwMDAAEUMDw4JAIRGR0XB9YGAAIBAAEBDTFHRzMRDREVFxsRCQEFCREJBQMNFSMtLyUdAiAuKBYNKzszIwMFCw0LBwgSGhgSAAELCw8FBAoKCAYCBgoKEAgAARMbHRUDFCQkGgYPExETBwIAAQIGCgoYIiIeCgMNERELBQMBAAIDDxsdFQ0FAAECBAABAQQKCAoGDhISDgYNHyUhEQMGBgoGAgICAgACBQMBAAEBAAEBAQEBAgADAQACAgIEAgABAgICAAECAgYICAQEAgQEBAABBQsNBwECCAgEAgABAgIEBAIFCw0JBx81PTUlAh4uLhoBBAgKBAABBAoMDAYPKTMrEwYLFRMNBjhebF40CgEHCQMDFR8fEwEIDA4IBPYIAgQGBAIXaZMBkwFnGQsXIScnEwgUFgINAgoGAxtBVVtPMRZeeGw6F2WFAX9VDw8fJR8VDi4+OCIEFyMfEQwUGBQOBggYHhoWAAEtRUMvBy5MVkAKHysrIREBAgYKEB44UFJAHAcbIyMbDQUBAAEBAyM3OS8dAwQKCgQCDhYcGBIgKCwkCh1HU0crBwgSEAwCBAgGBgUHBQcFAwEBAwUBAAIDAQMAAQQEBAQCBAQEBgQCBgwODAgEBAQCAgABERcZFQMMEhQUBgABChISEAoDDRMNBz9ziwF/RQ5WfHhEBhAcGBAFBhgaFgora4MBbzUGHzs/KwSAAd4B/gHaAXAQCx8fFQ09VVM3CQ4WGhYIggoCBggGBhl3qwGpAXcXCxsnLS8bDBwYBgsGDgoFI01rc2M7GHCUAYQBSB93oQGbAWcXFSkxLRsMOEpELgYbKSMPEiAeHBQIDBokJhwAATVRUzsLNl5iRgorNzcpFwABBAgSHChOYmpSJAshKyshDwkDAQEHKUFJPSEJBAoMBgIUICIgHCg0NCYSI1NlWTcJCBAUDgEECggCBQsLCQcDAAEDBQMDAAEBAQUAAgIEBgYEBAgGCAQAAQoMDhAGBAIAAQICARMfHxUDDBYcFAoEDBQaGhQAAQ0PDQNJjwGrAZsBXRBsmAGUAVgKFCAeDAcGGCAYCjeDAZ8BiQE/CidHSzUEoAGWAroCkAKMARoPIScXF09naUkNEB4eGAyACgIGCAgIEWWPAZEBYxEFGyUvNx8DDhAAAQ0AAQgCDSdRa3FjRQxefnI8GWuPAYsBXREVLTExIQQwPkAqCA8dGQUaJiQeFAoMGiQkHAQrRUk3CyxOUDYDLzk3KxUBCBIYJDJSaGRQJAcdKykhEwsFBQMDJ0FHPScPAAEGBAQEFh4mJBwqMjAkDB1HV1E3DwIMDAgBAgQEAwUNDQsHAwABAwMDAQACAwABAwIGBAQGBAYGCgYEAgYKDgoGAAEBAAEBAAEDERsbFQMMFhYUBAIOFhocGAYBBwMCQYEBoQGVAVsIXIYBgAFOBBIaFAgLAAEOGBQIMXONAXcvFBc5QS0GmgGEAqgC/AGKASAAARUbFxdHY19BDQwYHBgI/ggCBgQKBgUvSUkvAAEDESMvMysdEQ8NDwsJDRstRVVbVUMPIjw2HhE9U00zCREhKykhCRAgIh4OAgABBA4kJiIeFAYIFhwaGgoRIycdDRIeHAoTLzEtIxECDhgkLjhIUk46HAMTGSEbEw0LBwUHGy8zMyMVDwUBAwIQHiAeICIoIhgIESk3NScVBwMCAQMHAwMFDQ0NCwcBAAIBAwABBAECAQIEAgYGBAgCCAYGBAABAgYGAgIDAQUDAwUNERELAQgKDggAAQEKEBYcGBIKCggKIU1lY0UNJD5AIgIEBgIFEwkAAQQGAAEdO0MzCSIGDRUPCGSqAcABqgFoKhYGAwkRLzs7JwsIDhAOBowHAgQKCAoMEhgcGhQKAw8bKz1DRzsnGRslLS0zKyEjKTNBPzkpGwUIEBAMBAcTGx8dGRMLAQoUGh4iJiYkHhYOBggYHiIaDgoGAQMHFyErLSsrLycdDwQcLDo8OjQsIhgMBAkRERUPBQIGBAMVIysnIR0VEw0FAQ4UHB4aHBYUDgQDBwkPERURDw0JCREZGRcRCwcHAwABAgIAAQQAAQIEAgQEBAgICggIBgIEAAQDBQMHBw0NDw0JAAEEBAYCAwMHBwkHAwIKEhooMi4qFgIPGSElHx0VEQsJFyEjIRsfJSEbDw4mOkI8MigiHBIMFiYuNDY8PDgmDg0fKSUdBRAeJB4MjgUGEhQWEBZAWFhIIiAiGggXR215aUUbM0NJSTUEKjIaHW+pAbUBmQFVBDhYWEASBw8XGRsbGRMHBhQWGh4iJh4UCgQAAR5EVFA0EAABBQcNCylDS0c1N0FFNR0KNlReWjwcAgkJAAEBCxUXEwUiODoqAilBSUMrFRELBwUCEhweHhwYGBAKBAQKCAQJEQ0HBwULKT1DNRsHAgoMCAQBAAEDAgABBAYGBgYMFhoaDgQAAQMDAQABBwsLDQcRHSsrIQkQICgcBgsVHRsVDxcVDwEeWHaAAWImBAkXHx8XEQ0HBRFBXV9LJ0VhaVElOJoByAHEAYoBMCAYEAwGAwcDDCJKaGxcLBtfgQF/WRFIgAGSAXgy6AMKFB4cFh5ScnhaKCw4NB4FQ3uJAX1NHztRWVM5GFBePBWHAd8B9wHTAXkAAVR6fFYUAwsPExUXFxEHBhQWFBYeIhgKBAIAASRacGQ+CAUNEQ8PL0tXUTc1T1NDJQxEaHJiOggTIRsHAg8XGRMCOlZaOggxU1tLLRMJBwMBBBIcHh4YFBQQCgIGEBIKAQ8JAwEDCzNPV0UpAwwWFg4CAAEDAwEEBAYGCAYSHiYeFAQDBQUFAQcNEQ8LEys5PS0LGjY4KgoTIycpFxkhJR0LIG6eAaYBgAEwCAsVGxUPBQICBBVVfX9jK1eFAY0BcTNS1gGUAoQCqAEsFgoGBAYPHx0NFkh6hAFuOCODAbEBrQF9G2S4AcoBpAFGlAMIFh4eGB5UcnJaKjA+OigGNWl/cUcdOVFZVTcUTlg2E4sB4QH9AdsBgwEFUHh2VhYBBQkLCQsPCwMKFhASFBggEggCAwMmVmpcNAYHERUPDytHT0kvL0tNQyMMRGJqWDACGyUhBQELERMPCDxaVj4GLUtVSScNCQEBAQIOGBoYDhIMDAgCCA4QDgELBQEAAQUJL01VTScDEBoYEAQDAwEBAgYICAgIECIiIBIEAwUHAwABBw0REQsRLTs/MwkeNjwsBBMlKykbGSMpIQ8ecJ4BogGAAS4KAw0RDwMICAwEE1V3fV8tVYMBjwF1M1LYAY4C/gGgASIMAgECAhMlJRUGQGp6ZDIlf7EBsQGBARlksgHGAaQBQIwDCBIWGhYcQFZWRiQoMjQmDBtBUU0zFy89S0c7AyAoDiN1swHFAa8BbxEqTFA6EAICAgABAgACAgYOFhQSEhYWDggAAxg6QjwiAQsNDxELHzE5MSEjMTcvGwouRko+HgIRGxMDBgMJCwUKLEA+KAQdMzkzHQkFAwEBAQgMEAoKCAgGAgIECgwEAAEJBQEDAwsfOz87IQEKEhIOAgABAQECAgYGCAgGEBgeGA4CAAEFAQMBBQkLCwcRHy8xJQsWJigcBBMbJR8ZER8fGQkYUHZ6XCgMBgEDAAEICg4MBg07V1lFHz9faVcpOJYBwAGuAWoUBAEBAAEEDR0fFQABJEJMPh4fX4EBgwFfGUR6igFuMNIDAgoMEBISGBoeGhoeIiAcFAQHERUTERMbIy03NTU1OUFLV1VPPSkPBQYIBgYGCA4QFBYWGhgWFhYUEhIMBgIAAQEECAoGAAEHBwkJCQkPERMRDQsPDwsFAAEOEBQQDgQCBAQGCggICgQGDg4MBgEJDQ8NCQcDAQMFBQMDAwEBAAYBAQEFBQcFBQUHERMbFQ8HAAECBgIEAAEEAgIGBAYGBggIDgoKCAQAAQIBAAIBAQMDAwULDREPBwECAgEHCQ0NDQ0NCwkHAAEMHCIkIBgUFBQUFBAQDgYEAw0TFxMNER0ZGQ8EGB4cEgABBQEBAAEEAQcLDQkAAQYEBAcRHyUlHRECEhYSCLYEAAECBgQGBzFJRSkKMkZORi4SAQ0REwABLEI+Hh9rmwGrAZcBZSkDCggRLzMrIREBEyMhEQwwSFBEMBomLCweDAsbHRsJBQcNERMPCwMBAwUNDw0NBwMAAQQEAgEPEREFBA4MDAoOFiwyLB4EEx8fFwUFCQkHBQgQFBIGCxshHxMFBAoKCgQJDRcTDwMGCAoCBQsNDw8JBwUCAAEEAAEEAAEEBAgGAAECAAEGDA4QDAgBCQ0NCQEIDg4OBAQIDhAIByEtMSkbAwgQDgINDw8FBgwRHyMTBi5IWFQ4IBQIBAEGHiwoFgUGHCIcBiFHUUszFSEpJxkAAQsXHRkRCw0NDw8FKEBAKAUxSUk3GdQEAAEBAwMDGWeNAYsBWQABRmx4akYYARUbFwpchgGEAVYJgwHTAesBzQF7FShCNAY3PzkvGQcnQ0UtBEZudmhAHjA6OCgGGzM7LxMFDxMbHR0LBQIAAQkLExMPDQEGCgoGBRkhIREGDA4QEAoiQlBGLgQjNTclCQkLDQsDDiYqIBANKzcvIQMMFBQOBgkdISMVARAYGA4FCxEREQ8FAQABBAIEAAECAggMBAABBQABBA4UFhIGAxMdGxMDEBogGg4IGB4eFgkxTVVJKwcUJBwIDxURCwoSH0NJMwJEcooBhAFaLBoMBAEKOExKKgkSNj42EDNvjQF/UyU9S0UtBxUpLysVCw0NEREGUnx+VANRdX1jJ6gEAQUJCQ0jeaMBnQFnA0x2hAF6UCIAAREXFRBmlgGcAWgCfdMB8QHTAXsPNE5ACjlDPzEfBS1NTTEGTniEAW5EGjI8NiYGIzk/MxcHER0fJyUVBwMBBQ8TExUJAQgKDAgFHSchFQYOEg4QDCZIVk4yBCM5OSkLBQ8NCwEUKi4oEA0tNzcjBQ4WGBIGCx8nJRcBFh4cEAEPDxETDwkBAAEEBgICAgYKDAgBAAEBAg4WFBIIBxkjIxcDFCAmIhAOGiYkGgc3WWVbOQ0UICAIDxcTAxAaIUdNNwFKhgGeAZgBajgiFAYEDjxSSioNEDRGNhQ3fZcBkQFfK0lXUTUJFy81LRkLCw8PDwhcjgGMAVoBV4cBjQFvK7IDAwcNEREpZYsBhQFXAAFEbHhuUCgQAQsNClR+fFgMX58BuwGtAWkTIDIoAzc/PS8ZByE3OR8OSGxwXjgYJjAsHgIbLTMpEQkXHykrLR8TCwcHDw8TDw0BBgoIBAMTHxsNCBAOEgwMIEBKRCoGFyktHwUHBwsFARIkKB4OCyMvKx0FDBQSDgYJFyEfFQABEBoYDgULDxURDQkDAAECBAIEBggMEgoGAAIEDBIQCgQHFx0fFwUSIiIcEA4aHiIUBTFPXVtBGwEQDgABEQ0LBBwkCSs3JwZGeJIBjAFqQjAgEggMLDo2FhMAASQqKAwtZ4MBe1ctQ09FLQkTJS0nFQkNDQ8PBEpsbEQHTXF3XyX4AQMNERcXHTtDPSMIMEZQTkIyJBgKAgIiNjYoBh9DUVFDJRcNEyE3NyshFQEFCQUIIDZAQDYgEhYWFAwCCRETDwcLGyUrMzErIxsTCQkNDwsJAwECBAIBBwcFAgwODg4MChYkKiQcDgELCwkDAQEBAAEEChASDgYHDxUTCwMEBggIAgMLDQ8JAQYKBgIFCQ8PEQ0JBQMAAQIEBgoOEBQSCggKBgYEBgIBBw0REQsAAQoOFBAMChASEgoFHTVDRUEzIxsREQ0FChYoMh4OCAYMMk5iZlhIQDIkEAgKCgABDR0VCQIAAQMZNUVFOScvMSsZAwUPFRMNCwkNCw8HEiAgDBMtPTstEYIBBREZGx0ZDQcCCA4GAggWKjgyKh4KBQcLAwMGKEJAKglHZ3lxVy0PAQoOBhgsNjQsDBEfGwkOGB4YEAYIDA4KAhUzQ0U/NS0nHRMLAQICAgELCw8NBQABCAwODhAgKiwkFgMRFQ0BDggIAgICGCQsJBAAAQsPDQcAAQgMCgYBCQsLCQEBAwMDBQMJCw0JBwUAAQIAAQcFAQYEBAIICg4QFBIUEAoIBQsTEQkFAAECBAIAAQEBAQIEBgQIAAIDFyUvPT09NS8lGQcQJDI4Oj5AOCgaGiYyPEZSUk4+JAc7XWNVLw0EDAwCBw0PGRsdCQgMDgYJExUTDxk3QT8pCQYQCAITEwsFAwF8CRUdIR8VAwoQFAobNTcfEjI2LCAOBxcZGw0KYqABrgGCASBbpwHJAbcBfSMYOkIuECpGUkwwGWN5aTESLjg2JAoQHB4YCiNRZ2dRLyUZEwsDBhIYEAQNHR8bDwIKEhQUFDZWWkogF0FPQR0KBAMDBQY2XGRQIg8fJyETBBggIBIBFR0bFQkBBQcJAwcRGxsVCQgUHBYEAQoQEAgCAgYIDg4UEhIMBAsjJSEVAQgOEg4CCQ0NCwABBAIBAAEDCRknNz9BMysdGRUAASA6RkQ6QkY+MBYSFh4sOlJkYlIwE327AcUBlwE5EjxKQBgBAgEFExEaNkAuDBUpMSsTNXWPAYEBUQUuQkQmBwkCBgoGagcXHSEdEQQSGBgKLVNXOwIuLioeEAkZJR8VCnjIAdgBqAEyWbkB3wHRAYUBGTBWWEASLlBcTiotgwGjAYkBRRI4REIqChQiJB4II1lxb1MtGw8HBQMOHCAaCA8hJyMVAg4UGBQQQmZsViYjV2dbKQQAAQcLBQZEcnpiJg8pLykXBBwsJhYBFyMjGQkDBQsJBwUZHx8bCxAiJiQKAAESFBQMAgACBAgMDhIQCgQTJS8nFwEOFBgQBAsRFw0FBAABAQUFCRkpMzk7LRsVCw8EKkBKQjQ+QjwuEgoMDhwuSmBiVDIXlQHZAecBrwE9JFhqViIAAQoIAgkJLkxSPg4bMzkvGTuLAa0BoQFfAAE+Xlo4AgMKEhAETgcRGRkVCwQSGhQILVFXPQUiJCIYCgUZHyERCmy2AcIBmAE2S50BwQGzAXUNNFBUPBAoRE5CIi19lQF/Qw4yQDgoChIcIhgKIUthX0chEQcDAgMQHB4aCA8dJR8RAAEMEhQQEDpcYEwgH09lUysEAwkJBwRCam5UIg8lKSUTAh4iIhYBFR8dFwcBBwkJBQUTHx8XBw4iKiAMBAwWEgoAAQEAAQIECAwMCggAAQ0jJyMVAQwYFBAECxMRDwMAAQEDAwUHEyEpLS0fEwcHBwYkOEA2JjIyMCISAgIIDh44TlBEKBV/wwHLAZsBMShWYlAgBAoMCAUDLEpOOAwZLTMrETV9mwGPAVkCQFZWOAICDhAQCCgDCQ0NDQUGDAwMBBsxMyUHFBISDAgDDRMTCwRAanJYHidXa2VBBR40MiIKFiYqJBIbSVVLJwgcIiIWBgwSEBAGEyk3NScRBwMCAAIIEBYOBAURFRMLAggKCAwGIjY2LBITLzczGwIBBQUFBCQ+QjASCRMZEw0EEBYSDAELDxENAwMDBQMDAQ0REQ0FDBQYFAgAAQwKDAQAAwECBAYEBgQCCRMVEw0AAQgMDAoAAQMJDQcDAAIDAwEDCxEVGRcPBwEBAwIYICIeFBgcGhQGAgACBhAcKCwkFgtHbXVbGRgyPDISAgYKBAABARwqLiAGDRkbGQsdR1lTMQIkNjIiBAIGDAgENwYKDAwIAgABAgABAgABBAYGBAYAAQICAgABAQUJBwUEChAQDAYDCQsNBwULDwsJAAEMGBYQAgkNEQ8FAAEEBgYEAgYMDgoCAAEFAwUBAAEDCQkHAAEECAgGBAEBAAEBAAICBgYCAgEDAAEDAQEFCQcFAQIIBAYAAQMJBwUAAQQEBgIEAAEDAQEDAAECAgIEAQQCBAQCAAECAAQBAAEBAAIBCw0LAwIGCAoGAgABAgEABgIAAQQEBgQAAQMHBwkDAgQMCggAAwEAAgcNDw0HBggODAYAAQUJCwcBEBgcGgwFGR0fDwISICAWBAsRFREFAQICAAEBCRcZGxEDBAYGBgIMHCIiEgMdKScbBwYODAoGkwIUMEA8JgoEAQIBBhwoLigSBgADAgEXHx0NChogIBwQAg8VFRMTPVFPMQUyUFQ4BiMvLSMVBAgKCAoMIDAyJg4FBwMAAQoKEyUrIQUQHB4aCgEABAQOEBIKAxEbHRUNBw8RFRMNDhoeGAABIzk7JwkSICAcEAENFRUNBQgSEhAIBA4UEA4CBAgIBAIDCwkJAxMxPTUdBh4qKh4KAgMDAwABBAgKCggKDhAQCAIPHR8fEwIcKi4gCAYEBAYGGTtHOxsKJi4sGAcnOTcnATxwgAFsOid5nQGPAVcSdqoBsgGAARQ9ZW1bLQECAgEPLW2NAYkBXRUQJCgiEkCUAboBqAFoFYsBxwHFAYsBHRwwOCwSwQMcSlpWOhIGBAABAgomQEQ6HAYGAAEEAgMbKSMREiwyMioaBA8fIRcfWXVvSQlGbm5KBDlFRzkbAgwQEhAQLkZINhQHBQUEEBQXMzkrCRYoLCIMAAEBAAMEEhoWDgMbKSsjDw0XHyElEwwiKBwEM1FROwkiLjIqFgEVHx8ZBwoYHBYICBYaGhACCggKCgIJDRELARtJWVErCi5APi4SAgEDAwEGDA4QDgwWGhQSAAEVJzEtHwImOkAsDAYICAgMJU9jVycUNEQ8JAs5T083A16gAbYBngFSM6MB2wHLAXsYqgH0AfgBsAEaV5EBnwGFAUENAQELHUulAdMBywGPASEcMjw0Gl7YAYoC8gGQARvFAZkCmwLJASkoSE5CGLcEIExiWkAYDAgECAwsQkg8IgwGCAQEAhchHwUeMjw8MCIGDRkfGSFdeXFPCUBoZEALQ1FPPR0CDhIWFBQ0SEg2EgMFAAEMGBoPKTMlCRYmKiIMAwEBAgABAhIYFAoHGysrJRUPHSctKyMGGiQaAAEvT1E1BSY2NiwaAAEXIyMbDwwWHBYKCBQaGg4GBgwKBgABBw8PDQMZSV9VLww2RkQ0FgYAAQUBAAEIDhISEBQYGBgOBBcnMzMjAAEkODwqDggICA4OIU1hVSUWNEY+JAs5TU85AAFkqAG8AaIBViedAdEByQF3HKgB9AH0AaYBFFuTAaUBiwFJFQ8PGy1dtwHlAd0BmwElGjQ+OB5k4AGQAvgBkgEZwQGbAqECywErLExSQhztBBg+TEw4GhIODA4OKDY8MiAQDgoKDAYJCwMMJjo6OjAkEgMNFRUdRVtXOQkkOjIUHUVNRzkbBBAUFhYUKjg2KAwBBAoUHiIIDxcVAw4aGhQGAQEAAQMAAQQKDAwEBxshJR0TEyEpMTUpEQQODAIfMTMfBCYyMCgWAhMdIRsPAAEOEg4ICAwUEAwCBAgGBgABBwsNCQABETVFQSMMLDw8LBYIBAIBBAYOEhIUEBYYFAwAAQ0fKSkhBRIkJhwKCAgKDgwPLz83FRAqMC4WByU1Nx8GToABkgF+Sg1dhQGBAU8WeKYBogFsBElzf29BHyMnMUFhowHBAbcBgQEjEigwKhxOqgHKAbQBbA2FAcUBywGTAR0kNj4wFuEECBggJCAcHBgYFBYWICAgGhQSEA4ODBgcJCwyMjIsKiIYEgYAAQcNGx8dFQUNGyExOT09MyMRBAoQEhIUGBYUDAICEBwgKCgkHhQMAgIBAgEBAwEBAAUDBwsPFRcVDxMfKzM1MycfEwkBAAIGDhggIB4WDgABBw8TFxENBQEAAQECBAABBAACAgICAAEDAwMDAQABAQkLCwUGEhgaGBISDAoIAgYKDhIQEhIODAYAAQMLDRUVEQkHAAECBgYIDAoQCgICAAEGDAoKCAYBAwcBBBAgLjIwLBwQBAACChIYDgIRIy0zMy0vPUtTV1tjY1U7EwABDBAUECA0PDgkBhcpKRsAAQoSEg4EqQQHGRUPBh4oLi4mFhwgIB4cEAIDAwQaQFpkWDwaBgMCFioyMiYSAwYKCgYJM1tpY083Jx8TBwEXGxcFDgoCAwUDDiY0OjQuNjw2IgYNExcRCwABBAgKBAIKDAoCCyk3PTEdDw8XHS0xIxEHAQIQHiIkIBYMCAQGAgcRExUVGyEdEwMDAwUHAwABAQUDBQMECA4OCgweJCQYAxsjHQsQIioqHgoBAQECChIYGBAMBAABBgIAAQkVHRsZCQIMEBAWFCI2OjYgBBUfHxEIHCgwKBobP0cxAjxcaF48ATtbYU8nARIWCBNJeZEBlQGBAVENIDAkAAECChQUEgIHCwsBDhYcGhIKAAEFCQkDmwQXMT0tDR40PDoyHiAmKCYeDAkVDwUiXoIBigF0RAYbIxcMNkpQRCYGFCAeEgdJdYkBe1EvHQ0FAQ0vPzkfCgILDw8FFDRGSDwqPkRCLAgTHyMdDQABDhAOCAYUGBYKCzdZXU8nCQUDDyEtEQIKDAQUJjAqHg4CBQMDAgsPFxUVJS0vHw8AAQkJBwUDBwUJCQMIGB4YDBg0RD4kCTtPRyUMMEQ+MBQFDw0JBBIeHBwOBAgMDAoAARMhJyEPCBAUGBgYNkxaUDIBIzc1GQ4wRkhCHj+FAZEBbxdKgAGWAYoBVgddlQGZAXsxEjY+KAlbnQHFAcEBnQFHLHSIAWQOAhQYHBQDHykhDRAiKCoiDgMRFQ8HyQQZPUc7FR40QEIyIiQqLCoiDA0XFQMkZpABkgF8PgQjKx0MOlRcUC4OICgmFAVHeYUBd00hEQUAAQIPOU9FKQABAw8XEwsWNkhINCQ4RDwqBhEhJR8PBA4QEAwGFB4YCgs9XWVXLQcCAAEHFycJDBQQBhYoLCocCAMHCQMAAQcRExMRIzUxJw0FBwsLBwEFDQsJBQ4eIiAOGjxKRCoLQ19XMwg6SEg0FAcTFQ0AARIeIBgSAggOEg4EDx0lHxEKEBYcGho2VGBWNAIlOTMbFDhOVEQgRY8BpQF7IUyEAaABkAFYA2mbAakBgwEzFD5GMAldqQHLAc0BoQE/RJABpAF2EgISHBoSByUxKw8QJjAwJg4FERUVB7kFEzM7Lw8cLjg4MiIkKi4oJhADCwsCIlx6gAFoPgoRHQsSOk5USjAWIiYgFAc3WWdZORcJAQIAAQ0zQT8lAwcTFxMJDCo2NCYYKDAuIAQRGx0ZCQIKDg4IBBAUEgYLMU9XSSkLAAIFFR8FChIQBBAgJCAWBgEFBQECBQkNCwsdJyshDQEHCwkFAwcHCwkDDBgeGgwUND46IAc5TUsvCC4+PiwSBw8TCwMQGBwWDAYEDA4KBAcXGRkHCBAWGBoWMkZOSC4GFyMjDRY0REg+Ii1rfWMVPnCAAXRKA019hQFvLwooMBwTV48BrQGrAYsBOzR0ggFcCAABDBQUDgcdJR8NEiIqKiISAQ0RDQXpBgcREw0CFh4kJCYiJCgoKiQaEgoMECA4SExGNCASFBQkMjo6NioaHBoUCAMZJSknFwkDAQEDCRsfHRcHCQ8NDwkAAQwQEgwGDBAQCAABBwsNCQcCBAYGAgIEBAABAw0dJy0pHQ0NCQ0RFwkBBAQAAQgOEhIMCgQCBAQEAgEBAwMJDw8NBQMFAwUFAwEFBQMDBAoOCgQKFBwYEgEVIx8RBBggHBYKAwUFBwABCgwQDggAAQQCAgQAAQEFBwUAAQoOEhISFhwoLCgaDgQBAgoYJC4sKiABGR8VBCY4PjgmBh0xPTUhDwcJESNBV2VlVTUHGiAWBwkAAQYICAABBwkFBBAWGhoWEAYCAQMB1QcAAQQGDhYaGBQWGB4SBAABBhIyTFpSOhoSEhQWHiAiIiYsMjg8PjgsEAMNDQMGEBISBgMHCwkLBQ4SDgIRDwkJBQkNExcVDQcFAwMBAwMFAwMEBAIEAAECAwsTFRMNAwYGAgkTERERFRUrLSsdBwQICgwMAgUHBwQMDhIQDAgEAgQAAgQICAgGAAEECgoEAQkPFREJAwMDAAEBCBAWFBAGBwkJBQEIDA4OCAEJCwsFAAEECg4MCAADBAYKEBAYFhQIBQ0JBhQcHh4WEhoeIiAeICAiHh4kMjQyIhAYFA4BGTdNU089JxsTExkhLTMtJxcXGxsRBwkZHRcHFDBAQjYWDScvJw/RBwACBhIgLB4WFhYeDS05LQtGkgGuAaIBaBoBDxEJBgYIDhokPE5eYGBGChkrIwkaKjQsGAABCxMXDwABJjQqDB8dDQUDBxcnKycdCQcDAwEDBwkHAQgODggIAgMbJSUhEQ4iKh4CDw8REQ8hTWdhQw0IDhISEAUfJxsNECImJBwMCAoEBAIIFh4aDgYOFhgQBBkpLykXCw0NCQABCiQyMiIGFSMjGQcQHiQeEgchKSUXAxAgJCQUAgABBQMCDBQeICQcCSk1LwsWJCgkFAQUICgoHiAmJigkPFpoWjwcKjQsEhtXfYsBe00ZBhwgFAUlOTsvGyk3OS8VI09bTyUcYIgBjAFqJDFrf2cr1QcBBAwaLDoqHhYcGhdJWUkbSKoB1AHAAXoaDSEnIQ8JCQEOJEBicHx4WhQbKycLIDpCOCAECxUXEwABKjguCCsnFQkHBxktMzEfDwcFAwUBBwkHBA4WEhAKBAcbKy8lFRYuNigIEQ0PDxEjXXt7UxMIEhgWEAspNS8PEigyMiQQDAgIBgABDCAmIhQCFB4cFgIbNT03HRETEQ8FEDA+PCoIGy8vIwkSKi4mFAsrPTUjBRwsNi4cBAMDBQABDBogKCgiCzNFOxMYJiwgCgUOHiYkICImKi4qRmx4bEQcMjwyFhthkQGdAYkBUxEaNDwyDBctOS8bMUNHPR0zZXdnNx54pAGoAYIBLEGFAZkBgQE14wcECBgkPEY2KiIgIBlHV00lOpoBvAGuAXIYDycxKR0ZEwcIIkZmfogBiAFsLAEbGwUaODw2IgoHDRETAxokFgk1Lx8TCw0XJy8tHw8LBwcDAwUFAAEIFhoYEgwIBRspKyMRDCouJAQPDw0PEyFXd3FREQgSFBYQByszKRMSKDIwJhIMDAgGAgwaJCAQBBIaGhICGTE7NSMRFxUNAw4qPDooCBsrKyEJFCYqJBILKzk3JwUcMDgwGggAAQMDAg4aJCgsJAMlOS8LGCAgFAQRBBIcIBokKi4uMEJmcGI8GiowJgwhXYMBkQF/Tw8eOEQ8HgMbJSUZLUFHOSMxYXNhMRpsmAGcAXguM3WJAXEv+QcIGCY2RlBIOjYsIgcrOTUdGlRsakoYBx8rKyEfFwsKIkRmgAGMAYoBfFAsEgIFEiAqJhoOBgEHDQ0FCRMnPTcrHxcNERshIRsPCw0HBwUBBgoQGh4cFhAIAxMZHR0TBAwUDAMNERETER87S0s1DQYKEBAOAxMbGQkOHCIiHBIMCgoEAgQSEhIKAAEKDhAKAhMhKSkbFRMRCwUKHiYkGgYLFxsRAwwYGhYKBxslJRkDFCIoJBQIBgQGBg4WICYmKBIDCwkGFBIMBA0VCwABChIcICowMDA8SEpALBQSEgYJJ0ljaV0/ExIqODYqFAYHDxUjMTMtHSdBRzshEkRgZlAmETNDOxnDCBI0SlpaWFJORDgmFAYHDw8VGQ8HDBoAARMdJSMdFQcKJEx4kAGcAZYBggFuWD4eAAEDBAYMEBIWEAgDFy87Q0NBQT01IxMHCwkJDxERDwsLAwQQFh4gIh4gFgwBDRMXExEVERURDw8NDQ8VFRUPDQcFBwcFAwgOEhIQDAYQFBYSDAgIBAQBAwcHBwUBBwcHAwMBAQMHDRUXFxEDCBASEg4GAgADAgIDAwMDAAEDAAEBAAEBBAoICAoODhAQEAwOEBgeJCw0NDIiDgUXHR8bGREHBBYqMjo6MiggGhIOCAUZISsrLy8vLScbDQQUJCwuJhoICwsJBQsRGyEfGwsEDBIWGhwiJiQcCuUJKGiKAZABfFhUTkQ2KBwSCAINNWdzWyEWAAEVISUhGxMFDCRqugHiAeABuAF6XEAoEgMNDwsFCBYcHhIAARkzQ0lHP0tRSTkZAQwMCAERExETCwcMGCQmICYuLiYUBR8vLykVGyUnIxUHAgYAAQcPAhAWDgEhNTUjBSI+RjwkDB4oKB4OBAMFBQMNHSMfFQkbJSUbBRgoLiIEHTE1LRUMLDo4JAoBAwUDAQkZHxkPAg4UFA4EBQsJAwQMGBwcFggFBwkGHEJeaFo6AAEpP0k1GxcVCwYWOFheVjwcBgUHAAEEGzE9OzEXBwIDFScvKRkEKjAuIg4JFjhALgItRVFFJwUNDwsAARZIanBYJKMKNoQBrAGsAYgBUkxIPjYmIBoUCAABQ40BowGFATkWAAERHyEbFw8BECJ23gGIAvwBvgFmRCoWBgcTGRkNAhQeIBYEFzNBR0E5SVdTQR0EEhoUBA8TFRENBwoeKCYiIjQ0KhgFKT0/MRcdLS8rGQMOEg4AAQ8QIigcAS9PUT0NMFZkVjAOJDQwJgwDBwkNBQ8pMS8ZDyU3NScJJERKOhAhPUc9IRI+Uk40DAMJBwcAAQ8lLykVBBogIBgEDRETDwEQGiIgGAYNGxkJGEpsfHA+AzdVVT0XFQ0HAhZGZG5iPhIHFxMHAAEhOUNDKwcWHBQFK0FFNQ0iLioiDgkqWGJKEDVfbV83DRkjHw0QWIYBkAF2MOMJNIIBqAGmAXxGQD44LiIiIhwSBDeHAZ0BgwE5FggLExUPDQUEDiJy0AH2AeQBpAFOLBoGAQUTHxkRAw4aGBIEFSk5OzUvQU9POx0EFhoWBg8TERMNCQwaIiIYICwuKhQHJzs9LxMdKy8nGQABDhYUAAEJEiYqHgABM1FXPxEuWmZWLg4kMjAgCgMJDQkHESczLR8LKTc7KQkoSFBAFB89Rz0fEkBWUDYOBQcHBQENJy0rFwYaJCYWBAsVFxEDDBwgIBgEER8fDxRGanZmPAExS081DwkJAQgWQGBoWjYKDRcXBwABGzE9Nx8EHCggAiVBRzsXGCAiGAgLKlhiShAzX21hPQ8hJyUXCk6CAYwBbi6dCCZgeHhcNDAsLiYiIiAgGhIbUWVRHxwQBgEDAAEGBgoWHFCUAaoBmgFoMBgIAgUHDxUXEQcGDA4IAw8hJScnHy03Ny8XAAEOEg4AAQsPEQ0PBwQQFBYOEhwgGhIFHSkrIQ8VHSEfEQEMEAwAAQkMGhwUASU9Qy8NJEJIPiQIHCIkFgYDCQkJAw8dJSMTCx0rKx8FHDY8MA4XKzEtFw40Qj4sCgIDBQECCRkhHQ8GFhwaFAQJERELBQwSFhoQBg0VEQkQNE5WSiwEGysrGwACBAYMFjJGSj4mCAcRDQMEDR0jHQ8KHCAaBhspNS0VCBAOCAELGDY+LAQrRVNLLxMdJSETAAE0VGBMINEFDBokJiIcHiIeICAeHh4eHhYIBggUIh4aGhoYFhgYFhgcICQcFAgEAAEBAwUJCQkLBwUDAwMJDQ0REQ8NERETDw0HBQMDBQsHCQkHBwUDAwEBAAECBAQAAgUFBwMDBwkLCwcDAQMBBwcDAAMDDQ8PDQMIEBQSCAIGCAgGAgEDAwMBAwkLCQcDBwsLBwMICg4KAgUJDQkAAQgQFBIQDAYGBgQEAgEDAQABAgoICgQAAQEBAwEAAQIECAYKBgQGBAoOGBweHhgODAgIDhAQDhAQFBYaGBYMCAQEAgQMBgQEBAgKDg4KAAIFBwUJCQ0RERUTCwMFBw8VGRcZFREVFxcRCQYSFBIGlwMLFxcVBxIsOj42IgwEAg4eMDg2NCokMDo4LiImLCwkFh9bdWlBDQkFBQMBAAECAgIAAQQMDgoAAQsJBQUFCwECBAABAw0RExELAQYQDggFGSUpJxUbJyslEwYqOjosDgABBwkHBQMPDxMHAQUJCQkFCg4UDAABCQcFBQEJHyMlFQUGCggIAAEEFBoWDAIIDhIKAgcLDQkCEBocHBAFMUM9JQESFhgSCg4UFhYOBAcFCwUAAQIEBgQEAw0REQcMKjxCNBYIBAQGDhokJigcEAUPEQcKBAUHCwMCBgwODAgEBAQMEhwcGhYKGD5MRCQXU3N1WSUZHRsZDw46SkQiEzE/PzMXDCQqJhDPARMxPTUdDDhUWEgiAg8RAAEiPEhIPjImPEhIPCQuODYsGEGnAdEBuwF1IxMPBwUBBgwMDAoSHiIYBg0DAwEHCQEKDgoDDRcXFQ0AARYeIhQDJT9HQSkrSU9DJQ5IaG5QHgQHCwcAAQURGREHBgABBwsJBRAiIhYDExERCwkRN0tDLwUKEhQOBA4mMCwYBBQeHhYGCxUVCQYgMjgyHBNdgQF7TwsUIiIeDg4iKiQWAQsVEQ0DBAgKCAoHGSUfERJIZGpSHgIDBQEOHiwyMCQGGSstHQABBRUdGRUHCAwOEAYBAwABChwqLi4iEixmggF0QB+BAbkBvQGNAS0dHx8VDSxykgGCAUYRR2NlTyEaQE5AHG0XO0tBKQQ4VFpKJgABEw8AASZGTlBGMiQ+TEo+JC46OCwUR70B6QHXAY0BLx8VEQUBCA4UEg4YKCYcCA0JAQMFDQABCg4KAAEPFRkVDQYaJiIWAStHU0s1OVVhUS0QVn5+YigIAAEHAwIAAQ8TEQESBgEHCQMSIiIUBxkbFRMNFUFXUzULDhQWEAYSLDo0HAYWICQaCA0TEwkQLDxEOCITbZUBkQFjFRYiJB4MECYsKBgAAREXFw8DBgoKDgoDHSUlERhOcnZYHgIHBwEMICo2LiAEITU3IwMNHScnHw0BCg4KBgABAwIOJDI6OCoWMHCKAX5GI4sBxQHNAZkBNRcfGxMHOIYBogGWAVQPS2tvWyUcRFJGHHkVMz8/JwMqQEo+IggBAAEQLkpOTEQuJDZCQjQkKjAwJhI7nQHFAb0BgwE5KyEVDwABCBAUFhAYICIWBA8JCQULDQUGCAYDDRMTDwkEFh4eEgUnQU9JOTtVWU8rEFBydFoqEgYEBAoEAwUDDBgOBgIFAwwUFAgRIRsdEw8VO0tJMQkKFBIOBAwmMCoaAhAeHBYGBQsFBBgsPkA2HBFXfX1bFwwWHBQIDBwmIBIAAQ0TFQ0BBgoODg4AARMdFwsYSGBgShgCAQMCDBwmKCYYAAEbKy0fAw8hJysjGQcBBAgGAgYMGCg4PDguFCxYbmAwHXOfAakBfysXFxMLATRwjAF4Rgs9V2FNJwwyPDIW7QELHSMjHQkKGCAkHhweJC4+RkhCNiwcKCwsKB4gIiAYEBtLaW1bPTUtIREFBg4QEhASEg4GBxERDRENEQ0HBQUHCQkJBwEAAQoMCgQHHS83PTE1PT8xGQYuQkg+JhoYFBASEg4OFhoeHBIMBAUFAQsRGyEhGRcRDx8lIRkHAgQIBAEGDhISBgABBgoMDAQEChAYIC4uLiQWBSc7PS8VBwICAgMECg4OBgEFBwcFAQYIDg4OCgQAAQEIFCgwLiQQCgQGCg4WGhYSDAABCxETCwULGyUnJR8ZDQcBBg4UHiYyNjgwJBgUJiggChc3TU1BGw8JBQEIGjg+OBwFHS8zMyELAgwKBoMEAAIFBQkRFxUNAAEgTG56dlhKUExCLBYGBQEIFhYSDAwMEhIIByNBRUE1IQsFCQcBCBAYFAwFExcXExUTFxsbFQ0LCwkJBQICCAQBCR0vOz01Jx0PCQMCChAYHiQkHhoaFCA2PkA0KCAeFAgDCRMVHSMtMzMrGw8LCwkHBQUFBwUDAgoMDAYHERsXEQESICgsKiYmHBYKBAYEAAEJDQkDAQABAw8TGRMJAhAWFg4ECQsNBQgWHCQkHA4HERMLBgoODhASGCAkIBIEDA4SCgIRHycrKyMdGQ8BBgYKEh4uLB4YEgwCCxUZFw0GFBYMCRERCwMGCggGBAIECAoGAxknJyMZCYUGBAwQCAIRLTEpDR6AAcIB1gG6AXJQYmRUOgwdMS8ZEBAGBgQKKk5UNgI/U1FJLw8ZJSUbAxgsLCAIFxcXFxUTISspJRMNFRkVCwABDA4OBgkrRVNRPx0BDhQMAgMBBhQiIBwYFBgyWGxmUCgkIBYMAwcHDRcfOVNVSS8PDw8PCQcJDQsJBwogJiAUCy89QSsHGio0NjAkGhgSCgYUGBYIDQABEBYOAAEbMzUxGwYmODYmCBstLyEFGjI2OCgKIz0/KQEMDBAQDiQ+SEAmChooKBoEGSs1NSshHRMLAQINEw0GJBoIAwcAAQMfKS0lBy5OUjgAARshIRUAAQoCAQEAAQooODAaDys3OScP8QYKFBYWCA8tPzMVIpIB5AH4AdIBekxmaFo4CitDQyUKDgQBAgY2anJWFDdNVUczDyE1NScNGC40JAgTGRcXFRUhLzEpFw8bHxsRAQ4SEggJLU9bWT8VDiAkGAELCwEMIiAYGBYWOmp+dlQqJB4UCgEDBQcPHz1XY1U1FxERDw8JCw8RCwcOJjIuFgk3U087BxowODgqHhoSCggKGiQcDgkIGhoUAh87Rz8jCDJERDAGITlBLwsgNEI+LgYvTVE3BQ4SDhIQJEpYTiwKIi4uIAQZLTc1Jx0XEQcAAhUhHQkeEAcTDwMJITMzJQFAZmxMBB8pKRsDBgABAQUAAQ44SEgqBSk9OS0TvQYKFhoYDgcnMS0RIIoB0AHgAb4BaEJWXFA0CCc7OSEMCgQAAQIINGBsUhonPUM9KQ8hMTUrEw4kJh4EFxcZFRMRISsvJxMRGR0bFQEIEA4ICytFVU83DxIkKBYAAQ0JBQoiHhoWFhw6YnRsTCYeGhQIBQEBCQ0bM1FZTzMTExURDwsPDxEPBQwmLioYCzNJTTUHGioyMCQYEgwKBAgaHhwOBwgWHBAAAR05QzkjBi5APioIHzc5LwscMjw6KgYpQ0cxAAEOEhIQECZCTkQqChwoKBwAARclLSkhFREJAwABAhUfHwkWCgkREQMFGykpIQJAYmRGBBspIxsFBgABBQEAARA0SkQuAAEhLzEnD+EEBhAWFA4GDxMPARxchAGMAXZGMDw8NiYMCxsXCwwQCAYECiA8PjISFR8jHxcLFyMnIRUDCAoEBxUVFRMTExkfIRkRCxUVFQ8HAAEGBAEJHzE1NSUJChQWEAEBAQYQHB4aGBwcLkhOSjQcGBQMBAEHBQcPFSU3OTUpExUREw8PDRENCwcGFh4aEAUfKysfAxIcIBwYDgoKBAYGDhIOBAUCCgwKBRUlLScZAAEcKCYaBBEhIxsDFiIoKB4MDyEhFQgSEhQSDhoqMiwWBhAWFgwBDxkZGxMLBwcBBgIFDw0BEAoAAQcDBAQLDxMLBiY8PCYAARUXGQ8DBAACAgIQIi4uIAQNFxkVB7cCAQIGBgoSKDI0LhoECQsDDBQMCgoOGiIoKCYeFBAMCgoMDhAKAgABAgYGAAEFBQkNExUdJyMhGxUVExMPDQkHAwUFCw0REQ8NDREPDwsNCw8NDQcDAgICAgQGDhQaICQoIiIaEAwMEhggHhgQAwsRFRcVFREREREVExMTEQ0LBwUDBQUJCQkDAQEBAwIEBAgKCggODAwKBgQEBAIBCQ0LDw0JCQUDBwUJDQsHBQEEBggMCggGCAYMEBQUFBYUFhgWFhIODggKCAQDCw8LBwMBAwEDBwsFBQEKEhocGhIQEhQSFAoGAAIEBggGAgEHDRMRDwkFBQEBBgoWHh4cEgIHDQsF4QEFDxUPBxh2pgGqAXwiXacBtQGNATELJy8lBSZefIABbkQoFgoIBhIgHhYIAxYkIhYAAgQBBxs5T1dLMRcTEQ8LAxIgIhwIDx8nJRkTIysrIREBBAYECQkEDBAIAQsNBwgaNEBGQCwAASc1LQ0oTlpWNgYZKTEvIxMBAgABBxMTExEPDQMICggCFzE9NycJAwABBggEAgIEChQeKCYiFAYSFBAGCSExLScVBBgiGgABI0FNRS0FGC40MB4FFyMdCRYkMDQqGiAmKiQYDAgKDBYQFSs1Kw0GDhIOBBEhKR8PEDxWXkYcGiYuKB4GDx8dDwQCAwMFCSE1Qz8vJSMdEwUSPFBcUDgAASk7MxWjAQkdISMZHJwB4gHgAaIBIo0B+QGTAtkBVR1HU0EVMoABrgG4AZwBZDogEAgGFiYmHAQFGi4uIAABBAYGCR1FZ29fPRURDwkDBiI4PC4QFSUvMSMXLTs5LRMCEBIKCwsIEhQOBRMZEwMePFRYTi4JQ1lJFTJoggF0TAofNz89LxMBDAgBExETEQ8LBA4WDgQfS1tVORUHAAEGBgQCAAEGECAwOjwuGggWIBoMDTFBQzUdDCoyJAIzZ3ltSQ0oRlJIJgctOTMVEjJCQjQcIjI0KBwKDg4WICAVO0M1EwwaHBYGFy83MRkUUnh8YB4gMDQ0IgYfLS8bBAEDBQcHLVFfYU9BOzUjDRpQeoQBelQGM01FH20NHSspIRKUAdgB3AGaASCRAf8BlwLlAWEjUVtHGzSMAcAByAGwAXZKLBwOCBIeIBIAAQ0WJioaAgABBgABCx9FZW1hPRMPCQMGDio+QDQODycvLyMXLzs9LREEDBIGDREEEhIMAxcZEwEcQFZWTDANQVNFEzxwgAF4TAohN0FBMxkBBggFDxMRDw0LBBIUEgQjS2FdRR0NAwABAgQCCA4aJjpEQjYaChYaGggRL0NHORsKJiweB0FzhwF/Uw8sTlpMKgktPzcZGDRCQjYaJjAyLBYQDhggLiwJKz0xDwwaHhYGFS85MRkWUnZ6XB4aLDQwJgQbLS0bAgABAwcDBTFZb3FhU1FBLxMYXoYBlgGMAWgaIT09GT8LHScnIwABXpABlAFsGGO3Ac8BqwFNJUFHNxEueqgBtAGkAXpYRCoYCgYMCgINEwISFA4DAwMJEyM5UVdLLw8JAQQKFig0NiYODxslIx0VIy8vIw8BBgIFExUFAggGAQsPBwgeNkRIPCYCIS0hBDpeaFY4CBsrOTkxHxEHAwkREQ0NDwcCCg4KAhs/TVE7IRkPBwECCBAaJjA+Qj4uGgQMEAoAAREpNTctGQABEBAAARlDaXtvSwskQEg+JAMfKycPFCw2OCwYHiYoIhYOGiQuODYQCRcXCQgQEhAEESMnIQ8SPFRWQBYUICQkHAoNGxsPAgIDAQMFKVFncWlfWU0zFRpUfI4BjAFwOgwNFQsXBREXHx8XAAEQFhgOESk3MSEZGxkNAhxIZHR6cGhWRCgMBxEXGx8dFxMNCQcLFRkfISknJSEZDQEEDBIWHB4aEAQFDQ0REQ8VFRMPCQkPERUbHRcNCwMAAQQKEBYcJCQkIBwWGiAmLDIwKB4QAw0ZISUrJyMhGxMRCwkJBQkFAwUBBQsbKSkpIx8bFQkAARAgKjI4ODQsIA4BAQUJDRMZHRsXDw8XISs1Q0tLPykJDhoeHhQKAgACCBQYHBoYEhISEA4KFCg2PkJAOiwiDgIDAQABAQEFBwcDAggWGBoSBgYIDA4SDgoEAgIEBAgEAgIfP1dhZWFZRzMTEj5abnZyZFQ+KA4oBggGAAEPIzU3KxkIMEZELAITBQIICgQTEwImYnZ4ZEQUEyErKyUlO0E9KQsPFR0hIwkQHh4MDRcVDwESGhgWDgIHBw0NDQ8REREPBwEEAAEHFRsVEwkHAAIEBhAYDAQAAQQMMlxwblQiDy81KwkBAQURHS81My8fBxQiJBgBFx8fGw0JERcdIyENAwQEBA4YJiw0KiAOCgACBQcJDw8dJSUdEw8fKzM9QT83LRkDChAWFBQWGh4gGBQkLCwkEBUxNysNGjpKUE5ITEo+KgYNFRkTCwABBAYGCA4WHhwYCAcVEw0EDgYCAQIKKjg6LggpVXF5a1U9KxUDECY8UFxufHpsTB5kDB4gFAABJ0VRSSkCUH6EAWASDwABEBAME1FrURNIfoQBdFAWFSktLyknS11VOw8NFRsfGwY6UEYkDScxJxMOGBwUDgQFCQ8PDwsTFRUPBwoSFgwLFRELBwEBCQkDBBAAARUXDwRAgAGaAZIBZBY/ZWtLFQgODAABFS07PzcjAS5GSDAEHzE1LRMHDxUbHxkBFhwYBAYQFiIqIgoBCQUDAAEDBQcTITExKRMPFyMvNz85MSMVBQwWGBoUGCguLCAYMkJALhI1Y3FdISBCWFxSRk5SSC4KEyMjHwsAAQYKCgoOJDAwIgwRIykbAQgBBwcFEkJgZEoUNWeFAYUBbUElCQQMDBYmNkpgeoYBeFgikgEOJCoeBCFHVU0xAlaMAZQBbhwPBhISCBtrhwF3MzJsem5KFBUlLSshJUtfWT8NCRUXFxkUSF5ULA0pNzEbCBgUFA4EBQsLDwsLERUTDQcOHB4UAxELBwMBAwkRCwABCgsbIRkDPoYBngGQAV4ISXV5VxUKFBIICyc5PTUlBDZSUDgEHzU5LxcJDREXGRUKHCYcCAIEDhYgGAENEQsAAgIDBwsjLzUrFQkTHSMvNzElHREAAQoUGhgSGiwyMCAYNERCNAw9dYUBaykeRFZaSjxKTEQuCBEhKR8LBAgKDAoQJjI0KAwTIy0hBQgDCQsFEkpqblIWL2N5fV0vCwgSFAoMFCI0TGx2cFIgsAEOHiIaCBk3RT0pA0ZweFoWCwYMDgYZW3lrNR5KWFA0DhMdHx8ZGztHSS8NCQsREQ8QQE5GJAkjKysXBg4QDggCAwcLCQkHCw8PDQEKGBoSAgkJAwEAAQELDQsDBgsbIRcDMGZ6bkQAAT1fY0kRChASCAkdKy8rGQIsQkQsBBsrMScTBQsNERURChogFgQDAgYKFgwDDw8JAQABAgABAwkbJykjEQcNERkhJyMdFQkBChIUFAwUJComHBIsODgoDDNhb1sjGDhGRjouNjw0JAgNGRsXCQYKCAwKDiAsKiAODR0fGQMGAQcHARJAWFxCFiFFWVU/GQABEBgSCAYIEiAySlZSPBa+AQYMDgwAAQkZGx0TARosLiIGBQICAgENKTMvHQYaHhoSAgcNEQ0JCxchGxUHAQcHBwcGGiAcDgMPExELAAEGBAQEAAEBAwMFAwMHBQcDAQQKCgoAAQUBAQACAQMFBQMAAQULDQsDEigwKhgAARsrKR8JAgQGAgUNExcRDwESHBgQAAENFRcTCwcFCQsNCwABCAgGAwMBAAEEBgYDBQcFAAECAAMDDRETEQcDBQcLERMRDwkHAAEGCAwKCAoOFBIMChYeHBQIEyktJw0QHCQiHBYcGhgSCgMFBwUAAQgGCggKCBQWFhIIAAEHBwUCCAQEBAYOHiwoJA4FExsbDwEGDhAKBgQEBgoSHCIcGAh+DiYyMCYQBxkbFw0LDw0PCxktLy0XCCw6OB4NGyEdFQkHBQUBAgwYHhoSAgcHBwcFDx8jIQ8DAQMFAwMCCAgEAgocJCYaBAsVFQ8DCAoKDAgKDAoGBAMZJSkjEwoeJiAKDRkdGQ0DCw8RCwUDAgABAgQGCAwIBAMHCwkNCQMCBgQBCRMVEQkEEhoaEAABEyMnIRUPEREPBQABChASDggIBgYEAAICBAQEAg4SEg4AAR0tNSkPChYYGBIGBAAEDBgYGA4CAAEDAQIGCAgIBgQKCgoIBgEHBwcBBgUHCQEECAgGCAoQHiQkHhQMBggEDixIUkwwASc7OR8AAQ4QCgIRKTc1Kw8SJmJ+fmQuCy07NRkdHyMhHTlndWE3Fm6UAYYBShlFST8vEwUJBQABECRATkgsBgsPExEJJUtVRykDBQcJCwMGEhISBhxGXlpCDhUvMSMAARQYIh4cGhwaFgoLP11pXTsSSl5KGiM9RTsdBRchIx0JAgYMDhAYJCQcEAMPFRkZFQMOEhAGER8nHQsaPE5MNgYvUWFXOy0xMSUVBhwqLiQWGBoWEAICDBASEBYsOjgmBEl9iQFrKxYyPDokBAMHCwsJFjQ+NhoBExcVCQEECAoIBgoQDgwAARMbIx0LARMhHRUECgYICAoeOkxQPiISCAABBA5gngG6AaQBZA1nlwGRAVsFFiAUBy9rhwGHAWcnMzSAAaYBqgGKAUgAAS1FOyUlLTMvJ02DAZMBf0UahgG0AZwBUClZX1c7FwMFAAEMGjZaZFo8Bg0TFxMRK1trWzEDBwsNCwcIFhoUCiJadnZSGhcxNyUGGCgsMComKCQaEBFRe4sBgQFXCE5mVBgnTVVFJQMbKy0hBwQMEhgcKDIyKhQBEx8hIx8FDBgUBBElJyEBJlZqZEYMOWl/eVVDS0U3HwYoOjw2IiQkIhYEAg4YHhwiPExKNARXnQGrAY0BNRo+UEYqCAcRExcPGDpMQiIHGSEfFwMCBgYIAgoQEAYHGS0vJRUBGyknGQQKDAoMDCZMYmRULhoKBgQUcsQB4gHMAXoHebEBsQFvDRIYChVLjQG1AbEBhQE1TzaCAawBsgGUAVwYFy01KSs3NzkxT4EBjwF5RRZwkAF+MjdjZVc9FwEGDBoqQF5mWDoIDRUTFQ8rU2FVLQEHCw0NCQYSFhQGJFhwcFQcBx8lFwoiLjQ0Mi4uKBwOD09/kwGPAW0TLEY8EilFTT8fAxclJx8JCBIaICYsODgsGAMVHycnJw8GDA4EERsfDwY0XGxiRBA3a4EBgQFjUVtTQSMGKDxCPCwoKiQYBgIQHCAiJkJQSDAET4sBnQGBATMaPEZEKgYLFRkbFxAyPDocBRcjHxUJAQABBAICBggGAAELHy8xKxMBFyElEQgQDhAQEixOZGhaOiQYDAwWbrgB0gHAAXgGW48BkwFlFQEBES9dmQG3AbEBhwE1QyhsjgGYAYwBaDwYBRklMTc7OTdHX2VVLwQsNCAJR19ZTTERCBQiLjZEUFJEKAYJDRMPDR01OTMdAAEBCQsLCwABBgwKBBg8Tk5CJBACAAEEFCYuNDg2MCwkGgwLP2V/hwF3RRcBBAkdKy0jDwABCRUXEQMIFCAoKjIyMCYWAxMdJysnGxEFAwMHCQEIHDZMVEwyCidRaW9lW1tVQSMAASA0PDYsLCgiFAACEBogJCg4OjYkBC1NXUkfEiIuKBoGCRMbHRsDDh4aEAEPFRcTCwcDAwEBAQMFDRMhJSkfDQABBw8LARAUFhYYGChEVFxQQjQoIBgWTH6QAYYBYiQXOUU3HSEtP1FriQGVAYsBZSdSDCA6TGBqWD4eASMrKy0vMzMrIx0RESc5R09XVVE/Kw0OJDQ8QkI6MiYUAgEBAQcJCQUFAQABBAEDBwkJBQMBAQIGChIaICw2QDw0JCgwOjo2Kg4CBwUFESk/XXWDAX9rUSkPCwMBBAgOEhAKAAEMGiIqLioqIhgMAxcpLy0tLSklHQ0GFCQuNDY2NCgcChU1SVlbUUExJRMFAgoSHioqLCIUAgcCCBAaJCQiGhAGBAYMBgYCCAgGCgINGyEhHxUNCQABBgETGRsVCQcDAQEBBw8TFR0ZGxELAwYKChASGCAkJiYiICoyOEBESEI4KhgcICoyOkJGOiYGI1NxfYEBc11HMR8J9gMlVVs3EFg2EAkdKRMFAw8nKRMFAAEDDyk7R1FTaW1fQxcMGiYyPEI2KBoQBA4WFhACAQwSEhAEAgEBBwUDBAYIBAsdHxMINGaCAYYBbDIyREpKQAg9YVs5CCw4JA1jqwHNAcMBlQFHEwcBBAYQNEpINAoMHCYqKigcFg4ICzVPVU8xNT87KxcMMkZORjY4NCweCh07VV9bMQwuMhwVPU0/Gxo+RkIwDBkpKxsBHhwcFAwEIjxEOhgGBAYIBgEjNz01IQUMGBYOF0dbVTMJEBoaEgIDBQkRFRcHAQYICAQIDBAcMEBGPjIaDgwULEpYXlY+HAkfHw0cTnJ4YCgxgwG1AcMBqwF3IRw8PhqcBkObAbMBiQEjPBITKTMrCxASAh8hBwgMBA0hMT9FTWt7cU8dBgwYJjY+LiAWDAYYJCYcBgYWHiAUBgQCAQABBwQIDgwIEzE7Kwc4gAGmAaYBggE4OEpSTEAJY40BiQFRDFJuWh5LsQHlAekBtwFZEQsDAAECFEhmZkgQDBoiJigeFgwGAgtDYWtVMzlDQzkZFD5WXEoyNjQuHAobP1VdVxNAaGY4HWOBAXNDDkJUUjoOI0FJOxUWGBQQCgQwVGBSJgIGCAgIAy1DSz0bBCIsKBYjZYEBd0kDHC4wHgIAAgEHFQ8AAQwSDggEBAYSHjpOVlA4GAABCQQgRmJoXkgeHT9JLwZQfIoBdDIzmQHPAdsBvQFvAlZ8cDCgB0enAccBowFBIAkpPT8tCQ4UBBsfBQwOBAsbJzE3P19xa08dAAEEDBgoNCQYDgYIGCoqIAoKGiIiFggEBAQBAAEGDA4OCBMzPS8LOHyiAaQBejo0REpINgtdhwGFAU8OVnBmLjWbAc8B2QGxAVUZDQcFAwxIZGRGEAoUHB4eFgwGBAINO1tjUykvP0ExGRQ8UlRGKi4sJBwKFzNHTUUDUHZwOBtriQGBAVEEOExKNAwlR1FFHw4QDAwEBjJWYlAiBggICggDJz9DNxcSJjAuGCVlf3dJAyAyMCIEAQYAAQUNCwYSFBIIAgYIECA8UlZSOBoAAQcAARpCWmBaQiAdRU07Bz5sfGIsMY0BwQHJAa0BYxRmhAF0MpAHNX+ZAYUBRwEbNz89MxUDAAEFHSMJAQYAAQsTGx8lLUNPTT0XAwMCDBgeFg4GBgQWHCIaDAgWHhgUCAgGAgQCBhAOCggJIycdASxkenheLiw4ODQsAAE1V1MzCjpORCAhY48BmQF/SRcVEQ0LAAEsQkIwCAYMEhQQDggEAAIFKT1BNRsfKSklEQoqOjwuHB4gHBQICx8pLysEPlZMKBtPaWVBCR4wLCAEHzlBOR0CCAQCBAIkPkQ8HAYICggMAhkpKR8LECIoIhYXQ11VMwEYJCIWAgEAAQIBCwUIDhIOCAgGDhQeNkRMQjgeDgoMHjpGSkI2Gg8tOSsLIEBIPBItZ4sBkQF5RwpEXlAi+AUNJzU3MSczNzk5MykhHR8jHxkTDwkLCw8RExETFxURDQkHBQMCBAQAAQICAgQICAYGCA4OEAwIBggIBgYIAgQGBAQGCAwUHiYuLCogIiAgIBwWCgQBAQIGCgYDDx8rLzEjHR0dGRcPBQQIBgABAgABBAQEBAIAAQQCAAEFCQkHAwMFCQUHAggQEA4KCAQGBAgEBAEBAQABDA4IAREdJSUdDwcHBQcLExUXEw8FBQEBAQIGDBIOCggKCAwICAQAAQMCBgoQEg4KAgsPEQsDBAIEAgUDAwMDAwEECAoKCAoQEBoeJCwsMCosKCgqKigmIh4aFg4BBQsLAwACBw8hKzExLyEVBwABAgScBQgOCA8xXXF1b1c3EwABEBIMAgMHCQ0NGyMhGQkgPkIyChchIyMdEw0HBQEDGSktIQsSKjIuHgoMDBAQDhs3PzMRHkZWUDIGER0XBRYQCAgMGDBESEAkAAEDCwkJCQcLDxUZJS0zMSsbCwEEAgIHBw0JBwUHAwUAAQYQFhoWFBoiJBoIAAEKDA4KBzNHRy0DHiw0MBoKBQ8REw0JBQkTITM9OzEdAAESIh4SAAEIDAwKAAEPGx8XCQIEBggKCgwMEg4SCAQBBAQOEhQMAREVERENBQsPDwcEGCw2MCAGCxEJAAEcHh4eICgwPDw4LBIPIR8RDh4oIhYAAQsFBwkTEwgYGg4TS291WyWGBRIgFgtDhwGnAa8BnwF5NwIwREo+KhoQBAUTKTMxIwtCdnxeFiE1PzszJRkTCQkDLU1TQRseQlJKLAwQFBYaEi9ldV0jMHCGAXhCDTtHPyMOCgUHBh5EYnBeNgQHBwkFAAEFAwsPFyc9RUU3IwkGDg4AAQsRExcRDw8NBwMIGiYsKiIyQDwwEgYOFhIMD12BAX1VDSxIWE4yDgMPFRUPAQMJFy9RYWFLKQY0QkQsDhQcHBICHzE7LxMAAQECAgYODhIUFhgMAQMFBBIcGhIJHyUhHRMJDxMRBwoyUlxUNAIbKScNFB4YGhwkMkBEOiYAATVNTS0GKjo4JgQHAgYAAQsLIkBEKglrrQG7AZUBPZ4FDBgMG1mjAcUBzQG3AYcBOxRIaG5gSDomEAIVLTc1JwdKgAGMAWQaKT1JST8vJRkRBwczV19NHx5KWlAyDhAYHh4WMW1/aSk2eJABeDoZR1tRKwwGBQMGJFBwdmo6AgMFBQECAgEFCxUnP0tJQScNBg4KAAEJFx0bGRcVEw0FCCAwNjYsPEpMNhQGEBQWDBVjjwGNAWENLlZiWj4UAg0TFQ0HBQ8hO19xb1svDjpUUDYUICIgFgIfO0E3GQUDAQIGDBIWGBwcEAIFAQISGhgIDS8tLyEZCQ0VDQABGD5kbl44Ah0xKRUYHBgWGiIyOj42HAtBW1k5Ai48PCoICQQIBgkJLExMNgdttwHLAaMBQdgFAgEdP3evAc0BzQG3AYMBNxhQcn50ZE46JgYTJy8rIQk6ZmhOEiU9R0tBNysfFQsHKUdPQxsYPEpEKgwUGh4gHh9PX1MdLFxqViQhRVFJJQwOBggWLk5obFgyBAMDAAIGCAIBBxEjOUVJPS0VBQIEAg0XHSEdGxsVDwUIIC44ODI+SkIyFAIOEAwKEU9xc08LKkhYVjggCAMLEREPER0rQ19va1UtCDRISDIYHiQgFgIbMTkxFwcFAwIGDBIaGiIgFAoGAAEIChAIBR0zNzEpGQkJBQABDCBGXmZWNAQZHx0JGB4YGBwiKjIwJBIPO09JLwgmMDAgAgcAAQQAAQkHIDo8KANXkQGfAYMBN7oGETdXe5cBswG7AbMBmwFxMRJIaHp6cmZQNBAPGRkZEwkQIiQUAx0xOz09NS0lGw0DEx8nHw8IHCIiFgwSGiAiIggNFxMFEhwcEAcjLzEjDxIeICQuOERIRjYcAgABAgABBggKCAIFCx0rMzk3MSMbDwcDCxMdHSEdGxcRBQgYKC40MDo2MCQKAAIEBgABBx8vLSEFGi44OjAiFgwBCREdJzE9RVNVTz8lAxYiKCIWGhwWDgINGRsbEQUFAQABCAwUGiAiIh4YEg4KBAUTHy03OTEpGQcEDBQiMDxGRDgiBgIBAg4cHiAeICAiHhgMAg0dIyELChgYEAoDDQkHCQ0NBA4SCgUlO0M1FdIGIVmBAZsBqwGnAZMBfWVJJww+XnJ6gAGCAXZSJBkzQT0rEREVFxURFx0nLTMvIRUNBQABAwcHBwUFBQMBBAoOEBQeIjQ8OjAYEztNTTshCQYSGBwwQk5OQiwaDAQAAQEJCwkFCBYgIhQAARcjLS81MTk3LxsHBQkNFRkXCQABBgQEChAaIDA0NjAeDA8fJyMVAhQgIBgGAgoQFiIgGg4EBRUdKzU/RzsvJR0TDwkCBg4QCAEDAwACAgABAQUPGRkTAxAkLDQwKBwUDgwKBAkPHy09RUc9JwsUKDg8OC4iFg4IDBgiKCggIiImICQiJiAYCAcIEBoYFg0lMS0ZAwgKCAIRHSMjGQcKGiAaCvwFHVV9lQGdAZMBaUkvIx8QPFxqcIQBmgGSAW4uI2uHAYMBXRsXIyUfFQsRFx0nKQ0EDgoAAQEHCQkFCRUTDQMGAgQIECJIYGhSKCt1mQGNAWEZFDQ+OCA+XmZcQhgLHSETBxUhHxMEJkBGOA4VIy81LTNHT0UvCwYGAgUVCRYkJBgCCQcBEiY8Qj4uEB9JW1MvBDROTjgKCw8LAAEUGhAGAwsPFR0nMz0pEwUBCRMRDwUGCg8fIxkFChIQDgABHTU/MxUUOExQQCgSAgMDAgwQCAUZO1lnWz8LID5MTkAgBAkPBw4kODo0IiIkJCQeLEBEOhoEIDY6MhgtYXFjLQ4qNjAYDS8/Py0JJEJOPhygBRtLa3+HAXdFIQ8LEw4yTFpedJIBkAFuLit5owGdAW8hGyUpIxUJBwkTHR8BEhoUBgcHBwcFDRcZFQcGAwUBCiBMcHReKjOJAa0BpwFvFShOVEYiQF5oWjoKHTExHQkdKSUZATBKUkQWDyEpLSspSVNNNQkKDg4CDQEkNDQeAQ8VDwIgOEQ+LhAjV29nQQZCXmBCCBMdGw8IEgYAAQcNDxETGyc3GwEOCgMTGRUNAQQdLzElBxAWGhIGH0NNQR8WQFhaSiYIBw0NAAEUGhgODTNbbWVHCyRGVlI+GAsfHQ8MLjxCOCAeJCIiHC5IUkYmCipCSDwYM3OHAXExFjpIRCYHMUdFMwcyVFxOILoEFTVRX2NVKQkEAAENBCQ0QDxUcG5WJCdvjwGNAWMfGSUlIQ8HAwMLExcEGBoUAgMFBwUDCRUXEwUEAwcDBhZGYmRSJClzmQGRAWELLEpUPh4yUFRIKgEfLysdBxkjIxcAASZGSjoWCRkdIR8fOUVBLQkKEA4ICQQkMjAcAxEZEQUWLDQyJg4hT2VdOQI4VFI2BhUhIRUDBgABBwsNEQ0PFR8rFQYODgMVFxcRBQABHzExIwUKGBoUBBs7RzsbFD5QUEAgBgcRCQABEh4gEgMjS1tXOwkmQEpINBAPHyEPDig2PDAcGh4eHBooREpCJgosQEI2GC1dcV8lGjpGQCgAASU5OScBLE5WRh7KAwsdKTE1KxEBBAINAQoSFhYkNjQkDBlDV1VBFRMZGRUNBQMFBQsNBAwQDAIBAwMBAQMJDQcAAQIAAQEAAQQSKDo6MhgTPVFPMQMgMDIoEh4qLiQUAAETGRsPAw0TFQsCGCYsJBAHBw8PCxEdJSMZAwQKCgQDAhQeGg4BDQ0NAwgUGhwQCBMtOzslARwqKBoDFRkbFwsDCQ0NEQ8PDRMZHw8DBAQJERcTDwcFFR8hFwMIDBAMBAshJyENEig0NCoWBgMDBQQQGBYSBhElLzEdARoqMCwiCgcRDQcKGiYkIBYSFhYWEhwsMiwcECAqLCYSDyk1KwsYKDIsHgYLFxkPBCIwNCoQvAMNISklFwQYJCIUAxklJykfHyMjGxcLBQABAQEHAAEEAgIFCw0NDQsJDxEPCwEKEBQQDgQBAwEAAQoOFBIQBAQAAQIAAQYKCAwMDgwKCAgEAgIDAwUDBQEAAwIAAQIEBAYGBAIEAggMEhAKBgIDAwABAQUJDQkDAwcJBwUAAQQEBgQBAQEDAAEDAQMFBwkLDw0NDw8HBQMHCxETERMRERMVFRUTExMREw8TFRsVEQsLCwsFAQYICAoKChgaGBIIAAEDAwABCAwOEBAOBgQEBAYOFBgcFAwGBAICBggICgYICA4OEA4KBAUDAQYSGBoaFAwCAQECDhoeJCAYEA4KDBASEhQSFBIMCAQCAAHKBTWBAZ8BiQE/LIQBrgGqAXIQQ3mLAYUBZWWBAX1pOQEmODwuEiY6PCoGGSktKR8ZO0tFLQUqSFZOLAoLFxULEDI+PCgCGyUnGwUSJjQ0Lh4MAgcHAREdIx8bDwcFAgIAAQoODgwIAAEDAwMAARo2QEQuEgYEAQMBGzE1Kw8JGycjEwIYJCAUAAEJBwUDAQABBAQAAQkTHSclIRMKIiYcARcZGxcVExcfHRsXGR8dGQ8bN0NBLRUZGxMPARQkLCoaNmJyZjoJNU1JMwkYKjAqFAABDxMLCCxYbmxSHgshJSENBAwOEgoKEhoaFggjQUk1DxgsODAkAi1HRysEOFxkWCwIBwkFBhgmLjIyJg4JERMJ9AZJsQHVAbcBVUrIAf4B9AGoARxfpwHLAcEBmQGZAbsBuwGVAVMGPlpcSCJAXFo8DCE3QT0rKVdrZ0MFPm5+ckoQER0fDRJCVFIwAyk9PSsNGjhETEAqEgABBwkJHSs1NSsbEQcFAAEEDBIUEgoAAQUHBQQgSmBaQhwOBAEAAQMlQUc5EwspMS8bAiIuMBwDCwkFBQMEBgYAAQkbKzEzKRcSMjwuBBkfHRsXFR0fJR8VHyElGQ0dSV1bQSEjJSETAR46RD4uTI4BpgGOAUwTV3N3URUaOD42HAMZGw8QRIABoAGcAXImEy87MxsCChAQDgoYICAYBDlhaVUfHjhCQCoDR2tpSQJIdoYBcDYBDxENAhoyPkhCOBIHGRkLmgdDpQHDAacBQVrSAYgC+gGqAR5jswHbAdsBtQG1AdcB0wGpAVkEQmBoUChGYFxACic9RUMzK111a0kHRniKAX5SHgUZGQsWPFJIKgcxPz0vDRw4TFJKMBoIBQkLITU/OzcnGw8JAwIMFBQSCgIDAwECJk5eXkggEgoGAAEBIT1DMxEJJTErHQYeLCoaBwsLCwUBAgQEAwsdLzc1LR0OMDgqAh0dIR0VExslISEXGyMfGQUbSV9hRyUrKScXAyRATkgyVJoBqgGQAUgXXX97XxsYMjw0HgMREwMeUJABrAGiAXgoDy89NRsFCAwOCgoWHh4UAjdhbVUfGDZAPCYFR2lrRQNKdIABbDABDRULBB42SExOQCIEDxEJygYpX3FVD16uAc4BvAGAARhTnwHHAdEBvQG7Ac8BwwGdAVUCMk5SQig8SEQoAiU1PzsvL01dVzkFPGR2blAqDgIFAAEOKjQqFBEnMzEjCRY2RkpEOCISBgUNITU9QTkxIxsRBwABDAwSEAoGBAACCiA8UEw6JBoSCggCFSUnIQUDFR8fEQABEhwWCgcRDwkJAQICAwcTHyszMy0fAAEWHhIHHR8bGRcRHR0hGxUXGxcNAAEPNU1NPyUpLSEZASA2QkQySniEAWg0F0tlZ0shCCAqJhQEAgYWMFiEAZQBiAFgKAUfKScZBQEGBAgGEhQUDgABJUVJPxcQJC4oHAEtR0cxBDhSXEoiAAEJBwUKIjZGTkxILhoMAAL+BAYSIDJCVGBcSjAKNWuRAakBrQGrAacBkwFvPwUQHCQmHhwcEAYPHSktLS0lJyUdEQEaND5EQjYyJBwQCAIDCQ8TGRcRCQABEiQwODo2LCASBAsfLzk9OzEpIRULAgABBAYGDAoKDg4MFCAmKigkHhgUCgIEBAQIBgYEAgEBBQMHCQsPExERCQMAAQMHDxcfIyUlJSEbGRcZGxsbFxcRExUXFxUPEQkFAAEIBBEdJSUjIRsbDQEOICYoKi4wKhwGEyElKSUZDwkBBAoUHjA+SFhgXlI+HgwDCxENCwcDAAIGBAYCAgABBQsPCQUCBggICAQBBQMCDBIYFBIMBgQGDBQiMjxGSEhEPDIgDpoCLHKYAZwBhAFQOiocEgYbQ2eBAZcBmQGFAW1PLQsBCAwSFAgFDxUXKUFJSTkXGDY8LAoFCQIULj42MiQWAAEdLzMrGQYaIiAUCgwYHiouKh4SBA0pQU1LPR0KICYaBx0pKRkCGiYmJhgMCgoOGBwIBQ8LAAEcNDoyGhYkLCYSByczNSsXGSMjGQsGDAwGDRkVDQ8TIUFbYVU1HxcXExMRBwUDBwsAAQwUGBYQCAEJExUEFBwUBAEDBAwaATVLSzUJKEBAIg87S0szDRYuOkpUWGhoWkIiBR8rJxcEFBwaFAABDxsZEwMYKC4iDhEjKSMRChooKCIQAxEVEQMEDAwUGCIwPkRIRlhiXEYaIEKeAdIB0gGaAUosHBAICAsnQ2F9gQFpUzkfDwcDAgYKBxcfIx8zVWdfRw1GdHhYEB0pJQscODYsJBYDK0dFORccPEo+HAIAAQIMGiYgFgwAAQ0zT1dVQQc4VlY2BzVLTTMBJDg+NB4IAQMCDhYLJScfAyxOWEgkGjpCOCANM01NOxkhLS8lCwoaHhICGQcBAAEJH1mFAZEBf0cfGxcREw8BCgwIBwweKioiFhYQBgUHJj5EMgwPFxcLCiN3oQGTAWEDXIgBiAFWB1d7fWEdGCo+SFBecnZsTiQVN0k/HRAuOjggAR8vMSEFJkhMPhYdPUs/IwgqOD4wFA8jJyEJCgwSFBoiMj5CQkJedHJYIjFAqAHWAdIBmAE6JA4KBAoBEyc9X19JNycZDw0LCQMAAREjKyshNV1pZ0kJVIgBjAFgEiU3NRsSLi4mIBIDL0NJNxEkSlJGIAABBwkAAQwcFA4EBQ8vS1dRPQRIamQ6BTlTUTkBJjpCOCIGBQsDDA4TKTMlAzBQWk4gGjhCOhwLM0lNPRUfLS8jEQweIBwAARMFBAQHH2GPAaEBiwFRIRsXFRMNBBAUDgABFCo0NCQeIBwSBgY0UlY8DBUjIxcBNZUBvQGzAXUBaKABngFmB12JAZEBayUYKDhCSlRyeGxQIhM/TT8bGDxEPiYBIzM3JwcsSlREFh9HUUknBiw+QDQSDyUvIQUOEBQYHCAwODw8OFpsbFIgGDaEAaoBpAFyLBYMBAYICAEPITU7KR0VEQ8REw8PCx0pLykjM0tbVT0JRGxuTgwdMS0bCiIeIBYOAAEhMzUnCSI+RjgaAQkLBwQMCgIDBw8nO0U/LwQ8WFIuBSs/QysAASIyODAeCAMFAwgMDyElHQMkQEg2GhIqLigWCSc1OSsRFSMjHQsIFhoQARMHAQULH1N9iwF9TSMdFxUVDwQOFBAGGCo0MigiIiIcEBA2Tkg2ChMfJx0NNYcBqQGhAWcLVH58TgdPd3ddHRIkLjg8SF5oWkYkDSk1Lw0cNkA2IAABHS0rIQciPEIwEhs5RT0jBiA0NCgSCxkdEwESFhgaHCQqLjAuLEJWUD4ahgIYOEhIMhgMCAYIDgwKBAMLDQcHBwkRExUZGRkjKSkpISEvMy8lDxImKBoEDRMRBwQQEgwMDAYHDw8JAAEUHiAYDAIDBwcHAQcJCQ0NFx0jIRkBFh4eFAERFRULBhYcHh4SDgYGBggMAAEHCQkAARAYGhQGBgwODAQDDRMVDwUJDw0NAwQEBAABCxcTExEZIzlLVU81JR8bFxMRBQYKCgwWIigoJCIkJCAYGiYqKhoGCRcZGxUpS11XQxkQKCQUES87OysJEBgeJCwwPj4+MCAOAQcDChoiJB4SAAEJERURBQgWFhIAAQ8bIRsPAAEOFBoWDAQBAAEEEhgaHBwgHiIiIBwaIiYkGgqAAxAoNDAiCAcRDwcGCAIECBIUDAYBCQ0RExchLSsrJyEfEQEIBgMXLTUxHQkEBggEBAEDAwABAgoQFhQUDAELDQkAAQ4gJBwMEScxMSMRBhYcFgYTHyEfDwEICAwMDg4QEAwSDhIODAgIFiAeGAQNExsRCQsfIR8RAhYeIBgEAgQGBAQAAgEHCRUjKzEtJxkTERUhMTs7NSUNCBQeHBIBDQ0BFCgyODg2JBAAAQkJAQIEAwsZIRsZHR8lKyslIRMEGiIgFgwUGBgWChMdGwUYNEZKQCYGERkZDQokMjIiBAUNDQsJBwUFAwMFERUTCwgcLjg0LCYiJCQeGgwCAQABBAMHCwcFngImXHBqQggjNTUfAgEJBwYeJBYMAAEJDQkNGSc9OzMrIxsDJDIuEhtRa2NFDQYMCgYAAQ0TEw8BDh4kJhwSDR0nHQkkTFpOJhdHX11FFSJGVEYeGT1NPycAAQgQFBQOEBAQEg4QEhAKAgQkNjwsCBszMysPGTlLQSUGMEpMNgwGDAwMBgQKCgYBDS9BQz0lCQ4SChVBY21fPQkkQEQ6FiFDTTcBNE5cYFY6BBspHwkQFhQBHSUZExEZJztBOysNMFxsXCgQFBYSBhlpiQF7Rw5agAGOAXY6E0VZTykUWHx8WBYHDxMVCQkFAwMDCyk3NyMCNlhkZEgwMiokIhQLGyUfExslIx8JygEsbowBfFIIKUM/KQEDCQMMJjQiEgQFDQsRHTFJRzktJR0GMEI+IB9de3lRDwYKCgYHER8bEwUQIiwsKBINKS0jCypecF4wFVFvb1EXMFxqWCoXR1dRKwEMEhYWEBAUEBAQEA4OBAEBJDxAMAofOUExDRtJV08vCj5aXkAOBg4QDgYIDBAOCA0tQ0tBJwIaJBQRSXF/c0kHLlBaRhovX2lNCThecnhsSgofLykFEhwUAiEtGxEPFys/R0ErCUZ8jAF2NAoQEAgHM5UBvQGtAWkKap4BqAGIAUAfW3VlOxpwngGcAW4gBQ8VEQ8FBwMDAw8xR0UtAAFAan52VDw0MioeDhMvNTMhKTUzJQ1+KmZ6dkgMITU3IwQCAAEIGDY8LhwOAQ0TGSk9UU1BNycdBCo+NhwXU21pSw8ECAQBCRkbHxUDDiQqLiQYCx8lIQkoWGRUKhFFYWNNFSxWYlQmETdLRSkAAQoUFhQSEBASEA4MDAQAAQsHGjAyJggbMTMnDRM9T0crCDpSUjoKBgwMDgYEDhAQBgclN0E3JQEWHA4TR217b0UHMEpSQhYpVWFFCTxieIABeFYcCx8bBQ4SDgkrMyMXFRslOUE5JQABRHqEAWwwBggCBxU/kQG7AakBZwhkkgGaAXw4HVdrYTUYbJIBkAFqIAEJDw8LBwUDBQMNLz8/KQABPmh4clg+PDAqHg4ZLTs1JzM3NSUROhpAUEg2DgkVFwsGEBggMEBENiwYBA0dLTlHV1VJOy0dAxIeGg4RL0E9KQkAAQMHCRETGxUNAQ4aJCIgFgQLEQsDGjI8MBgJKTc9LxEYNjwyHAMbJSMXAgoMEhQQEBAQDAwKAgMJEQ8CEBYQBA0XGxMBBx8pKRcGIC4uIAQAAQYIBAIGCAoKBgEVIycnHQ0DAQkhO1NXTTMJGC4yKhQTKy0hBjhYcHh2YDwgDAIBAgMPHTM7MScjHyEpJx8RAjBMVEQcAAEHDxkjOWt7bT8KRl5eTCIPMT05HQ5EXl5EGggBAwcHBQUHAwUJHSclFwQuTl5cTEI4NCggCg8jLTErLTErIQkSAAEECg4QFA4IBggMJjxKUExIRDgoDBEjM0NPVWNlV0MhCwIECAABBQkJCQUBBQkNExMVEQ8JAAEOEhgaGhYWGBQOCAEBBwUDAQEDAwULAwMEBgoKCAgEBAABDBIUFBAOCgoKCgABDRUbGRcNCwcDAAECBggICgoQDgoGAwkLDQsDBwkHBwEAAQgICAYCAAEDBw8VIycvMy0tIx8ZEw0NDQcBChYeIiYiKkBQXGRqal5OMAgXKztBQT01MysjGxUPBwIIDAoOCAgFFyMpLSsdDwMKGBAMBgQEBgYGBAQEAwMBBg4UFBAKAQkNDw0LAQgMDg4IFCAsNjw+OjAmGAwFGR8nJx8VDQUDEgsbIRcBDAcVEw0KQGp8dFpGRj4sEgkRGyU1T4cBpQGfAXk5Cio4NBoBCQsJBwACAwUJDxshHRUFDB4kJiAYJi4wJA4RLzUtGQQQFhQKAxcbGw8AAQoODgoGBh4sMigUBgIDAgIJIzUzKRUDCA4KBAYOEhIKGi40MhoJKz8/Kw8RGx8XCwYSGhYQBAwODAABEStDSUk5GQABDhQIFT1NTS8BOGBuYDYYHCY0SmqOAZYBhAFUEDNba2tPLSEVERMXHRkVCQYAAQcLBwMFFSMrLRsiSFZGGg0nLyUNECQoJhYPR2VdOQQkMjAiDBMlKykXBDROUj4SBQUCEiw2LiYcFgYPIS0rJQIiLioSFBExMysPCBclKxcITHyOAYQBVj5CPCwSBQEFDyVBjwG/AcEBkwFBFkRYTiwBCw0NBwEEBgABBQsZJycdCxAgKCwiGCo6OiwUG0FNQyECGiAiFgMbKSkdBw4MDAwEDCY+PjQSAgUHBwIPM0FDMQ0IFhoUBAwQFBIOHjxIPiQLO1NXPxEXJSkjEQoYIiISCBAaFAwPLUdXTzkPGCwsGhdTd3FNB05+kgF8QgwCBBg0YJYBqAGUAWQSO22DAXlPIwsBAgUVHyMbDwQFERcRAwUVHScpEUZ8gAFiHiE/STsXFDA6NB4Vb5cBjwFdBTBAQDAQFTE7NyMMTHR2VhQRGxcDHiwkGhQOBBUlLS8hFkJSQh4SEy87KxMCGy0pHQRIdoQBckowNjImEAMEBgMRM3uvAbMBjwE7GEpaUDABCQ0LBwEEBgYAAQkXISUdCwoeKCYgFCQ4NioQGz9NQyUGGiQiFgABGystHwUIDgoICAgoOj4uEgEJCQcAAQ8xQT8rDQwcHhgGBhIUEAwcOEY6IAk3UVM9ERcnKSMPCBokHhYEFBgYDAcpQU1JMQUgNjQcEVd1dVMJSoIBkAF4OgQLCwQeUIIBlgGKAVoOOWV3aUUVAwoKBBEbIRsPAgkTFxMFBQ0bHSEHUH6GAWAaI0NLPRcSLjo0HhdvmQGZAWMHLEBCMhAXLz03IQxSdHpUFBUlIQ8SJBgSDAgCEyMrJRsgRlZIHAwPIScjDwITISMTAjJQWE4wHiAeFgwBBggECRtRdXthKxY0QjweAAEFBwkFAAEEBgYAAQUPFxkTCQgUGhoWChwkJBwMES81LRsEEhgaEgABFR0dFwUGBggIAAEIHCgsHgwBBQcFAQ0fLSkfBwoWFhAECAoMDgQUKC4oGAclNzkrDQ8ZHxcNBhQYGAwGDhISCAMZLTMxHwABHCgmFg07U1U9BzZcYlYoAQ0NBRIwVmRYPAojQ1FJKQ0EDAwCCREXEwsAAQUNEQ8FAQkPERUBOl5eRBAZMTMrExAgJiQUD01va0kHHi4uIg4PISsnGQo6VlQ8DA8bGw0IEhAIBgQCDxUbGw0WODwyFhYFCQsJBQABBQUHBQIIEhQQBggICggCAAIBAwEFDxUXEwcCCAgICAICAgQCAAECAAMBAQMAAQEBAAECBAIAAQICBAQEAAEDBwsJAQABBAgGBAABAQUFAwEEAgICAgABBgYGBgACAQEBAAEBCQcJBwEEAgYCAgIGBAQEBAgKCgQBBwkJBwEDCQcHAQIEAgQEAAIGAgIAAQUFBwcHAAECBAYCAwkRDwkAAQ4YHBQKAQMFBQABChISFAoEBw0NDwkAAQICBAIBCQkJBQABAgMBAAEBAgABBAEAAQEICg4IAgEDBQMBAgYIBAIBDxUTDQEGBAQEAgEDAwUBAgwOEAwEAwMDAwIEAwABAQABAQABAwEDAQQIDAgC5AEHGSEdEQcAAgYEAAEFDxMRDQEWHhwQByErKRkEHiouJBIZQUtBGxo8TEguCgkNDw8JCiYuKBINJTExIxclNTUnDQgSFBIIAAEIDA4KCAwWFhQMBg4ODAgDGycnGwUDAwUFAQIECAQEAAELDQ8HARAcIBgMAQMFAwMCFBweFgYHDxMPBQULDRENCQsNCQUEFh4mIhYNMUNBJQABGigsKBgmMjIkCg0bIR8bExUTEwkBDBYYEAABAwgQDgwHM0tJLQIYHhwUChY0QDgcF1l5dUsNNFZgTCILHyUhEwYmODYoCCVBRzkXGDZEPCALHyUjGQUOFhYQBBUpMykVBhwoKiAMGTM7KxGYAxMrNzUjCwEEBgQEDRkhIx0BIjAuGg83S0MpCDZOUEIcK2uBAWcpMm6EAXxUDA8XHRkVEDxKQBwbQ1VTRSlHX11HHQ4eJh4QBAwWFhQKHCQqIhQQFhoWDAktQUMxCwMJBwcDAQoIDAgDERcZEwEeMjYsEAEDCQkHBB4wNCYKERsfGQsFEx8dGREVFxMHCiI+QkAmE1dxbUMELkpQRjJEWlQ+EBsxOz0zJykpHxUBFiIkGgABCQ4WGhINWXt5TQgqODQmECRcamAuJ50B0QHNAY0BFViaAaYBhgE6EzNDOyEMQl5gRA4/b3lhJyZcdGQyEzdDQysFFiQoHAQlS1VJKQwySEo8FCtXYVEf2AMTMT87Kw0FAgYIBA8hKSsnBx4wMBgPO01JJxBAWl5KJCtvhQFrI0KCAZwBjAFaEBEfISEZDDpKPBghS2FjUzNVb3FXHxAiKCISAhAYGhgQHi4wKhgSHB4YDgsvRUszDQUJCwkHAgYMCggBExsbEwEgOD4wFAMHCQsHAh40NigIDx0jHQkJGSElHRcdHRUFDjBEUkoyEVd7d0kEOFhkWDxWZGRCDiE9SU1DMzkzKxcFFiYkGAkLBhYaFA1bhQGBAUsQOkJAMBAqYHRiMjGtAe0B5wGhARtmqgG6AZYBQBM5R0ElDkxoakwSQ3mFAWstLGZ6bDAbP09JMQkaKCgcAilTYVMtDD5SWEQcK1trWSWsAxEpNTUlEQcAAQIGBA0fKS8rEwweHhALLTczFxhAUlREIBtLWUMJSnyKAXhMChMZISEdASQuJgYhRVldTzlRaWdNHQwcIB4MAgwUGBYQIiwuKBgWGhoUDAUlOTkrCwUHCwkHAQYICAYBDRcVDwABGi4yKBABBwkJCQMaJCggBA0ZHRUJBRkfIyEbGxkRAxQwRkxGNAU7W1s3BjZSYFhCUGBSNggnO01PRz09OSsbAw4YFgYPFQEKEBIHQWFbLxxARDwsEiBKWEgeK48BxQHFAY0BGVSOAZoBejYNKTkzHQw8WFhAEDFbaVcjIlBcTCAdPUdBLQUSIB4UAAElR1FJJRA0Tk5AIBlFUUUdlgIHFx0fGRELBQEEBgcZJSktIxEHAwEDCQ0BCiAwOjYqGAQFAAEMKkhWUkIoAAELFRkbHxEJAwcNHzM9Q0E1P0c/LxMEDAoIBgMGCA4QFBoiIhwYEhQUDggDDRUXEwMDBwUHBwMBAAEEAgEDBwcFAAEOFBYSBgABAwUHCwUCCgwIAAEHDQsLAwUPGxsbGRcTCQYaLDI4OCoYBRUXEQgmPEZGPkI8LhgFIzVBRUdBOzUnGQMAAQMJDx0fEwkBBgYJEQcMLjo4MCAKCBYaEgABIUlfXUMTID5COBoAAQ0REQ0EHigqIg4LHyUfCwoWGg4FISkvJxcDBgoGBAUXIykhDwoiLjIuIAYLFxMHrAEHFRkbFwsCChAODAsdJy0tM0dFNxsEIDQ6OCgkHhoUECA8UFhOQjQiFgYFCQsPFRkrNTc1Jx0bIyUtMykbFQsDBRMTEQ0EDhwcHBYIAQABAg4QBgABAQECDhYUEAIDBQcHBwcDAAMCBAgIBgIDCQsHAwADAwUPFRkZFQcIEhgUDAsdJSslGRsXCwIeNkhOSjgqMCwiFgQIEBgmMiIOAw8ZGR0fKzM/Oy0lEQUTIysxKRsLAAEKDBYqQEZGODAmGg4CDRslJx8TAwYGAgkpOzspCQwOEA4IAgQKCg4OJDY6LBQJGyUnJR0dFRMJARUtLSkVAAESHBoUChokKCYgICIiFAiYAQ0hLSkdBRoqLiYQESczNS1Fa3dlNwxAYGZWMBwUEA4MKlZqblw4JBQGAQUHBwkRHTFNV0k5GRMRGR8vGwkECAQRIy0nFQgsPEI2Gg0fKRcCCgcREQ0IIjI2KAQFCQ0LCwUBAgICAgoQDg4CDxsbGQcAAQQEAAEFEyk1NycPGDY+NhgTM0VDMxslIx0DIFJ6igF4TjZAQjggAg8VDQYiCBcnLScRAQEJIzs3Kx8TBR03Q0ExDRYqLiIcQlxmWjwkFgwCARc1QT0vDRYuMB4LXZMBlQFxKRAaHhgQAgQKDAwUSnJ8ZCgPKTc5Kx0VEwsFAzddaVUnGD5QTCwSJjI2LiIoLCokDG4PJzEvHQImOj4uEA8nMzMnRXmJAXdDDlBwdmAuGA4ICg4uWnBuVjAYCAEHBwUDCREXN1NdVzkbDwsNGykVBBASCBMvNS0XEDpQUEAeETE1JQEIDxkZDwgsQEIsCAkLDw0LBQACBAACDBASDgQTIScdDwIEBgABBRUzRUM1EyBEUEYcFTtPTzUVHycZAyJmlAGgAZABVjxISkAmAAEbKSUNEgktPz0rDwoOBBc1LykbDQUhPUtFLQABKD5ALh5MaGxcNh4KAgMDGz1LRzMJJDxAKgt1tQHBAZUBNRAeIB4QAggGDA4UVowBlAF4MhMtOzktFxELBQMDQXWBAWkrIFZqYjwSKDY4MBwmMC4gDiQNISclFQYoOjouEAsdJycbOWV5az8KRmhqUioSCggIDCpQXlxEJA4CBQkHAwcJDRkxTVVNORsPCQ0ZJREEEhAIESctJw8SPFBSPh4NKS0jAAEICxcXDQgoOjooBgkPCw8LBwMABAgOEAoEER8lHQ8BBAACBxczQUU3Exw+Sj4aEzNDQScJExUPBipokAGcAYgBVjhGRj4kAh8rKxkGFzVDPy8NBBAEES8pHxcNAx0zPTcjCCxCQC4gQlpgUCwSCAMFBxc3QUErCSA4OCILaakBswGLATkKGB4aDgICCAoKDlCAAYQBaioNJTExIw8JBwMAAQM5aXVfJyRSZlw4ECIwMCYYHiQmHgpBBQ8VDwcKHiooIhAAAQsPEQ0ZO0U/JQQoOj40GA4MCgoMHjI8NioSCAABBQcFBQkLERclNz07LRsVERUXIRMFBAgAAQcTFw8AARowPjwyHAINEQkIDAEHCwcEFiAgFgEJCQ8LCwkFAwUDAwIGCAgCCRUVFwsFAwMFCxUnMzMrEwwiKiIQCx0hHQsMCAgQHDJWbnJiRjY6OCweAhEfHxkNGzE1NSkXCQUHFSEfGQ8LAQ8ZHxsLDCQsMCQYLjw6MBwOBAEDAxEhKScdCw4aGg4NQ2ltWyUCCA4KCAABAgQEBAwuSFA+GAcVGxcTBwUAAwIjOUE1ExY2QDgiDBYeGhgMEBgSEAqpAQQMEhISDAwIDAoODAwMDAgEAAEDBQcJAwIGBgoEBAYGDhIaGBYMAAEJDxEJBwsNExcXFRERExcfHSEfHxsXEw8HAwIKEBQWGBgWFhYaHBwYFhQSEAwKBAIGCAQAAQULDQ0PCwcHBQMFAwkLCQcAAQQEAgMLERUTFRMRExMTDw8NCwcFAQQMFBwgICouNDY6NjIwLi4oIBoUCgYGBAEJFR8jJychIyMjIyEdGRMPBQEQGiQiGAgFCwsAARQYFhoQCAABBQcHBQUFCwkNBQIIBgABCwsHBwcJDxUXEw0BAQABAQEDAwEDAwIABAQECAYGBgQIDAoMCAoMDgwKAgkTDwsAAQgOEAwEzwEULjw8KBAFDQ8HCBYcHBwUDgwIAgMLEREPBwELFxkNAAEgMDQmDBk7R0EtCw8bHx8bARggGgEbJysnIxkXDw8JAQocJiooFAUTEQEYJi4sKhoWGhgUCggUHh4QAxMdHRkPBQQKDg4AARknKx0DGCIcDg0tOT01Jw8DBggGAREfIRkLFDRITEIuNj4+RDogCgABBhgaAAELEwkKJjIsGgcfKS8rJSs5QT81JSMfFQ8EQnaMAYIBUglXe3E5DiIkHhAFHystIw8BAwUDAwYwQkQoARIsMiYKJUdXTSsFAQcJDRUlMS8hDQIECAoQFhweHBQIIjZANBwOFBIUCg1HZWNHERwuNiwQ1QEYRFRQOBALGxsPBhgiJiQcFhIMBgMPFR0XEQUZKSkdASA8QCwGLVtrX0EPESEpJRcIMD4wEh8pLS8nFxERCQcDECY2Ni4WFyklERQwNjowHhgeHhgOCCAsKhgDGSMlHxMBDhYaGgwdNTcnBRwuKhIXPVNXSzERBBQcGAgRIykjDRpMYmpSNDhCSEI6EgsVDQwOESUjFQ42SEosAh0rMS0lK0VRT0EvKyUfEQJgqgHKAbYBdA19rQGjAVkOJCoeDBM3RUU1FQEBAAEBARBMaGRCBiBCTDwSLWF1Zz8HBwkRGSU9TUc1EwIIDhQcKC4yKBwOLk5WSiYQGhoWDhVpkwGTAWkdIEJKPBrHARxEVlI6EgsXGREGFiQqJiAYFhIIAw8bGxsVCxstLR8FHjQyIAM5Y3NpRRETISclGQ42QjYQHSkvKyMXDQ0JBwEQKjg6MhYTKysRFi46Oi4gGhweGA4IIComGgcbISchEQABEhwiJBITLTMlAxwoIAofS11hUzkRChoiIhILHycfDSBOaGpUMjxCREA2EA8dEQoGEyUnFw42TEg0BBUlKyshK0NTU0U1MSshEwRisgHQAcABeAABa6MBmwFVCh4iFgABIT9RTzsXAAMCBBZMbmhCBiBCTDwSK1tzaTsJCRUbIy1JV1E9FQYMFh4qNDw4NCIMME5aTCQUGBwUDhVplQGZAXMjHEBIOhidARYyREAyFAABCw0HBBYeJCYgGhoQCAENFxsZEQ0ZISMXAxQWFgIZO1thWTkNDRshHRcIJC4kBhslJycdEQ8HBwUDDCYwNC4cAxUVBRYqMC4qHBIaGBQKBBYaGAoHFx8fGRECEB4mJB4AAREbEQEODgYPK0tZW1EzEQgWIBwYAgsTEQcaPlRURDA2Ojw8MBQBCQkKCgsXFw0KKDg2JgYNGR0hGyU7RUtDNTEtIRMAAVCMAaQBmgFwGjNXWTMKEgoAARErP0tDNRUAAgYEBhI8TEouAAESLjIoDB1BTUktBxEbJS81SU9JNRMIFiAsNDxAPjQiDCI+QjgiEBYUEggPSW1zWyUMIi4kDmsKGB4iHhYSDggIBA4YGhwcHhoYEAABDxEVExMLCwkHAwIDDyEnMzk/PTEhCQUFCQsNCwUHCxEbGxcVEw0JCwcFAwYSHiQmJh4aGhoYGhwaFhISEAwIBgIBAQMHDxMXFRMNAg4cICgkIhwYDAYLGykzPUdHRTkpDwMIEBoWFhIMCAQMGCIoKigsMjIuLiAYFBAQEgoMBgYGDhAQDAQBBQsNEx0pMTU3My8lHREAAR46TFJOQjImHBIICRklLTE1MS0dDQMCAgQIChQWEAQHBQEAAQIBBQsNCQcJFycxMzs9NzEfCwweKjY+QD46LBoKEBYcGhIODAwGBAABFSMpJR0VCwcDAAHZAQ4mNDQmGA4KCAQECAgQEhguSFA+HhMpMTUlEQwcJBwOETFBSUE9S01FKwckOkQwDilPX1c/GQABDhIMBRsnKR8PAAEIDhYgMFZqZEgeBgMHAQYSFBYSCgUTGyEbEyMxMysXAAEUICgoNFRiXkQOKVFfX1E9OSslGQ8AAQwUGB4cHBoSCgcVGQ8GJCoqLiwoIhwUFBAcNDg4IggCAgQAAQYMEhIIBRUfJy0vKx8VCQMBCw8HEDBcggGIAXhIAUNncWM9HxEHAQMLGR0VCQgOEgwECyk9PzMTGEBQSCoDHSk1Ozk3NSkdBwocLDQ6PDQkHBIEGCguJhgGAAEHAwMAAQMHDRUZKTUzIw2dAhg8SEQyEggAAQEBAAIEAgwQOGh2ZjATO09JORMWNj40Ehs9T1FHP1VjWTsJQHR6YiA7gQGbAY8BXxMWLDIkASc9RTkXAwEEDBg4gAGcAZIBZiIJHRsVARIeHhoOBR0vLScVL0lRQx8EGCIsLDp2kgGKAV4WO2l/d1c3JR8RDwkGEhoeHCAcGhQKETU/MxEcJiwoKiAeFBQMDCRIXFQ0CAIBAQIEGCQkGAIRGSElKSMPAAEGCAElOzkdGmqgAbYBngFeC2WZAaMBhQE9EwYUEAYVLzctFwoWGBYMCT9jaVUjLnCOAYABSAEbKTE3NTUxKxkLChggLjQ2JBYMBAgkPkA6GgILDREHAAEBAwcNGTVFRTUT6QEaPExILgwAAQUHBwMDBQEAAQw0bHhoNBU/T1E9FR46QjQSGz1NTz85VWFbPwlMfowBbig9jQGrAZ8BZw8eOj4qAitHTUEbBQcDBhA4hgGqAZwBahwRISUdBRIcIBwOAyEvMScXMU1bSyEEGCQqKjp8mgGQAWQUOWd9eVEpGQ0HBQUIFB4eGhocGBQMFz1NQxsWICQmIB4UFAoMCCRQYFo4BgABAQEBBhwmKB4GCRMZHx0ZAwgODgABL01LLwxengG0AZwBWg1pnQGnAYUBOwUSGhoIGTM9NxsGGBwYDAdDbXdfJTZ+nAGOAVABFyUpLSstKyEZBwYQHCIqLBgMAgABBChASDgcAQ0VDwkBAgABBQkTMUdJNRc3EjA4NCAEBQkNCQULCQkDAAEmTl5OJBMzQ0MxExIsMiYKFzE7OzEpQU9JNQc+anBYHjNtjQGFAVUPHjIyJAABJTtFNxkFCQcAAQwqbogBgAFUFg8dIxcDDhYaFA4FGSUnHxEnQ0lBHQgSICAiLmZ+dE4QJ1FhWTsbDQMAAwoSFhgYFhQUEAgPMUE5FxAaGhoaFBAKDAYGHD5QSCwIAQABAwIEFCAgGggHCQ8PFQ0AAQoQCgABJz9BLQABRHqGAXZEC1N5gwFpKQESGhYGEy0zLRsGEBQUCgc1W2FPHyhqgAFyQAERGR8dHx0dGw8FBgwQFh4cDgYBAQQgNDguFgENDQ8FAgIAAQEFCyU3Ny0R7gEGEBIOBAMJCw0NCwkPCwsHBhYcGAYPGyEjGw8CCgwIAQ8ZHR0XEx8jIxcFFCouIAoXMz09JwcGEBIKBRMfIRsPBwUDAwQSMD42KAYDDQsJAQgMDAwEAQsPEQ0HEx8hGwsCDA4UDhgwOjYkCg8fIyUXBwECBAQEBgwODgwMDgwICgMRFRMHCgwODgwKCAgEBgIQHCAgFAQEAQIAAQIIDg4KBgABAwMFAwUCBgYEAAENGx0TARosNi4YBx8zNSkTAQYKBgINERkVCwEEBgQCAxknKyERECoyLBgBCQkLDQsLCwcHAAECBAgKDAwGAAECAAEEDhgaEgoCAQUBAwABAgICAAEDDRUVEQesBBMzPTsnExcbGxcPExUVEwkAAQgIBAELCw8LDw0VGxsXCwEIDAoEAAEGCAoCBRMfIR0RBB4mJhILGyclGQEUJCYeCAUHCQUFCyErJRcAAQ4YGBIGCRUXFQsCDBIQDAIKFBoWCgEJDQ0DBREXFQ0CDBgWGBIKCAoICAYMCAgEAAEBAwECBBQiKiweDAIDAwUCBgwMCgYFCw0NBQYICgQAAQkrQUU1FwYSGBgSCAQCAQABAQMFBQUFBA4QDggEDBQUDAIAAgIAAQMDBwUDAgQCAQMFAwQKCgYFERsbFw0FBQcDAgYKCggGAgEHBwcHAQwSEhIEDRsdFwsHCQkJAQoWICAYDAoQDgwGzAUtb4kBf1EdKTUzJRUbIR8TAxQoLCQOCREPEw0PIzEzJREQJCwoHAwaICAWAy1FT0MnFk5sYjgLP1FNLwpEZnBWIAkPDw8NG1lzZ0UHIjg6KgofOUM5IQQgLi4eDCA6QjwaCx8pJR0VP0tFKQMgMj48KhwWEhAMEBAOCAABCxEREQsCIkxgXkIWAAEPFQ8HDBIYEAIPISkhEQYOEgoFH22jAa0BiQE5DCg6OioWCAQAAQEAAQcLEQ8JDCIsKhYKIjAyJAYECggIAAEHCQcAAQwYDAQBBQMQHh4QCSk9PzchBwkLBQgcHhwWDgQHERsbFwsWKCoeBidHT0MjFx8hGQkQOE5OPBYaJCQcDOAFN4cBpwGbAWMhNz89MRcdIx8TAiQ4PjAWCQ8TEREPJzs7Kw0WNDw8JhYmMioaATVbY1UzGGKEAXxGEUdfWTEUYooBjgFwKgsPExMRJW+RAYcBVQ8sREQyBitNWU0rCCg6PCwOKkxWRiAPKTU3JyVPZ109BShIVlI8KCIeGBgSEhAGBRUdHxkPASpeeHJSHgEPFxcLCBQWEAIZKzEtFQYQDgQNL40B0wHdAbUBTxI4SE48HBQIBAACCRMVEQ0MLDgyIAwqQDwuCgYMDggCBQkBCBgmFgwCAQESIiAQETlNU0UnBwsFAAESKi4oIBYGCxsjKSUPECwwJAYzW2lbNSMxLSMPFEhkZk4eIi4uJAzoBC95mQGPAV0nM0E9LxsbHRsLCiY8QDAWCREREREPJTM1JwkcNkI8LBwqLiwcATVXYVc3EFZyajwRO09HISRmigGMAWokCQ8TExEhZ4UBf1MLJDo6JgIvTVdPKwQqOjgqEChGTkIcDyk3NysnU2VdPQcwSlpaSDQuKiIcGBAKAgsbIyEfEwEoXG5uTiAICRMTDQABDgwIAxkrLSkVBAoCBxs3kQHLAdsBsQFNEDZOUEAmGBIMBgEJERUVDQomMi4cDig4PCYKCAwMCAABAwABCBImLiQWCgIDDhYWAhk7UVNFJwcDAhAeNDgyKBoIDR8pKysdChwkHgQzWWddPSs3NScRFEhgYkwgJC4uIg70Ah1LX11DIy8xMykZExEHAhAiLiwkDAsNEREPCxchHRcBGio0MiYeJCQgFAUjO0dBMQkkNjIYCx0jFQQwVmhkSBgJDQ8PDRk/UUk1CxAaGAgLJzk9NR8EHCQmHAoaKjAoEAsfKy8nIz1HPycBJEJOUEY+OjQuJBoOBAsRHSMjHREBIDxQTD4gEgYDCw0HBQEFDRUdIRkLAgcPHSs/b5cBmQF9NwoqPEA8KiQeEgwCBw0PEw0AARQaHBIIGiIgGgYCBggEAAIOGCQwOC4gFggBAAIHESc5QUM1HwUKFCIwPDw4LBwIDRsnLSslDwIKCAIjP09LNzEzLyEPDDBCRDYeHiQiGAouBQ0VGx0dGRcVFxcNAQoQFBYMBgABBQcEDAoGBQ8RDwUEEhwgICAcHh4YCgMTHSUpJSkvKyUZCQwiMjo4PDgsGggJCw0NDQcAAQQCAgMRGyEfHRcXFw8HAAECBAQEBAYEAgIAAQcTGR0dHxkVDwUGFigyPEJAPjYwJBoOAwsXHRsXDwUCEBwiJCQkIBwSBA8dJSklGREFAAEEBgEdNUFFQTUnHQ8JBhIgKCwsIBYOBgIECAQEBRMbHRcLBgoMDggCAAEBAwMDChwuODw4LiAUCgMRFyErLy8nHxcNAhYoND5ARkA2JAoPJTE3NScjGREJAQ8jLTMxKR0VDQMEBAYMDhYYGhgQBo8BAQABBQsVFwQQEAIREwcAAQgSAAEZJycbBkJiZEIIIzM3JQUWJCoqIiIwOC4eAAEbLTEzK0Fre21HCS5QYFpANC4iFgILFxkXDwQuREIsAAErRUtDKREJAAECBAMNEQ8LAAEFCw0PBQcRGR0fFwsBBggIFBwqMj44JhgWGBwUDgIJFQsCEA4ODhogJCAgKiwiDg1DZW9bNQEeLDAcB0FncWlHDlBuYiwEEhokKiAHJSkjAyQ6QjIOMWt5azkEICgoHgYFCw0JBw4qOkRAMA4HExMHCxMfIy0fCQgKCAIYLDY6QFBcVD4WI1FnZ00pEwkAAQYAARkxPT03FQgcHhQFIS0pFQwkKCYeDNUBBAQAAQURERIkJBITFw8JAAEKDTNBQScOZpQBlAFoECtHSzcLHCgsLCYiOEQ+KAEdMT03KUuLAaMBkwFfBzxmdGY8LCQeEAABDxkfHRMMSmRgPgQ3V19RLQ0CCgwGARUbHREFCRUZEwkJDxkbHRMBDBASCg4YIi44MBQKAhAeIBgSAAENBBggIBQQGBweHBwqLiYUEVWFAZEBfUEEMEZGLg1RfYsBd0M4lgG+AaIBRgYQGB4iFiNDTzcJMlhiTBo/jQGtAZkBUwgqPDoqCgcREREHDiw+RD4kCSMrIQsHERMfJRUMHCISAhgkMDY0VGJgRBgrY397WyMHBg4KBB03RUM1CSY4OiAJOUtFKQYkMjAiDqEBAgQCAxEPEiYkEBMZGQ8JAAEVPU1DLQ5qmgGaAWwUKUNHNQkaKC4oIB44PjwiAh8tNTMlR4cBoQGXAWEHOGJsWjAeHBIMAAEPGx8bEQ5KaGI+BDFTW00pCQgMEAwFFR8fFQMNFxkXCwURExcXDwQSGBIMDBQcJjQoEAABBAwgJCQYDgEOJCgoFA4UFhYSEiIkIAwRUYMBkQF9QwYySEYsB091gwFxNUioAcoBrAFKBgoQFhwOK01ROwk0WmBOHjuLAakBlwFTCC46PCoMCQ8TEQcMJjY4MBoTLTMlDQUJDxUfDRQmJhQEDh4kKChEVlQ8FCddd3VTGwEMEhACGzE9PSsBLEI6JAs5T08vAiQsLCQMBgIAAQEHERMEEhICFx8bGRENGTM/OyMGTHBwThQVKy0dAxoiIiIaFiYuJhgBFSEnIx0zY3dvSQ0mPkY6GA4MCAIDCxMXFQsIOEpILgQjOT81HQEEDAwEAQ8XFREFDREVEQsFCw0PDQcGEBQSCgwSGiAqJBIICBQiJiYiFgwYJCggFAwKDggGBA4SDAIRP19rXzMCHjIyIAUzT1dLIzqAAZIBfDYCCAoOEAYhNzsrByY+SDgWJV9zZzkKHjAsHggDCw8LBwYYICIeCBMhJx8JAwcJDxEHEBwaDgABCA4WEhgoNjImDB0/U1E3EQEMDgwAAQ8jKSkdAiIwLhgLKTs3JQABFiAgFgqUAgEDBw8TFxURFRcfIR8dHRkbHRsXEwUKFhgWDAoKCgwSEg4MCgoOEA4KBgUJDQ0RDxUfJycbDwUCBAEDBQUDBQMHBQUDAwIKEg4MAgkNEw0HAQICAAEEAQMFBQcDBQcHBQEDAQMBAAEBBAQGCAgMEBQaHiAcIB4gJiQkIiIcHBoaFA4GAgMFCQcLCQsPExkdHx8VDQECBAgCBQ0RCwMOHCAaDAIBAAEBAgIFDQsJAAEIFBAQCgMNEQ0HBhAOEAwGAQEFBQUBAAICAAEDBw8NCQMFAQUBBQMABAMBAgECAwADAQMJDQ8PDQUAAgICAgMDCQUFAAEECAYEAw0PDwsAAQQGBgYEogMEBgQDESEXGRcbHScrMS8xHQUKCgQVO0k/HRROcnxmMBVJV0slCBYYGAoBAQICAw8VFRMTExEfJSMdDwkJBQcHDio2MBYPKTUtHQUJFRcVCQIKEA4IAAEBAQEAAhIcIBoIChggHhQAAR0nKRkCAAEFAwQMIjxERjokJiYoKiocDAACBgYCAwUJCwkNDw8PCiAqIAQXJScfDwYICAgGAhEfJRsLCyEpIxEGDhIODAIKDA4IAwYeKigcChIYGhoSAgsPDwsJDRMTFxENBwUDAwABAggMEAYNFxcRARAUEgIZRVtdSSEWPlBOMgQFCQkHAAESHiYgGAETHRsTBQUHBQUDAwMFAwP8AwgQDAIPIR0TExUfKzU9Q0MpBBoeDB1ffW07HoYBwgHKAaABSDWHAaUBjwFLCh4qIhYAAQQIBgETHx0bFxMVKzM3JxUJBwcFBRxQYlImG01dVzkJEyUpJREEFhgcDgIAAQEBBAgkOj4yFA4uOjgkAS9LSzEHCRcZEwEmTmBeRCgoLC4yMhoEBwkAAQgCAAEFAwUFBwkLDSBETkISHTU9MxkGCgoKBgIfOT0zExc3RT0fDhweHhIEDBQSBgsCLEBAKAwcKC4oHAQNFxkNCxkbJSMhFQsHBQMCDhQcJBINISUZAAEeJh4AASd1oQGjAX81KnKUAY4BWA4HDREJAR42RD4sBh0vLx0FCQkLAwEHCQsJAdgEBhAOBBUlHRcRFx0rPUlPUzEDGBwQH2WFAXU7Kp4B3AHgAa4BSDuVAbUBoQFXBiIqKBYAAQIKBgcZJyMhGxcXKzs7LRMLBwcDByBYalwqIVtvYz8NFyszLRkGFCIcEgIBAAEEBhAuSEw8FhYyRD4sAAE1U1M3Bw8fJR8JIE5kYkgkKjA0ODoeBAUJAgYGAAIBAAEDAwUJCSJKWEgUHzc/NRsGCgoICAEhP0U3FRU7TT8bEiAmIhYIDA4MAAEXBShAPigQIC40NCQMCxUVDQ8ZKSsvJx8VCQcFCBQgKDQgBxkhFwIaJBQFN4cBuwG9AZUBQTKGAaoBoAFmEgENDQ0AASRATk42DhctLR8JBwsHBwEFDQsJA7IFBgYGBxcpIRsZFx0vP0tXWUMXAAEKBBlNX08dNpIBwAHAAZABPC11kQF/RwQaHh4SAQEBBQ0hKyslIRcVKTMxJxULBwcHBxpEVEQaIU1fWTcJEykxKxkBEhYYDgABBAYKDBQwQkQ2FhAuODomBik9QS0DDR8lHREYPFJOPCgsMjg6PigSBAICCgQEAgIAAQIBAwcJHDhAMg4bLTEtFQgKCAgEAAEdMTctDxEtMy8PGCIkIBYGAgYDDR8TEiYsHgweMDQyLBQECQsLDR8pMTMxJRsVCQMKGig0PC4QBQ8NAAEMCgABGUF/qQGrAYcBOypwkgGIAVgUBAMJBwIgPkpIPhgFGR0VCQcFBQMBBQkLCQOQBgMFDxUhLScjIR8bK0FPWV1ROysZEQ0TDQEcQmR2ak4gDyk1MxsAAQgKBgQFCw8VHystLSsjGxMZHx8ZDw0LBwcFBBQWDgEdKzMpGwULGR8dEwkDAgQEAggQEBYWJCwqHgwIFiIgHAoFExENBAUPFxcPAhgiKigiMDY8QD40KB4UDgYIBgQEAgIAAQEHBwIOEAoDExkZEwcGCAYEAgELFxcRBQABBwsBChgcHBgOAgcRFyMpJREFAAEGBhwkLjAsIhYQBAENHyczMzUtJxsTBRAgMjxCPCweEgYHERsnNUVha2tPIxA0REQwFgwIBgIAARYsOD40KhYMAgMFAwEAAwMHBwUBvAYHGSUrKyUjHxkVFyMxPUlVXV9RPyMBHjZGSEQeARERAx46RD4gAw8TExELFSUvMy8lFQkFBQ0LCQkNDRslJyERGTU9OysTAAEMEA4KDhoWDgMTGRkXCwACBAwOGBoYFA4CAgYIDBASDgwICggGAQUHCwsDBAoWIiQsMDY8QkQ8LhYIAAEBAAECBAULDQkJERsbHRMNBwECBgoOEhAKAhEbHRkHFjRGQjIaGhQQCgABCxkjKS0zPzktFwQGEBYeKjg+NiYOCw8ZIS81OzcvHQcOJjI8QkA4Lh4KCR8zP0VFMRsLBwUTKSsjCxQaHBYQBBIkMjQ2LBwQBgACFCAgGAQXJykfDZYGDSUvNS8fEQcFCQ8TFyMxRVtjXUsrAAEqSFRUOB1fb1kfPIQBmgGKAUoHHScnHQsfNT8/NxMWKiwYBQMBAwMPMU1TQSEvaYEBdUsLIjo8LhAyTlhCEBspLycXBQsRDQABFBgYFAwGAAIECAwQDAYICAgKCAYDBwkHAgoUGhQSHCQ2TFROPhwCBQkHAwIPFxkVCxklMSkbCQIKDAwKHiYoGgQlP0k3FypmhAGAAVQcFBQOCgIHERkfJT1RWUknAwkLBQwkSlxcRBgBAAEDDx01Q0c9KwsQHi44OjYqHBIGCR8tOT09CxowLhIxa4MBbzcQJCYiFgYYLjg8NBwGCRENCDJISjQINVNbSR26BQ8lMTUpGQUEBAILBwkNHzVPXVtJKwABLkhUSiw/iQGbAXsrSJ4BvAGiAVgHIy8tHw8hN0FDMwcoREImAQECBAMNOV1hUyU5f50BjwFZBTRSUjoWQGpyWhwbLzUvGwcVHRcJDhgUEgwEAAMGCg4ICAIGCgwOCgYFCQEAAQoSFAgGCBouSlZSPiAAAQkNCwEAAREbHRcHGS0zLRsDCBASDg4iMi4kBClNVUkbMnycAZIBXhgSEgwIAAEDCw8XHzVVX08tBRUdEwEeSmZmTBgECAgAARMtQ0dBLQkIGiQuMigcEgYAAQcZJS81MQY6TkQaOYcBowGTAU0MJComGAYaLDY4LBIJFxkTCjxYWD4GO2dvVSOqBAsdKScjEQABBgwCBQUCBQ8jPUdJPSUBJjpCOBpBhQGXAXctQI4BqAGOAUoHHyknHw0dLTs3KwUqQEAkAwECAAEBCTVRXUkjNXWPAYMBUwM0UFA4FDxmbFIcFykxKRkDFxsbCwwQEA4KBgECAAECCg4IAgYECA4SDggAAQMBBggOEAQAAQIOJjxKRjQaAgUJCQECDRcZEQURJSclFQABDBIUEA4iLC4iCCNDSz8XLnKKAYABThQOCgoGAgMFCQ0TK0VNRScJFR0ZCRJAVFQ+GAABDgoGDSE1OTclCwYQGhwgHBAGAgEJER0jJSUQPExAGjV5lQGJAUkIHCIgFAYQJCwqIgoPHRsPCDZOTDQINVlhTx/wAgcPFxUTBwABBAQCBQACAQcTHycrIxcBDh4eGggtUV1LHyJQXkwoBxUZGRMJEx8jJRsFFCIgEAUDAwEFCSMzNy8XIUdXUy8DIDAyJAomPEAyEA0ZGxsNBQsTDwUEDAwICAQCAAEEBggKCAQEBgoODA4MBAIGBgwODAYAAQYKGCYqKiQSBAEBAAIEAwcLBQIHDRMRBQYOEBIMDBgeIBgKDx8nHQsgQlBKLAoGCAQCAgEBBQcHFSMpJRUHDxMTBwYcLCoiCAABBgYAAQMVGyEfFQkCBggMDAoEAAEDBQMNEREVEQgiLCYMHUVZTy8CDBIQCgEKEhQUEAILERENBh4sKh4CHTM3LRGyAQYMEgoGBxMXGREFBAoQDgYAAQsNDwsHAQMDBw0PExEPCwkFBwMHBw0NDQkJAwEDAwcJCwsLCwkLCQkHCw0PEQ8LAQgODgQFFRsXEQEKDhAMCAICAgQCAAICAgABAQcNCwkBChAUEhIMDA4KDAoIBgYEBggICAgKDhAUFBIMCAgGBgYGAQMDAggKCgwMCBAYGBoUCgoICgoICAYICAoSFhgWDgIJDxELAwABAgYEAQcNDQcBDBIUEAgDCw0PDQcFBQUFAwUJBwkHAQYGCgYDCQ0LCwcDAAECAgABBQMDBQMFCQ0PDQUEDBQQCgURFxcPBQcJCQcHAwABAgADBggIBgABAQADAXoaOkhAIBdLZWFDDyJCTkw6FAkbHxkFAgQDESMhHxkXExkjJyEZER0fHxkHChggGgoHExsZEwkJCQ0NCxUfHRsPIlpyaj4TaZEBjQFfERgsMiwcGCImHhIECAwGAAEPM0tPORMaNEBAMCAgJCAYDgMHDwsDAQUFAQQSKjg0KBABDxELAAEJHyslEQYYHiIWEC5MWk4uEgYCAQQIAwcLAwouSFBEJBlRaWdHGQEIDAwAAR0zMyUDLlJeVC4FHy8tJw8NDQ0LBQ0ZGxkPBCg+PCgGGSctJQ8GEBYUCgEDAAIBASM9RTcXJl52cD4LQ11ZQRETIyUjFwMKEhIMAhQaHBICCg4QDghQIFRiViohaY0BiQFhDzZkeHhcLAABHSUZBwQCCRs3NysnIx0jNTUxIxsnMS0hCxQqNC4YBRUfHxUFBwsLCw8dJyshETCKAaoBnAFaGZUB0QHPAY8BGyZCSkYuKjg6MhoGDAoIBRlRc3VdGyZOYF5ILDI0LiIMAxUXGQ0NDw0JAAEUOEhINhIJGRkTAw8zPzshBh4oKCIQPmh4bDwUCAEBAggJEREJDDhealguJXOZAZUBaysJBg4MAAEpQ0UvAkR0iAF2QgMtPUExGQ8RDwsHDx8pIRcMPFZWOggfNzstFQgeIhwQAQACAgQDLVVfSx84iAGqAZoBWhFZfX1XGR0xOTUjCxAYGgwIHComGAYKFhgUCCgiTl5QIiNrjQGNAWERQnCKAYgBcD4UCRcVBwMHFStFRT01LSMtOT03KR8vNTMlCxYuODIeAhEZGw8DBQkLDw8dKSslFTiMAbIBngFeFZUB0wHVAZUBIShKVlA4NEJEOCACCAgAAQ0lWX1/Xx8uWGxqVDg8OjYmDgkXHRsVDxUTDQMSOExINBQJFRsTAREzRTsjBBoqKB4SPmp2aEIWCgQAAQQGBQ8RBwg6WmJUKiVxmwGdAXU3GQUGCAABJT0/JQxOfowBekYHKT1BNRcTEw8NBw8hIyMTCjxUVDoKHzM3LxMKGiQeDgEAAQIEAgABLVFZTRs8igGoAZgBWA1Xe3tXFx81QT0tEQYUFA4GHiYoGgYKFBgUBgQWMjwuEiNVa2dJCThqggGCAXRUMBgEBQURHSk/T1FJQzctLTc3NSsjLzMtIQsQJCwsHAYDCwkHAAEDBwsLERklJR0PJmqCAXhGDWWZAZsBcRkiQE5KPDZAPDQaBAEFDxcpT2lnSQ8qUmRiUj4+PDQkDgUTGxkVFRUTDQMOLDY4KhIBCw0LAAEHIy8vGwISGh4YDjBOWlAwHBAMCAoKAQcFAwooPkQ8HBlVd39rPykXCwUBEx8dCRpKbHJgMgEdKzEpFxMREwsJCxcbGQ0GKjw6KAQVIScfDQgUFBYKAQEAAQIGAAEbNT0vDy5ieGg6BztTUz8RHS87OTMbCQIGAgQSHhwSAAEIDhAOBhsAAgUHERshJSERAAEmRlpoamZYSjIUCyM5SVFZV1FJPzEtMTMvJyUjHx0TCwABCAwQEBAODgoIBAABBQkNCxEREQ8JCB4qJhoEEyMnHQsQJDI2NDIwKhwSAw0ZHykrMzEpFwQmOkJKREA8OC4eDgABCQ0VExETDQkAAQYQFBQUEAoKBgYGAgMJBwcDAAEEBgYKFBocIB4cHhwYEgoIBgQIDA4UFBAMByE3Q0dDQTcpGQMGEhwmLjw8OCgYAgULCw8RFxcTEQcFAQMBAwIEBgYCAQEDAQMBAgQGBAABBQMFAwEEAAEBAwABBhAaGhgMAwkTEw0JESEpMS0pIRsRBwMCBAQEAAICBAICKREpNzktDRg0PC4MCxEDHEp4jAGEAWg2DztXZ2ldOSEVFSVFZW1hSyEGGiAWASc7PS0LEBIODgwEAwkNDQkMICQeCAABCAwODAwMDgoCCQgaJi4wIAYFCwkDFyMtLysRBhYgIB4kMDI8RFRUTDQQAAEFDxETDQEAAQYEAREVEwcKEg4QDgoQFh4WCgUJCwkBBBctLSECKkZMSi4KAQcHAwQQEhQSDAYGARMnS217cUkPHjZERj4oGAgCAAEKKjo2IAstQUM1GRAoNDIcCS1DQSsJHjY6MBQEDhYQCAktOTklARQkKiQYAAELFRMJAgIGBAABAREbJSktKSEZEQsFBQkHBwEIEBQOBhkZQ1NPNwdGbnZWFjNTTSMugAGgAaQBhAFEDUVpdXNbIQgaDBlbjwGjAZcBZR0kSFI4AAE/b3djHw4MDgoKAgMJDQ0HJkZSQBoBBgoMDAoUGBQIBQIUICgqEBcrLR0HFyUtLycGLEJCLhweICwySGRuYEYUAAEJCw8RBwYQEAgFJTEpGQQQFBIODBIqLigWBQ0TDwUAAS9XYUcRMmB0aEIKCxMXDwQSHB4YDhIaGgwTR4cBqQGdAWsVKERWUkIcAAEPEw0USmZiQAFBYWdPIxpKXFIyD09tbU0NNFpkUh4IHiAgEBFFY11BByI4PjgeARslIRECCgoGBgMNGyMnKSMZDwsHBQ0NDwkDEB4gHAoQHUVVUzsEUoQBigFkFEFtaz8YcJwBnAGCAUIPRWN1bVMVGCYYFWOfAbUBpwF1Hy5cYEQET3+RAXErCgoKDAgIAwcJBwEwWGJQHAABBAoKBgYUGBQKBwABDhgeIAQnPT0hBxcjJycdEkJYUjYcGhwgMERkcGZEFgABAwsLDQMOFBQMCSk1Mx0CEBASDgoYLDIuGAMNEQ8HAjVjbVcZNmR6bEQICxkZEQQUHCAaDhIkJhoFOYUBpQGlAXEVKEhUUDgUCxsdDxRScm5GAkFla1clIFBiWjINVXV3VQ86ZG5UIAwcJB4SEUllZ0UHJjpEPCADGyMjEwYKDAoGAw0VGyEjHRMLBQUHDxEPCwMQICQcDlATNUVBJwRMbnJSFDVfXz0GUnJ2XDINNVNfXUcVDBgIHVuPAaMBlwFtIyJGTjgAAUNxf2klCAgMCgwKBAEBAQYuUFhEGgECBAQEAgoODAIJAwYOEhYDJzs1IwUNFxsbDx5AUlA2HhocIio6Vl5UOhQCAQMJBwQMFBIKBx8rKRcEDA4OEAgUJCwkFgILCQcDAAEnTVtFFSxUYFg0CgkPEQ0CEhgYFgoQICQaAidfgQF7Ww8eOEA8KA4NGRcNEERaWDwCMU9VQx8WPlBEKA1BX19DCyxQVkQYBhQcGAoLOVFNNQMgNDoyHgITGxcLBgoMDAQBCRETGRcTDQkFBQkNDw8NBQ4YGhgIrgEJERkVDQgiMjIoCBMhKRsCGigoIAwJHy81OzcnFxcbL0ldZ2NPKwcMFA4FJTs/MxMECggODA4ODAoMDCAsKiAKAQACAQMHBQUFCQ8HAQIGBAkXHx0RAQMFBQABAhguNDYoJCIiJiYuNDQwIhAKBAABAgABAgoKCggAAQkPDQUCCgwKCAgOFBYWDAQEAAEBBAYLGx0ZAxgmLCgaCAIBAQIECA4MCAgIEBAMBgsjLTEhBwwWHBgSBAMHCQMKHCYkFgABEx8hHw0EFhwYDgUZJScbBRIeIBgIAQYGBgQHFRsbDwYUHiAcFAgCAwMECAoKCAIDBQUJCw0LCQcHBQkLDQ0HBQABBgoGBNYBAQMBAgQGDRcZDwQaJigeAg0RDw0LBw0PGR0jO1NXV0MpDwMDFTE5OzMlDQIQFhgSEBIYFhgYIiYoHhANJSsjERUvOzcnCRQkJhYHEREPDwkDAwADAQ0NBwIYJCwuLCooLjAoIBIEAQMCCgwODgwMAREXFQkKFBoaDgYBAgIGCA4YHBgWCAcNDQkGGiguKBQHExkXBwgQFBYUDggEAAECAgQICggCAAEMDhAMAgkNDwcCCAgIBgQBBwsPCwESJCYeCBUxOy0ZCig2NiQGBxETFRMbKS0lFQABEiAiIhYQCAgIChIWGBYSDhwcGhIAAQMBAQABAwEAAQIDBQ8tOzcvEwMAAQQGArABAwkJBQYCJ0FDLwM4WFxACB0bGxcNBwcJCQ0XSXWHAX1ZDTZWTBovWWVdRRUgQFZQNiIqKiggIDhGQiwIPW17ZSkxc48BhQFbC0p6elIIHSEjGxMDCA4ODAchKyMFJkJKTEIuLDY0KBIJJS8rGQgSGBgaFgUxQzkfEC44Mh4FCwcDAAEIGC44NCgMGS0vIQI0WGBOIhk/Sz8jCBwmKCYaDgQCAAIIDg4MBAEOHh4YAhEjIRcCEBAMCgYBFyEjGQcuVF5MHDFrf2szImKAAYABWBAVIS0vL0FhZVczAi5GUEYqEgYBAAEIFCQoJhoYLjw4JAQBAQICBgoMDAgBHVl9f18pCwoODgaGAQMNCwUIBjFRUzsDQGhqSgYnKSEbEQcBAAEBAwlJf5UBjQFfAVZ+cDQtYXd1TxUqXnJwSjI2ODAoJD5MSC4AAVWNAZsBgQE3PZMBtwGvAXcPYJoBoAFsCh0rKyUXAwwWFgwJJzMnAzZUXl5OMiwyLh4GHz1JQSUCGhwiIiIHO1FLIxA2QjgeCxMTCQEGHjxGRDAOHTc7KQABQGpyXCIjT2FVLQogLjYuIhQKBAABAggOEAoCAxAeJBoEFycnFwIYFhAOCAUbLSsjCThkclggN32RAX83MH6iAZgBahYZLz1BPVl9hwFxQQY6XGZYOBoIAAEBBBokLiweGjZGQCoGAQIECggMFBIMAAEfaZMBmQF5NwsEEBAGVAMHBwEMCiVDSTMDOFhWNAUtKykdEQcAAQIIBgABO2uFAX9bA1JyajApWW9pTRcuXnRyTjg+PDgoHjpAOCYJUYcBlQF3MzeNAbMBrQF5G1CKAYwBYAofJysjFwUIEhAMBR8lFwxCYmhiUjYiKB4OCStFUUUnBhgiKCgmAi1FQSEQLjgsEBUdFw8HBCI6SEY2FhctNSEAATxeZE4cJUtZTykIIjA0NCYYEgoGAggKDAgDCQwWHBYEFR0fDwwcGBYOCAMdJysjDS5YYEocLWd9Zyk2fJgBjAFeEh0zQ0dJX3+HAXVBBDxcZlw8JBIIBggUJigmHhYyPDgmAgABBggKDhASEAoAARtdgQGLAXM7GQMGBgYaAAECBAoSEgkdHxcBGCYeChsxLycdFQUAAQoKDgYZPVFVRRUcODIQIT9JRzMPHEBUVkg8PjwyJBwgJBoKDTtZX00fJ119e2MnGDxCLgETHR0dEwsBAgYCAQMBECxSYmZeSi4eEAQLGys/PzUbBhoiJiwoFgsXGQ0IFBIKCR8jHRUJBhoyOjouHAQPEw0GJDQ2KAgZMTcxGQgcKC4wJiAYEgwIBAQCAQkLAQgICAIFCQUEEh4aFhAIAREbHxsNEiouJA4ZMTUnATRWZFg2CBkzP0tLV2trWTMCLkRSTj4sJBoSDg4YHhoUFh4mIBICAAEIDhAODg4KBAETN1FbUzkjGQ0DAQwCChIWGBoSDgYEBAsbIysxMzUrJRUJAggMDhAODAINJzMzMS0jHRcVDQ8JChwsNDg2LigiGhAOCgIJExkZGRMHBxchLTM3NS0jGQ0XHR0bFQsDAAEEAgQYLkBOWFxSRjgoCg8jKy8rHRMHAhAaJi4sLiomIBgQAAEVJy0tKychGQ0CDhYcHiQoLjIuIA4HDRUVDwsFAwIEChIcIiQmJBwUDggDCxMXEw0AAQwQDAQCCg4SHBwYEg4GAQMHCQsRExUVDQsIHCYwLiogFAwGAxcrO0NJST81JxcBChYiLDY6Ni4iEgoKCgwODBAKCgIAAQYOFBQQCgEFCQkFBQsRGyszMysfCXgEChAUGBICCw8NAw8hKzExPUVJOSMHAQQKDhAwRkYqB0lvfW1LGwMICAYFECg6QDoiCgkLARIkJiIQDRUTDwkDCBQQAhU5SU9DLRUzUVlJJQgqNDAgCBowQE5YQi4iGhoDNU9RQRsWND4yGiY6PkAyLjY0KhQHOVFXSy8rKyETAQILCwEUNm6IAYQBXhglT1VJJwIYJiYcDBIaICIkFgoCAQINKzs9LQU6XGZIEAcNCQABEBQOBgIAAgQGBAMPJzc1KxMYPlpaSB4DFRsXBxMlMz9DNR0JAAECCR0hGQQqOjo2JhIIBggKCg4SFA4MAQ4cIB4WBR0pJRcAASI0LhIbQUlJNRWwAQQIDA4UDgsdIRcHER8pLy07T1FDKwsBAAEGChBCYmZKCE+JAZ8BkQFfFQQUGBADFjBAQjgUFScpFxIwPjYeCREPCQcBECgsIAUzS1VNNRdBb3lhLxRCWFIuChgqOkRMLhgGCA4NSWFjRQ84YGhQIC5ASkQ0Ljg4LhwNRWVrUy0pKyUXAAEFISUVBkCSAbYBrgF8HDlld2MxCiw8PCYMEhYaIiAMAwkHAxE7T083BFiMAZIBbBgRGxkPBhACAAEFBQIIDAgCCy9DRTcXHFJoakgUEy0vIQsPHyszOyUDEBgQCzVDOxcgNjgwKBICCAYGCAwSGBYKAhAgJiISDSk3MyEINExKLA87UVE7GaYBAgQIDA4IEx8lHQcPGyMlJTNJS0EnCwcBAgQOPGRmTgxFhQGdAZEBXRUIGhoQARIsODosBh81Mx0MLkA6IgUPCwkDARYsNiYIJ0NLRTEPQ215Yy0cSl5WMAgUIiw4OiIEAwUMEUNdWT8FRm5yViIoQEZALCg0MiwYB0FbX00jISMfEwABCyUrHwABPJIBtgGsAXIaM2FxYysQMEA8KAwMFBgYGgoHDQ0AARM1S0szBmCQAZIBaBYPHR8TAAEIAAEHBwcBCgwKAgspP0M1FxxKYlo+ChstMyMLDRcfJSsbCB4gFAs3TUEjFiosKh4OBAABBAQEChQUEgwCEBogHBANKzUxGwY2TEwwBy9HRTcVXAEEBAYEBA8bGxUJCxMXGRkjMTUxHwkHAwUBBihCSDIINV9zaUUTBg4SCAMIHCQiGgEhKysXBCIsLBgDCwcFBQEOJigiBhUpMS0dCy1NU0UdFjhGPiYGChQcJCgSAgUDCAUrPTklBjpYWkYaJC40LiIeJiggFgMlPT0tFQ8TEwkCBRkhFwABLGqEAXZSEh9BSUEdECYwMBwKDg4QEhIEAwkFAQcjLy0hCkZmaEoODRMXDwEAAQMHCQUDBAgGAgkdLS8lERAyPDwkAhkjIx0HBw0TFxsLChYaEAsnNzMbCBgcGhIGAAECAAIEAgwMDgYCCBIWEgYJHSUhFQgkNjQgAAEdKS8lDSsABQEFCwsJBQUJCQsJCw0PDQkLCQ0JCQcCBggCBxMdIR8ZCwkFBQUJBwMDAQEFCQ8NCQEEBgYCAQEBAAECAgICBAICBQcJCQMAAQEHCwcAAQYOEA4IAAEEBgoMDA4KCgwMCAABAwECDhQaHBYSFBgYGBIQEhQSEAoAAQEBAAEEAgICAgYEAgECBBAaHBoSDAEBBwECCAwODAoICgwODAgIBAIEBgYAAQEBAgoSFhQMAgIBAAEDAwMFBQUFAwMDAQUDCw0NCQcAAQQGBAABBwsPDQkFBQUFBQUBBgYGBAEJBwsHAAECAgICAQEHBQUFAAEBAAEDAAEBAgQEAgQDAwMFAwABCAwKCgABBwkLBwWVAQYICgYCBwUBBQMHDxUVEQkGFhwYCAsdJScjEw0JCQcJBh4qIg4PFxsZEw0hNzkxHQABGCIgEgkpMTEdBDRQWkgcH01hWTcHDRELCQg0WGJQJgchKycXAhIaGhQIHjI4LBYGCAoKDgE1TU0tBCAmKB4ODhgcHBQSJC4uKBwIBwsHARAWHBQMCy05MR0AASI0PDYcBx0rJw8MJDIyJg4KDBAMDAYGBggOChMlJx0DHjY4LgwLCQsHBwMLDw0JAwABBAIBAAEPHScrJx0bGRUNCQsPDQcGFiAmIBAGIiwuHAcLCQkFAxcvPTUjERsfGxMDBAgICgQSJiooFgEAAQMAAQQIBxcdGwnDAQYQEA4CCQcFAQcJGR8jGQ0SLDYuFg0rPUEzHw0LBQUHFEBUSCQTISUjHRE1V2NXLwguRD4iDz9bUy8QYJgBogGEATg5iwGtAaEBXRUZHx8RCFyaAaoBjAFCDztNRysEICwqHgYsTlRGHAIAAQQICg1llwGRAV0DKjo2KhAKHCQmHBo2Sk5CLAoNFxUDEiIkIAoZUWdfOwE2VmRSLBE9S0cnEDpSUjoUDhQUEA4EBAQIEBAhPUUxBTRaZEgUCxENCwUHDRMVCwMGCAgIARUzRUlBNTMtJRsNExcTBxAsQEQ6IA42UkwsBxURCwsDI1NlYT8dMTk1IQUKEBIQDCREUEYmAAEFAAECCBINJzMtE8MBBhAUDgENCQcHCQsbJSUfCRY0PDYaDTFFST8fFQsHBQMcTlxUJhMlKykdEz1jcWM3BjJIRigPR11XLRh4tAG8AZgBQD2bAccBtQFzFSMnIxUIarIBwgGeAUwPP1dPLQQiLjAaBipSWkgaBQEEBAwPdakBpQFvBSw8PCgKBBwmJh4aQlZeUjgQBxUTBRIiJhwIH1t5a0UBPmJsXC4TQVdPKxBEWFZCFg4UFBYKCgQEDBgUHT9FNQc8YGZOFhMPDw0FAw8TEQsAAQgICgYCFz1RW1VJQz8xJRMXFxECGjxSVkgkED5SUCwLGRUTCwMnW3VvTSU9Rz8rBwwUGBYSKlJeTiwAAgEIEBYFKTcvE5EBBgwKBgURDwsNCw8XIx8XCRIsNCwWDS09RTkjFQ0LAwMaPk5AGhUjKScdDzVZZ1k3ASY2Nh4LNUU7GSZ2oAGqAYQBNjOBAaUBnQFhGR0jIREGXpYBpgGMAUIHLz8/JQQYJCASAx4+RDQQBQECCAoHW4kBhwFZASguKh4AAQEQHB4YHD5YWlY+IAgDBwEQGBoQAhtRYV05AjZUXE4mCzNDPyEONkxKMhIQEBQODggKDhQgHAsnMSMFLEpKNgwPEQ0JBwEHCw0HAgYKCAQAARc5U11fVVNJPS0ZEw8BDihGWFZEJgosQDQaERsZEQsFH09lZUsrPUU/KQUMFBYWEipKVEYkAgABCg4WHAIVIyMPKwABAQEJDRMTFxERERMREQkDCBIYEAYLGyktKR8ZEwsHAAEMGBoSARcdHx0VDyEzPTkpEwABCgwGAwsHAhYwVGRgSB4VPUtLMxMVFQ8LCC5OVEgsCgcTEwsAAQgGBAMNAg4UDgABBwABBAoQBh0vMRkGEhQMAAEJDQMGChAWNEZOTEQyJhgQCgQIAgEHFSctJxUEHi4sJhYEDRURCwocICIaDggOCgwIDBQcIiYmFAQBAwABChQUCgcNDQsFAQABAQECAAECBgICAgMRL0NVW1tZU0cxHQkGFCY2RkpENBoCDAoGBxcdFxMNBQ8rOz83Ky8zKRkDBg4SEhAYLCwiEgABCBAaICQWCgIBAToNIS8tJQ8WKioYBxcdHRUJCx8lIxcHDBAQAhMVEQ0DAgUbKSsjHzU/PS8RDBoeEgstQ0k/JQEiNEJCPCwiFAwEFDRANhwHAgoODgQnQ0UxBSpESkImAAEJDREVFR8lJx8TERUTCQIcSFxcRBYnTVlNLREJAwQKFCo0QkREVFpUPhoDFRsbFQ0BBAgMDAIHBQUIFCowKhwCExkZDQICAwEDBBo4Sk5CLComHhICITtDNx0JBQEBAggaJCQcCA8dIR0JBxMjNUldYV1TPR0MMkhQSDgoHhAEAxcfIyMfFQ0JAwABAQcPFR8nNzcxIQMQGh4aEgwEAgIDAwMAAQYSJCwwLiIOaBtHXVk/AVSIAYoBXgYtOz0xFSNNYVc1AjRSUDQFDwsDAAEKET1VVTknWXl7WRc4bHhcGEV/lQGHAU0CNFJcVjwaAAENDwc8kAGwAaABWAYYKi4iAnHFAdMBoQExSIoBogGQAVIECxEXFxszRUc9Ix83OzERLpQBzgHKAY4BHmW7AcsBqQFPDQEEDBAWJDI4QEpyigGGAWgqDScxLSEFChYcFg4NHyMZBShOYFg2AzNJRS0FAxETDQEoXn5+Xi4qKCIUAUV1f2ktBwIGCAgQOE5KNA4nQ009GwIGAAEVMVVnaV1JISZaeHJUJAgLEQsHHS8xKx0JAAEICAYGCggDDy1JWVU3CR40ODIYAAEHDQ8JDR0nHwcaOkRGNhZuI1dtbUkEdK4BrgF6CDFJTT0dK2V3b0UGSG5wSggLAwIGDhNLY2FBJWeRAZEBbRtQkgGmAYABKEuXAbUBowFhBDpcZlg4ChEdHQ1MtgHiAdABdAgmODosApEB+QGRAtEBSVKoAcYBrgFkBAkTGRkbPVNZRykpSVFDHzK4AfgB9gGsASKBAd8B/QHRAV0HAg4QEhYgKjI6QnicAZoBdC4RKzUzHwMWICQeDBEpMSMHLF5yZj4HO1lXNwcLExsRBS5sjAGKAWAoJCQeEgFJhQGRAXU1AAEGCgwKFEJYVj4KK09XRx8IEhAAASNLX2VfSSMsaoABeEoWDSEhFwshLTUrGQUMEhIIChISDAcnUWNhQQsmPkY2GgMTFxUPDzE7NRkUOExOQBhIH01lYUUCap4BngFoCDFDST0bLV1zZ0ECSGhqSgoAAQIGDBAPP1dRNRtbf4UBYxdKjAGcAXooQYcBowGVAVkCNlBaTCoEFyMfDUiqAdQBvgFqDiQ2NigAAYEB5QH3AccBRUyYAbQBngFYBg0RFxkbN1FRSScrR1FDIyimAeYB3AGcAR5xywHjAbsBVQEIDhISFBgiKC44aooBiAFkLA0jLScXAhgiIiAOESUnIQUqVGRcNgU3S0szBQcVFRMDLGB2dFAeHBwWDgE/bXtnKQIKDAwKFDxMSjAMJUFLPxsIFBQGEztNVVNDIyBYal42AhklJxUJGyspIxMAAQ4UEgoIFBYOAR1DWVU9CyQ6PDAWBRMXFxERL0E7IQ4yQkQ0FgURLz1BLQE2VlQ0AyMzMSsbHz1LQykBKEBCLg4CCAwQEAABHSkrFQsvSUs7ESxSWkgYJU9hVzMCIDAyLBoCDxMTBSpmfnBCChYiIhoCSYEBkQFxJyxaalo2AgcRERcXKzc7MyMlNTkxGxJihgGEAV4YPXGDAW0tAggOEBAOFBYaHiJEVFJAHAERExMFChQcHBYOAw0VCwEeNjw2HgABGyknGwEDBwsFAAEaOEhCLBAQEAwIAAEjOUMzFQQICgwIDCIuKBoGESEnIQ8ECgoCDyUvNzkxIwgiMCYSCRcfGxMFERUXFQkECg4OBAgMDAoAAQ8nMzMjBRQgJBwKBQ0RDwsPIS0pGwIaJCYeDHECBgQCBxMXGxsbFQ0HBwkLExMREQ0HDQkHAAEEEhYYGBISFBIUEA4MCAIAAQUPFRcTCwEAAQYEBAQMFBAMAAEJCw8LAwACAQABBgoKCgoICAwUEhAEAQkJBwUBBwkRExUZHyEfGRkZGRMNBQQQFBQODgoMDAwMDgoMDAgMCgwKCAQEAgQIChISFBAMCggKCgwODA4MDAoMDAgGBggKCAgECAQGBgYEAAEBAQQIDgwMCgACAQEBAAEIDhAOCgQDBwkBAgoODgwEAwkNDQ0JCQkLExktMzUrIxcXEQ0HAwABAgIGAgYGCAYGAAIBAQABAgYICAQCAAIBAwcPExcVDQcJBQcFAwIKCgoGwwEaOEg+HB9RaWlLIRIsNCgGDw8NCQkTN01HKwQiNDgwHhwwOjgsICguKBoDQXF9aTMILjo6JA4oLi4QFTdHRzMVEzM7KQkiHhYQCgpIcn5eHC9TXVEtBQkPExMTJS8zKyMVFxMRCwMEChAQFjJOWFA4IhoWDggIAgABBQkRKzs1JQkcLjg0IAYLFRMHDBYcHBgUCgYAAgQIHCgmHAQCCgwMCA0jLyUPFigyLBwIAQUFBQMMIi4oGgMjMy0bBCQ4PDAUCxkfGw8KJDAuGBNflQGlAY8BVzEnHRcLAAEMFhoUDhIaGBQKAQkLBQQWNEJEMAgJERcbGzNRWUsvBQwUFgwBFC40LBLhASJYaFoqJW2RAY0BbSEgSlZEEBENCwkHF09tZ0UEMkZKPiYiPkxMPi48QDwmAAFbnwGzAZcBTQw+VlQ0EDBCMhApWW9rVR0fRU85AzYyJBoQEGCiAawBgAEeS3uNAXdDBwkTExMVJTs9OSEZFRMPCQEIDhIUFkh0ggF8VDIqHBYKBAULER0lS19dPxEiQkxCKAABGyUlEQocIiQiGAwCAAEBAAEMKjI0IAQAAQoKDAoTN0E3ER48RkIsCgUJCw0LDi44NCAHM0M7IwY0SlBAGgsdIx8JGDxQTDAJdccB4QHJAYEBQzkvHREBGCImIhgcJCYcDgMLCwUIKlBoZEYODx0nKy1PeYUBc0UFGCYkFgMiRE5AGs8BJFRmXCYncZUBkQFrJSROWEISEQ0LCQcXT2tpQQY2Sk5CJiZCUFJENEBKQCoCXZ8BtwGdAVEKOFZSMBAsMiQCO2l9fVsjGT9DLQ5KQjQkGA5gnAGiAXYSU4cBlQF/RwULDxMTESc3PzUlFRUTDQkCCBASFBZQfJIBigFkQjIqGA4BCxchKTVZb2VLFSJESkImAR8rJxUIHiYoJBgOBgADDCY2LhwAAQEECggIETM/MQ8oRk5ELAgDCxEPDwgoNjIcBS09OR8MNEpOPhoJFx0TASJGVFA0BXHDAecB2QGRAVdJOSsXAAEYKi4oHCIsKCAOAQcFAAEWMF5yakoQFSUvNztZiQGVAYEBSwcaJiYWBSBGUkAciQEWOkQ0FCdZcXNTHxQ0OiwIDw8LCwcRNUtFJw4yQEI4HiI4SEpANEA+OCYAAT91hwF5QQUiNDQiChAQAAEdR2l5bVEhDRsXAAEsWlBEMhwQPmJkPgVPc3dlNwcHDQ8PDx8rLy0fFRMRCwUBCAwSEhRCboIBfmRKQDAiEAMTIy03PVdhXT8RHDA4LhoDGSEdDwwaIiQgGhAIBgQCChoiHhAHBQEEBAQLHSEZBiw+QjomCAcLDxMPAAEWICASARkjHw8QLDo6KhIDBwkDDiQ8RDwmBVGVAbkBuQGJAWFVSTUbAAEWJCooHiQoJhwMBAABCBIgNlRgWDoIEyk3PT9ZeYEBcUMHDhgWCAkSLjowEhkCAgABBREhKzEvJxUHAQABAQsPDw8LCQULBQMMGCImJBwYGCgyNjYyMjAoFgQVJS0rIxMJAwIGBRkvP0tVWU9FMQ8MHjJGWGJYSDggCAYCCRUrPUNDNRsBAwUJCQsNERMVExUTDw0FAQgMDg4SJkJQWFhSRjgmEgUfLzs/QUE9MR8FDA4MDAQBBQcDBAwQEhYUFBQUDgwIAgIBAwcNCwUFAAIECAwYICosKB4SAgEDBw0PDQkBAQQGBAQIEBQaGhgSBgIIEBYaJCQiGgwDJ0NbaW1hX085HwMMFBocGiAeGhQICBQeJi40ODQsHAQXKzc/RUlNTT8lCQUDBQ0TCwEBBAICBREVHR0dHyEdGQ8TFxUVERMVGRcPAxAYJCIcDAMHAwQePEhMPjRARjwqCAMDCQ0VEQMICgYRZ5sBqwGXAWEZFjIyFhY8VmRmUhoJHRkBCAoEDSVLZ2tXMQMAAgEFAxIoKiAAARcfHxkPBB4oKCAOEBYmNkZGNCQUCAsxTVdRPx8FDBAMBggKBAIAARAcIBgMCxsdEQIYGhwUDAEbKSslEQQSGBQKCBAaICgmEAIFBQIeMjgsBiM1PS0VBAgKDA4WIigkGAwBAwABBBIeHh4UCgMFCxktS1tPQS8XBxcbFwMWIiIgFgwMIiw2NDAmIBQKAAEfN0VLRz05KyMVAAEaJCAUEUNnZ08hAQcRGxsbGyEdHRkPExcbFRETHyEbEwEWKCwqGgUdJR0HJEpeXkgyTFZSNg4EAAIHDwUOGhwUHZkB3wHtAcUBZRhsjgF+NhhAWGZiPB1VZUsPHiwoEBdViQGVAYMBSQMEBAQAAQMuUlpCEhknKyUVCixAQiwMAQABCBo6OCAKAgENQV1lWzsDJDQyGAgODAgEAiIwMiYMIT9FMwkYICAaEgctQ0M1FRIoNCoUBgwWHiIcAxUbFQE4YmpUFi1TW00nAgABAgQMFCo4MigOBREVDQIcIBoaDAMKFAwLM0c9Lx8PCy9BOSEOJCgmHAwQJDI4NCoeEAwCASE/TU9DNycdEw0INlJOLgdrqQGzAY8BNwUDERcZGxUZGxkTDREXFxcPER0fHxMAARgqLigYESkxKQ0iTmBaRipIVlI4DAQIBAABCwEYIiIWH5sB6QH3AckBVzKOAbABlgFAGDpQVlIsOXWBAVsRJDg2IAtRhwGfAYkBTQUGCAYCAjReaFIYFycrJRcKMkZCMAwLDwcKKCoMAAEJBw0/W2NTMwo4SDwcCg4OCgIGJDg2KAgrSVE9ERQeIBwOBzFHTTsTGDY8MhQEDBIUHBQLIyUdAAFAbnhcHitVY1UrAAIFAAEGEiw4NigQCRUbFwUUGhwUDAIQHiAKHzcpHREHDTdNSSkIICYkHAoOJC40Lh4SDAICAyE5R0c7KRsRCQMKRFxcPAVxtwHJAaUBQwMFCxETDw8RExMLCQsTEREJDRUZFw0AARQgJCAODSctIwsaOkpIMiA0RDwuCgQGBgABBQIUHB4UGXu3Ab8BmQFBMH6WAX42Eig6PjoWN2VtTw0cMjAeAztreW09AwQGBgICLlBWQhYRHSEfDwYqNjYmBgkTDQABGBoGBwsFDS1HSz8jEjI+OBYGCgwKAAEEHiwsIAYjPUM3DxAYGBQMByc7Py0PFiw0KBIEBgoQFAwPHyMXATZaYkoaIUFNQyUBAwEDBAwiLCoeDAUTFxUHDhIUEAgCEBweEA0jFw8HAwktPzsnBBYeHBQIDBgiJB4WDAICAQMXKTUxKxkPCQEBDDZQSjIBV5MBpwGHATkBAQUFBwUFBQcFBQEHBQcFAwMJCQkHAggODgwICQ0TDwcMGB4cEgwWGhoQBAQCAgIBAAEKDgwGCTFJTz0XGDRANhYGEhQYFAgZKysjBQ4UFA4AARcpMSsbAAECAgICAhIgJBwMBwsNCwcEEBgWDgQFBwkAAQgIAAEDAwUDExkdGwsIFhwWCgIEBAQEAAEOEBQMAAENGRsVBwYICggEAREXGRMFChQUEggAAQQCBgYEBwsPCQABFiQqHgoLGx8bDQABAQMAAgQOEhIMBAEHCwkBBAgGBgQAAQgODAoFCQcDAwABAxMZGw0AAQoKCggGAgoODgwIAgIBAQABBxETFQ0JBQADAhggIhIAASE9QzkVqQESCBAKCgoICggKBAYGBgYEBgACAgICAgICAgABAgABAgIABAEAAQEAAgECAQABAQIAAQMDAgEFAwUBAQkDBwEFAQkJAwcFAQENDQABAwQEAgICHwYBAAIIAggBAgYCBgABAgYCAAIEAAEEBAIEAgIAAQQEAQQAAQQAAQICBAEGAQYBBAQBBAABBgACBAABAgABBgABAgQCAAECBAABAQIIAgACBAICAQABBAEEAgBVAQEDAwUFCQcHCQcJDQ8UBQYFAgEAAwEBAgABAQACAwMCAgACAgADAQIBAAIBAAYCAAIEAwIBAAEDAAEEBAMAAQYAAgMBCgUCAwYDAgYFAgABAwABDAABCwECAQABAQEmHwIBAAEBAgMEAQICBQEEAgMAAQIAAQIDAAECAAEBAAQCAQIBAQQBAQQBAgECAQEEAwACBAEDBgEAAQECAQABAQIBAgIAAQEAAQEAAQIBAAICAQEEAQEAVQIAAQIAAQIBAgIDAgIAAQIAARAAAQEAAQECAQABAQIAAQECAQEFBAACAgABAQADAQABAQACAQAFAgAEAQIAAQICAgEDAgIAAQECBgABAwABCAMAAQMCBAMGAwACAQEMAQMHAAICAAEBGhkEAwIABAECAQYDAQAEAQIABAEGAwABAgMCAAECAwIAAgEAAgQFAgQAAQEDAgAEAgMEAAEDAgIAAQUCAggHAAIBAgEAAgQBAQEAVwICAAEBAgIAAgQFAgIAAQ4AAQEAAwEAAQQDAAIFAQICAAECAAEBAQQDAQEAFgIEAQADAgEGAgcCCAEBAAEBBgECAAECBQACDAEJAAEDAgACAR4ZAgUCAgEBAAEBAgADAgECAQQAAQUEAQAEAQABBAMCAAEBAAMBBAEBBgMAAgIAAQMEAQEEAwIBAgABAgMAAQQDAgAFAwIAAgEAXAQAAwEGAwQBAAEEDgECAQABAwQCBQQHAQIGAwIBAQADAwAdAgIAAgEGBgcAAQYBAAEBAAECAgABAQABBAUAAQoDBQEAAwIAAQEEAQABAwIAAQIBAAMBAgACAgMAAQIBAgABAQEEAAQBAgMEAQIAAgEAAwECAgABAQACAQABBAMAAQIAAQMCAgEABgIAAQEBAF8EAgACAQABAgACAgwAAgECAgMAAQEFAgIAAwEAAgEBACICAgABAgQFAgYFBAEAAgIAAQIDAAIEAgMBAwABAgACHBsBAgACAgMEAAIDAAECAAEBAgACAgMAAQICAQACAwIBBgMEAwQFAgACAQABAgEEAQABAQABBgMBAgIDAAECAAEBAgECAAEBAAMBAGICAAIEAgEBAgACDgABBQQBAQQHAQQAAQIAAQMCAQEBACgECAUAAQYDAAEDCAMAAQQDCAUDDAEFAAEDAQIBAhgVAwQBAgEBAAICAAEBAgEAAQIBAAICAQIBAAICAwIBBAMAAQIAAgECAAECAgEDAgEEAwABAgEBBAEABwQFAGcCAgECAAECAAEBAgoBBAABAwIFAAECAAIBAAIDAC0GAQABCAUGBwQCAQYJBAEAAQwCDRAFBQUCBAEBBAwCBAkJAQQCBgcBAgMCAw4PAgECAQICAAIDBgABAQMCAQABAgABAQABAgECAgMCAAEBAgEAAQQDBgMCBQIBAGkCAAECAQQBAAEEAQgEAQABAQMAAQIMDQIAAQMBAC8EAgICAgYJBBQbCAABDg8IFAEkPygBKzoHKwMOHQEEJBcGAw02AysIEhECAgMCEBIjHAgFHx4GHxgBFRgbHBUOERYVEA0KDxQDDQwJCA0KAQMAAQQFAGkEAQIAAgIAAQIMAwIFAQQAAgUIAwMAMgIBBgUIAwABBQoUGQUKBgkDAgQWChkAARliRwUAAQoFEBEFEAsRDhYGIQwLNCMFEgcOCwcGHCMBAAEGAgMAAQIFAQACAQIBBAIKDwIKCwACBAMAAQQAAQMAawIAAQIBAAMIAgUBBAABAQABAwA3AgYHEgABBxQRHgMIDwABFDE+OQNAOzw3QhMaDQgpKAMREhsHDgsOCAUrJgQjCxIRAAICAQABIB8AARoZFhUWERATEA8MCwIBAAEEAwgHAgEAAQIBAGwEAQECAgECCgUBBAQFAgEDAPIBBAACAQIAAQQBBAIFAgABAQEA8gECAgIAAQEAAQIEAgMAAgMA9QECAAICAAEGAAEDAgABAwD2AQIAAQECAgYBAAIDAPgBBAEAAgICAgUA+gEEAAEDBAEBAPoBAgACAgQAAQMA+wECAAICAQD8AQIBAgIBAPwBAgIBAP8BAgCBAgIA/wECAQD/AQIAgQICAIACAgCBBAIAjBoBAIIGAQCABAECAP8BAgCCBAEA/wEBAgD/AQIDAIACAQD/AQMEAP8BAQD/AQEAggICAP0BAQEEAQCAAgEA/gECAQUA/gEBAwgA/AEBAAEDBgD9AQIDAgEBAPwBAQICAwD9AQMEAwYA/AEBBAMCAAECAPsBAgMCAgABBwD7AQECAgIHBgD7AQICAwUEAQD8AQEDBgEBAP0BBAkEBAD7AQEAAQEIAwD8AQMAAgICAgD6AQUEAAEEAwABAQD6AQIBAAEDAgEBAPoBAgIDAAEDAAEEAPoBAwABBAABAQYDAPoBAgMDAggBAwD6AQMAAQYCBwEGAPoBAgQDBwYBAPsBBAUCAgECAwD6AQECAQYCBQEA+gEDBAQDBwgGAPoBBAEFAAEIAQD8AQMEAgEBAPsBAwYAAQEAAgMA/gEBAAEEAPoBBAEDAAECBAEA+gEDAAEEAgIFAQD9AQEBAgD+AQEBAgQDAgIA+AEBAwIEAAIHAgYA9wEBAAEEAAMCAAEBAAECAPYBAQQAAgIBAQECAwQA9gEEAAEBAgEAAgECAQMGAPUBAgECAAEBBAMAAQECAAEDBAD0AQIAAQIDBAMEAwIAAQIBAAECAPQBAgECAQIAAQQDAQIAAQEAAQIA8gECAAIBAgEAAQIBAAEBAgD4AQIDAgEAAQECAwYBAPQBAgEBAgAEAwQAAQMEAAEBAgDzAQIAAQEAAwQDAAEBAgD2AQIDBAABAgABAgEAAQIDAgD0AQEAAQIAAQICAgABAQIBAPcBAQICAQEA+wECAAIDAgIBAgIAAQECAQD1AQIAAQIBAgACAgEEAQEA9AEEBQICAgMAAQIBAgABAQIAAQEA8gEBBgABAQECAAECAAIBAAEBAgDzAQICAwABAgEAAQMAAQYBAAECAQD1AQICAQECAAIFBAIBAPYBAgMCAgIAAwEBAgD3AQIAAgIBAgABAQIBAPYBAgADBAEAAQEA+wECAAICAQABAgEA+AECAQQCAAEBAgEA+QECAAEBAAICAQD7AQIDAgABAQD9AQIAgAIBAIACAgCAAgIAgAICAP8BAgMAgAICAIEEAgCDCAIA5hoDAAICAQABAgIA+QEBAAMCAgEBAAECAAEDBAD0AQQAAgMBAwACAgABAgIBAAICAPUBAgABAQABAwEBAgABBgABAQIA8AEBAgECAQQCAQACAQACBwABAgQAAQIBAgDtAQMCAgIFAAECBAMCAAECAgEDAwABBAQBAOwBAwABBAUEBgEAAQEEAAEBAQABAgABAwABBQABCADrAQcCBAEEAQUEAAICAQYDAgECAgABAQMFAgYCAOgBAwIBAQABAQYCAAIBAgMEAwQCAQACAgMBAwIGAOgBAgEAAwEBAAEBAgQAAQMCAAEBBAEEAAECAQMDAQYCAOYBBAACAQACAQABAgEFAAEEAAEBAAICAAECAAECAAEBAQMGAAEBAgDjAQECAAEEAQAFAgEBAAQBAQABAgABAgIDAQMCBgECAOYBAgABAQAEAgEABAIBAQACAgABAgEDAgMGAQIA4QEBAAECAwICAAYCAQACAQICAQAFAgEBAgcIAQIA4gEBBAEAAQIABAECAgABAQABAQABBAMABAICAwEGCQgBAOABAgACAwADAgABAgECAQACAgEAAgECAAMCAQADAwQHCADfAQIAAQEAAQIBAAIDAgQEAQIAAgICAAECAAMBAAEBAAICAAIBAAEHAN8BCgABAgACAgEAAQIBBQECAQACAQACAQDinQEBAIACAQD/AQUA/wEDAAEBAP0BAQMAAQMA/AEBAwACAgECAPkBAwABAwQAAgECAPkBAwECAQABAQMEAPcBAQMCAQIBAwQDAQYA9QEBAQEAAQIDBAEBAwABBAD1AQUAAgIDBAMBAAEDAAECAgDyAQEFBAACAQABAwMEBQUEAQQA8QEDAQQAAQEBAAEHAgADAwEBAgYA7wEDAgABAwEDAAIEAQIBAAEJBAICAgDtAQEBAAEBAAEBAQIAAQUGAQEAAQIFAAQGAOwBAQMBAAEBAQQBAgIHAgIBAwcCAAEDBgICAOgBAQEDAAEBAgABAQYDAgcCBAEBAQEJAAEEAgMCBAIA5gEBAwEAAgIAAQEEAwEDBgEAAgUAAQoA6aECBAACAQIAAQEBAPkBAgADAQECAgABAQABBAMA9AEDAAIEAgQAAgEAAQEBAgACAQD1AQEAAQIAAQQCAgEAAQUAAQIBAPABAgECAQIDAQIAAgIAAggAAQEDAAEBAgEA7QEEAQEBBgABAQMEAQABAQECBAQAAQMDAgDsAQQAAQMGAwUCAAECAwABAgIAAQEAAQQAAQYAAQcA6wEIAQMCAwIGAwACAQIFBAECAQEAAQIEBgEFAQDoAQQBAgIAAQIFAQACAgEEAwQDAQIAAgEEAgQBBQDoAQECAAMCAgABAgEDAAEEAQABAgMCAwABAQIEBAIFAQDmAQMAAgIAAgIAAQECBgABAwABAgACAQABAQABAQABAgIEBQABAgEA4wECAQABAwIABQECAgAEAgIAAQEAAQEBBAIEAQUCAQDmAQEAAQIABAECAAQBAgIAAgEAAQECBAEEBQIBAOEBAgABAQQBAQAGAQIAAgIBAQIABQECAgEIBwIBAOIBAgMCAAEBAAQCAQEAAQIAAQIAAQMEAAQBAQQCBQoHAgDgAQEAAgQAAwEAAQECAQIAAgECAAICAQADAQIAAwQDCAcA3wEBAAECAAEBAgACBAEDAwIBAAIBAQABAQADAgABAgACAQACAgABCADfAQkAAQEAAgECAAEBAgYCAQIAAgIAAgIA9L8BqgERBw8JCQkHCQcJAwUFBQUDBQACAQEBAQEBAQABAQABAQEABAIAAQIAAgIBAgABAgEAAQQEAQIGBAYCAgoECAIGAgoKBAgGAgIODgABBAMDAQEBIAUCAAIHAQcCAQUBBQABAQUBAAIDAAEDAwEDAQEAAQMDAgMAAQMAAQEBAwIFAgUCAwMCAwABBQACAwABAQABBQABAQMBAAEBAwABAgEHAQACAwEBAgABAwIDAQBVAgIEBAYGCggICggKDhATBgUGAQIAAwICAQABAgACBAQBAQACAQADAgECAAICAAYBAAIDBAECAAEEAAEDAwQAAQUAAgQCCQYBBAUEAQUGAQABBAABCwABDAIBAgABAgIlIAECAAECAQQDAgEBBgIDAQQAAQEAAQEEAAEBAAECAAQBAgECAgMCAgMCAQIBAgIDBAACAwIEBQIAAQIBAgABAgECAQEAAQIAAQIAAQECAAIBAgIDAgIAVQEAAQEAAQECAQEEAQEAAQEAAQ8AAQIAAQIBAgABAgEAAQIBAgIGAwACAQABAgADAgABAgACAgAFAQAEAgEAAQEBAQIEAQEAAQIBBQABBAABBwQAAQQBAwQFBAACAgILAgQIAAIBAAECGRoDBAEABAIBAgUEAgAEAgEABAIFBAABAQQBAAEBBAEAAgIAAgMGAQMAAQIEAQAEAQQDAAEEAQEAAQYBAQcIAAICAQIAAgMCAgIAVwEBAAECAQEAAgMGAQEAAQ0AAQIAAwIAAQMEAAIGAgEBAAEBAAECAgMEAgIAFgEDAgADAQIFAQgBBwICAAECBQIBAAEBBgACCwIKAAEEAQACAh0aAQYBAQICAAECAQADAQIBAgMAAQYDAgAEAgABAwQBAAECAAMCAwICBQQAAgEAAQQDAgIDBAECAQABAQQAAQMEAQAFBAEAAgIAXAMAAwIFBAMCAAEDDQIBAgABBAMBBgMIAgEFBAECAgADBAAdAQEAAgIFBQgAAQUCAAECAAEBAQABAgABAwYAAQkEBgIAAwEAAQIDAgABBAEAAQECAAMCAQACAQQAAQECAQABAgIDAAQCAQQDAgEAAgIAAwIBAQABAgACAgABAwQAAQEAAQQBAQIABgEAAQICAF8DAQACAgABAQACAQsAAgIBAQQAAQIGAQEAAwIAAgICACIBAQABAQMGAQUGAwIAAgEAAQEEAAIDAQQCBAABAQACGxwCAQACAQQDAAIEAAEBAAECAQACAQQAAQEBAgACBAECBQQDBAMGAQACAgABAQIDAgABAgABBQQCAQEEAAEBAAECAQIBAAECAAMCAGIBAAIDAQICAQACDQABBgMCAgMIAgMAAQEAAQQBAgICACgDBwYAAQUEAAEEBwQAAQMEBwYECwIGAAEEAgECARcWBAMCAQICAAIBAAECAQIAAQECAAIBAgECAAIBBAECAwQAAQEAAgIBAAEBAQIEAQIDBAABAQICAwIABwMGAGcBAQIBAAEBAAECAQkCAwABBAEGAAEBAAICAAIEAC0FAgABBwYFCAMBAgUKAwIAAQsBDg8GBgYBAwICAwsBAwoKAgMBBQgCAQQBBA0QAQIBAgEBAAIEBQABAgQBAgABAQABAgABAQIBAQQBAAECAQIAAQMEBQQBBgECAGkBAAEBAgMCAAEDAgcDAgABAgQAAQELDgEAAQQCAC8DAQEBAQUKAxMcBwABDRAHEwIjQCcCLDkILAQNHgIDIxgFBA41BCwHERIBAQQBDxEkGwcGIB0FIBcCFhccGxYNEhUWDw4JEBMEDgsKBw4JAgQAAQMGAGkDAgEAAgEAAQELBAEGAgMAAgYHBAQAMgECBQYHBAABBgkTGgYJBQoEAQMVCRoAARphSAYAAQkGDxIGDwwSDRUFIgsMMyQGEQgNDAgFGyQCAAEFAQQAAQEGAgACAgECAwEJEAEJDAACAwQAAQMAAQQAawEAAQECAAMHAQYCAwABAgABBAA3AQUIEQABCBMSHQQHEAABEzI9OgQ/PDs4QRQZDgcqJwQSERwIDQwNBwYsJQMkDBESAAIBAgABHyAAARkaFRYVEg8UDxALDAECAAEDBAcIAQIAAQECAGwDAgIBAQIBCQYCAwMGAQIEAPIBAwACAgEAAQMCAwEGAQABAgIA8gEBAQEAAQIAAQEDAQQAAgQA9QEBAAIBAAEFAAEEAQABBAD2AQEAAQIBAQUCAAIEAPgBAwIAAgEBAQYA+gEDAAEEAwICAPoBAQACAQMAAQQA+wEBAAIBAgD8AQECAQECAPwBAQECAP8BAQCBAgEA/wEBAgD/AQEAgQIBAIACAQCBBAEAjBoCAIIGAgCABAIBAP8BAQCCBAIA/wECAQD/AQEEAIACAgD/AQQDAP8BAgD/AQIAggIBAP0BAgIDAgCAAgIA/gEBAgYA/gECBAcA/AECAAEEBQD9AQEEAQICAPwBAgEBBAD9AQQDBAUA/AECAwQBAAEBAPsBAQQBAQABCAD7AQIBAQEIBQD7AQEBBAYDAgD8AQIEBQICAP0BAwoDAwD7AQIAAQIHBAD8AQQAAgEBAQD6AQYDAAEDBAABAgD6AQECAAEEAQICAPoBAQEEAAEEAAEDAPoBBAABAwABAgUEAPoBAQQEAQcCBAD6AQQAAQUBCAIFAPoBAQMECAUCAPsBAwYBAQIBBAD6AQIBAgUBBgIA+gEEAwMECAcFAPoBAwIGAAEHAgD8AQQDAQICAPsBBAUAAQIAAgQA/gECAAEDAPoBAwIEAAEBAwIA+gEEAAEDAQEGAgD9AQICAQD+AQICAQMEAQEA+AECBAEDAAIIAQUA9wECAAEDAAMBAAECAAEBAPYBAgMAAgECAgIBBAMA9gEDAAECAQIAAgIBAgQFAPUBAQIBAAECAwQAAQIBAAEEAwD0AQEAAQEEAwQDBAEAAQECAAEBAPQBAQIBAgEAAQMEAgEAAQIAAQEA8gEBAAICAQIAAQECAAECAQD4AQEEAQIAAQIBBAUCAPQBAQICAQAEBAMAAQQDAAECAQDzAQEAAQIAAwMEAAECAQD2AQEEAwABAQABAQIAAQEEAQD0AQIAAQEAAQEBAQABAgECAPcBAgEBAgIA+wEBAAIEAQECAQEAAQIBAgD1AQEAAQECAQACAQIDAgIA9AEDBgEBAQQAAQECAQABAgEAAQIA8gECBQABAgIBAAEBAAICAAECAQDzAQEBBAABAQIAAQQAAQUCAAEBAgD1AQEBAgIBAAIGAwECAPYBAQQBAQEAAwICAQD3AQEAAgECAQABAgECAPYBAQADAwIAAQIA+wEBAAIBAgABAQIA+AEBAgMBAAECAQIA+QEBAAECAAIBAgD7AQEEAQABAgD9AQEAgAICAIACAQCAAgEAgAIBAP8BAQQAgAIBAIEEAQCDCAEA7NoBAgCAAgIA/wEGAP8BBAABAgD9AQIEAAEEAPwBAgQAAgECAQD5AQQAAQQDAAICAQD5AQQCAQIAAQIEAwD3AQIEAQIBAgQDBAIFAPUBAgICAAEBBAMCAgQAAQMA9QEGAAIBBAMEAgABBAABAQEA8gECBgMAAgIAAQQEAwYGAwIDAPEBBAIDAAECAgABCAEAAwQCAgEFAO8BBAEAAQQCBAACAwIBAgABCgMBAQEA7QECAgABAgABAgIBAAEGBQICAAEBBgAEBQDsAQIEAgABAgIDAgEBCAEBAgQIAQABBAUBAQDoAQICBAABAgEAAQIFBAEIAQMCAgICCgABAwEEAQMBAOYBAgQCAAIBAAECAwQCBAUCAAIGAAEJAIGEBACBhAQAgYQEAIGEBACBhAQ=", + "Layer1Texture": "assets/StartingGround.png", + "Layer1Color": "255;255;255", + "HeightBlending": 0.5, + "TextureVariation": 0, + "TextureSize": 128, + "CliffLayer": "3", + "Layer2Texture": "", + "Layer3Texture": "", + "CliffAngle": 45, + "CastShadow": false + } } ], "objectsFolderStructure": { @@ -2216,7 +2213,7 @@ "objectName": "Obstacle" }, { - "objectName": "Ground" + "objectName": "Terrain" }, { "objectName": "Log" @@ -9803,7 +9800,6 @@ "name": "PlatformerMultitouchMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -10057,7 +10053,6 @@ "name": "Platformer3DMultitouchMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -10232,7 +10227,6 @@ "name": "Shooter3DMultitouchMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -10396,7 +10390,6 @@ "name": "FirstPersonMultitouchMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -11601,7 +11594,6 @@ "name": "PhysicsCar3DMultitouchMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -11814,7 +11806,6 @@ "name": "TopDownMultitouchMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -20550,7 +20541,6 @@ "name": "Tween3D", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -23787,7 +23777,6 @@ "name": "Wobble", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -34001,7 +33990,6 @@ "name": "ShakeModel3D", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -35557,6 +35545,7802 @@ } ], "eventsBasedObjects": [] + }, + { + "author": "", + "category": "General", + "dimension": "3D", + "extensionNamespace": "", + "fullName": "3D terrain", + "gdevelopVersion": ">=5.5.222", + "helpPath": "", + "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLXRlcnJhaW4iIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTQsNkwxMC4yNSwxMUwxMy4xLDE0LjhMMTEuNSwxNkM5LjgxLDEzLjc1IDcsMTAgNywxMEwxLDE4SDIzTDE0LDZaIiAvPjwvc3ZnPg==", + "name": "Terrain3D", + "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", + "shortDescription": "A ground with hills and valleys that can be sculpted and painted in the editor, with collisions.", + "version": "0.1.0", + "description": [ + "A 3D terrain object for open worlds, adventure or racing games: hills, mountains, islands or valleys, with painted layers of grass, dirt, rock or sand.", + "", + "- **Sculpt and paint in the scene editor**: select a terrain to show the tools (raise, lower, smooth, flatten and paint 4 layers).", + "- **Start from a relief** (hills, mountains, island) or **from a heightmap image**.", + "- **Steep slopes automatically show rock** (or any layer).", + "- **Natural borders between layers**: stones and tufts of a texture cover the gaps of the other where layers meet (height blending), and the repetition of textures is made less visible (texture variation).", + "- **Physics**: add the **3D physics** behavior to the terrain. Characters and objects collide with the ground exactly. The terrain is always static.", + "- **Pathfinding**: add the **floor/obstacle for pathfinding (navmesh based)** behavior to the terrain, so that characters with the navmesh pathfinding behavior walk on its hills and around its cliffs (in recent GDevelop versions).", + "- **Shape it with the Edits property**: a list of raise, flatten, smooth and paint strokes written in JSON, shown in the scene editor. It's readable and can be changed by hand or by an AI agent, like the relief, the heightmap and the other properties. Actions only change the terrain during the game.", + "- **Change it during the game** with actions (raise, lower, flatten, smooth, paint, along a line for paths and rivers) and read the ground height, slope or layer with expressions.", + "- **Grass**: add a **terrain grass** object over the terrain. Thousands of blades sway in the wind, grow where a layer (like grass) is painted and where the ground is not too steep, and are pushed aside by characters. Sculpt or paint the terrain and the grass follows.", + "- **Forests, rocks and bushes**: add a **scattered models** object over the terrain. It places hundreds or thousands of copies of a 3D model (trees, rocks, bushes, flowers...) on the ground and draws them all at once, which is much faster than an object for each. Copies grow where a layer is painted, away from steep slopes, and follow the ground when it's sculpted. Add or erase them with the brushes of the scene editor or with the Edits property, give trees trunk collisions, and cut them down with actions. Ready-made forests, rocks and bushes are in the asset store.", + "- **Tunnels and caves**: make holes in the ground (and fill them again) with the brushes, the Edits property or actions, and put a 3D model of the tunnel or the cave (with a mesh collision shape) under the ground. Nothing is drawn in holes, and characters, cars and rays go through them.", + "- **Made for large worlds**: the terrain is split in chunks that are hidden when off-screen and simplified when far away, and collisions are updated only where the ground changes." + ], + "origin": { + "identifier": "Terrain3D", + "name": "gdevelop-extension-store" + }, + "tags": [ + "3d", + "terrain", + "ground", + "heightmap", + "landscape", + "open world", + "grass", + "foliage", + "forest", + "trees", + "tunnel", + "cave" + ], + "authorIds": [], + "dependencies": [], + "globalVariables": [], + "sceneVariables": [], + "eventsFunctions": [ + { + "description": "Define helper classes JavaScript code.", + "fullName": "Define helper classes", + "functionType": "Action", + "name": "DefineHelperClasses", + "private": true, + "sentence": "Define helper classes JavaScript code", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "if (gdjs.__terrain3DExtension) {", + " return;", + "}", + "", + "// Tested by the gameplay tests of the \"3D island terrain\" example (GDevelop-examples repository).", + "", + "const groundsByScene = new WeakMap();", + "/**", + " * The grounds of a scene (the terrains), followed by the terrain grass.", + " *", + " * A ground has: `getVersion()` (changing when the ground changes),", + " * `containsPoint(x, y)`, `getHeightAt(x, y)` (Z position of the ground),", + " * `getSlopeAt(x, y)` (in degrees), `getLayerWeightAt(x, y, layer)` (how", + " * much a painted layer, from 1 to 4, is there, from 0 to 1) and optionally", + " * `getTintAt(x, y)` (the painted tint, as red, green and blue from 0 to 255).", + " * @param {gdjs.RuntimeScene} runtimeScene", + " * @returns {Set}", + " */", + "const getGrounds = (runtimeScene) => {", + " let grounds = groundsByScene.get(runtimeScene);", + " if (!grounds) {", + " grounds = new Set();", + " groundsByScene.set(runtimeScene, grounds);", + " }", + " return grounds;", + "};", + "", + "/**", + " * @param {number} seed", + " * @returns {() => number} Random numbers between 0 (included) and 1 (excluded), the same for the same seed.", + " */", + "const createRandom = (seed) => {", + " let state = seed >>> 0 || 1;", + " return () => {", + " state ^= state << 13;", + " state ^= state >>> 17;", + " state ^= state << 5;", + " return (state >>> 0) / 4294967296;", + " };", + "};", + "", + "/**", + " * Find the highest ground at a position: grounds can overlap (like an island over a sea floor).", + " * @param {Iterable} grounds", + " * @param {{ground: object | null, z: number}} result Set to the ground (null if there is none) and its Z position.", + " */", + "const findHighestGround = (grounds, x, y, result) => {", + " result.ground = null;", + " for (const ground of grounds) {", + " if (!ground.containsPoint(x, y)) continue;", + " const z = ground.getHeightAt(x, y);", + " if (!result.ground || z > result.z) {", + " result.ground = ground;", + " result.z = z;", + " }", + " }", + " return result;", + "};", + "", + "// The grounds or the objects placed on them can change every frame (while", + "// sculpting or moving them in the editor): things are placed again at most this often.", + "const PLACEMENT_DELAY = 0.3;", + "", + "/**", + " * Tells when things placed on the grounds of a scene (grass blades, scattered", + " * models) must be placed again: after the grounds or the given properties", + " * changed, at most every `PLACEMENT_DELAY` seconds.", + " */", + "class GroundPlacement {", + " /** @param {gdjs.RuntimeScene} runtimeScene */", + " constructor(runtimeScene) {", + " this.grounds = getGrounds(runtimeScene);", + " this.placedFrom = '';", + " this.timeSincePlacement = 0;", + " }", + "", + " /**", + " * @param {number} elapsedSeconds", + " * @param {string} propertiesKey Changes when the things must be placed again.", + " */", + " shouldPlaceAgain(elapsedSeconds, propertiesKey) {", + " this.timeSincePlacement += elapsedSeconds;", + " if (this.placedFrom && this.timeSincePlacement < PLACEMENT_DELAY) return false;", + " let placementKey = propertiesKey;", + " for (const ground of this.grounds) placementKey += '|' + ground.getVersion();", + " if (placementKey === this.placedFrom) return false;", + " this.placedFrom = placementKey;", + " this.timeSincePlacement = 0;", + " return true;", + " }", + "", + " placeAgain() {", + " this.placedFrom = '';", + " }", + "}", + "", + "/**", + " * Replace parts of the shader code of a Three.js material, with an error if", + " * a part is missing (for example after an update of Three.js).", + " * @param {string} shaderCode", + " * @param {Array<[string, string]>} replacements The searched code and its replacement.", + " * @param {string} materialName", + " */", + "const patchShaderCode = (shaderCode, replacements, materialName) => {", + " for (const [searchedCode, newCode] of replacements) {", + " if (!shaderCode.includes(searchedCode)) {", + " console.error(`${materialName}: \"${searchedCode}\" was not found in the shader, which won't look as expected.`);", + " }", + " shaderCode = shaderCode.replace(searchedCode, newCode);", + " }", + " return shaderCode;", + "};", + "", + "// Size of the object inner area. Instances are scaled from it, so these only", + "// define the default size of a new terrain.", + "const AREA_SIZE = 4096;", + "const AREA_DEPTH = 600;", + "", + "// Terrains have at most 8 x 8 chunks (so at most 64 draw calls), of at least", + "// 32 x 32 cells.", + "const MIN_CHUNK_CELLS = 32;", + "const MAX_CHUNKS_PER_SIDE = 8;", + "/** @param {number} resolution */", + "const getChunkCells = (resolution) => Math.max(MIN_CHUNK_CELLS, resolution / MAX_CHUNKS_PER_SIDE);", + "// Grid steps of each level of detail, and the distance (in chunk sizes)", + "// from which each one is used.", + "const LOD_STEPS = [1, 2, 4];", + "const LOD_DISTANCES = [0, 3, 6];", + "", + "const LAYER_COUNT = 4;", + "const RESOLUTIONS = [64, 128, 256, 512, 1024];", + "const DEFAULT_RESOLUTION = 256;", + "// Each undo step is a copy of the data: fewer are kept for big terrains.", + "const UNDO_STEPS_SAMPLES = 20 * 257 * 257;", + "", + "const clamp01 = (value) => (value < 0 ? 0 : value > 1 ? 1 : value);", + "const clampInteger = (value, min, max) =>", + " value < min ? min : value > max ? max : Math.floor(value);", + "", + "/**", + " * A rectangle of grid samples, inclusive. Empty when `minI > maxI`.", + " */", + "class SampleRectangle {", + " constructor() {", + " this.clear();", + " }", + "", + " clear() {", + " this.minI = Number.MAX_SAFE_INTEGER;", + " this.minJ = Number.MAX_SAFE_INTEGER;", + " this.maxI = -1;", + " this.maxJ = -1;", + " }", + "", + " isEmpty() {", + " return this.minI > this.maxI;", + " }", + "", + " /** @param {SampleRectangle} other */", + " add(other) {", + " if (other.isEmpty()) return;", + " this.minI = Math.min(this.minI, other.minI);", + " this.minJ = Math.min(this.minJ, other.minJ);", + " this.maxI = Math.max(this.maxI, other.maxI);", + " this.maxJ = Math.max(this.maxJ, other.maxJ);", + " }", + "", + " set(minI, minJ, maxI, maxJ) {", + " this.minI = minI;", + " this.minJ = minJ;", + " this.maxI = maxI;", + " this.maxJ = maxJ;", + " return this;", + " }", + "}", + "", + "/**", + " * The heights (between 0 and 1) and the painted layers weights of a square", + " * grid of `size * size` samples.", + " */", + "class TerrainData {", + " /** @param {number} resolution The number of cells on each side. */", + " constructor(resolution) {", + " this.resolution = resolution;", + " this.size = resolution + 1;", + " this.heights = new Float32Array(this.size * this.size);", + " /** 4 weights (0-255) per sample, one for each layer. */", + " this.splat = new Uint8Array(this.size * this.size * LAYER_COUNT);", + " this.fillLayer(0);", + " /**", + " * 4 bytes per sample: a tint multiplying the layers (red, green and", + " * blue, white for none) and the wetness (0 for dry).", + " */", + " this.colors = new Uint8Array(this.size * this.size * 4);", + " this.clearColors();", + " }", + "", + " clearColors() {", + " this.colors.fill(255);", + " for (let index = 3; index < this.colors.length; index += 4) {", + " this.colors[index] = 0;", + " }", + " }", + "", + " /** @param {number} layer */", + " fillLayer(layer) {", + " this.splat.fill(0);", + " for (let index = layer; index < this.splat.length; index += LAYER_COUNT) {", + " this.splat[index] = 255;", + " }", + " }", + "", + " getHeight(i, j) {", + " const last = this.resolution;", + " const clampedI = i < 0 ? 0 : i > last ? last : i;", + " const clampedJ = j < 0 ? 0 : j > last ? last : j;", + " return this.heights[clampedJ * this.size + clampedI];", + " }", + "", + " /**", + " * @param {number} gridX", + " * @param {number} gridY", + " * @returns {number} The height interpolated on the triangles that are drawn.", + " */", + " sampleHeight(gridX, gridY) {", + " const x = Math.min(Math.max(gridX, 0), this.resolution);", + " const y = Math.min(Math.max(gridY, 0), this.resolution);", + " const i = Math.min(Math.floor(x), this.resolution - 1);", + " const j = Math.min(Math.floor(y), this.resolution - 1);", + " const fx = x - i;", + " const fy = y - j;", + " const topRight = this.getHeight(i + 1, j);", + " const bottomLeft = this.getHeight(i, j + 1);", + " if (fx + fy <= 1) {", + " const topLeft = this.getHeight(i, j);", + " return topLeft + fx * (topRight - topLeft) + fy * (bottomLeft - topLeft);", + " }", + " const bottomRight = this.getHeight(i + 1, j + 1);", + " return (", + " bottomRight +", + " (1 - fx) * (bottomLeft - bottomRight) +", + " (1 - fy) * (topRight - bottomRight)", + " );", + " }", + "", + " /** @returns {number} The weight (0-1) of a layer, interpolated between samples. */", + " sampleLayerWeight(gridX, gridY, layer) {", + " const x = Math.min(Math.max(gridX, 0), this.resolution);", + " const y = Math.min(Math.max(gridY, 0), this.resolution);", + " const i = Math.min(Math.floor(x), this.resolution - 1);", + " const j = Math.min(Math.floor(y), this.resolution - 1);", + " const fx = x - i;", + " const fy = y - j;", + " const weight = (i, j) => this.splat[(j * this.size + i) * LAYER_COUNT + layer];", + " const top = weight(i, j) + fx * (weight(i + 1, j) - weight(i, j));", + " const bottom = weight(i, j + 1) + fx * (weight(i + 1, j + 1) - weight(i, j + 1));", + " return (top + fy * (bottom - top)) / 255;", + " }", + "", + " /**", + " * @param {number} channel 0, 1 and 2 for the tint, 3 for the wetness.", + " * @returns {number} The channel (0-1), interpolated between samples.", + " */", + " sampleColor(gridX, gridY, channel) {", + " const x = Math.min(Math.max(gridX, 0), this.resolution);", + " const y = Math.min(Math.max(gridY, 0), this.resolution);", + " const i = Math.min(Math.floor(x), this.resolution - 1);", + " const j = Math.min(Math.floor(y), this.resolution - 1);", + " const fx = x - i;", + " const fy = y - j;", + " const value = (i, j) => this.colors[(j * this.size + i) * 4 + channel];", + " const top = value(i, j) + fx * (value(i + 1, j) - value(i, j));", + " const bottom = value(i, j + 1) + fx * (value(i + 1, j + 1) - value(i, j + 1));", + " return (top + fy * (bottom - top)) / 255;", + " }", + "", + " /**", + " * @param {number} index The index of a sample.", + " * @returns {boolean} true if no layer is painted on the sample: it's a hole.", + " */", + " isHole(index) {", + " const offset = index * LAYER_COUNT;", + " return (this.splat[offset] | this.splat[offset + 1] | this.splat[offset + 2] | this.splat[offset + 3]) === 0;", + " }", + "", + " /**", + " * @returns {number} The sum of the layer weights (0-1), interpolated between samples: there is a", + " * hole where it's under 0.5, like what is drawn.", + " */", + " sampleSolidity(gridX, gridY) {", + " let solidity = 0;", + " for (let layer = 0; layer < LAYER_COUNT; layer++) solidity += this.sampleLayerWeight(gridX, gridY, layer);", + " return solidity;", + " }", + "", + " /** @returns {number} The index (from 0) of the layer painted the most. */", + " getDominantLayer(gridX, gridY) {", + " const i = clampInteger(Math.round(gridX), 0, this.resolution);", + " const j = clampInteger(Math.round(gridY), 0, this.resolution);", + " const offset = (j * this.size + i) * LAYER_COUNT;", + " let dominantLayer = 0;", + " for (let layer = 1; layer < LAYER_COUNT; layer++) {", + " if (this.splat[offset + layer] > this.splat[offset + dominantLayer]) {", + " dominantLayer = layer;", + " }", + " }", + " return dominantLayer;", + " }", + "", + " /** @param {TerrainData} other */", + " copyFrom(other) {", + " this.heights.set(other.heights);", + " this.splat.set(other.splat);", + " this.colors.set(other.colors);", + " }", + "", + " /**", + " * @param {number} resolution", + " * @returns {TerrainData} A copy of this data with another resolution.", + " */", + " resampled(resolution) {", + " const data = new TerrainData(resolution);", + " const ratio = this.resolution / resolution;", + " for (let j = 0; j < data.size; j++) {", + " for (let i = 0; i < data.size; i++) {", + " const index = j * data.size + i;", + " data.heights[index] = this.sampleHeight(i * ratio, j * ratio);", + " const sourceOffset =", + " (Math.round(j * ratio) * this.size + Math.round(i * ratio)) * LAYER_COUNT;", + " for (let layer = 0; layer < LAYER_COUNT; layer++) {", + " data.splat[index * LAYER_COUNT + layer] = this.splat[sourceOffset + layer];", + " }", + " for (let channel = 0; channel < 4; channel++) {", + " data.colors[index * 4 + channel] = this.colors[sourceOffset + channel];", + " }", + " }", + " }", + " return data;", + " }", + "}", + "", + "/**", + " * Brushes change the samples around a segment, with an effect going smoothly", + " * from full on the segment to none at `radius`. A point is a segment of", + " * length 0. Positions are in scene units relative to the terrain origin.", + " */", + "const brushes = {", + " /**", + " * @param {TerrainData} data", + " * @param {{cellWidth: number, cellHeight: number, ax: number, ay: number, bx: number, by: number, radius: number}} stroke", + " * @param {(sampleIndex: number, weight: number, t: number) => void} apply Called with the weight of", + " * the brush and the position (from 0 to 1) of the nearest point of the segment.", + " * @param {SampleRectangle} changedSamples", + " */", + " forEachSample(data, stroke, apply, changedSamples) {", + " const { cellWidth, cellHeight, ax, ay, bx, by, radius } = stroke;", + " const last = data.resolution;", + " const minI = clampInteger(Math.floor((Math.min(ax, bx) - radius) / cellWidth), 0, last);", + " const maxI = clampInteger(Math.ceil((Math.max(ax, bx) + radius) / cellWidth), 0, last);", + " const minJ = clampInteger(Math.floor((Math.min(ay, by) - radius) / cellHeight), 0, last);", + " const maxJ = clampInteger(Math.ceil((Math.max(ay, by) + radius) / cellHeight), 0, last);", + " changedSamples.set(minI, minJ, maxI, maxJ);", + " if (radius <= 0) return;", + "", + " const abX = bx - ax;", + " const abY = by - ay;", + " const abLengthSquared = abX * abX + abY * abY;", + " const radiusSquared = radius * radius;", + " for (let j = minJ; j <= maxJ; j++) {", + " const y = j * cellHeight;", + " for (let i = minI; i <= maxI; i++) {", + " const x = i * cellWidth;", + " const t =", + " abLengthSquared > 0", + " ? clamp01(((x - ax) * abX + (y - ay) * abY) / abLengthSquared)", + " : 0;", + " const dx = x - (ax + t * abX);", + " const dy = y - (ay + t * abY);", + " const distanceSquared = dx * dx + dy * dy;", + " if (distanceSquared >= radiusSquared) continue;", + " const falloff = 1 - distanceSquared / radiusSquared;", + " apply(j * data.size + i, falloff * falloff, t);", + " }", + " }", + " },", + "", + " raise(data, stroke, amount, changedSamples) {", + " const { heights } = data;", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index, weight) => {", + " heights[index] = clamp01(heights[index] + amount * weight);", + " },", + " changedSamples", + " );", + " },", + "", + " /**", + " * @param {number} targetHeight", + " * @param {number} [endTargetHeight] The target at the end of the segment, for a slope (like a road", + " * going up). The same target as at its start by default.", + " */", + " flatten(data, stroke, targetHeight, strength, changedSamples, endTargetHeight = targetHeight) {", + " const { heights } = data;", + " const startTarget = clamp01(targetHeight);", + " const endTarget = clamp01(endTargetHeight);", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index, weight, t) => {", + " const target = startTarget + (endTarget - startTarget) * t;", + " heights[index] += (target - heights[index]) * clamp01(weight * strength);", + " },", + " changedSamples", + " );", + " },", + "", + " smooth(data, stroke, strength, changedSamples) {", + " const { heights, size } = data;", + " const changes = [];", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index, weight) => {", + " const i = index % size;", + " const j = (index - i) / size;", + " let sum = 0;", + " for (let dj = -1; dj <= 1; dj++) {", + " for (let di = -1; di <= 1; di++) {", + " sum += data.getHeight(i + di, j + dj);", + " }", + " }", + " changes.push(index, heights[index] + (sum / 9 - heights[index]) * clamp01(weight * strength));", + " },", + " changedSamples", + " );", + " // Written after all averages are computed, so that the result", + " // doesn't depend on the order in which samples are visited.", + " for (let index = 0; index < changes.length; index += 2) {", + " heights[changes[index]] = changes[index + 1];", + " }", + " },", + "", + " paint(data, stroke, layer, strength, changedSamples) {", + " const { splat } = data;", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index, weight) => {", + " const amount = clamp01(weight * strength);", + " const offset = index * LAYER_COUNT;", + " for (let otherLayer = 0; otherLayer < LAYER_COUNT; otherLayer++) {", + " const target = otherLayer === layer ? 255 : 0;", + " const value = splat[offset + otherLayer];", + " // Rounded towards the target, so that light strokes still paint.", + " const change = (target - value) * amount;", + " splat[offset + otherLayer] = value + (change > 0 ? Math.ceil(change) : Math.floor(change));", + " }", + " },", + " changedSamples", + " );", + " },", + "", + " /** Remove all the layers, making a hole (`fill` or painting a layer fills it again). */", + " hole(data, stroke, changedSamples) {", + " const { splat } = data;", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index) => {", + " splat.fill(0, index * LAYER_COUNT, (index + 1) * LAYER_COUNT);", + " },", + " changedSamples", + " );", + " },", + "", + " /**", + " * Fill the holes in the radius with the layers of the ground around them:", + " * each pass fills the holes next to the ground with the average of their", + " * neighbors, from the edges of the holes inward.", + " */", + " fill(data, stroke, changedSamples) {", + " const { splat, size, resolution } = data;", + " /** @type {number[]} */", + " let holes = [];", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index) => {", + " if (data.isHole(index)) holes.push(index);", + " },", + " changedSamples", + " );", + " const filledWeights = new Float32Array(holes.length * LAYER_COUNT);", + " while (holes.length > 0) {", + " /** @type {number[]} */", + " const remainingHoles = [];", + " /** @type {number[]} */", + " const filledHoles = [];", + " for (const index of holes) {", + " const i = index % size;", + " const j = (index - i) / size;", + " const weights = filledWeights.subarray(filledHoles.length * LAYER_COUNT, (filledHoles.length + 1) * LAYER_COUNT);", + " weights.fill(0);", + " for (let neighborJ = Math.max(j - 1, 0); neighborJ <= Math.min(j + 1, resolution); neighborJ++) {", + " for (let neighborI = Math.max(i - 1, 0); neighborI <= Math.min(i + 1, resolution); neighborI++) {", + " const neighbor = neighborJ * size + neighborI;", + " if (data.isHole(neighbor)) continue;", + " for (let layer = 0; layer < LAYER_COUNT; layer++) weights[layer] += splat[neighbor * LAYER_COUNT + layer];", + " }", + " }", + " if (weights[0] + weights[1] + weights[2] + weights[3] > 0) filledHoles.push(index);", + " else remainingHoles.push(index);", + " }", + " if (filledHoles.length === 0) {", + " // No ground around (a hole bigger than the brush): the first layer is used.", + " for (const index of remainingHoles) splat[index * LAYER_COUNT] = 255;", + " break;", + " }", + " // Filled after the pass, so that the holes are filled from the edges inward.", + " filledHoles.forEach((index, filledIndex) => {", + " const weights = filledWeights.subarray(filledIndex * LAYER_COUNT, (filledIndex + 1) * LAYER_COUNT);", + " const total = weights[0] + weights[1] + weights[2] + weights[3];", + " for (let layer = 0; layer < LAYER_COUNT; layer++) {", + " splat[index * LAYER_COUNT + layer] = Math.round((weights[layer] / total) * 255);", + " }", + " });", + " holes = remainingHoles;", + " }", + " },", + "", + " /** @param {number[]} color The red, green and blue (0-255) of the tint: white removes it. */", + " tint(data, stroke, color, strength, changedSamples) {", + " brushes.paintColors(data, stroke, 0, color, strength, changedSamples);", + " },", + "", + " /** @param {number} wetness From 0 (dry) to 1. */", + " wet(data, stroke, wetness, strength, changedSamples) {", + " brushes.paintColors(data, stroke, 3, [Math.round(clamp01(wetness) * 255)], strength, changedSamples);", + " },", + "", + " /**", + " * Bring channels of the colors (see `TerrainData.colors`) towards targets.", + " * @param {number} firstChannel", + " * @param {number[]} targets From 0 to 255, for the channels from the first one.", + " */", + " paintColors(data, stroke, firstChannel, targets, strength, changedSamples) {", + " const { colors } = data;", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index, weight) => {", + " const amount = clamp01(weight * strength);", + " for (let channel = 0; channel < targets.length; channel++) {", + " const offset = index * 4 + firstChannel + channel;", + " // Rounded towards the target, so that light strokes still paint.", + " const change = (targets[channel] - colors[offset]) * amount;", + " colors[offset] += change > 0 ? Math.ceil(change) : Math.floor(change);", + " }", + " },", + " changedSamples", + " );", + " },", + "};", + "", + "const relief = (() => {", + " const hash = (x, y, seed) => {", + " let h = Math.imul(x, 374761393) + Math.imul(y, 668265263) + Math.imul(seed, 982451653);", + " h = Math.imul(h ^ (h >>> 13), 1274126177);", + " return ((h ^ (h >>> 16)) >>> 0) / 4294967296;", + " };", + " const valueNoise = (x, y, seed) => {", + " const x0 = Math.floor(x);", + " const y0 = Math.floor(y);", + " const fx = x - x0;", + " const fy = y - y0;", + " const sx = fx * fx * (3 - 2 * fx);", + " const sy = fy * fy * (3 - 2 * fy);", + " const top = hash(x0, y0, seed) + sx * (hash(x0 + 1, y0, seed) - hash(x0, y0, seed));", + " const bottom =", + " hash(x0, y0 + 1, seed) + sx * (hash(x0 + 1, y0 + 1, seed) - hash(x0, y0 + 1, seed));", + " return top + sy * (bottom - top);", + " };", + " /** Fractal noise between 0 and 1. */", + " const fractalNoise = (x, y, seed) => {", + " let value = 0;", + " let amplitude = 0.5;", + " let frequency = 1;", + " for (let octave = 0; octave < 5; octave++) {", + " value += amplitude * valueNoise(x * frequency, y * frequency, seed + octave);", + " amplitude *= 0.5;", + " frequency *= 2;", + " }", + " return value / 0.96875;", + " };", + " const smoothstep = (edge0, edge1, x) => {", + " const t = clamp01((x - edge0) / (edge1 - edge0));", + " return t * t * (3 - 2 * t);", + " };", + " const reliefHeights = {", + " Flat: () => 0,", + " Hills: (u, v, seed) => 0.05 + 0.5 * Math.pow(fractalNoise(u * 3, v * 3, seed), 2),", + " Mountains: (u, v, seed) => {", + " const ridge = 1 - Math.abs(2 * fractalNoise(u * 2.5, v * 2.5, seed) - 1);", + " return 0.9 * Math.pow(ridge, 2.5);", + " },", + " Island: (u, v, seed) => {", + " const distanceToCenter = 2 * Math.hypot(u - 0.5, v - 0.5);", + " const coast = 0.2 * (fractalNoise(u * 4, v * 4, seed + 7) - 0.5);", + " const land = smoothstep(0.95, 0.55, distanceToCenter + coast);", + " return land * (0.12 + 0.6 * Math.pow(fractalNoise(u * 3, v * 3, seed), 1.5));", + " },", + " };", + " const beachLayer = 3;", + "", + " return {", + " /**", + " * @param {TerrainData} data", + " * @param {string} reliefName Flat, Hills, Mountains or Island.", + " * @param {number} seed", + " */", + " generate(data, reliefName, seed) {", + " const getHeight = reliefHeights[reliefName] || reliefHeights.Flat;", + " const integerSeed = Math.floor(seed) | 0;", + " data.fillLayer(0);", + " for (let j = 0; j < data.size; j++) {", + " for (let i = 0; i < data.size; i++) {", + " const index = j * data.size + i;", + " const height = clamp01(", + " getHeight(i / data.resolution, j / data.resolution, integerSeed)", + " );", + " data.heights[index] = height;", + " if (reliefName === 'Island') {", + " const sand = Math.round(255 * smoothstep(0.16, 0.1, height));", + " const offset = index * LAYER_COUNT;", + " data.splat[offset] = 255 - sand;", + " data.splat[offset + beachLayer] = sand;", + " }", + " }", + " }", + " },", + " };", + "})();", + "", + "/**", + " * Edits written in the Edits property: a JSON list of brush strokes, like", + " * `[{\"tool\": \"raise\", \"x\": 0.5, \"y\": 0.5, \"radius\": 0.2, \"height\": 0.3}]`,", + " * applied on the relief or the heightmap. Positions and radius are fractions", + " * of the terrain size, heights and Z positions fractions of its depth: they", + " * are the same for all instances, whatever their size. They are made to be", + " * read and written by people and by AI agents.", + " */", + "const edits = {", + " /**", + " * @param {TerrainData} data", + " * @param {string} text", + " */", + " apply(data, text) {", + " if (!text.trim()) return;", + " let editList;", + " try {", + " editList = JSON.parse(text);", + " } catch (error) {", + " console.warn('The edits of a terrain are not valid JSON: ' + error.message);", + " return;", + " }", + " if (!Array.isArray(editList)) {", + " console.warn('The edits of a terrain must be a JSON list (like [{\"tool\": \"raise\", ...}]).');", + " return;", + " }", + " const numberOr = (value, defaultValue) => (typeof value === 'number' && isFinite(value) ? value : defaultValue);", + " const changedSamples = new SampleRectangle();", + " for (const edit of editList) {", + " if (!edit || typeof edit !== 'object') continue;", + " const x = numberOr(edit.x, 0.5);", + " const y = numberOr(edit.y, 0.5);", + " const stroke = {", + " cellWidth: 1 / data.resolution,", + " cellHeight: 1 / data.resolution,", + " ax: x,", + " ay: y,", + " bx: numberOr(edit.toX, x),", + " by: numberOr(edit.toY, y),", + " radius: numberOr(edit.radius, 0.1),", + " };", + " const strength = clamp01(numberOr(edit.strength, 1));", + " if (edit.tool === 'raise') {", + " brushes.raise(data, stroke, numberOr(edit.height, 0.1), changedSamples);", + " } else if (edit.tool === 'flatten') {", + " const z = numberOr(edit.z, 0);", + " brushes.flatten(data, stroke, z, strength, changedSamples, numberOr(edit.toZ, z));", + " } else if (edit.tool === 'smooth') {", + " // Smoothing the same fraction of the terrain at any resolution.", + " const passes = Math.max(Math.round(((1 + 9 * strength) * data.resolution) / 256), 1);", + " for (let pass = 0; pass < passes; pass++) brushes.smooth(data, stroke, 1, changedSamples);", + " } else if (edit.tool === 'paint') {", + " const layer = clampInteger(numberOr(edit.layer, 1) - 1, 0, LAYER_COUNT - 1);", + " brushes.paint(data, stroke, layer, strength, changedSamples);", + " } else if (edit.tool === 'tint') {", + " const color = gdjs.rgbOrHexToRGBColor(typeof edit.color === 'string' ? edit.color : '255;255;255');", + " brushes.tint(data, stroke, color, strength, changedSamples);", + " } else if (edit.tool === 'wetness') {", + " brushes.wet(data, stroke, numberOr(edit.amount, 1), strength, changedSamples);", + " } else if (edit.tool === 'hole') {", + " brushes.hole(data, stroke, changedSamples);", + " } else if (edit.tool === 'fill') {", + " brushes.fill(data, stroke, changedSamples);", + " } else {", + " console.warn('Unknown tool in the edits of a terrain: \"' + edit.tool + '\" (use raise, flatten, smooth, paint, tint, wetness, hole or fill).');", + " }", + " }", + " },", + "};", + "", + "/**", + " * @param {gdjs.RuntimeGame} game", + " * @param {string} imageResourceName", + " * @returns {CanvasImageSource | null} The image, or null if it's not loaded (yet).", + " */", + "const getLoadedImage = (game, imageResourceName) => {", + " const imageManager = game.getImageManager();", + " if (typeof imageManager.getImageSource === 'function') return imageManager.getImageSource(imageResourceName);", + " // Older GDevelop versions: a 192 x 192 placeholder is given for images that are not loaded.", + " const texture = imageManager.getPIXITexture(imageResourceName);", + " return texture && texture !== imageManager.getInvalidPIXITexture() ? texture.baseTexture.getDrawableSource() : null;", + "};", + "", + "/**", + " * Loads heights from an image: black is the lowest, white the highest.", + " * Heights have 16 bits of precision when the red channel holds the most", + " * significant byte and the green one the least significant (a gray image", + " * works as expected too).", + " * @param {TerrainData} data", + " * @param {gdjs.RuntimeGame} game", + " * @param {string} imageResourceName", + " * @returns {boolean} true if the image could be read.", + " */", + "const loadHeightsFromImage = (data, game, imageResourceName) => {", + " const source = getLoadedImage(game, imageResourceName);", + " if (!source) return false;", + "", + " const canvas = document.createElement('canvas');", + " canvas.width = source.width;", + " canvas.height = source.height;", + " const context = canvas.getContext('2d', { willReadFrequently: true });", + " if (!context) return false;", + " context.drawImage(source, 0, 0);", + " const pixels = context.getImageData(0, 0, source.width, source.height).data;", + " // Pixels are decoded before being interpolated, to keep the 16 bits.", + " const pixelHeight = (x, y) => {", + " const offset = (y * source.width + x) * 4;", + " return (256 * pixels[offset] + pixels[offset + 1]) / 65535;", + " };", + " for (let j = 0; j < data.size; j++) {", + " const y = (j / data.resolution) * (source.height - 1);", + " const y0 = Math.floor(y);", + " const y1 = Math.min(y0 + 1, source.height - 1);", + " for (let i = 0; i < data.size; i++) {", + " const x = (i / data.resolution) * (source.width - 1);", + " const x0 = Math.floor(x);", + " const x1 = Math.min(x0 + 1, source.width - 1);", + " const top = pixelHeight(x0, y0) + (x - x0) * (pixelHeight(x1, y0) - pixelHeight(x0, y0));", + " const bottom = pixelHeight(x0, y1) + (x - x0) * (pixelHeight(x1, y1) - pixelHeight(x0, y1));", + " data.heights[j * data.size + i] = top + (y - y0) * (bottom - top);", + " }", + " }", + " return true;", + "};", + "", + "/**", + " * Stores the data in a string. Only the differences with the base data (from", + " * the relief or the heightmap) are stored: heights on 16 bits, layer weights", + " * on 8 bits, then (since format 2) the tint and wetness on 8 bits. Each value is predicted from its neighbors and the", + " * prediction errors are written as variable length integers, with runs of", + " * zeros collapsed. Untouched or smoothly changed areas take almost no space.", + " */", + "const codec = (() => {", + " const formatVersion = '2';", + " // Format 1 has no tint and wetness.", + " const readableFormatVersions = ['1', '2'];", + "", + " class ByteWriter {", + " constructor() {", + " this.bytes = new Uint8Array(1024);", + " this.length = 0;", + " }", + " writeVarUint(value) {", + " if (this.length + 5 > this.bytes.length) {", + " const bytes = new Uint8Array(this.bytes.length * 2);", + " bytes.set(this.bytes);", + " this.bytes = bytes;", + " }", + " while (value >= 0x80) {", + " this.bytes[this.length++] = (value & 0x7f) | 0x80;", + " value >>>= 7;", + " }", + " this.bytes[this.length++] = value;", + " }", + " }", + "", + " class ByteReader {", + " /** @param {Uint8Array} bytes */", + " constructor(bytes) {", + " this.bytes = bytes;", + " this.position = 0;", + " }", + " readVarUint() {", + " let value = 0;", + " let shift = 0;", + " let byte;", + " do {", + " if (this.position >= this.bytes.length) throw new Error('Truncated terrain data.');", + " byte = this.bytes[this.position++];", + " value += (byte & 0x7f) * Math.pow(2, shift);", + " shift += 7;", + " } while (byte & 0x80);", + " return value;", + " }", + " }", + "", + " /** Predicts a value from the left, top and top-left ones. */", + " const predict = (values, size, index) => {", + " const i = index % size;", + " if (index < size) return i === 0 ? 0 : values[index - 1];", + " if (i === 0) return values[index - size];", + " return values[index - 1] + values[index - size] - values[index - size - 1];", + " };", + "", + " /**", + " * @param {ByteWriter} writer", + " * @param {Int32Array} values A grid of `size * size` values.", + " */", + " const writeGrid = (writer, values, size) => {", + " let zeroRun = 0;", + " for (let index = 0; index < values.length; index++) {", + " const error = values[index] - predict(values, size, index);", + " if (error === 0) {", + " zeroRun++;", + " continue;", + " }", + " if (zeroRun > 0) {", + " writer.writeVarUint(0);", + " writer.writeVarUint(zeroRun);", + " zeroRun = 0;", + " }", + " // Zigzag: small negative and positive numbers get small codes.", + " writer.writeVarUint(error > 0 ? error * 2 : -error * 2 - 1);", + " }", + " if (zeroRun > 0) {", + " writer.writeVarUint(0);", + " writer.writeVarUint(zeroRun);", + " }", + " };", + "", + " /** @param {ByteReader} reader */", + " const readGrid = (reader, size) => {", + " const values = new Int32Array(size * size);", + " let zeroRun = 0;", + " for (let index = 0; index < values.length; index++) {", + " let error = 0;", + " if (zeroRun > 0) {", + " zeroRun--;", + " } else {", + " const code = reader.readVarUint();", + " if (code === 0) {", + " zeroRun = reader.readVarUint() - 1;", + " } else {", + " error = code % 2 === 0 ? code / 2 : -(code + 1) / 2;", + " }", + " }", + " values[index] = predict(values, size, index) + error;", + " }", + " return values;", + " };", + "", + " const toBase64 = (bytes, length) => {", + " let binary = '';", + " for (let offset = 0; offset < length; offset += 0x8000) {", + " binary += String.fromCharCode.apply(null, bytes.subarray(offset, Math.min(offset + 0x8000, length)));", + " }", + " return btoa(binary);", + " };", + "", + " const fromBase64 = (base64) => {", + " const binary = atob(base64);", + " const bytes = new Uint8Array(binary.length);", + " for (let index = 0; index < binary.length; index++) {", + " bytes[index] = binary.charCodeAt(index);", + " }", + " return bytes;", + " };", + "", + " return {", + " /**", + " * @param {TerrainData} data", + " * @param {TerrainData} baseData The data without changes, with the same resolution.", + " */", + " encode(data, baseData) {", + " const writer = new ByteWriter();", + " const values = new Int32Array(data.heights.length);", + " for (let index = 0; index < values.length; index++) {", + " values[index] = Math.round((data.heights[index] - baseData.heights[index]) * 65535);", + " }", + " writeGrid(writer, values, data.size);", + " for (let layer = 0; layer < LAYER_COUNT; layer++) {", + " for (let index = 0; index < values.length; index++) {", + " const offset = index * LAYER_COUNT + layer;", + " values[index] = data.splat[offset] - baseData.splat[offset];", + " }", + " writeGrid(writer, values, data.size);", + " }", + " // Not painted, these grids are zeros: a few bytes each.", + " for (let channel = 0; channel < 4; channel++) {", + " for (let index = 0; index < values.length; index++) {", + " const offset = index * 4 + channel;", + " values[index] = data.colors[offset] - baseData.colors[offset];", + " }", + " writeGrid(writer, values, data.size);", + " }", + " return formatVersion + ';' + data.resolution + ';' + toBase64(writer.bytes, writer.length);", + " },", + "", + " /**", + " * Store grids of integers in a string, like the terrain data. Used by other objects (the scattered models).", + " * @param {Int32Array[]} grids Grids of `size * size` values.", + " * @param {number} size", + " */", + " encodeGrids(grids, size) {", + " const writer = new ByteWriter();", + " for (const grid of grids) writeGrid(writer, grid, size);", + " return toBase64(writer.bytes, writer.length);", + " },", + "", + " /**", + " * @param {string} base64 A string from `encodeGrids`.", + " * @param {number} size", + " * @param {number} count The number of grids.", + " * @returns {Int32Array[]} The grids (an error is thrown if the string is not valid).", + " */", + " decodeGrids(base64, size, count) {", + " const reader = new ByteReader(fromBase64(base64));", + " return Array.from({ length: count }, () => readGrid(reader, size));", + " },", + "", + " /**", + " * @param {string} text", + " * @param {(resolution: number) => TerrainData} createBaseData", + " * @returns {TerrainData | null} null if the text is empty or not valid.", + " */", + " decode(text, createBaseData) {", + " if (!text) return null;", + " const parts = text.split(';');", + " const resolution = parseInt(parts[1], 10);", + " if (readableFormatVersions.indexOf(parts[0]) === -1 || RESOLUTIONS.indexOf(resolution) === -1) {", + " return null;", + " }", + " try {", + " const reader = new ByteReader(fromBase64(parts[2]));", + " const data = createBaseData(resolution);", + " const heightChanges = readGrid(reader, data.size);", + " for (let index = 0; index < heightChanges.length; index++) {", + " data.heights[index] = clamp01(data.heights[index] + heightChanges[index] / 65535);", + " }", + " for (let layer = 0; layer < LAYER_COUNT; layer++) {", + " const weightChanges = readGrid(reader, data.size);", + " for (let index = 0; index < weightChanges.length; index++) {", + " const offset = index * LAYER_COUNT + layer;", + " // The base may have changed since the data was saved.", + " data.splat[offset] = Math.min(Math.max(data.splat[offset] + weightChanges[index], 0), 255);", + " }", + " }", + " if (parts[0] !== '1') {", + " for (let channel = 0; channel < 4; channel++) {", + " const colorChanges = readGrid(reader, data.size);", + " for (let index = 0; index < colorChanges.length; index++) {", + " const offset = index * 4 + channel;", + " data.colors[offset] = Math.min(Math.max(data.colors[offset] + colorChanges[index], 0), 255);", + " }", + " }", + " }", + " return data;", + " } catch (error) {", + " console.error('Terrain data could not be read:', error);", + " return null;", + " }", + " },", + " };", + "})();", + "", + "const lodIndices = new Map();", + "/**", + " * The triangles of a chunk, shared by all chunks of the same size. Chunk", + " * borders have a skirt going down, hiding the gaps between chunks drawn", + " * with different levels of detail.", + " * @param {number} chunkCells", + " * @param {number} step", + " */", + "const getLodIndex = (chunkCells, step) => {", + " const key = chunkCells + ';' + step;", + " const existingIndex = lodIndices.get(key);", + " if (existingIndex) return existingIndex;", + "", + " const sideVertices = chunkCells + 1;", + " const gridVertices = sideVertices * sideVertices;", + " const indices = [];", + " const grid = (i, j) => j * sideVertices + i;", + " for (let j = 0; j < chunkCells; j += step) {", + " for (let i = 0; i < chunkCells; i += step) {", + " const topLeft = grid(i, j);", + " const topRight = grid(i + step, j);", + " const bottomLeft = grid(i, j + step);", + " const bottomRight = grid(i + step, j + step);", + " indices.push(topLeft, topRight, bottomLeft, topRight, bottomRight, bottomLeft);", + " }", + " }", + " const borders = [", + " (k) => grid(k, 0),", + " (k) => grid(k, chunkCells),", + " (k) => grid(0, k),", + " (k) => grid(chunkCells, k),", + " ];", + " borders.forEach((borderVertex, border) => {", + " const skirtVertex = (k) => gridVertices + border * sideVertices + k;", + " for (let k = 0; k < chunkCells; k += step) {", + " const a = borderVertex(k);", + " const b = borderVertex(k + step);", + " const skirtA = skirtVertex(k);", + " const skirtB = skirtVertex(k + step);", + " // Both faces, as a skirt can be seen from either side.", + " indices.push(a, b, skirtB, a, skirtB, skirtA, a, skirtB, b, a, skirtA, skirtB);", + " }", + " });", + " const index = new THREE.BufferAttribute(new Uint16Array(indices), 1);", + " lodIndices.set(key, index);", + " return index;", + "};", + "", + "const vertexShaderDeclarations = `", + "varying vec2 vTerrainLocalXY;", + "varying float vTerrainLocalZ;", + "uniform float terrainWorldScale;", + "varying vec2 vTerrainWorldXY;", + "varying float vTerrainUp;", + "`;", + "const vertexShaderCode = `", + "vTerrainLocalXY = position.xy;", + "vTerrainLocalZ = position.z;", + "vTerrainWorldXY = (modelMatrix * vec4(transformed, 1.0)).xy * terrainWorldScale;", + "vTerrainUp = normalize(transpose(inverse(mat3(modelMatrix))) * objectNormal).z;", + "`;", + "const fragmentShaderDeclarations = `", + "uniform sampler2D terrainSplatMap;", + "uniform vec2 terrainSplatUvScaleAndOffset;", + "uniform vec3 terrainLayerColors[4];", + "uniform sampler2D terrainLayerMap0;", + "uniform sampler2D terrainLayerMap1;", + "uniform sampler2D terrainLayerMap2;", + "uniform sampler2D terrainLayerMap3;", + "uniform sampler2D terrainLayerNormalMap0;", + "uniform sampler2D terrainLayerNormalMap1;", + "uniform sampler2D terrainLayerNormalMap2;", + "uniform sampler2D terrainLayerNormalMap3;", + "uniform float terrainHasNormalMaps;", + "uniform float terrainNormalMapStrength;", + "uniform float terrainTextureSize;", + "uniform float terrainHeightBlending;", + "uniform float terrainTextureVariation;", + "uniform vec4 terrainCliffLayer;", + "uniform vec2 terrainCliffCosines;", + "uniform vec4 terrainBrush;", + "uniform sampler2D terrainColorMap;", + "uniform vec3 terrainWetShore;", + "varying vec2 vTerrainLocalXY;", + "varying float vTerrainLocalZ;", + "varying vec2 vTerrainWorldXY;", + "varying float vTerrainUp;", + "", + "float terrainHash(vec2 p) {", + " return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);", + "}", + "// The rotation of the copies of textures used to make repetition less visible.", + "const mat2 terrainCopyRotation = mat2(0.8, -0.6, 0.6, 0.8);", + "/** A bump of a normal map (from -1 to 1), mixed with its rotated copy in patches. */", + "vec3 terrainSampleNormal(sampler2D normalMap, vec2 uv, vec2 uv2, float patches) {", + " vec3 bump = texture2D(normalMap, uv).xyz * 2.0 - 1.0;", + " if (patches <= 0.0) return bump;", + " vec3 copyBump = texture2D(normalMap, uv2).xyz * 2.0 - 1.0;", + " // The bumps of the copy are rotated back with the inverse rotation.", + " copyBump.xy = transpose(terrainCopyRotation) * copyBump.xy;", + " return mix(bump, copyBump, patches);", + "}", + "float terrainNoise(vec2 p) {", + " vec2 i = floor(p);", + " vec2 f = fract(p);", + " vec2 u = f * f * (3.0 - 2.0 * f);", + " return mix(", + " mix(terrainHash(i), terrainHash(i + vec2(1.0, 0.0)), u.x),", + " mix(terrainHash(i + vec2(0.0, 1.0)), terrainHash(i + vec2(1.0, 1.0)), u.x),", + " u.y", + " );", + "}", + "`;", + "// Holes are samples without any painted layer: the interpolated sum of the", + "// weights gives smooth edges. Only compiled for terrains having holes, as", + "// discard makes drawing slower on some GPUs.", + "const holeShaderCode = `", + "#ifdef TERRAIN_HOLES", + "if (dot(texture2D(terrainSplatMap, vTerrainLocalXY * terrainSplatUvScaleAndOffset.x + terrainSplatUvScaleAndOffset.y), vec4(1.0)) < 0.5) discard;", + "#endif", + "`;", + "// For the shadows: the depth of holes is not drawn.", + "const holeDepthShaderDeclarations = `", + "uniform sampler2D terrainSplatMap;", + "uniform vec2 terrainSplatUvScaleAndOffset;", + "varying vec2 vTerrainLocalXY;", + "`;", + "", + "const fragmentShaderCode = holeShaderCode + `", + "// Borders between layers are made irregular and sharper, to look painted.", + "vec2 terrainNoisePosition = vTerrainWorldXY / 24.0;", + "vec4 terrainJitter = vec4(", + " terrainNoise(terrainNoisePosition),", + " terrainNoise(terrainNoisePosition + 19.1),", + " terrainNoise(terrainNoisePosition + 37.7),", + " terrainNoise(terrainNoisePosition + 53.3)", + ");", + "// Layer weights are in a texture, so that painted details stay the same", + "// whatever the level of detail of the ground.", + "vec2 terrainSplatUv = vTerrainLocalXY * terrainSplatUvScaleAndOffset.x + terrainSplatUvScaleAndOffset.y;", + "vec4 terrainWeights = texture2D(terrainSplatMap, terrainSplatUv) * (0.7 + 0.6 * terrainJitter);", + "terrainWeights = terrainWeights * terrainWeights;", + "terrainWeights /= max(dot(terrainWeights, vec4(1.0)), 0.00001);", + "float terrainCliff = 1.0 - smoothstep(terrainCliffCosines.x, terrainCliffCosines.y, vTerrainUp);", + "terrainWeights = mix(terrainWeights, terrainCliffLayer, terrainCliff * dot(terrainCliffLayer, vec4(1.0)));", + "", + "vec2 terrainUv = vTerrainWorldXY / terrainTextureSize;", + "vec3 terrainTexture0 = texture2D(terrainLayerMap0, terrainUv).rgb;", + "vec3 terrainTexture1 = texture2D(terrainLayerMap1, terrainUv).rgb;", + "vec3 terrainTexture2 = texture2D(terrainLayerMap2, terrainUv).rgb;", + "vec3 terrainTexture3 = texture2D(terrainLayerMap3, terrainUv).rgb;", + "", + "// Less visible repetition: in irregular patches, textures are replaced by a", + "// rotated and shifted copy of themselves, and colors slowly vary.", + "float terrainPatches = 0.0;", + "vec2 terrainUv2 = terrainUv;", + "float terrainColorVariation = 1.0;", + "if (terrainTextureVariation > 0.0) {", + " // Up to half of the ground is covered by the copies, each patch showing", + " // only one of them (mixing them would make them blurry).", + " float terrainPatchThreshold = 1.0 - 0.5 * terrainTextureVariation;", + " terrainPatches = smoothstep(terrainPatchThreshold - 0.05, terrainPatchThreshold + 0.05, terrainNoise(terrainUv * 0.23 + 7.3));", + " terrainUv2 = terrainCopyRotation * terrainUv + vec2(0.31, 0.73);", + " terrainTexture0 = mix(terrainTexture0, texture2D(terrainLayerMap0, terrainUv2).rgb, terrainPatches);", + " terrainTexture1 = mix(terrainTexture1, texture2D(terrainLayerMap1, terrainUv2).rgb, terrainPatches);", + " terrainTexture2 = mix(terrainTexture2, texture2D(terrainLayerMap2, terrainUv2).rgb, terrainPatches);", + " terrainTexture3 = mix(terrainTexture3, texture2D(terrainLayerMap3, terrainUv2).rgb, terrainPatches);", + " terrainColorVariation += terrainTextureVariation * 0.3 *", + " (terrainNoise(terrainUv * 0.11) + 0.5 * terrainNoise(terrainUv * 0.37 + 3.1) - 0.75);", + "}", + "", + "// Height-based blending: the brightness of a texture, compared to its", + "// average, is used as its height. Where layers meet, the high parts (stones,", + "// tufts) cover the low parts (sand, gaps) instead of fading into each other.", + "if (terrainHeightBlending > 0.0) {", + " const vec3 terrainLuminance = vec3(0.299, 0.587, 0.114);", + " vec4 terrainHeights = vec4(", + " dot(terrainTexture0 - textureLod(terrainLayerMap0, vec2(0.5), 16.0).rgb, terrainLuminance),", + " dot(terrainTexture1 - textureLod(terrainLayerMap1, vec2(0.5), 16.0).rgb, terrainLuminance),", + " dot(terrainTexture2 - textureLod(terrainLayerMap2, vec2(0.5), 16.0).rgb, terrainLuminance),", + " dot(terrainTexture3 - textureLod(terrainLayerMap3, vec2(0.5), 16.0).rgb, terrainLuminance)", + " );", + " vec4 terrainHeightWeights = terrainWeights + terrainHeights;", + " float terrainTop = max(max(terrainHeightWeights.x, terrainHeightWeights.y), max(terrainHeightWeights.z, terrainHeightWeights.w));", + " // Only painted layers (and the cliff layer) can show.", + " vec4 terrainHeightBlend = max(terrainHeightWeights - terrainTop + 0.1, 0.0) * step(0.004, terrainWeights);", + " terrainHeightBlend /= max(dot(terrainHeightBlend, vec4(1.0)), 0.00001);", + " terrainWeights = mix(terrainWeights, terrainHeightBlend, terrainHeightBlending);", + "}", + "", + "vec3 terrainColor = terrainColorVariation * (", + " terrainWeights.x * terrainLayerColors[0] * terrainTexture0 +", + " terrainWeights.y * terrainLayerColors[1] * terrainTexture1 +", + " terrainWeights.z * terrainLayerColors[2] * terrainTexture2 +", + " terrainWeights.w * terrainLayerColors[3] * terrainTexture3);", + "", + "// The painted tint multiplies all layers. Wet ground is darker (and", + "// shinier, see the roughness).", + "vec4 terrainTintAndWetness = texture2D(terrainColorMap, terrainSplatUv);", + "float terrainWetness = max(", + " terrainTintAndWetness.a,", + " terrainWetShore.z * (1.0 - smoothstep(terrainWetShore.x, terrainWetShore.x + terrainWetShore.y, vTerrainLocalZ))", + ");", + "terrainColor *= terrainTintAndWetness.rgb * (1.0 - 0.4 * terrainWetness);", + "diffuseColor.rgb *= terrainColor;", + "", + "if (terrainBrush.w > 0.5) {", + " float brushDistance = distance(vTerrainWorldXY, terrainBrush.xy);", + " float ringWidth = max(terrainBrush.z * 0.04, 1.5);", + " float ring = 1.0 - smoothstep(0.0, ringWidth, abs(brushDistance - terrainBrush.z));", + " float disc = brushDistance < terrainBrush.z ? 0.12 : 0.0;", + " diffuseColor.rgb = mix(diffuseColor.rgb, vec3(1.0), max(ring * 0.85, disc));", + "}", + "`;", + "", + "const fragmentShaderRoughnessCode = `", + "roughnessFactor = mix(roughnessFactor, 0.3, terrainWetness);", + "`;", + "", + "const fragmentShaderNormalCode = `", + "if (terrainHasNormalMaps > 0.5) {", + " vec3 terrainMapNormal =", + " terrainWeights.x * terrainSampleNormal(terrainLayerNormalMap0, terrainUv, terrainUv2, terrainPatches) +", + " terrainWeights.y * terrainSampleNormal(terrainLayerNormalMap1, terrainUv, terrainUv2, terrainPatches) +", + " terrainWeights.z * terrainSampleNormal(terrainLayerNormalMap2, terrainUv, terrainUv2, terrainPatches) +", + " terrainWeights.w * terrainSampleNormal(terrainLayerNormalMap3, terrainUv, terrainUv2, terrainPatches);", + " terrainMapNormal.xy *= terrainNormalMapStrength;", + " // The X and Y of textures follow the X and Y of the 3D world, projected on", + " // the ground (the camera space of the normal is used).", + " vec3 terrainTangent = (viewMatrix * vec4(1.0, 0.0, 0.0, 0.0)).xyz;", + " terrainTangent -= normal * dot(normal, terrainTangent);", + " // A ground facing the X axis (a rotated terrain) uses the Y axis instead.", + " if (dot(terrainTangent, terrainTangent) < 0.000001) {", + " terrainTangent = (viewMatrix * vec4(0.0, 1.0, 0.0, 0.0)).xyz;", + " terrainTangent -= normal * dot(normal, terrainTangent);", + " }", + " terrainTangent = normalize(terrainTangent);", + " vec3 terrainBitangent = cross(normal, terrainTangent);", + " normal = normalize(mat3(terrainTangent, terrainBitangent, normal) * terrainMapNormal);", + "}", + "`;", + "", + "let whiteTexture = null;", + "const getWhiteTexture = () => {", + " if (!whiteTexture) {", + " whiteTexture = new THREE.DataTexture(new Uint8Array([255, 255, 255, 255]), 1, 1);", + " whiteTexture.needsUpdate = true;", + " }", + " return whiteTexture;", + "};", + "", + "let flatNormalTexture = null;", + "const getFlatNormalTexture = () => {", + " if (!flatNormalTexture) {", + " flatNormalTexture = new THREE.DataTexture(new Uint8Array([128, 128, 255, 255]), 1, 1);", + " flatNormalTexture.needsUpdate = true;", + " }", + " return flatNormalTexture;", + "};", + "", + "const layerTextures = new WeakMap();", + "const layerNormalMaps = new WeakMap();", + "/**", + " * Textures of the image manager have no mipmaps: they would shimmer when", + " * repeated in the distance.", + " * @param {gdjs.RuntimeGame} game", + " * @param {string} resourceName", + " * @param {boolean} isNormalMap Normal maps are directions, not colors.", + " */", + "const getLayerTexture = (game, resourceName, isNormalMap) => {", + " if (!resourceName) return isNormalMap ? getFlatNormalTexture() : getWhiteTexture();", + " const imageTexture = game.getImageManager().getThreeTexture(resourceName);", + " const textures = isNormalMap ? layerNormalMaps : layerTextures;", + " let texture = textures.get(imageTexture);", + " if (!texture) {", + " texture = imageTexture.clone();", + " if (isNormalMap) texture.colorSpace = THREE.NoColorSpace;", + " texture.wrapS = THREE.RepeatWrapping;", + " texture.wrapT = THREE.RepeatWrapping;", + " texture.minFilter = THREE.LinearMipmapLinearFilter;", + " texture.generateMipmaps = true;", + " texture.anisotropy = 4;", + " texture.needsUpdate = true;", + " textures.set(imageTexture, texture);", + " }", + " return texture;", + "};", + "", + "/**", + " * A texture of 4 bytes per sample of a grid of `size * size` samples (layer", + " * weights, tint and wetness...), interpolated between samples.", + " * @param {Uint8Array} bytes", + " * @param {number} size", + " */", + "const createGridTexture = (bytes, size) => {", + " const texture = new THREE.DataTexture(bytes, size, size);", + " texture.magFilter = THREE.LinearFilter;", + " texture.minFilter = THREE.LinearFilter;", + " texture.userData.isUploaded = false;", + " texture.onUpdate = () => {", + " texture.userData.isUploaded = true;", + " };", + " return texture;", + "};", + "", + "/**", + " * Send the rows of the changed samples to the GPU, or the whole texture", + " * before its first upload (and in older GDevelop versions).", + " * @param {THREE.DataTexture} texture", + " * @param {SampleRectangle} samples", + " */", + "const uploadGridTextureRows = (texture, samples) => {", + " if (texture.userData.isUploaded && typeof texture.addUpdateRange === 'function') {", + " const size = texture.image.width;", + " for (let j = samples.minJ; j <= samples.maxJ; j++) {", + " texture.addUpdateRange((j * size + samples.minI) * 4, (samples.maxI - samples.minI + 1) * 4);", + " }", + " }", + " texture.needsUpdate = true;", + "};", + "", + "class TerrainRenderer {", + " /** @param {Terrain} terrain */", + " constructor(terrain) {", + " this.terrain = terrain;", + " this.group = new THREE.Group();", + " terrain.object.get3DRendererObject().add(this.group);", + " /** @type {Array<{mesh: THREE.Mesh, geometry: THREE.BufferGeometry, firstI: number, firstJ: number, center: THREE.Vector3, lod: number}>} */", + " this.chunks = [];", + " this.chunksPerSide = 0;", + " this.chunkCells = 0;", + " this.chunkSideVertices = 0;", + " this.chunkGridVertices = 0;", + " this.chunkVertices = 0;", + " /** @type {THREE.DataTexture | null} */", + " this.splatTexture = null;", + " /** @type {THREE.DataTexture | null} */", + " this.colorTexture = null;", + " this.uniforms = {", + " terrainSplatMap: { value: getWhiteTexture() },", + " terrainSplatUvScaleAndOffset: { value: new THREE.Vector2() },", + " terrainLayerColors: { value: [new THREE.Color(), new THREE.Color(), new THREE.Color(), new THREE.Color()] },", + " terrainLayerMap0: { value: getWhiteTexture() },", + " terrainLayerMap1: { value: getWhiteTexture() },", + " terrainLayerMap2: { value: getWhiteTexture() },", + " terrainLayerMap3: { value: getWhiteTexture() },", + " terrainLayerNormalMap0: { value: getFlatNormalTexture() },", + " terrainLayerNormalMap1: { value: getFlatNormalTexture() },", + " terrainLayerNormalMap2: { value: getFlatNormalTexture() },", + " terrainLayerNormalMap3: { value: getFlatNormalTexture() },", + " terrainHasNormalMaps: { value: 0 },", + " terrainNormalMapStrength: { value: 1 },", + " terrainTextureSize: { value: 256 },", + " terrainHeightBlending: { value: 0 },", + " terrainTextureVariation: { value: 0 },", + " terrainCliffLayer: { value: new THREE.Vector4() },", + " terrainCliffCosines: { value: new THREE.Vector2() },", + " terrainBrush: { value: new THREE.Vector4() },", + " terrainColorMap: { value: getWhiteTexture() },", + " // Z (in the terrain group) under which the ground is wet, fade height, and 1 if it's used.", + " terrainWetShore: { value: new THREE.Vector3() },", + " // Scene units by 3D world unit.", + " terrainWorldScale: { value: 1 },", + " };", + " this.material = new THREE.MeshStandardMaterial({ roughness: 1, metalness: 0 });", + " this.material.onBeforeCompile = (shader) => {", + " Object.assign(shader.uniforms, this.uniforms);", + " shader.vertexShader = patchShaderCode(shader.vertexShader, [", + " ['#include ', '#include \\n' + vertexShaderDeclarations],", + " ['#include ', '#include \\n' + vertexShaderCode],", + " ], 'Terrain3D');", + " shader.fragmentShader = patchShaderCode(shader.fragmentShader, [", + " ['#include ', '#include \\n' + fragmentShaderDeclarations],", + " ['#include ', fragmentShaderCode],", + " ['#include ', '#include \\n' + fragmentShaderRoughnessCode],", + " ['#include ', '#include \\n' + fragmentShaderNormalCode],", + " ], 'Terrain3D');", + " };", + " this.material.customProgramCacheKey = () => (this.hasHoles ? 'Terrain3DWithHoles' : 'Terrain3D');", + " this.hasHoles = false;", + " // Without them, holes would cast shadows.", + " this.depthMaterial = this._createHoleDepthMaterial(new THREE.MeshDepthMaterial());", + " this.distanceMaterial = this._createHoleDepthMaterial(new THREE.MeshDistanceMaterial());", + " this._cameraPosition = new THREE.Vector3();", + " this._chunkPosition = new THREE.Vector3();", + " this._scale = new THREE.Vector3();", + " }", + "", + " /**", + " * @param {THREE.MeshDepthMaterial | THREE.MeshDistanceMaterial} material", + " * @returns The material, drawing nothing in holes.", + " */", + " _createHoleDepthMaterial(material) {", + " material.defines = { TERRAIN_HOLES: '' };", + " material.onBeforeCompile = (shader) => {", + " Object.assign(shader.uniforms, this.uniforms);", + " shader.vertexShader = patchShaderCode(shader.vertexShader, [", + " ['#include ', '#include \\nvarying vec2 vTerrainLocalXY;'],", + " ['#include ', '#include \\nvTerrainLocalXY = position.xy;'],", + " ], 'Terrain3D shadows');", + " shader.fragmentShader = patchShaderCode(shader.fragmentShader, [", + " ['#include ', '#include \\n' + holeDepthShaderDeclarations],", + " ['#include ', holeShaderCode + '#include '],", + " ], 'Terrain3D shadows');", + " };", + " material.customProgramCacheKey = () => 'Terrain3DHoles';", + " return material;", + " }", + "", + " /**", + " * Use the shaders drawing holes, or the faster ones without them.", + " * @param {boolean} hasHoles", + " */", + " setHasHoles(hasHoles) {", + " if (hasHoles === this.hasHoles) return;", + " this.hasHoles = hasHoles;", + " // The material has its own defines (STANDARD).", + " if (hasHoles) this.material.defines.TERRAIN_HOLES = '';", + " else delete this.material.defines.TERRAIN_HOLES;", + " this.material.needsUpdate = true;", + " for (const chunk of this.chunks) this._setChunkShadowMaterials(chunk.mesh);", + " }", + "", + " /** @param {THREE.Mesh} mesh */", + " _setChunkShadowMaterials(mesh) {", + " mesh.customDepthMaterial = this.hasHoles ? this.depthMaterial : undefined;", + " mesh.customDistanceMaterial = this.hasHoles ? this.distanceMaterial : undefined;", + " }", + "", + " /** Recreate all the chunks, for example after the resolution changed. */", + " rebuild() {", + " this.disposeChunks();", + " const { data } = this.terrain;", + " this.chunkCells = getChunkCells(data.resolution);", + " this.chunkSideVertices = this.chunkCells + 1;", + " this.chunkGridVertices = this.chunkSideVertices * this.chunkSideVertices;", + " this.chunkVertices = this.chunkGridVertices + 4 * this.chunkSideVertices;", + " this.chunksPerSide = data.resolution / this.chunkCells;", + " const cellSize = AREA_SIZE / data.resolution;", + " // Sample centers are at the texel centers.", + " this.splatTexture = createGridTexture(data.splat, data.size);", + " this.uniforms.terrainSplatMap.value = this.splatTexture;", + " this.colorTexture = createGridTexture(data.colors, data.size);", + " // Tints are colors chosen like the layer colors.", + " this.colorTexture.colorSpace = THREE.SRGBColorSpace;", + " this.uniforms.terrainColorMap.value = this.colorTexture;", + " this.uniforms.terrainSplatUvScaleAndOffset.value.set(1 / (cellSize * data.size), 0.5 / data.size);", + " for (let chunkJ = 0; chunkJ < this.chunksPerSide; chunkJ++) {", + " for (let chunkI = 0; chunkI < this.chunksPerSide; chunkI++) {", + " const firstI = chunkI * this.chunkCells;", + " const firstJ = chunkJ * this.chunkCells;", + " const positions = new Float32Array(this.chunkVertices * 3);", + " for (let j = 0; j < this.chunkSideVertices; j++) {", + " for (let i = 0; i < this.chunkSideVertices; i++) {", + " const vertex = j * this.chunkSideVertices + i;", + " positions[vertex * 3] = (firstI + i) * cellSize;", + " positions[vertex * 3 + 1] = (firstJ + j) * cellSize;", + " }", + " }", + " // Skirt vertices are below border vertices.", + " this._forEachSkirtVertex((skirtVertex, borderVertex) => {", + " positions[skirtVertex * 3] = positions[borderVertex * 3];", + " positions[skirtVertex * 3 + 1] = positions[borderVertex * 3 + 1];", + " });", + " const geometry = new THREE.BufferGeometry();", + " geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));", + " geometry.setAttribute('normal', new THREE.BufferAttribute(new Float32Array(this.chunkVertices * 3), 3));", + " geometry.setIndex(getLodIndex(this.chunkCells, LOD_STEPS[0]));", + " geometry.boundingBox = new THREE.Box3();", + " geometry.boundingSphere = new THREE.Sphere();", + " const mesh = new THREE.Mesh(geometry, this.material);", + " mesh.matrixAutoUpdate = false;", + " // Rays hit the ground at its full resolution, whatever the level of", + " // detail drawn, and go through holes.", + " const chunkCells = this.chunkCells;", + " const terrainData = () => this.terrain.data;", + " const localPoint = new THREE.Vector3();", + " mesh.raycast = function (raycaster, intersects) {", + " const drawnIndex = geometry.index;", + " const firstIntersection = intersects.length;", + " geometry.setIndex(getLodIndex(chunkCells, LOD_STEPS[0]));", + " THREE.Mesh.prototype.raycast.call(this, raycaster, intersects);", + " geometry.setIndex(drawnIndex);", + " const data = terrainData();", + " const cellSize = AREA_SIZE / data.resolution;", + " for (let index = intersects.length - 1; index >= firstIntersection; index--) {", + " this.worldToLocal(localPoint.copy(intersects[index].point));", + " if (data.sampleSolidity(localPoint.x / cellSize, localPoint.y / cellSize) < 0.5) {", + " intersects.splice(index, 1);", + " }", + " }", + " };", + " this._setChunkShadowMaterials(mesh);", + " this.group.add(mesh);", + " this.chunks.push({", + " mesh,", + " geometry,", + " firstI,", + " firstJ,", + " center: new THREE.Vector3(", + " (firstI + this.chunkCells / 2) * cellSize,", + " (firstJ + this.chunkCells / 2) * cellSize,", + " 0", + " ),", + " lod: 0,", + " });", + " }", + " }", + " this.updateSamples(new SampleRectangle().set(0, 0, data.resolution, data.resolution), true);", + " this.updateMaterial();", + " }", + "", + " _forEachSkirtVertex(callback) {", + " for (let k = 0; k < this.chunkSideVertices; k++) {", + " callback(this.chunkGridVertices + k, k);", + " callback(this.chunkGridVertices + this.chunkSideVertices + k, this.chunkCells * this.chunkSideVertices + k);", + " callback(this.chunkGridVertices + 2 * this.chunkSideVertices + k, k * this.chunkSideVertices);", + " callback(", + " this.chunkGridVertices + 3 * this.chunkSideVertices + k,", + " k * this.chunkSideVertices + this.chunkCells", + " );", + " }", + " }", + "", + " /**", + " * Skirts go down only to the lowest point a coarser neighbor chunk can", + " * draw along the border, so that they can't be seen through holes.", + " * @param {Float32Array} positions", + " */", + " _updateSkirtHeights(positions) {", + " const coarsestStep = LOD_STEPS[LOD_STEPS.length - 1];", + " const borderVertices = [", + " (k) => k,", + " (k) => this.chunkCells * this.chunkSideVertices + k,", + " (k) => k * this.chunkSideVertices,", + " (k) => k * this.chunkSideVertices + this.chunkCells,", + " ];", + " borderVertices.forEach((borderVertex, border) => {", + " for (let k = 0; k < this.chunkSideVertices; k++) {", + " let minZ = Infinity;", + " const from = Math.max(k - coarsestStep, 0);", + " const to = Math.min(k + coarsestStep, this.chunkCells);", + " for (let other = from; other <= to; other++) {", + " minZ = Math.min(minZ, positions[borderVertex(other) * 3 + 2]);", + " }", + " const skirtVertex = this.chunkGridVertices + border * this.chunkSideVertices + k;", + " positions[skirtVertex * 3 + 2] = minZ;", + " }", + " });", + " }", + "", + " /**", + " * Update the vertices of the chunks sharing the given samples.", + " * @param {SampleRectangle} samples", + " * @param {boolean} hasPaintChanged", + " */", + " updateSamples(samples, hasPaintChanged) {", + " if (samples.isEmpty()) return;", + " const { data } = this.terrain;", + " const cellSize = AREA_SIZE / data.resolution;", + " const slopeFactor = AREA_DEPTH / cellSize;", + " // Normals of samples next to changed ones change too.", + " const minI = Math.max(samples.minI - 1, 0);", + " const minJ = Math.max(samples.minJ - 1, 0);", + " const maxI = Math.min(samples.maxI + 1, data.resolution);", + " const maxJ = Math.min(samples.maxJ + 1, data.resolution);", + " const firstChunkI = Math.max(Math.ceil(minI / this.chunkCells) - 1, 0);", + " const firstChunkJ = Math.max(Math.ceil(minJ / this.chunkCells) - 1, 0);", + " const lastChunkI = Math.min(Math.floor(maxI / this.chunkCells), this.chunksPerSide - 1);", + " const lastChunkJ = Math.min(Math.floor(maxJ / this.chunkCells), this.chunksPerSide - 1);", + "", + " for (let chunkJ = firstChunkJ; chunkJ <= lastChunkJ; chunkJ++) {", + " for (let chunkI = firstChunkI; chunkI <= lastChunkI; chunkI++) {", + " const chunk = this.chunks[chunkJ * this.chunksPerSide + chunkI];", + " const positions = chunk.geometry.attributes.position.array;", + " const normals = chunk.geometry.attributes.normal.array;", + " const fromI = Math.max(minI - chunk.firstI, 0);", + " const toI = Math.min(maxI - chunk.firstI, this.chunkCells);", + " const fromJ = Math.max(minJ - chunk.firstJ, 0);", + " const toJ = Math.min(maxJ - chunk.firstJ, this.chunkCells);", + " for (let j = fromJ; j <= toJ; j++) {", + " const gridJ = chunk.firstJ + j;", + " for (let i = fromI; i <= toI; i++) {", + " const gridI = chunk.firstI + i;", + " const vertex = j * this.chunkSideVertices + i;", + " const sample = gridJ * data.size + gridI;", + " positions[vertex * 3 + 2] = data.heights[sample] * AREA_DEPTH;", + "", + " const leftI = Math.max(gridI - 1, 0);", + " const rightI = Math.min(gridI + 1, data.resolution);", + " const upJ = Math.max(gridJ - 1, 0);", + " const downJ = Math.min(gridJ + 1, data.resolution);", + " const nx =", + " ((data.getHeight(leftI, gridJ) - data.getHeight(rightI, gridJ)) * slopeFactor) /", + " (rightI - leftI);", + " const ny =", + " ((data.getHeight(gridI, upJ) - data.getHeight(gridI, downJ)) * slopeFactor) /", + " (downJ - upJ);", + " const inverseLength = 1 / Math.sqrt(nx * nx + ny * ny + 1);", + " normals[vertex * 3] = nx * inverseLength;", + " normals[vertex * 3 + 1] = ny * inverseLength;", + " normals[vertex * 3 + 2] = inverseLength;", + " }", + " }", + " this._forEachSkirtVertex((skirtVertex, borderVertex) => {", + " for (let axis = 0; axis < 3; axis++) {", + " normals[skirtVertex * 3 + axis] = normals[borderVertex * 3 + axis];", + " }", + " });", + " this._updateSkirtHeights(positions);", + " let maxZ = 0;", + " for (let vertex = 0; vertex < this.chunkGridVertices; vertex++) {", + " maxZ = Math.max(maxZ, positions[vertex * 3 + 2]);", + " }", + " const chunkSize = this.chunkCells * cellSize;", + " chunk.geometry.boundingBox.min.set(chunk.firstI * cellSize, chunk.firstJ * cellSize, 0);", + " chunk.geometry.boundingBox.max.set(", + " chunk.firstI * cellSize + chunkSize,", + " chunk.firstJ * cellSize + chunkSize,", + " maxZ", + " );", + " chunk.geometry.boundingBox.getBoundingSphere(chunk.geometry.boundingSphere);", + " chunk.center.z = maxZ / 2;", + " chunk.geometry.attributes.position.needsUpdate = true;", + " chunk.geometry.attributes.normal.needsUpdate = true;", + " }", + " }", + " if (hasPaintChanged) {", + " // The data may have been replaced by one with the same resolution.", + " this.splatTexture.image.data = data.splat;", + " uploadGridTextureRows(this.splatTexture, samples);", + " this.colorTexture.image.data = data.colors;", + " uploadGridTextureRows(this.colorTexture, samples);", + " }", + " }", + "", + " updateMaterial() {", + " const { object } = this.terrain;", + " const game = object.getRuntimeScene().getGame();", + " const colors = [object._getLayer1Color(), object._getLayer2Color(), object._getLayer3Color(), object._getLayer4Color()];", + " const textures = [", + " object._getLayer1Texture(),", + " object._getLayer2Texture(),", + " object._getLayer3Texture(),", + " object._getLayer4Texture(),", + " ];", + " const normalMaps = [", + " object._getLayer1NormalMap(),", + " object._getLayer2NormalMap(),", + " object._getLayer3NormalMap(),", + " object._getLayer4NormalMap(),", + " ];", + " for (let layer = 0; layer < LAYER_COUNT; layer++) {", + " this.uniforms.terrainLayerColors.value[layer].set(gdjs.rgbOrHexStringToNumber(colors[layer]));", + " this.uniforms['terrainLayerMap' + layer].value = getLayerTexture(game, textures[layer], false);", + " this.uniforms['terrainLayerNormalMap' + layer].value = getLayerTexture(game, normalMaps[layer], true);", + " }", + " this.uniforms.terrainHasNormalMaps.value = normalMaps.some((normalMap) => !!normalMap) ? 1 : 0;", + " this.uniforms.terrainNormalMapStrength.value = Math.max(object._getNormalMapStrength(), 0);", + " this.uniforms.terrainTextureSize.value = Math.max(object._getTextureSize(), 1);", + " this.uniforms.terrainHeightBlending.value = clamp01(object._getHeightBlending());", + " this.uniforms.terrainTextureVariation.value = clamp01(object._getTextureVariation());", + "", + " const cliffLayer = parseInt(object._getCliffLayer(), 10);", + " const cliffAngle = gdjs.toRad(object._getCliffAngle());", + " const cliffBlendAngle = gdjs.toRad(4);", + " this.uniforms.terrainCliffLayer.value.set(", + " cliffLayer === 1 ? 1 : 0,", + " cliffLayer === 2 ? 1 : 0,", + " cliffLayer === 3 ? 1 : 0,", + " cliffLayer === 4 ? 1 : 0", + " );", + " this.uniforms.terrainCliffCosines.value.set(", + " Math.cos(cliffAngle + cliffBlendAngle),", + " Math.cos(cliffAngle - cliffBlendAngle)", + " );", + "", + " const castShadow = object._getCastShadow();", + " const receiveShadow = object._getReceiveShadow();", + " for (const chunk of this.chunks) {", + " chunk.mesh.castShadow = castShadow;", + " chunk.mesh.receiveShadow = receiveShadow;", + " }", + " }", + "", + " /**", + " * Show the brush circle of the editor.", + " * @param {THREE.Vector3 | null} localPosition", + " * @param {number} radius In scene units.", + " */", + " setBrush(localPosition, radius) {", + " const brush = this.uniforms.terrainBrush.value;", + " if (!localPosition) {", + " brush.w = 0;", + " return;", + " }", + " this.group.updateWorldMatrix(true, false);", + " const worldPosition = this._chunkPosition.copy(localPosition).applyMatrix4(this.group.matrixWorld);", + " const worldScale = this.uniforms.terrainWorldScale.value;", + " brush.set(worldPosition.x * worldScale, worldPosition.y * worldScale, radius, 1);", + " }", + "", + " /** @param {THREE.Camera | null} camera */", + " updateLevelsOfDetail(camera) {", + " if (!camera || !this.group.visible) return;", + " const matrixWorld = this.group.matrixWorld;", + " this._cameraPosition.setFromMatrixPosition(camera.matrixWorld);", + " this._scale.setFromMatrixScale(matrixWorld);", + " const cellSize = AREA_SIZE / this.terrain.data.resolution;", + " const chunkWorldSize = this.chunkCells * cellSize * Math.max(this._scale.x, this._scale.y);", + " for (const chunk of this.chunks) {", + " const distance =", + " this._chunkPosition.copy(chunk.center).applyMatrix4(matrixWorld).distanceTo(this._cameraPosition) /", + " chunkWorldSize;", + " let lod = LOD_DISTANCES.length - 1;", + " while (lod > 0 && distance < LOD_DISTANCES[lod]) lod--;", + " if (lod !== chunk.lod) {", + " chunk.lod = lod;", + " chunk.geometry.setIndex(getLodIndex(this.chunkCells, LOD_STEPS[lod]));", + " }", + " }", + " }", + "", + " disposeChunks() {", + " for (const chunk of this.chunks) {", + " chunk.mesh.removeFromParent();", + " // The index is shared by all chunks: it must stay on the GPU.", + " chunk.geometry.setIndex(null);", + " chunk.geometry.dispose();", + " }", + " this.chunks.length = 0;", + " if (this.splatTexture) this.splatTexture.dispose();", + " this.splatTexture = null;", + " if (this.colorTexture) this.colorTexture.dispose();", + " this.colorTexture = null;", + " }", + "", + " dispose() {", + " this.disposeChunks();", + " this.material.dispose();", + " this.depthMaterial.dispose();", + " this.distanceMaterial.dispose();", + " this.group.removeFromParent();", + " }", + "}", + "", + "/**", + " * Makes a Physics3D behavior use a height field shape following the terrain.", + " * Changes of heights are applied to the shape without recreating it.", + " * @implements {gdjs.Physics3DRuntimeBehavior.BodyUpdater}", + " */", + "class TerrainBodyUpdater {", + " /**", + " * @param {Terrain} terrain", + " * @param {gdjs.Physics3DRuntimeBehavior} physics", + " */", + " constructor(terrain, physics) {", + " this.terrain = terrain;", + " this.physics = physics;", + " this.defaultBodyUpdater = physics.bodyUpdater;", + " this.changedSamples = new SampleRectangle();", + " /** @type {Jolt.HeightFieldShape | null} */", + " this.heightField = null;", + " this.heightScale = 1;", + " this.heightsBufferPointer = 0;", + " this.heightsBufferLength = 0;", + " this.bodyX = 0;", + " this.bodyY = 0;", + " this.bodyZ = 0;", + " this.bodyHeight = 0;", + " physics.bodyType = 'Static';", + " physics.bodyUpdater = this;", + " }", + "", + " createShape() {", + " const { data, object } = this.terrain;", + " const worldInvScale = this.physics._sharedData.worldInvScale;", + " const { size } = data;", + " this.heightScale = object.getDepth() * worldInvScale;", + "", + " // Changes of heights (SetHeights) break collisions unless the sample", + " // count is a power of 2: extra samples have no collision.", + " const sampleCount = Math.pow(2, Math.ceil(Math.log2(size)));", + " const settings = new Jolt.HeightFieldShapeSettings();", + " settings.mScale.Set(", + " (object.getWidth() / data.resolution) * worldInvScale,", + " 1,", + " (object.getHeight() / data.resolution) * worldInvScale", + " );", + " settings.mSampleCount = sampleCount;", + " settings.mBlockSize = 4;", + " // Allows later changes of heights on the whole depth of the object.", + " settings.mMinHeightValue = 0;", + " settings.mMaxHeightValue = this.heightScale;", + " settings.mHeightSamples.resize(sampleCount * sampleCount);", + " const samples = new Float32Array(", + " Jolt.HEAPF32.buffer,", + " Jolt.getPointer(settings.mHeightSamples.data()),", + " sampleCount * sampleCount", + " );", + " const noCollision = Jolt.HeightFieldShapeConstantValues.prototype.cNoCollisionValue;", + " samples.fill(noCollision);", + " // Height fields go along X and Z: rows are flipped so that, once", + " // rotated to have heights along Z, rows go along Y.", + " for (let j = 0; j < size; j++) {", + " const row = (size - 1 - j) * sampleCount;", + " for (let i = 0; i < size; i++) {", + " samples[row + i] = this._getSampleHeight(j * size + i, noCollision);", + " }", + " }", + " const shape = settings.Create().Get();", + " Jolt.destroy(settings);", + " this.heightField = Jolt.castObject(shape, Jolt.HeightFieldShape);", + " this.changedSamples.clear();", + " return shape;", + " }", + "", + " /** @returns {number} The height of a sample in the shape, or no collision in a hole. */", + " _getSampleHeight(index, noCollision) {", + " const height = this.terrain.surfaceHeights[index];", + " return Number.isNaN(height) ? noCollision : height * this.heightScale;", + " }", + "", + " _getBodyPosition() {", + " const { object } = this.terrain;", + " const worldInvScale = this.physics._sharedData.worldInvScale;", + " this.bodyX = object.getX();", + " this.bodyY = object.getY();", + " this.bodyZ = object.getZ();", + " // The origin of the height field is at the bottom of the terrain (see createShape).", + " this.bodyHeight = object.getHeight();", + " return this.physics._sharedData.getRVec3(", + " object.getX() * worldInvScale,", + " (object.getY() + object.getHeight()) * worldInvScale,", + " object.getZ() * worldInvScale", + " );", + " }", + "", + " _getBodyRotation() {", + " // A quarter turn around X: heights go along Z.", + " return this.physics._sharedData.getQuat(Math.SQRT1_2, 0, 0, Math.SQRT1_2);", + " }", + "", + " createAndAddBody() {", + " const { physics } = this;", + " const { bodyInterface } = physics._sharedData;", + " const shape = this.createShape();", + " const settings = new Jolt.BodyCreationSettings(", + " shape,", + " this._getBodyPosition(),", + " this._getBodyRotation(),", + " Jolt.EMotionType_Static,", + " physics.getBodyLayer()", + " );", + " settings.mFriction = physics.friction;", + " settings.mRestitution = physics.restitution;", + " const body = bodyInterface.CreateBody(settings);", + " Jolt.destroy(settings);", + " bodyInterface.AddBody(body.GetID(), Jolt.EActivation_DontActivate);", + " return body;", + " }", + "", + " updateObjectFromBody() {", + " // The terrain is static: the body never moves it.", + " }", + "", + " updateBodyFromObject() {", + " const { physics } = this;", + " const body = physics._body;", + " if (!body) return;", + " const { object } = this.terrain;", + " const { bodyInterface } = physics._sharedData;", + " if (", + " object.getX() !== this.bodyX ||", + " object.getY() !== this.bodyY ||", + " object.getZ() !== this.bodyZ ||", + " object.getHeight() !== this.bodyHeight", + " ) {", + " bodyInterface.SetPositionAndRotation(", + " body.GetID(),", + " this._getBodyPosition(),", + " this._getBodyRotation(),", + " Jolt.EActivation_DontActivate", + " );", + " }", + " if (!this.changedSamples.isEmpty()) {", + " this._applyChangedHeights(body);", + " }", + " }", + "", + " _applyChangedHeights(body) {", + " const { data } = this.terrain;", + " const { size } = data;", + " const { minI, minJ, maxI, maxJ } = this.changedSamples;", + " // Jolt only changes blocks of samples. Rows are flipped and extra", + " // samples have no collision (see createShape).", + " const blockSize = this.heightField.GetBlockSize();", + " const sampleCount = this.heightField.GetSampleCount();", + " const alignDown = (value) => Math.floor(value / blockSize) * blockSize;", + " const alignUp = (value) => Math.min(Math.ceil(value / blockSize) * blockSize, sampleCount);", + " const firstX = alignDown(minI);", + " const firstRow = alignDown(size - 1 - maxJ);", + " const width = alignUp(maxI + 1) - firstX;", + " const height = alignUp(size - minJ) - firstRow;", + " if (width * height > this.heightsBufferLength) {", + " if (this.heightsBufferPointer) Jolt._webidl_free(this.heightsBufferPointer);", + " this.heightsBufferLength = width * height;", + " this.heightsBufferPointer = Jolt._webidl_malloc(this.heightsBufferLength * 4);", + " }", + " const heights = new Float32Array(Jolt.HEAPF32.buffer, this.heightsBufferPointer, width * height);", + " const noCollision = Jolt.HeightFieldShapeConstantValues.prototype.cNoCollisionValue;", + " for (let row = 0; row < height; row++) {", + " const j = size - 1 - (firstRow + row);", + " for (let column = 0; column < width; column++) {", + " const i = firstX + column;", + " heights[row * width + column] =", + " i < size && j >= 0 ? this._getSampleHeight(j * size + i, noCollision) : noCollision;", + " }", + " }", + " const sharedData = this.physics._sharedData;", + " this.heightField.SetHeights(", + " firstX,", + " firstRow,", + " width,", + " height,", + " this.heightsBufferPointer,", + " width,", + " sharedData.jolt.GetTempAllocator()", + " );", + " sharedData.bodyInterface.NotifyShapeChanged(", + " body.GetID(),", + " sharedData.getVec3(0, 0, 0),", + " false,", + " Jolt.EActivation_DontActivate", + " );", + " this._wakeUpBodiesOnChangedSamples();", + " this.changedSamples.clear();", + " }", + "", + " /** Bodies sleeping on the changed area would otherwise float or sink. */", + " _wakeUpBodiesOnChangedSamples() {", + " const { object, data } = this.terrain;", + " const worldInvScale = this.physics._sharedData.worldInvScale;", + " const cellWidth = object.getWidth() / data.resolution;", + " const cellHeight = object.getHeight() / data.resolution;", + " const { minI, minJ, maxI, maxJ } = this.changedSamples;", + " const min = new Jolt.Vec3(", + " (object.getX() + (minI - 1) * cellWidth) * worldInvScale,", + " (object.getY() + (minJ - 1) * cellHeight) * worldInvScale,", + " (object.getZ() - 1) * worldInvScale", + " );", + " const max = new Jolt.Vec3(", + " (object.getX() + (maxI + 1) * cellWidth) * worldInvScale,", + " (object.getY() + (maxJ + 1) * cellHeight) * worldInvScale,", + " (object.getZ() + object.getDepth() + 1) * worldInvScale", + " );", + " const box = new Jolt.AABox(min, max);", + " const broadPhaseLayerFilter = new Jolt.BroadPhaseLayerFilter();", + " const objectLayerFilter = new Jolt.ObjectLayerFilter();", + " this.physics._sharedData.bodyInterface.ActivateBodiesInAABox(box, broadPhaseLayerFilter, objectLayerFilter);", + " Jolt.destroy(min);", + " Jolt.destroy(max);", + " Jolt.destroy(box);", + " Jolt.destroy(broadPhaseLayerFilter);", + " Jolt.destroy(objectLayerFilter);", + " }", + "", + " recreateShape() {", + " const body = this.physics._body;", + " if (!body) return;", + " this.physics._sharedData.bodyInterface.SetShape(", + " body.GetID(),", + " this.createShape(),", + " false,", + " Jolt.EActivation_DontActivate", + " );", + " const { resolution } = this.terrain.data;", + " this.changedSamples.set(0, 0, resolution, resolution);", + " this._wakeUpBodiesOnChangedSamples();", + " this.changedSamples.clear();", + " }", + "", + " destroyBody() {", + " this.defaultBodyUpdater.destroyBody();", + " this.heightField = null;", + " }", + "", + " dispose() {", + " if (this.heightsBufferPointer) Jolt._webidl_free(this.heightsBufferPointer);", + " this.heightsBufferPointer = 0;", + " this.heightsBufferLength = 0;", + " }", + "}", + "", + "/** @param {TerrainData} data */", + "const copyTerrainData = (data) => {", + " const copy = new TerrainData(data.resolution);", + " copy.copyFrom(data);", + " return copy;", + "};", + "", + "// Generating a relief or reading an image is long for big terrains, and done", + "// for every instance and every hot-reload: the results are kept.", + "const baseDataCache = new Map();", + "const MAX_CACHED_BASE_DATA = 8;", + "// Changes of the shape of a terrain kept for consumers of its surface that", + "// missed some versions: older changes update the whole surface.", + "const MAX_SURFACE_CHANGES = 64;", + "// Unique among all terrains: a terrain replaced by another one never has its version.", + "let lastGroundVersion = 0;", + "", + "/**", + " * The terrain of a Terrain3D object: its data, how it's drawn and its", + " * surface (for collisions and navigation meshes). Positions are in the scene.", + " */", + "class Terrain {", + " /** @param {gdjs.CustomRuntimeObject3D} object */", + " constructor(object) {", + " this.object = object;", + " /** @type {TerrainData} */", + " this.data = new TerrainData(DEFAULT_RESOLUTION);", + " /** The data before any sculpting or painting, from the relief or heightmap. */", + " this.baseData = this.data;", + " this.renderer = new TerrainRenderer(this);", + " /** @type {TerrainBodyUpdater[]} */", + " this.bodyUpdaters = [];", + " this.changedSamples = new SampleRectangle();", + " this.hasPaintChanged = false;", + " /** Increased each time the ground changes, for objects following it (like grass). */", + " this.version = 0;", + " /** Increased each time the shape of the ground changes (not its paint or position). */", + " this.surfaceVersion = 0;", + " /**", + " * The last changes of the shape of the ground, for `getChangedArea`.", + " * @type {Array<{version: number, samples: SampleRectangle}>}", + " */", + " this.surfaceChanges = [];", + " /** The changes since this version are all in `surfaceChanges`. */", + " this.surfaceChangesStartVersion = 0;", + " this._stroke = { cellWidth: 1, cellHeight: 1, ax: 0, ay: 0, bx: 0, by: 0, radius: 0 };", + " this._strokeChangedSamples = new SampleRectangle();", + " /** The properties the data was loaded from, to reload it only if they change. */", + " this.loadedFrom = '';", + " /** The heightmap image being loaded: the relief is used until it is. */", + " this.loadingHeightmapImage = '';", + " this.isDisposed = false;", + " /** The last value of the sculpt data property read or saved. */", + " this.sculptData = '';", + " /**", + " * Values saved by the editor tools: the editor sends them back with a", + " * hot-reload, possibly after newer ones were saved.", + " * @type {string[]}", + " */", + " this.savedSculptData = [];", + " /** The heights of the surface: the heights of the data, and NaN in holes. */", + " this.surfaceHeights = new Float32Array(0);", + " this.loadFromProperties();", + " this._transform = this._getTransform();", + " getGrounds(object.getRuntimeScene()).add(this);", + " /**", + " * The shape of the ground, for the physics engine and navigation meshes.", + " * @type {gdjs.Surface}", + " */", + " this.surface = {", + " getVersion: () => this.surfaceVersion,", + " getChangedArea: (sinceVersion) => this.getChangedArea(sinceVersion),", + " getTriangles: () => null,", + " getHeightField: () => ({ columns: this.data.size, rows: this.data.size, heights: this.surfaceHeights }),", + " };", + " if (typeof object.setSurface === 'function') {", + " object.setSurface(this.surface);", + " } else if (gdjs.Physics3DRuntimeBehavior) {", + " // Older GDevelop versions don't use surfaces.", + " for (const behavior of object._behaviors) {", + " if (behavior instanceof gdjs.Physics3DRuntimeBehavior) {", + " this.bodyUpdaters.push(new TerrainBodyUpdater(this, behavior));", + " }", + " }", + " }", + " }", + "", + " _getSourceProperties() {", + " const { object } = this;", + " return [", + " object._getResolution(),", + " object._getRelief(),", + " object._getSeed(),", + " object._getHeightmapImage(),", + " object._getEdits(),", + " ].join('|');", + " }", + "", + " loadFromProperties() {", + " const { object } = this;", + " const resolution = parseInt(object._getResolution(), 10);", + " const validResolution = RESOLUTIONS.indexOf(resolution) !== -1 ? resolution : DEFAULT_RESOLUTION;", + " this.sculptData = object._getSculptData();", + " this.loadedFrom = this._getSourceProperties();", + "", + " this.baseData = this._getBaseData(validResolution);", + " let data = codec.decode(this.sculptData, (resolution) => copyTerrainData(this._getBaseData(resolution)));", + " if (data && data.resolution !== validResolution) {", + " data = data.resampled(validResolution);", + " }", + " this.setData(data || copyTerrainData(this.baseData));", + " }", + "", + " /**", + " * @param {number} resolution", + " * @returns {TerrainData} The data from the relief or the heightmap, shared: it must not be changed.", + " */", + " _getBaseData(resolution) {", + " const { object } = this;", + " const key = [resolution, object._getRelief(), object._getSeed(), object._getHeightmapImage(), object._getEdits()].join('|');", + " const cachedData = baseDataCache.get(key);", + " if (cachedData) return cachedData;", + "", + " const data = new TerrainData(resolution);", + " const heightmapImage = object._getHeightmapImage();", + " const isHeightmapLoaded =", + " !!heightmapImage && loadHeightsFromImage(data, object.getRuntimeScene().getGame(), heightmapImage);", + " if (!isHeightmapLoaded) {", + " relief.generate(data, object._getRelief(), object._getSeed());", + " if (heightmapImage) this._reloadOnceLoaded(heightmapImage);", + " }", + " edits.apply(data, object._getEdits());", + " // Not kept if the image is missing: it may be loaded later.", + " if (isHeightmapLoaded || !heightmapImage) baseDataCache.set(key, data);", + " if (baseDataCache.size > MAX_CACHED_BASE_DATA) {", + " baseDataCache.delete(baseDataCache.keys().next().value);", + " }", + " return data;", + " }", + "", + " /** @param {TerrainData} data */", + " setData(data) {", + " this.version = ++lastGroundVersion;", + " this.surfaceVersion = this.version;", + " this.surfaceChanges.length = 0;", + " this.surfaceChangesStartVersion = this.version;", + " const hasSameResolution = data.resolution === this.data.resolution && this.renderer.chunks.length > 0;", + " this.data = data;", + " const allSamples = new SampleRectangle().set(0, 0, data.resolution, data.resolution);", + " this.surfaceHeights = new Float32Array(data.heights.length);", + " this.renderer.setHasHoles(false);", + " this._updateSurfaceHeights(allSamples);", + " if (hasSameResolution) {", + " this.renderer.updateSamples(allSamples, true);", + " } else {", + " this.renderer.rebuild();", + " }", + " for (const bodyUpdater of this.bodyUpdaters) {", + " bodyUpdater.recreateShape();", + " }", + " }", + "", + " onHotReloading() {", + " const sculptData = this.object._getSculptData();", + " const savedIndex = this.savedSculptData.indexOf(sculptData);", + " if (savedIndex !== -1 && this._getSourceProperties() === this.loadedFrom) {", + " // The terrain already has this data, or newer changes.", + " this.savedSculptData.splice(0, savedIndex + 1);", + " } else if (sculptData !== this.sculptData || this._getSourceProperties() !== this.loadedFrom) {", + " this.savedSculptData.length = 0;", + " this.loadFromProperties();", + " }", + " this.renderer.updateMaterial();", + " }", + "", + " /** @returns {string} The data to save in the sculpt data property. */", + " save() {", + " this.sculptData = codec.encode(this.data, this.baseData);", + " this.savedSculptData.push(this.sculptData);", + " return this.sculptData;", + " }", + "", + " getCellWidth() {", + " return this.object.getWidth() / this.data.resolution;", + " }", + "", + " getCellHeight() {", + " return this.object.getHeight() / this.data.resolution;", + " }", + "", + " _toGridX(x) {", + " return (x - this.object.getX()) / this.getCellWidth();", + " }", + "", + " _toGridY(y) {", + " return (y - this.object.getY()) / this.getCellHeight();", + " }", + "", + " /** @returns {number} The Z position of the ground. */", + " getHeightAt(x, y) {", + " return this.object.getZ() + this.data.sampleHeight(this._toGridX(x), this._toGridY(y)) * this.object.getDepth();", + " }", + "", + " /** @returns {number} The angle of the ground with the horizontal, in degrees. */", + " getSlopeAt(x, y) {", + " const gridX = this._toGridX(x);", + " const gridY = this._toGridY(y);", + " const { data } = this;", + " const depth = this.object.getDepth();", + " const slopeX = ((data.sampleHeight(gridX + 0.5, gridY) - data.sampleHeight(gridX - 0.5, gridY)) * depth) / this.getCellWidth();", + " const slopeY = ((data.sampleHeight(gridX, gridY + 0.5) - data.sampleHeight(gridX, gridY - 0.5)) * depth) / this.getCellHeight();", + " return gdjs.toDegrees(Math.atan(Math.hypot(slopeX, slopeY)));", + " }", + "", + " /** @returns {number} The layer painted the most, from 1 to 4. */", + " getLayerAt(x, y) {", + " return this.data.getDominantLayer(this._toGridX(x), this._toGridY(y)) + 1;", + " }", + "", + " /**", + " * @param {number} layer From 1 to 4.", + " * @returns {number} How much the layer is painted, from 0 to 1.", + " */", + " getLayerWeightAt(x, y, layer) {", + " const layerIndex = clampInteger(layer - 1, 0, LAYER_COUNT - 1);", + " return this.data.sampleLayerWeight(this._toGridX(x), this._toGridY(y), layerIndex);", + " }", + "", + " /** @returns {number[]} The painted tint: red, green and blue, from 0 to 255 (white when there is none). */", + " getTintAt(x, y) {", + " const gridX = this._toGridX(x);", + " const gridY = this._toGridY(y);", + " return [0, 1, 2].map((channel) => Math.round(this.data.sampleColor(gridX, gridY, channel) * 255));", + " }", + "", + " /** @returns {number} How wet the ground is, from 0 (dry) to 1, painted or near the water (see the wet shore). */", + " getWetnessAt(x, y) {", + " const paintedWetness = this.data.sampleColor(this._toGridX(x), this._toGridY(y), 3);", + " const { object } = this;", + " if (!object._getWetShore()) return paintedWetness;", + " const fade = Math.max(object._getWetShoreFade(), 0.001);", + " const shoreWetness = clamp01((object._getWetShoreZ() + fade - this.getHeightAt(x, y)) / fade);", + " // Like the smoothstep of the shader.", + " return Math.max(paintedWetness, shoreWetness * shoreWetness * (3 - 2 * shoreWetness));", + " }", + "", + " _isInArea(x, y) {", + " const { object } = this;", + " return (", + " x >= object.getX() &&", + " x <= object.getX() + object.getWidth() &&", + " y >= object.getY() &&", + " y <= object.getY() + object.getHeight()", + " );", + " }", + "", + " /** @returns {boolean} true if the position is above or under the terrain, and not in a hole. */", + " containsPoint(x, y) {", + " return this._isInArea(x, y) && this.data.sampleSolidity(this._toGridX(x), this._toGridY(y)) >= 0.5;", + " }", + "", + " /** @returns {boolean} true if there is a hole in the terrain at the position, like what is drawn. */", + " isHoleAt(x, y) {", + " return this._isInArea(x, y) && this.data.sampleSolidity(this._toGridX(x), this._toGridY(y)) < 0.5;", + " }", + "", + " _getStroke(x1, y1, x2, y2, radius) {", + " const stroke = this._stroke;", + " stroke.cellWidth = this.getCellWidth();", + " stroke.cellHeight = this.getCellHeight();", + " stroke.ax = x1 - this.object.getX();", + " stroke.ay = y1 - this.object.getY();", + " stroke.bx = x2 - this.object.getX();", + " stroke.by = y2 - this.object.getY();", + " stroke.radius = Math.max(radius, 0);", + " return stroke;", + " }", + "", + " /**", + " * @param {SampleRectangle} samples", + " * @param {boolean} haveHeightsChanged false when only the paint changed.", + " */", + " _onSamplesChanged(samples, haveHeightsChanged) {", + " if (samples.isEmpty()) return;", + " this.version = ++lastGroundVersion;", + " this.changedSamples.add(samples);", + " // Painting changes the surface when it makes holes or fills them.", + " const hasSurfaceChanged = this._updateSurfaceHeights(samples);", + " if (!haveHeightsChanged && !hasSurfaceChanged) return;", + " this.surfaceVersion = this.version;", + " this.surfaceChanges.push({ version: this.version, samples: new SampleRectangle().set(samples.minI, samples.minJ, samples.maxI, samples.maxJ) });", + " if (this.surfaceChanges.length > MAX_SURFACE_CHANGES) {", + " this.surfaceChangesStartVersion = this.surfaceChanges.shift().version;", + " }", + " for (const bodyUpdater of this.bodyUpdaters) {", + " bodyUpdater.changedSamples.add(samples);", + " }", + " }", + "", + " /**", + " * @param {SampleRectangle} samples", + " * @returns {boolean} true if heights of the surface changed, or holes were made or filled.", + " */", + " _updateSurfaceHeights(samples) {", + " const { data, surfaceHeights } = this;", + " let hasChanged = false;", + " let hasHoles = false;", + " for (let j = samples.minJ; j <= samples.maxJ; j++) {", + " for (let i = samples.minI; i <= samples.maxI; i++) {", + " const index = j * data.size + i;", + " const isHole = data.isHole(index);", + " const height = isHole ? NaN : data.heights[index];", + " const previousHeight = surfaceHeights[index];", + " if (isHole) {", + " hasHoles = true;", + " if (Number.isNaN(previousHeight)) continue;", + " } else if (height === previousHeight) {", + " continue;", + " }", + " surfaceHeights[index] = height;", + " hasChanged = true;", + " }", + " }", + " // Filled holes keep the shaders drawing holes until the data is loaded again.", + " if (hasHoles) this.renderer.setHasHoles(true);", + " return hasChanged;", + " }", + "", + " /** @param {number} height In scene units, negative to lower the ground. */", + " raise(x1, y1, x2, y2, radius, height) {", + " const samples = this._strokeChangedSamples;", + " brushes.raise(this.data, this._getStroke(x1, y1, x2, y2, radius), height / this.object.getDepth(), samples);", + " this._onSamplesChanged(samples, true);", + " }", + "", + " /** @param {number} z The Z position to flatten the ground to. */", + " /** @param {number} [toZ] The Z position at the end of the segment, for a slope. The same as z by default. */", + " flatten(x1, y1, x2, y2, radius, z, strength, toZ = z) {", + " const samples = this._strokeChangedSamples;", + " const toHeight = (value) => (value - this.object.getZ()) / this.object.getDepth();", + " brushes.flatten(this.data, this._getStroke(x1, y1, x2, y2, radius), toHeight(z), strength, samples, toHeight(toZ));", + " this._onSamplesChanged(samples, true);", + " }", + "", + " smooth(x1, y1, x2, y2, radius, strength) {", + " const samples = this._strokeChangedSamples;", + " brushes.smooth(this.data, this._getStroke(x1, y1, x2, y2, radius), strength, samples);", + " this._onSamplesChanged(samples, true);", + " }", + "", + " /** @param {number} layer From 1 to 4. */", + " paint(x1, y1, x2, y2, radius, layer, strength) {", + " const layerIndex = clampInteger(layer - 1, 0, LAYER_COUNT - 1);", + " const samples = this._strokeChangedSamples;", + " brushes.paint(this.data, this._getStroke(x1, y1, x2, y2, radius), layerIndex, strength, samples);", + " this.hasPaintChanged = true;", + " this._onSamplesChanged(samples, false);", + " }", + "", + " /** @param {number[]} color The red, green and blue (0-255) of the tint: white removes it. */", + " tint(x1, y1, x2, y2, radius, color, strength) {", + " const samples = this._strokeChangedSamples;", + " brushes.tint(this.data, this._getStroke(x1, y1, x2, y2, radius), color, strength, samples);", + " this.hasPaintChanged = true;", + " this._onSamplesChanged(samples, false);", + " }", + "", + " /** @param {number} wetness From 0 (dry) to 1. */", + " wet(x1, y1, x2, y2, radius, wetness, strength) {", + " const samples = this._strokeChangedSamples;", + " brushes.wet(this.data, this._getStroke(x1, y1, x2, y2, radius), wetness, strength, samples);", + " this.hasPaintChanged = true;", + " this._onSamplesChanged(samples, false);", + " }", + "", + " /** Make a hole around a segment (`fillHoles` fills it again). */", + " makeHole(x1, y1, x2, y2, radius) {", + " const samples = this._strokeChangedSamples;", + " brushes.hole(this.data, this._getStroke(x1, y1, x2, y2, radius), samples);", + " this.hasPaintChanged = true;", + " this._onSamplesChanged(samples, false);", + " }", + "", + " /** Fill the holes around a segment with the layers of the ground around them. */", + " fillHoles(x1, y1, x2, y2, radius) {", + " const samples = this._strokeChangedSamples;", + " brushes.fill(this.data, this._getStroke(x1, y1, x2, y2, radius), samples);", + " this.hasPaintChanged = true;", + " this._onSamplesChanged(samples, false);", + " }", + "", + " /**", + " * Make the entrance of a tunnel or a cave: a hole, with the ground in", + " * front of it flattened to the floor of the tunnel.", + " * @param {number} angle The direction going into the ground, in degrees.", + " * @param {number} floorZ", + " * @param {number} length The length of the flat ground in front of the entrance.", + " */", + " carveEntrance(x, y, radius, angle, floorZ, length) {", + " const directionX = Math.cos(gdjs.toRad(angle));", + " const directionY = Math.sin(gdjs.toRad(angle));", + " this.flatten(x - directionX * length, y - directionY * length, x, y, radius * 1.5, floorZ, 1);", + " this.makeHole(x, y, x + directionX * radius, y + directionY * radius, radius);", + " }", + "", + " generate(reliefName, seed) {", + " this.baseData = new TerrainData(this.data.resolution);", + " relief.generate(this.baseData, reliefName, seed);", + " this.setData(copyTerrainData(this.baseData));", + " }", + "", + " loadHeightmap(imageResourceName) {", + " const baseData = new TerrainData(this.data.resolution);", + " if (loadHeightsFromImage(baseData, this.object.getRuntimeScene().getGame(), imageResourceName)) {", + " baseData.splat.set(this.data.splat);", + " baseData.colors.set(this.data.colors);", + " this.baseData = baseData;", + " this.setData(copyTerrainData(baseData));", + " }", + " }", + "", + " /** @param {gdjs.RuntimeObject[]} objects */", + " placeOnGround(objects) {", + " for (const object of objects) {", + " if (object === this.object || !gdjs.Base3DHandler.is3D(object)) continue;", + " const groundZ = this.getHeightAt(object.getCenterXInScene(), object.getCenterYInScene());", + " // The origin of the object is not always at its bottom.", + " object.setZ(groundZ + object.getZ() - object.getUnrotatedAABBMinZ());", + " }", + " }", + "", + " /**", + " * @returns {THREE.Vector3 | null} The point of the ground under the cursor,", + " * in the terrain group coordinates. Holes are hit too, so that brushes can fill them.", + " */", + " raycastFromCursor(game, raycaster, ndc) {", + " const layer = this.object.getInstanceContainer().getLayer(this.object.getLayer());", + " const camera = layer.getRenderer().getThreeCamera();", + " if (!camera) return null;", + " const inputManager = game.getInputManager();", + " ndc.set(", + " (inputManager.getCursorX() / game.getGameResolutionWidth()) * 2 - 1,", + " -(inputManager.getCursorY() / game.getGameResolutionHeight()) * 2 + 1", + " );", + " raycaster.setFromCamera(ndc, camera);", + " this.renderer.group.updateWorldMatrix(true, false);", + " const ray = raycaster.ray.clone().applyMatrix4(this.renderer.group.matrixWorld.clone().invert());", + " ray.direction.normalize();", + "", + " const box = new THREE.Box3(new THREE.Vector3(0, 0, 0), new THREE.Vector3(AREA_SIZE, AREA_SIZE, AREA_DEPTH));", + " const entry = box.containsPoint(ray.origin) ? ray.origin.clone() : ray.intersectBox(box, new THREE.Vector3());", + " if (!entry) return null;", + " const cellSize = AREA_SIZE / this.data.resolution;", + " const groundZAt = (point) => this.data.sampleHeight(point.x / cellSize, point.y / cellSize) * AREA_DEPTH;", + " const step = cellSize / 2;", + " const point = entry.clone();", + " const previousPoint = entry.clone();", + " for (let distance = 0; distance < AREA_SIZE * 2; distance += step) {", + " if (point.z <= groundZAt(point)) {", + " // Refine between the last point above the ground and this one.", + " for (let iteration = 0; iteration < 8; iteration++) {", + " const middle = previousPoint.clone().lerp(point, 0.5);", + " if (middle.z <= groundZAt(middle)) point.copy(middle);", + " else previousPoint.copy(middle);", + " }", + " return point;", + " }", + " if (!box.containsPoint(point)) return null;", + " previousPoint.copy(point);", + " point.addScaledVector(ray.direction, step);", + " }", + " return null;", + " }", + "", + " /**", + " * @param {THREE.Vector3} localPosition In the terrain group coordinates (like from `raycastFromCursor`).", + " * @returns {{x: number, y: number, z: number}} The position in the scene.", + " */", + " toScenePosition(localPosition) {", + " const { object } = this;", + " return {", + " x: object.getX() + (localPosition.x / AREA_SIZE) * object.getWidth(),", + " y: object.getY() + (localPosition.y / AREA_SIZE) * object.getHeight(),", + " z: object.getZ() + (localPosition.z / AREA_DEPTH) * object.getDepth(),", + " };", + " }", + "", + " /** Draw the changes of the ground (collisions are updated by the physics engine). */", + " applyChanges() {", + " this.renderer.updateSamples(this.changedSamples, this.hasPaintChanged);", + " this.changedSamples.clear();", + " this.hasPaintChanged = false;", + " }", + "", + " /** @returns {number} A number changing each time the ground changes (see `getGrounds`). */", + " getVersion() {", + " return this.version;", + " }", + "", + " /**", + " * @param {number} sinceVersion", + " * @returns {gdjs.SurfaceArea | null} The part of the ground whose shape changed since the version (see `gdjs.Surface`).", + " */", + " getChangedArea(sinceVersion) {", + " if (sinceVersion < this.surfaceChangesStartVersion) return null;", + " const samples = new SampleRectangle();", + " for (const change of this.surfaceChanges) {", + " if (change.version > sinceVersion) samples.add(change.samples);", + " }", + " if (samples.isEmpty()) return { minX: 0, minY: 0, maxX: 0, maxY: 0 };", + " const { resolution } = this.data;", + " return {", + " minX: samples.minI / resolution,", + " minY: samples.minJ / resolution,", + " maxX: samples.maxI / resolution,", + " maxY: samples.maxJ / resolution,", + " };", + " }", + "", + " _getTransform() {", + " const { object } = this;", + " return [object.getX(), object.getY(), object.getZ(), object.getWidth(), object.getHeight(), object.getDepth()].join(',');", + " }", + "", + " /** @param {string} imageResourceName A heightmap image that is not loaded yet. */", + " _reloadOnceLoaded(imageResourceName) {", + " if (this.loadingHeightmapImage === imageResourceName) return;", + " this.loadingHeightmapImage = imageResourceName;", + " const game = this.object.getRuntimeScene().getGame();", + " game.getImageManager()", + " .loadResource(imageResourceName)", + " .then(() => {", + " if (this.isDisposed || this.loadingHeightmapImage !== imageResourceName) return;", + " this.loadingHeightmapImage = '';", + " if (getLoadedImage(game, imageResourceName)) this.loadFromProperties();", + " })", + " .catch(() => {});", + " }", + "", + " update() {", + " const transform = this._getTransform();", + " if (transform !== this._transform) {", + " this._transform = transform;", + " this.version = ++lastGroundVersion;", + " }", + " this.applyChanges();", + " this.renderer.uniforms.terrainWorldScale.value = getWorldScale(this.object);", + " this._updateWetShore();", + " const layer = this.object.getInstanceContainer().getLayer(this.object.getLayer());", + " this.renderer.updateLevelsOfDetail(layer.getRenderer().getThreeCamera());", + " }", + "", + " _updateWetShore() {", + " const { object } = this;", + " // In the coordinates of the terrain group, like the vertices.", + " const depthScale = AREA_DEPTH / Math.max(object.getDepth(), 0.0001);", + " this.renderer.uniforms.terrainWetShore.value.set(", + " (object._getWetShoreZ() - object.getZ()) * depthScale,", + " Math.max(object._getWetShoreFade(), 0.001) * depthScale,", + " object._getWetShore() ? 1 : 0", + " );", + " }", + "", + " dispose() {", + " this.isDisposed = true;", + " getGrounds(this.object.getRuntimeScene()).delete(this);", + " if (typeof this.object.setSurface === 'function' && this.object.getSurface() === this.surface) {", + " this.object.setSurface(null);", + " }", + " this.renderer.dispose();", + " for (const bodyUpdater of this.bodyUpdaters) {", + " bodyUpdater.dispose();", + " }", + " }", + "}", + "", + "/**", + " * @param {gdjs.RuntimeObject} object", + " * @returns {number} The number of scene units by 3D world unit (1 in GDevelop versions before the world scale).", + " */", + "const getWorldScale = (object) => {", + " const scene = object.getRuntimeScene();", + " return typeof scene.getRenderer3DWorldScale === 'function' ? scene.getRenderer3DWorldScale() : 1;", + "};", + "", + "const averageColors = new WeakMap();", + "let white = null;", + "/**", + " * @param {THREE.Texture} texture", + " * @returns {THREE.Color} The average color of the texture (white when it's not loaded).", + " */", + "const getAverageColor = (texture) => {", + " const cachedColor = averageColors.get(texture);", + " if (cachedColor) return cachedColor;", + " const image = texture.image;", + " const isDrawable =", + " typeof document !== 'undefined' &&", + " ((image instanceof HTMLImageElement && image.complete && image.naturalWidth > 0) ||", + " image instanceof HTMLCanvasElement ||", + " (typeof ImageBitmap !== 'undefined' && image instanceof ImageBitmap));", + " if (!isDrawable) return white || (white = new THREE.Color(1, 1, 1));", + " const size = 8;", + " const canvas = document.createElement('canvas');", + " canvas.width = size;", + " canvas.height = size;", + " const context = canvas.getContext('2d');", + " context.drawImage(image, 0, 0, size, size);", + " const pixels = context.getImageData(0, 0, size, size).data;", + " let red = 0;", + " let green = 0;", + " let blue = 0;", + " for (let index = 0; index < pixels.length; index += 4) {", + " red += pixels[index];", + " green += pixels[index + 1];", + " blue += pixels[index + 2];", + " }", + " const count = size * size * 255;", + " const color = new THREE.Color().setRGB(red / count, green / count, blue / count, THREE.SRGBColorSpace);", + " averageColors.set(texture, color);", + " return color;", + "};", + "", + "const svgIcon = (content) =>", + " 'data:image/svg+xml,' +", + " encodeURIComponent('' + content + '');", + "const strokes = 'fill=\"none\" stroke=\"#000\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"';", + "const toolIcons = {", + " Raise: svgIcon(''),", + " Lower: svgIcon(''),", + " Smooth: svgIcon(''),", + " Flatten: svgIcon(''),", + " Size: svgIcon(''),", + " Strength: svgIcon(''),", + " Undo: svgIcon(''),", + " Hole: svgIcon(''),", + " FillHoles: svgIcon(''),", + " Wetness: svgIcon(''),", + "};", + "// Tints for the color brush when the editor has no color picker (older GDevelop versions).", + "const tintPresets = ['#e2c879', '#a5c45a', '#8a6a48', '#c98a62', '#9a9a92', '#ffffff'];", + "", + "/**", + " * The sculpt and paint tools shown in the scene editor when a terrain is", + " * selected. Without the needed editor functions (older GDevelop versions),", + " * nothing is shown: the terrain can still be changed with events.", + " */", + "class TerrainEditorTools {", + " constructor() {", + " /** '' when no brush is used. */", + " this.toolName = '';", + " this.sizePercent = 30;", + " this.strengthPercent = 50;", + " /** @type {Terrain | null} */", + " this.terrain = null;", + " /** @type {gdjs.InGameEditor | null} */", + " this.editor = null;", + " this.isStroking = false;", + " this.flattenHeight = 0;", + " /** The color of the \"Tint\" brush, as \"#rrggbb\". */", + " this.tintColor = tintPresets[0];", + " this.wetnessPercent = 100;", + " this.lastStrokeTime = 0;", + " /** @type {{data: TerrainData, baseData: TerrainData}[]} */", + " this.undoSteps = [];", + " this.raycaster = new THREE.Raycaster();", + " this.ndc = new THREE.Vector2();", + " this._swatchColor = new THREE.Color();", + " }", + "", + " /** @param {gdjs.InGameEditor} editor */", + " update(editor) {", + " if (typeof editor.showToolbar !== 'function') {", + " // Older GDevelop versions: the terrain can only be changed with events.", + " return;", + " }", + " this.editor = editor;", + " const selectedObjects = editor.getSelectedObjects();", + " const terrain =", + " selectedObjects.length === 1 && selectedObjects[0].__terrain3D ? selectedObjects[0].__terrain3D : null;", + " if (terrain !== this.terrain) {", + " this._endStroke();", + " if (this.terrain) this.terrain.renderer.setBrush(null, 0);", + " this.terrain = terrain;", + " this.undoSteps.length = 0;", + " }", + " if (terrain) editor.showToolbar('Terrain3D', this._getToolbarItems(terrain));", + " const game = editor.getRuntimeGame();", + " if (!terrain || !this.toolName) {", + " if (terrain) terrain.renderer.setBrush(null, 0);", + " return;", + " }", + "", + " const inputManager = game.getInputManager();", + " if (inputManager.wasKeyJustPressed(gdjs.evtTools.input.keysNameToCode.Escape)) {", + " this._endStroke();", + " this.toolName = '';", + " terrain.renderer.setBrush(null, 0);", + " return;", + " }", + " editor.captureLeftMouseButton();", + " const hit = terrain.raycastFromCursor(game, this.raycaster, this.ndc);", + " const radius = this._getRadius();", + " terrain.renderer.setBrush(hit, radius);", + "", + " // Older GDevelop versions don't have it.", + " const isPressedForTool =", + " typeof editor.isCapturedLeftMouseButtonPressed === 'function'", + " ? editor.isCapturedLeftMouseButtonPressed()", + " : inputManager.isMouseButtonPressed(0);", + " // The button is released outside the canvas when the mouse is on a toolbar.", + " const isPressed = isPressedForTool && inputManager.isMouseInsideCanvas();", + " if (isPressed && hit && !this.isStroking) {", + " this._startStroke(terrain, hit);", + " }", + " if (this.isStroking && isPressed && hit) {", + " this._applyBrush(terrain, hit, radius);", + " }", + " if (!isPressed) {", + " this._endStroke();", + " }", + " }", + "", + " _getRadius() {", + " const { object } = this.terrain;", + " const maxRadius = Math.max(object.getWidth(), object.getHeight()) / 4;", + " return Math.max(maxRadius * Math.pow(this.sizePercent / 100, 2), this.terrain.getCellWidth());", + " }", + "", + " _startStroke(terrain, hit) {", + " this.isStroking = true;", + " this.lastStrokeTime = performance.now();", + " this.flattenHeight = terrain.toScenePosition(hit).z;", + " this.undoSteps.push({ data: copyTerrainData(terrain.data), baseData: terrain.baseData });", + " const maxUndoSteps = Math.max(Math.floor(UNDO_STEPS_SAMPLES / terrain.data.heights.length), 2);", + " while (this.undoSteps.length > maxUndoSteps) this.undoSteps.shift();", + " }", + "", + " _endStroke() {", + " if (!this.isStroking) return;", + " this.isStroking = false;", + " this._save();", + " }", + "", + " _applyBrush(terrain, hit, radius) {", + " const now = performance.now();", + " const elapsedSeconds = Math.min((now - this.lastStrokeTime) / 1000, 0.1);", + " this.lastStrokeTime = now;", + " const { x, y } = terrain.toScenePosition(hit);", + " const amount = (this.strengthPercent / 100) * elapsedSeconds;", + " const depth = terrain.object.getDepth();", + " if (this.toolName === 'Raise') terrain.raise(x, y, x, y, radius, amount * depth * 0.5);", + " else if (this.toolName === 'Lower') terrain.raise(x, y, x, y, radius, -amount * depth * 0.5);", + " else if (this.toolName === 'Smooth') terrain.smooth(x, y, x, y, radius, amount * 10);", + " else if (this.toolName === 'Flatten') terrain.flatten(x, y, x, y, radius, this.flattenHeight, amount * 10);", + " else if (this.toolName.startsWith('Paint')) {", + " terrain.paint(x, y, x, y, radius, parseInt(this.toolName.slice(5), 10), amount * 8);", + " } else if (this.toolName === 'Hole') {", + " terrain.makeHole(x, y, x, y, radius);", + " } else if (this.toolName === 'FillHoles') {", + " terrain.fillHoles(x, y, x, y, radius);", + " } else if (this.toolName === 'Tint') {", + " terrain.tint(x, y, x, y, radius, gdjs.rgbOrHexToRGBColor(this.tintColor), amount * 8);", + " } else if (this.toolName === 'Wetness') {", + " terrain.wet(x, y, x, y, radius, this.wetnessPercent / 100, amount * 8);", + " }", + " // Drawn now, as the terrain was updated earlier in this frame.", + " terrain.applyChanges();", + " }", + "", + " _save() {", + " const { terrain, editor } = this;", + " if (!terrain || !editor) return;", + " editor.updateObjectProperties(terrain.object.getName(), { SculptData: terrain.save() });", + " }", + "", + " _undo() {", + " const undoStep = this.undoSteps.pop();", + " // The relief, the edits or the resolution may have changed since the stroke.", + " if (!this.terrain || !undoStep || undoStep.baseData !== this.terrain.baseData) {", + " this.undoSteps.length = 0;", + " return;", + " }", + " this.terrain.setData(undoStep.data);", + " this._save();", + " }", + "", + " /** @returns {Array} The settings of the tint or wetness brush, when it's used. */", + " _getColorToolItems() {", + " const { editor } = this;", + " if (this.toolName === 'Tint') {", + " if (editor && typeof editor.isToolbarItemTypeSupported === 'function' && editor.isToolbarItemTypeSupported('color')) {", + " return [", + " {", + " type: 'color',", + " id: 'TintColor',", + " tooltip: 'Color of the tint',", + " value: this.tintColor,", + " onChange: (color) => (this.tintColor = color),", + " },", + " ];", + " }", + " return tintPresets.map((color) => ({", + " type: 'button',", + " id: 'TintPreset' + color,", + " tooltip: color === '#ffffff' ? 'No tint' : 'Tint with this color',", + " color,", + " isActive: this.tintColor === color,", + " onClick: () => (this.tintColor = color),", + " }));", + " }", + " if (this.toolName === 'Wetness') {", + " return [", + " {", + " type: 'slider',", + " id: 'WetnessAmount',", + " tooltip: 'Wetness',", + " iconUrl: toolIcons.Wetness,", + " min: 0,", + " max: 100,", + " value: this.wetnessPercent,", + " onChange: (value) => (this.wetnessPercent = value),", + " },", + " ];", + " }", + " return [];", + " }", + "", + " /**", + " * @param {Terrain} terrain", + " * @returns {Array} The toolbar for the current state (the editor only", + " * updates what changed).", + " */", + " _getToolbarItems(terrain) {", + " const { object } = terrain;", + " const toolButton = (toolName, tooltip, color) => ({", + " type: 'button',", + " id: toolName,", + " tooltip,", + " iconUrl: toolIcons[toolName],", + " color,", + " isActive: this.toolName === toolName,", + " onClick: () => (this.toolName = this.toolName === toolName ? '' : toolName),", + " });", + " const layerColors = [object._getLayer1Color(), object._getLayer2Color(), object._getLayer3Color(), object._getLayer4Color()];", + " // Layers are shown with their color, tinting their texture.", + " const swatchColors = layerColors.map((color, index) =>", + " this._swatchColor", + " .set(gdjs.rgbOrHexStringToNumber(color))", + " .multiply(getAverageColor(terrain.renderer.uniforms['terrainLayerMap' + index].value))", + " .getHexString()", + " );", + " return [", + " toolButton('Raise', 'Raise the ground (drag on the terrain)'),", + " toolButton('Lower', 'Lower the ground'),", + " toolButton('Smooth', 'Smooth the ground'),", + " toolButton('Flatten', 'Flatten the ground to the height where the drag starts'),", + " { type: 'divider', id: 'PaintDivider' },", + " ...swatchColors.map((swatchColor, index) =>", + " toolButton('Paint' + (index + 1), 'Paint layer ' + (index + 1), '#' + swatchColor)", + " ),", + " toolButton('Hole', 'Make a hole, for the entrance of a tunnel or a cave'),", + " toolButton('FillHoles', 'Fill holes, with the layers of the ground around them'),", + " { type: 'divider', id: 'ColorDivider' },", + " toolButton('Tint', 'Tint the ground (white removes the tint)', this.tintColor),", + " toolButton('Wetness', 'Make the ground wet, or dry with a wetness of 0'),", + " ...this._getColorToolItems(),", + " { type: 'divider', id: 'BrushDivider' },", + " {", + " type: 'slider',", + " id: 'Size',", + " tooltip: 'Brush size',", + " iconUrl: toolIcons.Size,", + " min: 1,", + " max: 100,", + " value: this.sizePercent,", + " onChange: (value) => (this.sizePercent = value),", + " },", + " {", + " type: 'slider',", + " id: 'Strength',", + " tooltip: 'Brush strength',", + " iconUrl: toolIcons.Strength,", + " min: 5,", + " max: 100,", + " value: this.strengthPercent,", + " onChange: (value) => (this.strengthPercent = value),", + " },", + " { type: 'button', id: 'Undo', tooltip: 'Undo the last stroke on this terrain', iconUrl: toolIcons.Undo, onClick: () => this._undo() },", + " ];", + " }", + "}", + "", + "const editorTools = new TerrainEditorTools();", + "if (gdjs.registerInGameEditorPostStepCallback) {", + " gdjs.registerInGameEditorPostStepCallback((editor) => editorTools.update(editor));", + "}", + "", + "gdjs.__terrain3DExtension = {", + " Terrain,", + " // Used by the terrain grass and the scattered models.", + " getWorldScale,", + " getGrounds,", + " patchShaderCode,", + " createRandom,", + " findHighestGround,", + " GroundPlacement,", + " svgIcon,", + " toolIcons,", + " // Exposed for tests.", + " TerrainBodyUpdater,", + " TerrainData,", + " SampleRectangle,", + " brushes,", + " edits,", + " codec,", + " relief,", + "};" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [], + "objectGroups": [] + }, + { + "description": "Define grass helper classes JavaScript code.", + "fullName": "Define grass helper classes", + "functionType": "Action", + "name": "DefineGrassHelperClasses", + "private": true, + "sentence": "Define grass helper classes JavaScript code", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "if (gdjs.__terrainGrassExtension) {", + " return;", + "}", + "// Blades grow on the grounds of the scene: the 3D terrains (see `getGrounds`", + "// in the terrain helper).", + "", + "// Size of the object inner area: instances are scaled from it.", + "const AREA_SIZE = 1000;", + "const AREA_DEPTH = 40;", + "const CHUNK_SIZE = 500;", + "const MAX_BENDING_OBJECTS = 8;", + "", + "/**", + " * A blade: a thin, tapered strip. Its width and height are 1: blades are", + " * scaled by the shader.", + " */", + "const createBladeGeometry = () => {", + " const halfWidths = [0.5, 0.42, 0.28, 0];", + " const heights = [0, 0.35, 0.7, 1];", + " const positions = [];", + " halfWidths.forEach((halfWidth, level) => {", + " positions.push(-halfWidth, 0, heights[level]);", + " if (halfWidth > 0) positions.push(halfWidth, 0, heights[level]);", + " });", + " const indices = [0, 1, 2, 1, 3, 2, 2, 3, 4, 3, 5, 4, 4, 5, 6];", + " const geometry = new THREE.InstancedBufferGeometry();", + " geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));", + " geometry.setAttribute('normal', new THREE.Float32BufferAttribute(new Array(positions.length).fill(0), 3));", + " geometry.setIndex(indices);", + " return geometry;", + "};", + "", + "const vertexShaderDeclarations = `", + "attribute vec4 grassRoot;", + "attribute vec2 grassShape;", + "// The tint of the ground at the root (linear, from 0 to 1).", + "attribute vec3 grassGroundTint;", + "uniform mat3 grassWorldToLocal;", + "uniform float grassWorldScale;", + "uniform float grassBladeWidth;", + "uniform float grassTime;", + "uniform float grassWindStrength;", + "uniform float grassWindSpeed;", + "uniform vec4 grassBenders[${MAX_BENDING_OBJECTS}];", + "uniform vec3 grassBaseColor;", + "uniform vec3 grassTipColor;", + "varying vec3 vGrassColor;", + "`;", + "const vertexShaderPosition = `", + "// Blades are shaped in scene units (with Y flipped like the 3D world), then", + "// moved back in the (scaled) object space.", + "float bladeHeight = position.z;", + "float grassAngle = grassRoot.w;", + "float bladeWidth = position.x * grassBladeWidth;", + "vec3 blade = vec3(", + " bladeWidth * cos(grassAngle),", + " bladeWidth * sin(grassAngle),", + " bladeHeight * grassShape.x", + ");", + "vec3 rootWorld = (modelMatrix * vec4(grassRoot.xyz, 1.0)).xyz * grassWorldScale;", + "// Tips bend more than the base.", + "float bendFactor = bladeHeight * bladeHeight * grassShape.x;", + "float gust = sin(grassTime * grassWindSpeed + rootWorld.x * 0.013 + rootWorld.y * 0.011);", + "float flutter = sin(grassTime * grassWindSpeed * 2.7 + grassShape.y * 40.0) * 0.3;", + "blade.xy += vec2(0.8, 0.5) * (gust + flutter + 0.6) * grassWindStrength * 0.35 * bendFactor;", + "for (int i = 0; i < ${MAX_BENDING_OBJECTS}; i++) {", + " vec4 bender = grassBenders[i];", + " if (bender.z <= 0.0) continue;", + " vec2 away = rootWorld.xy - bender.xy;", + " float distanceToBender = length(away);", + " float push = 1.0 - smoothstep(bender.z * 0.3, bender.z, distanceToBender);", + " if (push > 0.0) {", + " blade.xy += normalize(away + vec2(0.001)) * push * bendFactor * 0.9;", + " blade.z *= 1.0 - push * 0.5;", + " }", + "}", + "vec3 transformed = grassRoot.xyz + grassWorldToLocal * (blade / grassWorldScale);", + "vGrassColor = mix(grassBaseColor, grassTipColor, bladeHeight) * (0.85 + 0.3 * grassShape.y) * grassGroundTint;", + "`;", + "const vertexShaderNormal = `", + "// Blades are lit like the ground below them.", + "transformedNormal = normalize((viewMatrix * vec4(0.0, 0.0, 1.0, 0.0)).xyz);", + "`;", + "const fragmentShaderDeclarations = `", + "varying vec3 vGrassColor;", + "`;", + "const fragmentShaderColor = `", + "diffuseColor.rgb *= vGrassColor;", + "`;", + "// Both faces of blades are lit like the ground (not flipped for back faces).", + "const fragmentShaderNormal = `", + "#include ", + "normal = normalize(vNormal);", + "nonPerturbedNormal = normal;", + "`;", + "", + "const { getWorldScale, patchShaderCode, createRandom, findHighestGround, GroundPlacement } = gdjs.__terrain3DExtension;", + "", + "/**", + " * The grass of a terrain grass object: blades drawn with GPU instancing, in chunks", + " * that are hidden when off-screen and thinned out with the distance. They grow", + " * on the grounds below, like 3D terrains (only where a given layer is painted).", + " */", + "class Grass {", + " /** @param {gdjs.CustomRuntimeObject3D} object */", + " constructor(object) {", + " this.object = object;", + " this.group = new THREE.Group();", + " object.get3DRendererObject().add(this.group);", + " this.bladeGeometry = createBladeGeometry();", + " /** @type {Array<{mesh: THREE.Mesh, blades: number, center: THREE.Vector3}>} */", + " this.chunks = [];", + " this.placement = new GroundPlacement(object.getRuntimeScene());", + " this._groundAtBlade = { ground: null, z: 0 };", + " this.uniforms = {", + " grassWorldToLocal: { value: new THREE.Matrix3() },", + " // Scene units by 3D world unit.", + " grassWorldScale: { value: 1 },", + " grassBladeWidth: { value: 1 },", + " grassTime: { value: 0 },", + " grassWindStrength: { value: 1 },", + " grassWindSpeed: { value: 1 },", + " grassBenders: { value: Array.from({ length: MAX_BENDING_OBJECTS }, () => new THREE.Vector4()) },", + " grassBaseColor: { value: new THREE.Color() },", + " grassTipColor: { value: new THREE.Color() },", + " };", + " this.material = new THREE.MeshStandardMaterial({ roughness: 1, metalness: 0, side: THREE.DoubleSide });", + " this.material.onBeforeCompile = (shader) => {", + " Object.assign(shader.uniforms, this.uniforms);", + " shader.vertexShader = patchShaderCode(shader.vertexShader, [", + " ['#include ', '#include \\n' + vertexShaderDeclarations],", + " ['#include ', vertexShaderPosition],", + " ['#include ', '#include \\n' + vertexShaderNormal],", + " ], 'TerrainGrass');", + " shader.fragmentShader = patchShaderCode(shader.fragmentShader, [", + " ['#include ', '#include \\n' + fragmentShaderDeclarations],", + " ['#include ', '#include \\n' + fragmentShaderColor],", + " ['#include ', fragmentShaderNormal],", + " ], 'TerrainGrass');", + " };", + " this.material.customProgramCacheKey = () => 'TerrainGrass';", + "", + " this._matrix = new THREE.Matrix4();", + " this._cameraPosition = new THREE.Vector3();", + " this._chunkPosition = new THREE.Vector3();", + " this._scale = new THREE.Vector3();", + " this.updateFromProperties();", + " }", + "", + " updateFromProperties() {", + " const { object } = this;", + " this.uniforms.grassBaseColor.value.set(gdjs.rgbOrHexStringToNumber(object._getBaseColor()));", + " this.uniforms.grassTipColor.value.set(gdjs.rgbOrHexStringToNumber(object._getTipColor()));", + " this.uniforms.grassWindStrength.value = object._getWindStrength();", + " this.uniforms.grassWindSpeed.value = object._getWindSpeed();", + " this.uniforms.grassBladeWidth.value = Math.max(object._getBladeWidth(), 0);", + " this.material.needsUpdate = true;", + " this.placement.placeAgain();", + " }", + "", + " _getPlacementKey() {", + " const { object } = this;", + " return [", + " object.getX(), object.getY(), object.getZ(), object.getWidth(), object.getHeight(), object.getDepth(),", + " object._getDensity(), object._getBladeHeight(), object._getGroundLayer(), object._getMaxSlope(),", + " object._getSeed(),", + " ].join('|');", + " }", + "", + " /** Place the blades again, for example after the ground or the object changed. */", + " rebuild() {", + " const grounds = Array.from(this.placement.grounds);", + " for (const chunk of this.chunks) {", + " chunk.mesh.removeFromParent();", + " chunk.mesh.geometry.dispose();", + " }", + " this.chunks.length = 0;", + "", + " const { object } = this;", + " const width = object.getWidth();", + " const height = object.getHeight();", + " const scaleX = width / AREA_SIZE;", + " const scaleY = height / AREA_SIZE;", + " const scaleZ = Math.max(object.getDepth() / AREA_DEPTH, 0.0001);", + " const bladesPerChunk = Math.round((Math.max(object._getDensity(), 0) * CHUNK_SIZE * CHUNK_SIZE) / 10000);", + " const groundLayer = parseInt(object._getGroundLayer(), 10) || 0;", + " const maxSlope = object._getMaxSlope();", + " const chunksX = Math.max(Math.ceil(width / CHUNK_SIZE), 1);", + " const chunksY = Math.max(Math.ceil(height / CHUNK_SIZE), 1);", + " const nextRandom = createRandom(object._getSeed() * 7919 + 1);", + " const bladeHeight = Math.max(object._getBladeHeight(), 0);", + "", + " for (let chunkY = 0; chunkY < chunksY; chunkY++) {", + " for (let chunkX = 0; chunkX < chunksX; chunkX++) {", + " const roots = new Float32Array(bladesPerChunk * 4);", + " const shapes = new Float32Array(bladesPerChunk * 2);", + " const groundTints = new Uint8Array(bladesPerChunk * 3).fill(255);", + " let blades = 0;", + " let minZ = Infinity;", + " let maxZ = -Infinity;", + " for (let attempt = 0; attempt < bladesPerChunk; attempt++) {", + " // In scene units, relative to the object.", + " const x = (chunkX + nextRandom()) * CHUNK_SIZE;", + " const y = (chunkY + nextRandom()) * CHUNK_SIZE;", + " const angle = nextRandom() * Math.PI * 2;", + " const heightFactor = 0.6 + 0.8 * nextRandom();", + " const tint = nextRandom();", + " const growth = nextRandom();", + " if (x > width || y > height) continue;", + " const sceneX = object.getX() + x;", + " const sceneY = object.getY() + y;", + " const { ground, z } = findHighestGround(grounds, sceneX, sceneY, this._groundAtBlade);", + " const groundZ = ground ? z : object.getZ();", + " if (ground) {", + " if (groundLayer > 0 && growth > ground.getLayerWeightAt(sceneX, sceneY, groundLayer)) continue;", + " if (ground.getSlopeAt(sceneX, sceneY) > maxSlope) continue;", + " if (typeof ground.getTintAt === 'function') {", + " const tint = ground.getTintAt(sceneX, sceneY);", + " for (let channel = 0; channel < 3; channel++) {", + " groundTints[blades * 3 + channel] = Math.round(255 * Math.pow(tint[channel] / 255, 2.2));", + " }", + " }", + " }", + " const localZ = (groundZ - object.getZ()) / scaleZ;", + " roots[blades * 4] = x / scaleX;", + " roots[blades * 4 + 1] = y / scaleY;", + " roots[blades * 4 + 2] = localZ;", + " roots[blades * 4 + 3] = angle;", + " shapes[blades * 2] = heightFactor * bladeHeight;", + " shapes[blades * 2 + 1] = tint;", + " minZ = Math.min(minZ, localZ);", + " maxZ = Math.max(maxZ, localZ);", + " blades++;", + " }", + " if (blades === 0) continue;", + "", + " const geometry = new THREE.InstancedBufferGeometry();", + " geometry.index = this.bladeGeometry.index;", + " geometry.setAttribute('position', this.bladeGeometry.attributes.position);", + " geometry.setAttribute('normal', this.bladeGeometry.attributes.normal);", + " geometry.setAttribute('grassRoot', new THREE.InstancedBufferAttribute(roots.subarray(0, blades * 4), 4));", + " geometry.setAttribute('grassShape', new THREE.InstancedBufferAttribute(shapes.subarray(0, blades * 2), 2));", + " geometry.setAttribute('grassGroundTint', new THREE.InstancedBufferAttribute(groundTints.subarray(0, blades * 3), 3, true));", + " geometry.instanceCount = blades;", + " // The furthest tips can go with the wind and the bending objects (see the shader).", + " const maxBladeHeight = bladeHeight * 1.4;", + " const lean =", + " (0.35 * 1.9 * 0.943 * Math.max(object._getWindStrength(), 0) + 0.9) * maxBladeHeight +", + " Math.max(object._getBladeWidth(), 0) / 2;", + " const leanX = lean / scaleX;", + " const leanY = lean / scaleY;", + " geometry.boundingBox = new THREE.Box3(", + " new THREE.Vector3((chunkX * CHUNK_SIZE) / scaleX - leanX, (chunkY * CHUNK_SIZE) / scaleY - leanY, minZ),", + " new THREE.Vector3(", + " Math.min((chunkX + 1) * CHUNK_SIZE, width) / scaleX + leanX,", + " Math.min((chunkY + 1) * CHUNK_SIZE, height) / scaleY + leanY,", + " maxZ + (bladeHeight * 1.4) / scaleZ", + " )", + " );", + " geometry.boundingSphere = geometry.boundingBox.getBoundingSphere(new THREE.Sphere());", + " const mesh = new THREE.Mesh(geometry, this.material);", + " mesh.receiveShadow = true;", + " // Raycasts would only test one blade: the selection plane is used instead.", + " mesh.raycast = () => {};", + " this.group.add(mesh);", + " this.chunks.push({ mesh, blades, center: geometry.boundingSphere.center.clone() });", + " }", + " }", + " }", + "", + " _updateBendingObjects() {", + " const benders = this.uniforms.grassBenders.value;", + " const objectName = this.object._getBendingObject();", + " const bendingObjects = objectName ? this.object.getInstanceContainer().getObjects(objectName) || [] : [];", + " for (let index = 0; index < MAX_BENDING_OBJECTS; index++) {", + " const bendingObject = bendingObjects[index];", + " if (!bendingObject) {", + " benders[index].set(0, 0, 0, 0);", + " continue;", + " }", + " // Blades are pushed in the 3D scene, which is flipped on Y.", + " const radius = Math.max(bendingObject.getWidth(), bendingObject.getHeight()) * 1.2;", + " benders[index].set(bendingObject.getCenterXInScene(), -bendingObject.getCenterYInScene(), radius, 0);", + " }", + " }", + "", + " /**", + " * @param {number} elapsedSeconds", + " * @param {number} time In seconds, the same for all grass objects.", + " */", + " update(elapsedSeconds, time, camera) {", + " if (this.placement.shouldPlaceAgain(elapsedSeconds, this._getPlacementKey())) {", + " this.rebuild();", + " }", + "", + " this.uniforms.grassTime.value = time;", + " this.uniforms.grassWorldScale.value = getWorldScale(this.object);", + " // The object transformation is applied just before rendering, it's needed now.", + " this.object.getRenderer().ensureUpToDate();", + " this.group.updateWorldMatrix(true, false);", + " this.uniforms.grassWorldToLocal.value.setFromMatrix4(this._matrix.copy(this.group.matrixWorld).invert());", + " this._updateBendingObjects();", + " if (!camera) return;", + "", + " // Far chunks are hidden, and thinned out before (in 3D world units).", + " const fadeDistance = Math.max(this.object._getFadeDistance(), 1) / getWorldScale(this.object);", + " this._cameraPosition.setFromMatrixPosition(camera.matrixWorld);", + " for (const chunk of this.chunks) {", + " const distance = this._chunkPosition", + " .copy(chunk.center)", + " .applyMatrix4(this.group.matrixWorld)", + " .distanceTo(this._cameraPosition);", + " const density = gdjs.evtTools.common.clamp((fadeDistance - distance) / (fadeDistance * 0.5), 0, 1);", + " chunk.mesh.visible = density > 0;", + " chunk.mesh.geometry.instanceCount = Math.ceil(chunk.blades * density);", + " }", + " }", + "", + " dispose() {", + " for (const chunk of this.chunks) {", + " chunk.mesh.geometry.dispose();", + " }", + " this.bladeGeometry.dispose();", + " this.material.dispose();", + " this.group.removeFromParent();", + " }", + "}", + "", + "gdjs.__terrainGrassExtension = { Grass };" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [], + "objectGroups": [] + }, + { + "description": "Define scattered models helper classes JavaScript code.", + "fullName": "Define scattered models helper classes", + "functionType": "Action", + "name": "DefineScatterHelperClasses", + "private": true, + "sentence": "Define scattered models helper classes JavaScript code", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "if (gdjs.__terrainScatterExtension) {", + " return;", + "}", + "// Copies grow on the grounds of the scene, like the terrain grass (see", + "// `getGrounds` in the terrain helper).", + "", + "const {", + " getWorldScale,", + " getGrounds,", + " patchShaderCode,", + " createRandom,", + " findHighestGround,", + " GroundPlacement,", + " SampleRectangle,", + " brushes,", + " codec,", + " svgIcon,", + " toolIcons,", + "} = gdjs.__terrain3DExtension;", + "", + "// Size of the object inner area: instances are scaled from it.", + "const AREA_SIZE = 2000;", + "const AREA_DEPTH = 200;", + "// The painted density is stored in a grid of this many cells on each side,", + "// whatever the size of the object.", + "const MASK_RESOLUTION = 256;", + "const MASK_FORMAT_VERSION = '1';", + "// Copies are hidden in chunks: big chunks mean fewer draw calls, small ones", + "// hide more copies out of the screen.", + "const MIN_CHUNK_SIZE = 500;", + "const MAX_CHUNK_SIZE = 2000;", + "// Copies shrink to nothing over this part of the draw distance, instead of popping out.", + "const FADE_PART = 0.15;", + "const MAX_COPIES = 200000;", + "// Prisms of the trunk collisions.", + "const TRUNK_SIDES = 6;", + "", + "const clamp01 = (value) => (value < 0 ? 0 : value > 1 ? 1 : value);", + "", + "/**", + " * @returns {number} A number for a cell of the copies grid, changing with the seed.", + " */", + "const hashCell = (seed, i, j) => {", + " let h = Math.imul(i, 374761393) + Math.imul(j, 668265263) + Math.imul(seed, 982451653);", + " h = Math.imul(h ^ (h >>> 13), 1274126177);", + " return (h ^ (h >>> 16)) >>> 0;", + "};", + "", + "/**", + " * How dense copies are, from 0 to 1, in a grid covering the object: painted", + " * with the editor brushes, the edits or events.", + " */", + "class DensityMask {", + " /** @param {number} fillValue From 0 to 255. */", + " constructor(fillValue) {", + " // `resolution` and `size` let terrain brushes paint it.", + " this.resolution = MASK_RESOLUTION;", + " this.size = MASK_RESOLUTION + 1;", + " this.values = new Uint8Array(this.size * this.size).fill(fillValue);", + " }", + "", + " /**", + " * @param {number} u From 0 to 1, along the object width.", + " * @param {number} v From 0 to 1, along the object height.", + " * @returns {number} The density, from 0 to 1, interpolated between samples.", + " */", + " sample(u, v) {", + " const x = clamp01(u) * this.resolution;", + " const y = clamp01(v) * this.resolution;", + " const i = Math.min(Math.floor(x), this.resolution - 1);", + " const j = Math.min(Math.floor(y), this.resolution - 1);", + " const fx = x - i;", + " const fy = y - j;", + " const value = (i, j) => this.values[j * this.size + i];", + " const top = value(i, j) + fx * (value(i + 1, j) - value(i, j));", + " const bottom = value(i, j + 1) + fx * (value(i + 1, j + 1) - value(i, j + 1));", + " return (top + fy * (bottom - top)) / 255;", + " }", + "", + " /**", + " * Bring the density towards a target around a segment: fully up to 70%", + " * of the radius, then fading out (forests have dense centers and soft edges).", + " * @param {{cellWidth: number, cellHeight: number, ax: number, ay: number, bx: number, by: number, radius: number}} stroke", + " * @param {number} density The target, from 0 to 1.", + " * @param {number} strength From 0 to 1.", + " * @param {SampleRectangle} changedSamples", + " */", + " paint(stroke, density, strength, changedSamples) {", + " const { values } = this;", + " const target = Math.round(clamp01(density) * 255);", + " brushes.forEachSample(", + " this,", + " stroke,", + " (index, falloff) => {", + " // The falloff of terrain brushes is (1 - distance² / radius²)².", + " const weight = clamp01(2 * Math.sqrt(falloff));", + " // Rounded towards the target, so that light strokes still paint.", + " const change = (target - values[index]) * clamp01(weight * strength);", + " values[index] += change > 0 ? Math.ceil(change) : Math.floor(change);", + " },", + " changedSamples", + " );", + " }", + "", + " /** @returns {DensityMask} */", + " clone() {", + " const mask = new DensityMask(0);", + " mask.values.set(this.values);", + " return mask;", + " }", + "}", + "", + "/**", + " * Edits written in the Edits property: a JSON list of brush strokes, like", + " * `[{\"tool\": \"add\", \"x\": 0.5, \"y\": 0.5, \"radius\": 0.2}]`. Positions and", + " * radius are fractions of the object size, like the terrain edits.", + " */", + "const applyEdits = (mask, text) => {", + " if (!text.trim()) return;", + " let editList;", + " try {", + " editList = JSON.parse(text);", + " } catch (error) {", + " console.warn('The edits of scattered models are not valid JSON: ' + error.message);", + " return;", + " }", + " if (!Array.isArray(editList)) {", + " console.warn('The edits of scattered models must be a JSON list (like [{\"tool\": \"add\", ...}]).');", + " return;", + " }", + " const numberOr = (value, defaultValue) => (typeof value === 'number' && isFinite(value) ? value : defaultValue);", + " const changedSamples = new SampleRectangle();", + " for (const edit of editList) {", + " if (!edit || typeof edit !== 'object') continue;", + " const x = numberOr(edit.x, 0.5);", + " const y = numberOr(edit.y, 0.5);", + " const stroke = {", + " cellWidth: 1 / mask.resolution,", + " cellHeight: 1 / mask.resolution,", + " ax: x,", + " ay: y,", + " bx: numberOr(edit.toX, x),", + " by: numberOr(edit.toY, y),", + " radius: numberOr(edit.radius, 0.1),", + " };", + " const strength = clamp01(numberOr(edit.strength, 1));", + " if (edit.tool === 'add') {", + " mask.paint(stroke, numberOr(edit.density, 1), strength, changedSamples);", + " } else if (edit.tool === 'erase') {", + " mask.paint(stroke, 0, strength, changedSamples);", + " } else {", + " console.warn('Unknown tool in the edits of scattered models: \"' + edit.tool + '\" (use add or erase).');", + " }", + " }", + "};", + "", + "/**", + " * The density painted in the editor, stored as its difference with the", + " * density from the properties (the coverage and the edits), like the terrain", + " * sculpt data.", + " */", + "const maskCodec = {", + " /**", + " * @param {DensityMask} mask", + " * @param {DensityMask} baseMask", + " */", + " encode(mask, baseMask) {", + " const changes = new Int32Array(mask.values.length);", + " let hasChanges = false;", + " for (let index = 0; index < changes.length; index++) {", + " changes[index] = mask.values[index] - baseMask.values[index];", + " if (changes[index] !== 0) hasChanges = true;", + " }", + " if (!hasChanges) return '';", + " return MASK_FORMAT_VERSION + ';' + MASK_RESOLUTION + ';' + codec.encodeGrids([changes], mask.size);", + " },", + "", + " /**", + " * @param {string} text", + " * @param {DensityMask} baseMask", + " * @returns {DensityMask | null} null if the text is empty or not valid.", + " */", + " decode(text, baseMask) {", + " if (!text) return null;", + " const parts = text.split(';');", + " if (parts[0] !== MASK_FORMAT_VERSION || parseInt(parts[1], 10) !== MASK_RESOLUTION) return null;", + " try {", + " const mask = baseMask.clone();", + " const [changes] = codec.decodeGrids(parts[2], mask.size, 1);", + " for (let index = 0; index < changes.length; index++) {", + " // The base may have changed since the data was saved.", + " mask.values[index] = Math.min(Math.max(mask.values[index] + changes[index], 0), 255);", + " }", + " return mask;", + " } catch (error) {", + " console.error('The painted data of scattered models could not be read:', error);", + " return null;", + " }", + " },", + "};", + "", + "const fadeShaderDeclarations = `", + "uniform float scatterDrawDistance;", + "`;", + "// Instance matrices are applied after this: copies shrink towards their base.", + "const fadeShaderCode = `", + "#ifdef USE_INSTANCING", + "vec3 scatterCopyPosition = (modelMatrix * instanceMatrix * vec4(0.0, 0.0, 0.0, 1.0)).xyz;", + "transformed *= clamp(", + " (scatterDrawDistance - distance(scatterCopyPosition, cameraPosition)) / (scatterDrawDistance * ${FADE_PART}),", + " 0.0,", + " 1.0", + ");", + "#endif", + "`;", + "", + "const flipTriangles = (geometry) => {", + " const index = geometry.index;", + " for (let offset = 0; offset + 2 < index.count; offset += 3) {", + " const second = index.getX(offset + 1);", + " index.setX(offset + 1, index.getX(offset + 2));", + " index.setX(offset + 2, second);", + " }", + "};", + "", + "/**", + " * The parts of a model, ready to be drawn with instancing: each glTF", + " * primitive with its material. Geometries are moved so that the model base", + " * is at the origin, its height is 1 and it stands along Z (like 3D model", + " * objects with a 90° rotation around X).", + " * @param {THREE.Object3D} modelScene", + " * @param {object} uniforms Uniforms added to the materials.", + " * @returns {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}}", + " */", + "const createModelParts = (modelScene, uniforms) => {", + " modelScene.updateMatrixWorld(true);", + " const rootInverse = modelScene.matrixWorld.clone().invert();", + " const standUp = new THREE.Matrix4().makeRotationX(Math.PI / 2);", + " /** @type {Array<{mesh: THREE.Mesh, matrix: THREE.Matrix4}>} */", + " const meshes = [];", + " const box = new THREE.Box3();", + " modelScene.traverse((node) => {", + " if (!node.isMesh || !node.geometry || !node.geometry.attributes.position) return;", + " const matrix = standUp.clone().multiply(rootInverse).multiply(node.matrixWorld);", + " if (!node.geometry.boundingBox) node.geometry.computeBoundingBox();", + " box.union(node.geometry.boundingBox.clone().applyMatrix4(matrix));", + " meshes.push({ mesh: node, matrix });", + " });", + " if (box.isEmpty()) return { parts: [], width: 0, depth: 0 };", + "", + " const height = Math.max(box.max.z - box.min.z, 0.000001);", + " // The Y axis of the scene goes down: the model is flipped, like 3D model objects.", + " const normalization = new THREE.Matrix4()", + " .makeScale(1 / height, -1 / height, 1 / height)", + " .multiply(", + " new THREE.Matrix4().makeTranslation(", + " -(box.min.x + box.max.x) / 2,", + " -(box.min.y + box.max.y) / 2,", + " -box.min.z", + " )", + " );", + " const parts = meshes.map(({ mesh, matrix }) => {", + " let geometry = mesh.geometry.clone();", + " if (!geometry.index) {", + " const indices = Array.from({ length: geometry.attributes.position.count }, (_, index) => index);", + " geometry.setIndex(indices);", + " }", + " geometry.applyMatrix4(normalization.clone().multiply(matrix));", + " // The flip turned the faces inside out.", + " flipTriangles(geometry);", + " geometry.computeBoundingSphere();", + " geometry.computeBoundingBox();", + " const patchMaterial = (originalMaterial) => {", + " const material = originalMaterial.clone();", + " // Like 3D model objects (\"Standard without metalness\"), which look better without an environment map.", + " if (material.metalness) material.metalness = 0;", + " material.onBeforeCompile = (shader) => {", + " Object.assign(shader.uniforms, uniforms);", + " shader.vertexShader = patchShaderCode(shader.vertexShader, [", + " ['#include ', '#include \\n' + fadeShaderDeclarations],", + " ['#include ', '#include \\n' + fadeShaderCode],", + " ], 'ScatteredModels');", + " };", + " material.customProgramCacheKey = () => 'ScatteredModels';", + " return material;", + " };", + " const material = Array.isArray(mesh.material) ? mesh.material.map(patchMaterial) : patchMaterial(mesh.material);", + " return { geometry, material };", + " });", + " return {", + " parts,", + " width: (box.max.x - box.min.x) / height,", + " depth: (box.max.y - box.min.y) / height,", + " };", + "};", + "", + "/**", + " * A copy of the model: its base position in the scene, its scale (the", + " * height of the copy) and the key of its cell in the grid of copies.", + " * @typedef {{x: number, y: number, z: number, angle: number, height: number, key: number}} Copy", + " */", + "", + "/**", + " * The scattered models of an object: copies of a 3D model placed on the", + " * grounds of the scene, drawn with GPU instancing in chunks that are hidden", + " * when far or off-screen.", + " */", + "class Scatter {", + " /** @param {gdjs.CustomRuntimeObject3D} object */", + " constructor(object) {", + " this.object = object;", + " this.group = new THREE.Group();", + " object.get3DRendererObject().add(this.group);", + " /** @type {Array<{firstCellI: number, firstCellJ: number, meshes: THREE.InstancedMesh[], copies: Copy[], center: THREE.Vector3, radius: number}>} */", + " this.chunks = [];", + " /**", + " * The grid of copies: one copy at most in each cell, cells grouped in chunks.", + " * @type {{spacing: number, cellsPerChunk: number, cellsX: number, cellsY: number} | null}", + " */", + " this.grid = null;", + " this.placement = new GroundPlacement(object.getRuntimeScene());", + " this.uniforms = {", + " // In 3D world units.", + " scatterDrawDistance: { value: 1 },", + " };", + " /** @type {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}} */", + " this.model = { parts: [], width: 0, depth: 0 };", + " this.modelResourceName = '';", + " this.loadingModelResourceName = '';", + " /**", + " * Copies removed by events (for example, cut trees), by key.", + " * @type {Map}", + " */", + " this.removedCopies = new Map();", + " /** Increased each time copies change, for the collisions. */", + " this.version = 0;", + " this.isDisposed = false;", + " /** The last value of the painted data property read or saved. */", + " this.paintData = '';", + " /**", + " * Values saved by the editor tools: the editor sends them back with a", + " * hot-reload, possibly after newer ones were saved.", + " * @type {string[]}", + " */", + " this.savedPaintData = [];", + " this.maskSource = '';", + " /** @type {DensityMask} */", + " this.baseMask = new DensityMask(255);", + " /** @type {DensityMask} */", + " this.mask = this.baseMask;", + " this.changedSamples = new SampleRectangle();", + " this._stroke = { cellWidth: 1, cellHeight: 1, ax: 0, ay: 0, bx: 0, by: 0, radius: 0 };", + " this._groundAtCopy = { ground: null, z: 0 };", + " this._matrix = new THREE.Matrix4();", + " this._raycastSphere = new THREE.Sphere();", + " this._raycastRay = new THREE.Ray();", + " this._raycastBox = new THREE.Box3();", + " this._raycastInverseMatrix = new THREE.Matrix4();", + " this._raycastScale = new THREE.Vector3();", + " /** Placed on a copy to test the triangles of the model. */", + " this._raycastMesh = new THREE.Mesh();", + " /** @type {THREE.Intersection[]} */", + " this._raycastHits = [];", + " this._color = new THREE.Color();", + " this._tint = new THREE.Color();", + " this._cameraPosition = new THREE.Vector3();", + " this._chunkPosition = new THREE.Vector3();", + " /**", + " * Trunks, for the 3D physics behavior and navigation meshes. Always given,", + " * empty without trunk collisions: otherwise these behaviors would use the", + " * object box, covering the whole area.", + " * @type {gdjs.Surface}", + " */", + " this.surface = {", + " getVersion: () => this.version,", + " getChangedArea: () => null,", + " getTriangles: () => this._getTrunkTriangles(),", + " getHeightField: () => null,", + " };", + " /** The trunk collision properties, changing the surface. */", + " this.trunkKey = '';", + " this.loadMaskFromProperties();", + " this.updateFromProperties();", + " // Before the behaviors create their shapes.", + " this._updateSurface();", + " }", + "", + " _getMaskSource() {", + " return this.object._getCoverage() + '|' + this.object._getEdits();", + " }", + "", + " loadMaskFromProperties() {", + " const { object } = this;", + " this.maskSource = this._getMaskSource();", + " this.paintData = object._getPaintData();", + " this.baseMask = new DensityMask(object._getCoverage() === 'Painted' ? 0 : 255);", + " applyEdits(this.baseMask, object._getEdits());", + " this.mask = maskCodec.decode(this.paintData, this.baseMask) || this.baseMask.clone();", + " this.placement.placeAgain();", + " }", + "", + " onHotReloading() {", + " const paintData = this.object._getPaintData();", + " const savedIndex = this.savedPaintData.indexOf(paintData);", + " if (savedIndex !== -1 && this._getMaskSource() === this.maskSource) {", + " // The object already has this data, or newer changes.", + " this.savedPaintData.splice(0, savedIndex + 1);", + " } else if (paintData !== this.paintData || this._getMaskSource() !== this.maskSource) {", + " this.savedPaintData.length = 0;", + " this.loadMaskFromProperties();", + " }", + " this.updateFromProperties();", + " }", + "", + " /** @returns {string} The data to save in the painted data property. */", + " save() {", + " this.paintData = maskCodec.encode(this.mask, this.baseMask);", + " this.savedPaintData.push(this.paintData);", + " return this.paintData;", + " }", + "", + " updateFromProperties() {", + " const { object } = this;", + " const modelResourceName = object._getModel();", + " if (modelResourceName !== this.modelResourceName) {", + " this.modelResourceName = modelResourceName;", + " this._loadModel();", + " }", + " this.placement.placeAgain();", + " }", + "", + " _disposeModel() {", + " for (const part of this.model.parts) {", + " part.geometry.dispose();", + " for (const material of Array.isArray(part.material) ? part.material : [part.material]) {", + " material.dispose();", + " }", + " }", + " this.model = { parts: [], width: 0, depth: 0 };", + " }", + "", + " _loadModel() {", + " this._disposeChunks(this.chunks);", + " this._disposeModel();", + " const resourceName = this.modelResourceName;", + " if (!resourceName) return;", + " const game = this.object.getRuntimeScene().getGame();", + " const modelManager = game.getModel3DManager();", + " const gltf = modelManager.getModel(resourceName);", + " if (gltf === modelManager._invalidModel) {", + " // Not loaded yet, for example a model just chosen in the editor.", + " if (this.loadingModelResourceName === resourceName) return;", + " this.loadingModelResourceName = resourceName;", + " game.getResourceLoader()", + " .loadResource(resourceName)", + " .then(() => {", + " if (this.isDisposed || this.modelResourceName !== resourceName) return;", + " this.loadingModelResourceName = '';", + " if (modelManager.getModel(resourceName) !== modelManager._invalidModel) {", + " this._loadModel();", + " this.placement.placeAgain();", + " }", + " })", + " .catch(() => {});", + " return;", + " }", + " this.model = createModelParts(gltf.scene, this.uniforms);", + " }", + "", + " _getPlacementKey() {", + " const { object } = this;", + " return [", + " object.getX(), object.getY(), object.getZ(), object.getWidth(), object.getHeight(), object.getDepth(),", + " object._getDensity(), object._getSize(), object._getMinScale(), object._getMaxScale(),", + " object._getRandomRotation(), object._getRandomTint(), object._getGroundLayer(), object._getMaxSlope(),", + " object._getMinGroundZ(), object._getMaxGroundZ(), object._getSeed(), object._getDrawDistance(),", + " object._getCastShadow(), object._getReceiveShadow(), this.model.parts.length,", + " ].join('|');", + " }", + "", + " _getChunkSize() {", + " return Math.min(Math.max(this.object._getDrawDistance() / 4, MIN_CHUNK_SIZE), MAX_CHUNK_SIZE);", + " }", + "", + " /** @returns {number} The distance between copies (one copy per cell of this size at most). */", + " _getSpacing() {", + " const { object } = this;", + " const density = Math.max(object._getDensity(), 0);", + " if (density <= 0) return 0;", + " const spacing = 100 / Math.sqrt(density);", + " // Too many cells would freeze the game: the density is lowered.", + " const cellCount = (object.getWidth() / spacing) * (object.getHeight() / spacing);", + " return cellCount > MAX_COPIES ? spacing * Math.sqrt(cellCount / MAX_COPIES) : spacing;", + " }", + "", + " /**", + " * Find the copy of a cell of the grid, if there is one.", + " * @returns {Copy | null}", + " */", + " _placeCopy(cellI, cellJ, spacing, grounds) {", + " const { object } = this;", + " const width = object.getWidth();", + " const height = object.getHeight();", + " const nextRandom = createRandom(hashCell(Math.floor(object._getSeed()), cellI, cellJ));", + " // Always drawn in the same order, so that a copy stays the same when properties change.", + " const x = (cellI + nextRandom()) * spacing;", + " const y = (cellJ + nextRandom()) * spacing;", + " const presence = nextRandom();", + " const growth = nextRandom();", + " const scaleFactor = nextRandom();", + " const angle = nextRandom() * Math.PI * 2;", + " if (x > width || y > height) return null;", + " if (presence >= this.mask.sample(x / width, y / height)) return null;", + " const key = cellJ * 65536 + cellI;", + " if (this.removedCopies.has(key)) return null;", + "", + " const sceneX = object.getX() + x;", + " const sceneY = object.getY() + y;", + " const { ground, z } = findHighestGround(grounds, sceneX, sceneY, this._groundAtCopy);", + " const groundZ = ground ? z : object.getZ();", + " if (groundZ < object._getMinGroundZ() || groundZ > object._getMaxGroundZ()) return null;", + " const copyHeight =", + " Math.max(object._getSize(), 0) *", + " (object._getMinScale() + (object._getMaxScale() - object._getMinScale()) * scaleFactor);", + " if (ground) {", + " if (ground.getSlopeAt(sceneX, sceneY) > object._getMaxSlope()) return null;", + " const groundLayer = parseInt(object._getGroundLayer(), 10) || 0;", + " if (groundLayer > 0) {", + " if (growth > ground.getLayerWeightAt(sceneX, sceneY, groundLayer)) return null;", + " // Copies don't stand at the border of their layer, so that trees don't cover roads.", + " const margin = (copyHeight * Math.max(this.model.width, this.model.depth)) / 4;", + " for (const [offsetX, offsetY] of [[margin, 0], [-margin, 0], [0, margin], [0, -margin]]) {", + " if (ground.getLayerWeightAt(sceneX + offsetX, sceneY + offsetY, groundLayer) < 0.5) return null;", + " }", + " }", + " }", + " return {", + " x: sceneX,", + " y: sceneY,", + " z: groundZ,", + " angle: object._getRandomRotation() ? angle : 0,", + " height: copyHeight,", + " key,", + " };", + " }", + "", + " _disposeChunks(chunks) {", + " for (const chunk of chunks) {", + " for (const mesh of chunk.meshes) {", + " mesh.removeFromParent();", + " // Geometries and materials are the ones of the model.", + " mesh.dispose();", + " }", + " }", + " if (chunks === this.chunks) this.chunks = [];", + " }", + "", + " /** Place the copies again, for example after the ground or the object changed. */", + " rebuild() {", + " this._disposeChunks(this.chunks);", + " this.version++;", + " const { object } = this;", + " const spacing = this._getSpacing();", + " if (spacing <= 0 || this.model.parts.length === 0) {", + " this.grid = null;", + " return;", + " }", + " const cellsPerChunk = Math.max(Math.round(this._getChunkSize() / spacing), 1);", + " const grid = {", + " spacing,", + " cellsPerChunk,", + " cellsX: Math.ceil(object.getWidth() / spacing),", + " cellsY: Math.ceil(object.getHeight() / spacing),", + " };", + " this.grid = grid;", + " for (let firstCellJ = 0; firstCellJ < grid.cellsY; firstCellJ += cellsPerChunk) {", + " for (let firstCellI = 0; firstCellI < grid.cellsX; firstCellI += cellsPerChunk) {", + " this._createChunk(firstCellI, firstCellJ);", + " }", + " }", + " }", + "", + " /**", + " * Place again the copies of the chunks touching a rectangle of the scene,", + " * for example after painting.", + " */", + " _rebuildArea(minX, minY, maxX, maxY) {", + " const { grid, object } = this;", + " if (!grid) return;", + " const chunkSize = grid.spacing * grid.cellsPerChunk;", + " const firstChunkI = Math.max(Math.floor((minX - object.getX()) / chunkSize), 0);", + " const firstChunkJ = Math.max(Math.floor((minY - object.getY()) / chunkSize), 0);", + " const lastChunkI = Math.min(Math.floor((maxX - object.getX()) / chunkSize), Math.ceil(grid.cellsX / grid.cellsPerChunk) - 1);", + " const lastChunkJ = Math.min(Math.floor((maxY - object.getY()) / chunkSize), Math.ceil(grid.cellsY / grid.cellsPerChunk) - 1);", + " if (firstChunkI > lastChunkI || firstChunkJ > lastChunkJ) return;", + " const isInArea = (chunk) => {", + " const chunkI = chunk.firstCellI / grid.cellsPerChunk;", + " const chunkJ = chunk.firstCellJ / grid.cellsPerChunk;", + " return chunkI >= firstChunkI && chunkI <= lastChunkI && chunkJ >= firstChunkJ && chunkJ <= lastChunkJ;", + " };", + " this._disposeChunks(this.chunks.filter(isInArea));", + " this.chunks = this.chunks.filter((chunk) => !isInArea(chunk));", + " for (let chunkJ = firstChunkJ; chunkJ <= lastChunkJ; chunkJ++) {", + " for (let chunkI = firstChunkI; chunkI <= lastChunkI; chunkI++) {", + " this._createChunk(chunkI * grid.cellsPerChunk, chunkJ * grid.cellsPerChunk);", + " }", + " }", + " this.version++;", + " }", + "", + " _createChunk(firstCellI, firstCellJ) {", + " const { grid } = this;", + " const grounds = Array.from(this.placement.grounds);", + " /** @type {Copy[]} */", + " const copies = [];", + " for (let cellJ = firstCellJ; cellJ < Math.min(firstCellJ + grid.cellsPerChunk, grid.cellsY); cellJ++) {", + " for (let cellI = firstCellI; cellI < Math.min(firstCellI + grid.cellsPerChunk, grid.cellsX); cellI++) {", + " const copy = this._placeCopy(cellI, cellJ, grid.spacing, grounds);", + " if (copy) copies.push(copy);", + " }", + " }", + " if (copies.length === 0) return;", + "", + " const { object } = this;", + " // Copies are placed in scene units, and the object group is scaled", + " // from its inner area: they are scaled back to keep their proportions.", + " const scaleX = Math.max(object.getWidth() / AREA_SIZE, 0.0001);", + " const scaleY = Math.max(object.getHeight() / AREA_SIZE, 0.0001);", + " const scaleZ = Math.max(object.getDepth() / AREA_DEPTH, 0.0001);", + " const matrices = new Float32Array(copies.length * 16);", + " const tintColor = gdjs.rgbOrHexStringToNumber(object._getRandomTint());", + " const hasTint = tintColor !== 0xffffff;", + " const colors = hasTint ? new Float32Array(copies.length * 3) : null;", + " this._tint.setHex(tintColor);", + " const min = new THREE.Vector3(Infinity, Infinity, Infinity);", + " const max = new THREE.Vector3(-Infinity, -Infinity, -Infinity);", + " copies.forEach((copy, index) => {", + " const cos = Math.cos(copy.angle) * copy.height;", + " const sin = Math.sin(copy.angle) * copy.height;", + " const localX = (copy.x - object.getX()) / scaleX;", + " const localY = (copy.y - object.getY()) / scaleY;", + " const localZ = (copy.z - object.getZ()) / scaleZ;", + " this._matrix.set(", + " cos / scaleX, -sin / scaleX, 0, localX,", + " sin / scaleY, cos / scaleY, 0, localY,", + " 0, 0, copy.height / scaleZ, localZ,", + " 0, 0, 0, 1", + " );", + " this._matrix.toArray(matrices, index * 16);", + " if (colors) {", + " // The tint of each copy is between white and the tint color.", + " this._color.setRGB(1, 1, 1).lerp(this._tint, createRandom(copy.key + 1)());", + " this._color.toArray(colors, index * 3);", + " }", + " const reach = (copy.height * Math.max(this.model.width, this.model.depth)) / 2;", + " min.min(this._chunkPosition.set(copy.x - reach, copy.y - reach, copy.z));", + " max.max(this._chunkPosition.set(copy.x + reach, copy.y + reach, copy.z + copy.height));", + " });", + " const instanceMatrix = new THREE.InstancedBufferAttribute(matrices, 16);", + " const instanceColor = colors ? new THREE.InstancedBufferAttribute(colors, 3) : null;", + " const castShadow = object._getCastShadow();", + " const receiveShadow = object._getReceiveShadow();", + " const meshes = this.model.parts.map((part) => {", + " const mesh = new THREE.InstancedMesh(part.geometry, part.material, 0);", + " // All the parts of the model share the positions of the copies.", + " mesh.instanceMatrix = instanceMatrix;", + " mesh.instanceColor = instanceColor;", + " mesh.count = copies.length;", + " mesh.computeBoundingSphere();", + " mesh.castShadow = castShadow;", + " mesh.receiveShadow = receiveShadow;", + " // The first mesh tests all the parts of the copies.", + " mesh.raycast = () => {};", + " this.group.add(mesh);", + " return mesh;", + " });", + " const center = new THREE.Vector3(", + " ((min.x + max.x) / 2 - object.getX()) / scaleX,", + " ((min.y + max.y) / 2 - object.getY()) / scaleY,", + " ((min.z + max.z) / 2 - object.getZ()) / scaleZ", + " );", + " const chunk = { firstCellI, firstCellJ, meshes, copies, center, radius: min.distanceTo(max) / 2 };", + " // Used by the scene editor to select the object when a copy is clicked.", + " meshes[0].raycast = (raycaster, intersects) => this._raycastChunk(chunk, raycaster, intersects);", + " this.chunks.push(chunk);", + " }", + "", + " /**", + " * Add the hits of a ray on the copies of a chunk, like they are drawn. There", + " * can be thousands of copies: only the ones whose box is crossed by the ray", + " * are tested with the triangles of the model.", + " * @param {{meshes: THREE.InstancedMesh[], copies: Copy[], center: THREE.Vector3, radius: number}} chunk", + " * @param {THREE.Raycaster} raycaster", + " * @param {THREE.Intersection[]} intersects", + " */", + " _raycastChunk(chunk, raycaster, intersects) {", + " const { object, group } = this;", + " this._raycastSphere.center.copy(chunk.center).applyMatrix4(group.matrixWorld);", + " this._raycastSphere.radius = chunk.radius / getWorldScale(object);", + " if (!raycaster.ray.intersectsSphere(this._raycastSphere)) return;", + "", + " // The ray in the scene coordinates, like the copies.", + " const scale = this._raycastScale.set(", + " Math.max(object.getWidth() / AREA_SIZE, 0.0001),", + " Math.max(object.getHeight() / AREA_SIZE, 0.0001),", + " Math.max(object.getDepth() / AREA_DEPTH, 0.0001)", + " );", + " const ray = this._raycastRay.copy(raycaster.ray).applyMatrix4(this._raycastInverseMatrix.copy(group.matrixWorld).invert());", + " ray.origin.multiply(scale).add(this._chunkPosition.set(object.getX(), object.getY(), object.getZ()));", + " ray.direction.multiply(scale);", + "", + " const reachPerHeight = Math.max(this.model.width, this.model.depth) / 2;", + " const instanceMatrices = chunk.meshes[0].instanceMatrix.array;", + " chunk.copies.forEach((copy, index) => {", + " const reach = copy.height * reachPerHeight;", + " this._raycastBox.min.set(copy.x - reach, copy.y - reach, copy.z);", + " this._raycastBox.max.set(copy.x + reach, copy.y + reach, copy.z + copy.height);", + " if (!ray.intersectsBox(this._raycastBox)) return;", + " this._raycastMesh.matrixWorld.fromArray(instanceMatrices, index * 16).premultiply(group.matrixWorld);", + " for (const mesh of chunk.meshes) {", + " this._raycastMesh.geometry = mesh.geometry;", + " this._raycastMesh.material = mesh.material;", + " this._raycastHits.length = 0;", + " this._raycastMesh.raycast(raycaster, this._raycastHits);", + " for (const hit of this._raycastHits) {", + " hit.object = mesh;", + " hit.instanceId = index;", + " intersects.push(hit);", + " }", + " }", + " });", + " }", + "", + " /** @param {THREE.Camera | null} camera */", + " _hideFarChunks(camera) {", + " if (!camera) return;", + " const worldScale = getWorldScale(this.object);", + " const drawDistance = Math.max(this.object._getDrawDistance(), 1) / worldScale;", + " this.uniforms.scatterDrawDistance.value = drawDistance;", + " this._cameraPosition.setFromMatrixPosition(camera.matrixWorld);", + " for (const chunk of this.chunks) {", + " const distance = this._chunkPosition", + " .copy(chunk.center)", + " .applyMatrix4(this.group.matrixWorld)", + " .distanceTo(this._cameraPosition);", + " const isVisible = distance - chunk.radius / worldScale < drawDistance;", + " for (const mesh of chunk.meshes) mesh.visible = isVisible;", + " }", + " }", + "", + " /**", + " * @param {number} elapsedSeconds", + " * @param {THREE.Camera | null} camera", + " */", + " update(elapsedSeconds, camera) {", + " if (this.placement.shouldPlaceAgain(elapsedSeconds, this._getPlacementKey())) {", + " this.rebuild();", + " }", + " if (!this.changedSamples.isEmpty()) {", + " const { object } = this;", + " const sampleWidth = object.getWidth() / MASK_RESOLUTION;", + " const sampleHeight = object.getHeight() / MASK_RESOLUTION;", + " const { minI, minJ, maxI, maxJ } = this.changedSamples;", + " // Copies between samples use the density of the samples around them.", + " this._rebuildArea(", + " object.getX() + (minI - 1) * sampleWidth,", + " object.getY() + (minJ - 1) * sampleHeight,", + " object.getX() + (maxI + 1) * sampleWidth,", + " object.getY() + (maxJ + 1) * sampleHeight", + " );", + " this.changedSamples.clear();", + " }", + " // The object transformation is applied just before rendering, it's needed now.", + " this.object.getRenderer().ensureUpToDate();", + " this.group.updateWorldMatrix(true, false);", + " this._hideFarChunks(camera);", + " this._updateSurface();", + " }", + "", + " _updateSurface() {", + " const { object } = this;", + " if (typeof object.setSurface !== 'function') return;", + " const trunkKey = object._getTrunkCollisions() + '|' + object._getTrunkDiameter();", + " if (trunkKey !== this.trunkKey) {", + " this.trunkKey = trunkKey;", + " this.version++;", + " }", + " if (object.getSurface() !== this.surface) object.setSurface(this.surface);", + " }", + "", + " /** @returns {Copy[]} */", + " getCopies() {", + " const copies = [];", + " for (const chunk of this.chunks) copies.push(...chunk.copies);", + " return copies;", + " }", + "", + " /**", + " * @returns {Copy | null} The copy whose base is the nearest to a position (on X and Y), if any.", + " */", + " findNearestCopy(x, y) {", + " let nearestCopy = null;", + " let nearestDistanceSquared = Infinity;", + " for (const chunk of this.chunks) {", + " for (const copy of chunk.copies) {", + " const distanceSquared = (copy.x - x) * (copy.x - x) + (copy.y - y) * (copy.y - y);", + " if (distanceSquared < nearestDistanceSquared) {", + " nearestDistanceSquared = distanceSquared;", + " nearestCopy = copy;", + " }", + " }", + " }", + " return nearestCopy;", + " }", + "", + " getCopyCount() {", + " let count = 0;", + " for (const chunk of this.chunks) count += chunk.copies.length;", + " return count;", + " }", + "", + " /** Remove the copies whose base is in a circle (they come back when the scene is restarted). */", + " removeCopies(x, y, radius) {", + " let hasRemovedCopies = false;", + " for (const chunk of this.chunks) {", + " for (const copy of chunk.copies) {", + " if ((copy.x - x) * (copy.x - x) + (copy.y - y) * (copy.y - y) <= radius * radius) {", + " this.removedCopies.set(copy.key, copy);", + " hasRemovedCopies = true;", + " }", + " }", + " }", + " if (hasRemovedCopies) this._rebuildArea(x - radius, y - radius, x + radius, y + radius);", + " }", + "", + " /** Paint the full density around a position, and bring back the copies removed there. */", + " addCopies(x, y, radius) {", + " for (const [key, copy] of this.removedCopies) {", + " if ((copy.x - x) * (copy.x - x) + (copy.y - y) * (copy.y - y) <= radius * radius) {", + " this.removedCopies.delete(key);", + " }", + " }", + " this.paint(x, y, x, y, radius, 1, 1);", + " }", + "", + " _getStroke(x1, y1, x2, y2, radius) {", + " const { object } = this;", + " const stroke = this._stroke;", + " stroke.cellWidth = object.getWidth() / MASK_RESOLUTION;", + " stroke.cellHeight = object.getHeight() / MASK_RESOLUTION;", + " stroke.ax = x1 - object.getX();", + " stroke.ay = y1 - object.getY();", + " stroke.bx = x2 - object.getX();", + " stroke.by = y2 - object.getY();", + " stroke.radius = Math.max(radius, 0);", + " return stroke;", + " }", + "", + " /**", + " * Paint the density around a segment (copies are placed again at the next frame).", + " * @param {number} density From 0 (erase) to 1 (as dense as the Density property).", + " * @param {number} strength From 0 to 1.", + " */", + " paint(x1, y1, x2, y2, radius, density, strength) {", + " const changedSamples = new SampleRectangle();", + " this.mask.paint(this._getStroke(x1, y1, x2, y2, radius), density, strength, changedSamples);", + " this.changedSamples.add(changedSamples);", + " }", + "", + " /** @param {DensityMask} mask */", + " setMask(mask) {", + " this.mask = mask;", + " this.rebuild();", + " }", + "", + " /**", + " * Prisms around the trunks, from 0 to 1 in the object box (see `gdjs.Surface`).", + " * @returns {gdjs.SurfaceTriangles}", + " */", + " _getTrunkTriangles() {", + " const { object } = this;", + " const copies = object._getTrunkCollisions() ? this.getCopies() : [];", + " const verticesPerCopy = TRUNK_SIDES * 2 + 1;", + " const positions = new Float32Array(copies.length * verticesPerCopy * 3);", + " const indices = new Uint32Array(copies.length * TRUNK_SIDES * 3 * 3);", + " const width = object.getWidth() || 1;", + " const height = object.getHeight() || 1;", + " const depth = object.getDepth() || 1;", + " let indexOffset = 0;", + " copies.forEach((copy, copyIndex) => {", + " const radius = (Math.max(object._getTrunkDiameter(), 0) / 2) * (copy.height / Math.max(object._getSize(), 1));", + " const top = copy.z + copy.height * 0.8;", + " const firstVertex = copyIndex * verticesPerCopy;", + " const setVertex = (vertex, x, y, z) => {", + " positions[vertex * 3] = (x - object.getX()) / width;", + " positions[vertex * 3 + 1] = (y - object.getY()) / height;", + " positions[vertex * 3 + 2] = (z - object.getZ()) / depth;", + " };", + " for (let side = 0; side < TRUNK_SIDES; side++) {", + " const angle = ((side + 0.5) / TRUNK_SIDES) * Math.PI * 2;", + " const x = copy.x + Math.cos(angle) * radius;", + " const y = copy.y + Math.sin(angle) * radius;", + " // Starting a bit under the ground, which can be sloped.", + " setVertex(firstVertex + side * 2, x, y, copy.z - radius);", + " setVertex(firstVertex + side * 2 + 1, x, y, top);", + " }", + " const center = firstVertex + TRUNK_SIDES * 2;", + " setVertex(center, copy.x, copy.y, top);", + " for (let side = 0; side < TRUNK_SIDES; side++) {", + " const bottom = firstVertex + side * 2;", + " const nextBottom = firstVertex + ((side + 1) % TRUNK_SIDES) * 2;", + " // Triangles face out of the prism (vertices at odd indices are at the top).", + " indices.set([bottom, nextBottom, bottom + 1, nextBottom, nextBottom + 1, bottom + 1], indexOffset);", + " indices.set([center, bottom + 1, nextBottom + 1], indexOffset + 6);", + " indexOffset += 9;", + " }", + " });", + " if (copies.length === 0) {", + " // Without triangles, the physics engine and navigation meshes would use", + " // the object box: a tiny triangle under the object is used instead.", + " return { positions: new Float32Array([0, 0, -1, 0.0001, 0, -1, 0, 0.0001, -1]), indices: new Uint32Array([0, 2, 1]) };", + " }", + " return { positions, indices };", + " }", + "", + " dispose() {", + " this.isDisposed = true;", + " if (typeof this.object.setSurface === 'function' && this.object.getSurface() === this.surface) {", + " this.object.setSurface(null);", + " }", + " this._disposeChunks(this.chunks);", + " this._disposeModel();", + " this.group.removeFromParent();", + " }", + "}", + "", + "const scatterToolIcons = {", + " Add: svgIcon(", + " ''", + " ),", + " Erase: svgIcon(", + " ''", + " ),", + "};", + "", + "/**", + " * The brushes shown in the scene editor when scattered models are selected,", + " * to add or erase copies. Without the needed editor functions (older GDevelop", + " * versions), nothing is shown: copies can still be painted with events.", + " */", + "class ScatterEditorTools {", + " constructor() {", + " /** '' when no brush is used. */", + " this.toolName = '';", + " this.sizePercent = 30;", + " this.strengthPercent = 50;", + " /** @type {Scatter | null} */", + " this.scatter = null;", + " /** @type {gdjs.InGameEditor | null} */", + " this.editor = null;", + " this.isStroking = false;", + " this.lastStrokeTime = 0;", + " /** @type {Array<{mask: DensityMask, baseMask: DensityMask}>} */", + " this.undoSteps = [];", + " this.raycaster = new THREE.Raycaster();", + " this.ndc = new THREE.Vector2();", + " this._groundAtCursor = { ground: null, z: 0 };", + " /** A circle following the ground under the cursor. */", + " this.brushRing = new THREE.LineLoop(", + " new THREE.BufferGeometry().setAttribute('position', new THREE.BufferAttribute(new Float32Array(64 * 3), 3)),", + " new THREE.LineBasicMaterial({ color: 0xffffff, depthTest: false, transparent: true })", + " );", + " this.brushRing.renderOrder = 1000;", + " this.brushRing.frustumCulled = false;", + " this.brushRing.raycast = () => {};", + " }", + "", + " /** @param {gdjs.InGameEditor} editor */", + " update(editor) {", + " if (typeof editor.showToolbar !== 'function') return;", + " this.editor = editor;", + " const selectedObjects = editor.getSelectedObjects();", + " const scatter =", + " selectedObjects.length === 1 && selectedObjects[0].__terrainScatter ? selectedObjects[0].__terrainScatter : null;", + " if (scatter !== this.scatter) {", + " this._endStroke();", + " this.scatter = scatter;", + " this.undoSteps.length = 0;", + " }", + " if (scatter) editor.showToolbar('ScatteredModels', this._getToolbarItems());", + " const game = editor.getRuntimeGame();", + " if (!scatter || !this.toolName) {", + " this.brushRing.removeFromParent();", + " return;", + " }", + " const inputManager = game.getInputManager();", + " if (inputManager.wasKeyJustPressed(gdjs.evtTools.input.keysNameToCode.Escape)) {", + " this._endStroke();", + " this.toolName = '';", + " this.brushRing.removeFromParent();", + " return;", + " }", + " editor.captureLeftMouseButton();", + " const hit = this._getPositionUnderCursor(scatter, game);", + " const radius = this._getRadius();", + " this._showBrushRing(scatter, hit, radius);", + "", + " const isPressedForTool =", + " typeof editor.isCapturedLeftMouseButtonPressed === 'function'", + " ? editor.isCapturedLeftMouseButtonPressed()", + " : inputManager.isMouseButtonPressed(0);", + " // The button is released outside the canvas when the mouse is on a toolbar.", + " const isPressed = isPressedForTool && inputManager.isMouseInsideCanvas();", + " if (isPressed && hit && !this.isStroking) {", + " this._startStroke(scatter);", + " }", + " if (this.isStroking && isPressed && hit) {", + " this._applyBrush(scatter, hit, radius);", + " }", + " if (!isPressed) {", + " this._endStroke();", + " }", + " }", + "", + " _getRadius() {", + " const { object } = this.scatter;", + " const maxRadius = Math.max(object.getWidth(), object.getHeight()) / 4;", + " return Math.max(maxRadius * Math.pow(this.sizePercent / 100, 2), 10);", + " }", + "", + " /**", + " * @returns {{x: number, y: number} | null} The position of the scene under the cursor: on the", + " * highest ground hit, or on the plane at the object Z.", + " */", + " _getPositionUnderCursor(scatter, game) {", + " let hit = null;", + " for (const ground of getGrounds(scatter.object.getRuntimeScene())) {", + " if (typeof ground.raycastFromCursor !== 'function') continue;", + " const localHit = ground.raycastFromCursor(game, this.raycaster, this.ndc);", + " if (!localHit) continue;", + " const sceneHit = ground.toScenePosition(localHit);", + " if (!hit || sceneHit.z > hit.z) hit = sceneHit;", + " }", + " if (hit) return hit;", + "", + " const { object } = scatter;", + " const layer = object.getInstanceContainer().getLayer(object.getLayer());", + " const camera = layer.getRenderer().getThreeCamera();", + " if (!camera) return null;", + " const inputManager = game.getInputManager();", + " this.ndc.set(", + " (inputManager.getCursorX() / game.getGameResolutionWidth()) * 2 - 1,", + " -(inputManager.getCursorY() / game.getGameResolutionHeight()) * 2 + 1", + " );", + " this.raycaster.setFromCamera(this.ndc, camera);", + " scatter.group.updateWorldMatrix(true, false);", + " const ray = this.raycaster.ray.clone().applyMatrix4(scatter.group.matrixWorld.clone().invert());", + " const point = ray.intersectPlane(new THREE.Plane(new THREE.Vector3(0, 0, 1), 0), new THREE.Vector3());", + " if (!point) return null;", + " return {", + " x: object.getX() + (point.x / AREA_SIZE) * object.getWidth(),", + " y: object.getY() + (point.y / AREA_SIZE) * object.getHeight(),", + " };", + " }", + "", + " _showBrushRing(scatter, hit, radius) {", + " if (!hit) {", + " this.brushRing.removeFromParent();", + " return;", + " }", + " const { object } = scatter;", + " if (this.brushRing.parent !== scatter.group) scatter.group.add(this.brushRing);", + " const grounds = getGrounds(object.getRuntimeScene());", + " const positions = this.brushRing.geometry.attributes.position;", + " for (let index = 0; index < positions.count; index++) {", + " const angle = (index / positions.count) * Math.PI * 2;", + " const x = hit.x + Math.cos(angle) * radius;", + " const y = hit.y + Math.sin(angle) * radius;", + " const { ground, z } = findHighestGround(grounds, x, y, this._groundAtCursor);", + " positions.setXYZ(", + " index,", + " ((x - object.getX()) / object.getWidth()) * AREA_SIZE,", + " ((y - object.getY()) / object.getHeight()) * AREA_SIZE,", + " (((ground ? z : object.getZ()) + 2 - object.getZ()) / Math.max(object.getDepth(), 0.0001)) * AREA_DEPTH", + " );", + " }", + " positions.needsUpdate = true;", + " }", + "", + " _startStroke(scatter) {", + " this.isStroking = true;", + " this.lastStrokeTime = performance.now();", + " this.undoSteps.push({ mask: scatter.mask.clone(), baseMask: scatter.baseMask });", + " while (this.undoSteps.length > 30) this.undoSteps.shift();", + " }", + "", + " _endStroke() {", + " if (!this.isStroking) return;", + " this.isStroking = false;", + " this._save();", + " }", + "", + " _applyBrush(scatter, hit, radius) {", + " const now = performance.now();", + " const elapsedSeconds = Math.min((now - this.lastStrokeTime) / 1000, 0.1);", + " this.lastStrokeTime = now;", + " const strength = (this.strengthPercent / 100) * elapsedSeconds * 8;", + " scatter.paint(hit.x, hit.y, hit.x, hit.y, radius, this.toolName === 'Add' ? 1 : 0, strength);", + " }", + "", + " _save() {", + " const { scatter, editor } = this;", + " if (!scatter || !editor) return;", + " editor.updateObjectProperties(scatter.object.getName(), { PaintData: scatter.save() });", + " }", + "", + " _undo() {", + " const undoStep = this.undoSteps.pop();", + " // The coverage or the edits may have changed since the stroke.", + " if (!this.scatter || !undoStep || undoStep.baseMask !== this.scatter.baseMask) {", + " this.undoSteps.length = 0;", + " return;", + " }", + " this.scatter.setMask(undoStep.mask);", + " this._save();", + " }", + "", + " /** @returns {Array} */", + " _getToolbarItems() {", + " const toolButton = (toolName, tooltip) => ({", + " type: 'button',", + " id: toolName,", + " tooltip,", + " iconUrl: scatterToolIcons[toolName],", + " isActive: this.toolName === toolName,", + " onClick: () => (this.toolName = this.toolName === toolName ? '' : toolName),", + " });", + " return [", + " toolButton('Add', 'Add copies (drag on the ground)'),", + " toolButton('Erase', 'Erase copies'),", + " { type: 'divider', id: 'BrushDivider' },", + " {", + " type: 'slider',", + " id: 'Size',", + " tooltip: 'Brush size',", + " iconUrl: toolIcons.Size,", + " min: 1,", + " max: 100,", + " value: this.sizePercent,", + " onChange: (value) => (this.sizePercent = value),", + " },", + " {", + " type: 'slider',", + " id: 'Strength',", + " tooltip: 'Brush strength',", + " iconUrl: toolIcons.Strength,", + " min: 5,", + " max: 100,", + " value: this.strengthPercent,", + " onChange: (value) => (this.strengthPercent = value),", + " },", + " {", + " type: 'button',", + " id: 'Undo',", + " tooltip: 'Undo the last stroke on these scattered models',", + " iconUrl: toolIcons.Undo,", + " onClick: () => this._undo(),", + " },", + " ];", + " }", + "}", + "", + "const editorTools = new ScatterEditorTools();", + "if (gdjs.registerInGameEditorPostStepCallback) {", + " gdjs.registerInGameEditorPostStepCallback((editor) => editorTools.update(editor));", + "}", + "", + "gdjs.__terrainScatterExtension = {", + " Scatter,", + " // Exposed for tests.", + " DensityMask,", + " maskCodec,", + " applyEdits,", + "};", + "" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [], + "objectGroups": [] + } + ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "DefineHelperClasses" + }, + { + "functionName": "DefineGrassHelperClasses" + }, + { + "functionName": "DefineScatterHelperClasses" + } + ] + }, + "eventsBasedBehaviors": [], + "eventsBasedObjects": [ + { + "areaMaxX": 4096, + "areaMaxY": 4096, + "areaMaxZ": 600, + "areaMinX": 0, + "areaMinY": 0, + "areaMinZ": 0, + "assetStoreTag": "3d terrains", + "defaultName": "Terrain", + "description": "A ground with hills, mountains or valleys that can be sculpted and painted in the scene editor, or changed with events. Add the 3D physics behavior to it to make objects walk or roll on it.", + "fullName": "3D terrain", + "helpPath": "", + "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLXRlcnJhaW4iIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTQsNkwxMC4yNSwxMUwxMy4xLDE0LjhMMTEuNSwxNkM5LjgxLDEzLjc1IDcsMTAgNywxMEwxLDE4SDIzTDE0LDZaIiAvPjwvc3ZnPg==", + "is3D": true, + "isUsingLegacyInstancesRenderer": true, + "name": "Terrain3D", + "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", + "objects": [ + { + "assetStoreId": "", + "name": "AreaPlaceholder", + "type": "Scene3D::Cube3DObject", + "variables": [], + "effects": [], + "behaviors": [], + "content": { + "width": 4096, + "height": 4096, + "depth": 600, + "enableTextureTransparency": false, + "facesOrientation": "Y", + "backFaceUpThroughWhichAxisRotation": "X", + "materialType": "Basic", + "tint": "255;255;255", + "frontFaceResourceName": "", + "frontFaceVisible": false, + "frontFaceResourceRepeat": false, + "backFaceResourceName": "", + "backFaceVisible": false, + "backFaceResourceRepeat": false, + "leftFaceResourceName": "", + "leftFaceVisible": false, + "leftFaceResourceRepeat": false, + "rightFaceResourceName": "", + "rightFaceVisible": false, + "rightFaceResourceRepeat": false, + "topFaceResourceName": "", + "topFaceVisible": false, + "topFaceResourceRepeat": false, + "bottomFaceResourceName": "", + "bottomFaceVisible": false, + "bottomFaceResourceRepeat": false + } + } + ], + "objectsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "objectName": "AreaPlaceholder" + } + ] + }, + "objectsGroups": [], + "layers": [ + { + "ambientLightColorB": 200, + "ambientLightColorG": 200, + "ambientLightColorR": 200, + "camera2DPlaneMaxDrawingDistance": 5000, + "camera3DFarPlaneDistance": 10000, + "camera3DFieldOfView": 45, + "camera3DNearPlaneDistance": 3, + "cameraType": "", + "followBaseLayerCamera": false, + "isLightingLayer": false, + "isLocked": false, + "name": "", + "renderingType": "", + "visibility": true, + "cameras": [ + { + "defaultSize": true, + "defaultViewport": true, + "height": 0, + "viewportBottom": 1, + "viewportLeft": 0, + "viewportRight": 1, + "viewportTop": 0, + "width": 0 + } + ], + "effects": [] + } + ], + "instances": [ + { + "angle": 0, + "customSize": true, + "depth": 600, + "height": 4096, + "keepRatio": true, + "layer": "", + "name": "AreaPlaceholder", + "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-004096004096", + "width": 4096, + "x": 0, + "y": 0, + "zOrder": 1, + "numberProperties": [], + "stringProperties": [], + "initialVariables": [] + } + ], + "editionSettings": {}, + "eventsFunctions": [ + { + "fullName": "", + "functionType": "Action", + "name": "onCreated", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Terrain3D::DefineHelperClasses" + }, + "parameters": [ + "", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "for (const child of object.getChildrenContainer().getAdhocListOfAllInstances()) {", + " child.deleteFromScene();", + "}", + "if (object.__terrain3D) object.__terrain3D.dispose();", + "object.__terrain3D = new gdjs.__terrain3DExtension.Terrain(object);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "onDestroy", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrain3D.dispose();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "doStepPostEvents", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrain3D.update();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "onHotReloading", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrain3D.onHotReloading();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "description": "Raise the ground around a position (or lower it with a negative height). It's raised the most at the center and less and less up to the radius.", + "fullName": "Raise or lower the ground", + "functionType": "Action", + "group": "Sculpt", + "name": "RaiseGround", + "sentence": "Raise the ground of _PARAM0_ by _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const height = eventsFunctionContext.getArgument(\"Height\");", + "object.__terrain3D.raise(x, y, x, y, radius, height);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Height to add (negative to lower the ground)", + "name": "Height", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Raise the ground along a line, for example for a ridge, or lower it with a negative height to dig a river bed or a valley.", + "fullName": "Raise or lower the ground along a line", + "functionType": "Action", + "group": "Sculpt", + "name": "RaiseGroundAlongLine", + "sentence": "Raise the ground of _PARAM0_ by _PARAM6_ from _PARAM1_; _PARAM2_ to _PARAM3_; _PARAM4_ with a radius of _PARAM5_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x1 = eventsFunctionContext.getArgument(\"X1\");", + "const y1 = eventsFunctionContext.getArgument(\"Y1\");", + "const x2 = eventsFunctionContext.getArgument(\"X2\");", + "const y2 = eventsFunctionContext.getArgument(\"Y2\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const height = eventsFunctionContext.getArgument(\"Height\");", + "object.__terrain3D.raise(x1, y1, x2, y2, radius, height);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the start", + "name": "X1", + "type": "expression" + }, + { + "description": "Y position of the start", + "name": "Y1", + "type": "expression" + }, + { + "description": "X position of the end", + "name": "X2", + "type": "expression" + }, + { + "description": "Y position of the end", + "name": "Y2", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance from the line.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Height to add (negative to lower the ground)", + "name": "Height", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Flatten the ground around a position to a given height, for example to place a building.", + "fullName": "Flatten the ground", + "functionType": "Action", + "group": "Sculpt", + "name": "FlattenGround", + "sentence": "Flatten the ground of _PARAM0_ to a Z of _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const z = eventsFunctionContext.getArgument(\"Z\");", + "object.__terrain3D.flatten(x, y, x, y, radius, z, 1);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Z position of the flat ground", + "name": "Z", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Smooth the ground around a position, softening bumps and cliffs.", + "fullName": "Smooth the ground", + "functionType": "Action", + "group": "Sculpt", + "name": "SmoothGround", + "sentence": "Smooth the ground of _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_ (strength: _PARAM4_)", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const strength = eventsFunctionContext.getArgument(\"Strength\");", + "object.__terrain3D.smooth(x, y, x, y, radius, strength);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Strength (between 0 and 1)", + "name": "Strength", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Paint a layer (like grass, dirt, rock or sand) on the ground around a position.", + "fullName": "Paint a layer", + "functionType": "Action", + "group": "Paint", + "name": "PaintLayer", + "sentence": "Paint layer _PARAM4_ on _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const layer = eventsFunctionContext.getArgument(\"Layer\");", + "object.__terrain3D.paint(x, y, x, y, radius, layer, 1);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Layer (1 to 4)", + "name": "Layer", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Paint a layer on the ground along a line, for example for a path or a road.", + "fullName": "Paint a layer along a line", + "functionType": "Action", + "group": "Paint", + "name": "PaintLayerAlongLine", + "sentence": "Paint layer _PARAM6_ on _PARAM0_ from _PARAM1_; _PARAM2_ to _PARAM3_; _PARAM4_ with a radius of _PARAM5_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x1 = eventsFunctionContext.getArgument(\"X1\");", + "const y1 = eventsFunctionContext.getArgument(\"Y1\");", + "const x2 = eventsFunctionContext.getArgument(\"X2\");", + "const y2 = eventsFunctionContext.getArgument(\"Y2\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const layer = eventsFunctionContext.getArgument(\"Layer\");", + "object.__terrain3D.paint(x1, y1, x2, y2, radius, layer, 1);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the start", + "name": "X1", + "type": "expression" + }, + { + "description": "Y position of the start", + "name": "Y1", + "type": "expression" + }, + { + "description": "X position of the end", + "name": "X2", + "type": "expression" + }, + { + "description": "Y position of the end", + "name": "Y2", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance from the line.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Layer (1 to 4)", + "name": "Layer", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Tint the ground around a position with a color multiplying its layers, like dry grass or darker mud. White removes the tint.", + "fullName": "Tint the ground", + "functionType": "Action", + "group": "Paint", + "name": "TintGround", + "sentence": "Tint the ground of _PARAM0_ with _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const color = gdjs.rgbOrHexToRGBColor(eventsFunctionContext.getArgument(\"Color\"));", + "object.__terrain3D.tint(x, y, x, y, radius, color, 1);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Color", + "name": "Color", + "type": "color" + } + ], + "objectGroups": [] + }, + { + "description": "Make the ground around a position wet (darker and shiny, like after rain or near water) or dry.", + "fullName": "Change the wetness of the ground", + "functionType": "Action", + "group": "Paint", + "name": "WetGround", + "sentence": "Change the wetness of the ground of _PARAM0_ to _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const wetness = eventsFunctionContext.getArgument(\"Wetness\");", + "object.__terrain3D.wet(x, y, x, y, radius, wetness, 1);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Wetness (from 0 for dry to 1)", + "name": "Wetness", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "the wetness of the ground at a position, from 0 (dry) to 1, painted or near the water (wet shore).", + "fullName": "Ground wetness", + "functionType": "ExpressionAndCondition", + "group": "Paint", + "name": "GroundWetness", + "sentence": "the wetness of the ground at _PARAM1_; _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "eventsFunctionContext.returnValue = object.__terrain3D.getWetnessAt(x, y);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "The tint painted on the ground at a position, like \"255;255;255\" (white when there is none).", + "fullName": "Ground tint", + "functionType": "StringExpression", + "group": "Paint", + "name": "GroundTint", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "eventsFunctionContext.returnValue = object.__terrain3D.getTintAt(x, y).join(\";\");" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "string" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Remove the ground in a circle, for the entrance of a tunnel, a cave or a well: there is nothing drawn and no collision there. Fill it again with the \"Fill holes\" action. A height field can't go under itself: put a 3D model (with a collision shape) for the tunnel or the cave under the ground. The physics engine removes the triangles touching a hole, so the hole in the collisions can be up to a cell larger than the one drawn: make the model go a bit further than the hole, or put rocks around the entrance.", + "fullName": "Make a hole", + "functionType": "Action", + "group": "Holes", + "name": "MakeHole", + "sentence": "Make a hole in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "object.__terrain3D.makeHole(x, y, x, y, radius);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "name": "Radius", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Fill the holes in a circle with the layers of the ground around them: the ground is drawn and has collisions again.", + "fullName": "Fill holes", + "functionType": "Action", + "group": "Holes", + "name": "FillHoles", + "sentence": "Fill the holes in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "object.__terrain3D.fillHoles(x, y, x, y, radius);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "name": "Radius", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Make the entrance of a tunnel or a cave on a slope: a hole, with the ground in front of it flattened to the floor of the tunnel.", + "fullName": "Carve an entrance", + "functionType": "Action", + "group": "Holes", + "name": "CarveEntrance", + "sentence": "Carve an entrance in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_, going in at _PARAM4_° with its floor at Z _PARAM5_ and _PARAM6_ pixels of flat ground in front", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const angle = eventsFunctionContext.getArgument(\"Angle\");", + "const floorZ = eventsFunctionContext.getArgument(\"FloorZ\");", + "const length = eventsFunctionContext.getArgument(\"Length\");", + "object.__terrain3D.carveEntrance(x, y, radius, angle, floorZ, length);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "name": "Radius", + "type": "expression" + }, + { + "description": "Direction going into the ground (in degrees)", + "name": "Angle", + "type": "expression" + }, + { + "description": "Z position of the floor", + "name": "FloorZ", + "type": "expression" + }, + { + "description": "Length of the flat ground in front of the entrance (in pixels)", + "name": "Length", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Check if there is a hole in the ground at a position.", + "fullName": "Hole", + "functionType": "Condition", + "group": "Holes", + "name": "IsHole", + "sentence": "There is a hole in _PARAM0_ at _PARAM1_; _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "eventsFunctionContext.returnValue = object.__terrain3D.isHoleAt(x, y);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "The Z position of the ground at a position, or the given value where there is a hole.", + "fullName": "Ground Z position, with holes", + "functionType": "Expression", + "group": "Holes", + "name": "GroundZWithHoles", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const terrain = object.__terrain3D;", + "eventsFunctionContext.returnValue = terrain.isHoleAt(x, y) ? eventsFunctionContext.getArgument(\"ZInHoles\") : terrain.getHeightAt(x, y);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + }, + { + "description": "Value in holes", + "longDescription": "For example, a Z position far under the ground.", + "name": "ZInHoles", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Replace the ground with a generated relief. Painted layers are reset.", + "fullName": "Generate a relief", + "functionType": "Action", + "group": "Generation", + "name": "GenerateRelief", + "sentence": "Generate a relief of _PARAM1_ on _PARAM0_ with seed _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const relief = eventsFunctionContext.getArgument(\"Relief\");", + "const seed = eventsFunctionContext.getArgument(\"Seed\");", + "object.__terrain3D.generate(relief, seed);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "Relief", + "name": "Relief", + "supplementaryInformation": "[\"Flat\", \"Hills\", \"Mountains\", \"Island\"]", + "type": "stringWithSelector" + }, + { + "description": "Seed", + "longDescription": "Each seed gives a different terrain with the same relief.", + "name": "Seed", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Replace the ground with the heights of an image. Painted layers are kept.", + "fullName": "Load a heightmap", + "functionType": "Action", + "group": "Generation", + "name": "LoadHeightmap", + "sentence": "Load the heights of _PARAM0_ from _PARAM1_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const image = eventsFunctionContext.getArgument(\"Image\");", + "object.__terrain3D.loadHeightmap(image);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "Heightmap image", + "longDescription": "Black is the lowest, white the highest. For more precision, store the height on 16 bits in the red (most significant byte) and green channels.", + "name": "Image", + "type": "imageResource" + } + ], + "objectGroups": [] + }, + { + "description": "Move objects up or down so that their bottom is on the ground, under their center. Useful to place trees, rocks or characters on a terrain.", + "fullName": "Put objects on the ground", + "functionType": "Action", + "group": "Position", + "name": "PutOnGround", + "sentence": "Put _PARAM1_ on the ground of _PARAM0_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrain3D.placeOnGround(eventsFunctionContext.getObjects(\"Objects\"));" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "Objects to put on the ground", + "name": "Objects", + "type": "objectList" + } + ], + "objectGroups": [] + }, + { + "description": "the Z position of the ground at a position of the scene.", + "fullName": "Ground Z position", + "functionType": "ExpressionAndCondition", + "group": "Position", + "name": "GroundZ", + "sentence": "the ground Z position at _PARAM1_; _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "eventsFunctionContext.returnValue = object.__terrain3D.getHeightAt(x, y);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "the slope of the ground (angle with the horizontal, in degrees) at a position of the scene.", + "fullName": "Ground slope", + "functionType": "ExpressionAndCondition", + "group": "Position", + "name": "GroundSlope", + "sentence": "the ground slope at _PARAM1_; _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "eventsFunctionContext.returnValue = object.__terrain3D.getSlopeAt(x, y);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "the layer (from 1 to 4) painted the most at a position of the scene. Useful to play footstep sounds or to slow down characters in water or mud.", + "fullName": "Ground layer", + "functionType": "ExpressionAndCondition", + "group": "Paint", + "name": "GroundLayer", + "sentence": "the ground layer at _PARAM1_; _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "eventsFunctionContext.returnValue = object.__terrain3D.getLayerAt(x, y);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + } + ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + }, + { + "folderName": "Sculpt", + "children": [ + { + "functionName": "RaiseGround" + }, + { + "functionName": "RaiseGroundAlongLine" + }, + { + "functionName": "FlattenGround" + }, + { + "functionName": "SmoothGround" + } + ] + }, + { + "folderName": "Paint", + "children": [ + { + "functionName": "PaintLayer" + }, + { + "functionName": "PaintLayerAlongLine" + }, + { + "functionName": "TintGround" + }, + { + "functionName": "WetGround" + }, + { + "functionName": "GroundWetness" + }, + { + "functionName": "GroundTint" + }, + { + "functionName": "GroundLayer" + } + ] + }, + { + "folderName": "Holes", + "children": [ + { + "functionName": "MakeHole" + }, + { + "functionName": "FillHoles" + }, + { + "functionName": "CarveEntrance" + }, + { + "functionName": "IsHole" + }, + { + "functionName": "GroundZWithHoles" + } + ] + }, + { + "folderName": "Generation", + "children": [ + { + "functionName": "GenerateRelief" + }, + { + "functionName": "LoadHeightmap" + } + ] + }, + { + "folderName": "Position", + "children": [ + { + "functionName": "PutOnGround" + }, + { + "functionName": "GroundZ" + }, + { + "functionName": "GroundSlope" + } + ] + } + ] + }, + "propertyDescriptors": [ + { + "value": "Hills", + "type": "Choice", + "label": "Relief", + "description": "The initial shape, before sculpting.", + "group": "Shape", + "choices": [ + { + "label": "Flat", + "value": "Flat" + }, + { + "label": "Hills", + "value": "Hills" + }, + { + "label": "Mountains", + "value": "Mountains" + }, + { + "label": "Island", + "value": "Island" + } + ], + "name": "Relief" + }, + { + "value": "1", + "type": "Number", + "label": "Seed", + "description": "Each seed gives a different terrain with the same relief.", + "group": "Shape", + "name": "Seed" + }, + { + "value": "", + "type": "Resource", + "label": "Heightmap image", + "description": "Optional. Replaces the relief: black is the lowest, white the highest (the object depth).", + "group": "Shape", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "HeightmapImage" + }, + { + "value": "", + "type": "MultilineString", + "label": "Edits", + "description": "Changes of the relief or heightmap, applied in order, as a JSON list, for example [{\"tool\": \"raise\", \"x\": 0.3, \"y\": 0.4, \"radius\": 0.15, \"height\": 0.4}, {\"tool\": \"paint\", \"x\": 0.1, \"y\": 0.9, \"toX\": 0.8, \"toY\": 0.2, \"radius\": 0.02, \"layer\": 2}]. Tools: raise (height added to the ground, negative to lower), flatten (z: the ground is brought to this height, fully at the center, cutting what is above it and filling what is below; with toX and toY, toZ gives the height at the end of the line, for a road going up), smooth (strength), paint (layer, from 1 to 4, and strength), tint (color, like \"120;140;60\", multiplying the layers, white removing the tint, and strength), wetness (amount, from 0 for dry to 1 for wet, and strength), hole (no ground in the radius, for the entrances of tunnels and caves) and fill (the holes in the radius are filled with the layers of the ground around them). x, y and radius are fractions (0 to 1) of the terrain size (x is (scene X - terrain X) / terrain width). height and z are fractions of its depth: the ground at z is at Z = terrain Z + z * terrain depth. Add toX and toY to change the ground along a line (paths, rivers, ridges). Shown in the scene editor.", + "group": "Shape", + "advanced": true, + "name": "Edits" + }, + { + "value": "256", + "type": "Choice", + "label": "Resolution", + "description": "The number of cells on each side of the terrain. Higher is more detailed but slower.", + "group": "Shape", + "choices": [ + { + "label": "64 x 64", + "value": "64" + }, + { + "label": "128 x 128", + "value": "128" + }, + { + "label": "256 x 256", + "value": "256" + }, + { + "label": "512 x 512", + "value": "512" + }, + { + "label": "1024 x 1024", + "value": "1024" + } + ], + "name": "Resolution" + }, + { + "value": "96;160;64", + "type": "Color", + "label": "Layer 1 color (grass)", + "group": "Layers", + "name": "Layer1Color" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 1 texture", + "description": "Optional. Tinted by the layer color.", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer1Texture" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 1 normal map", + "description": "Optional. The bumps of the texture, lit by the lights (an OpenGL normal map, like the textures of ambientCG).", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer1NormalMap" + }, + { + "value": "150;112;72", + "type": "Color", + "label": "Layer 2 color (dirt)", + "group": "Layers", + "name": "Layer2Color" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 2 texture", + "description": "Optional. Tinted by the layer color.", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer2Texture" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 2 normal map", + "description": "Optional. The bumps of the texture, lit by the lights (an OpenGL normal map, like the textures of ambientCG).", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer2NormalMap" + }, + { + "value": "124;118;112", + "type": "Color", + "label": "Layer 3 color (rock)", + "group": "Layers", + "name": "Layer3Color" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 3 texture", + "description": "Optional. Tinted by the layer color.", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer3Texture" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 3 normal map", + "description": "Optional. The bumps of the texture, lit by the lights (an OpenGL normal map, like the textures of ambientCG).", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer3NormalMap" + }, + { + "value": "224;204;148", + "type": "Color", + "label": "Layer 4 color (sand)", + "group": "Layers", + "name": "Layer4Color" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 4 texture", + "description": "Optional. Tinted by the layer color.", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer4Texture" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 4 normal map", + "description": "Optional. The bumps of the texture, lit by the lights (an OpenGL normal map, like the textures of ambientCG).", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer4NormalMap" + }, + { + "value": "1", + "type": "Number", + "label": "Normal map strength", + "description": "How much the normal maps bump the ground: 0 for none, 1 as they are, more to exaggerate them.", + "group": "Layers", + "name": "NormalMapStrength" + }, + { + "value": "256", + "type": "Number", + "unit": "Pixel", + "label": "Texture size", + "description": "The size, in the scene, of one repetition of the layer textures.", + "group": "Layers", + "name": "TextureSize" + }, + { + "value": "0.5", + "type": "Number", + "label": "Height blending", + "description": "Where layers meet, the bright parts of textures (stones, tufts) cover the dark ones (gaps, sand) instead of fading into each other: 0 for none, 1 for the most.", + "group": "Layers", + "name": "HeightBlending" + }, + { + "value": "0.5", + "type": "Number", + "label": "Texture variation", + "description": "Makes the repetition of textures less visible, with patches of bigger and rotated textures and colors slowly varying: 0 for none, 1 for the most.", + "group": "Layers", + "name": "TextureVariation" + }, + { + "value": "false", + "type": "Boolean", + "label": "Wet shore", + "description": "Make the ground wet (darker and shiny) under a Z position, like the sand of a beach near the water.", + "group": "Wetness", + "name": "WetShore" + }, + { + "value": "0", + "type": "Number", + "unit": "Pixel", + "label": "Wet below Z", + "description": "The ground is wet under this Z position (for example, the Z of a 3D water object).", + "group": "Wetness", + "name": "WetShoreZ" + }, + { + "value": "60", + "type": "Number", + "unit": "Pixel", + "label": "Wet shore fade", + "description": "The ground becomes dry over this height above \"Wet below Z\".", + "group": "Wetness", + "name": "WetShoreFade" + }, + { + "value": "3", + "type": "Choice", + "label": "Layer on steep slopes", + "description": "This layer is shown where the ground is steep, whatever is painted.", + "group": "Layers", + "choices": [ + { + "label": "None", + "value": "0" + }, + { + "label": "Layer 1", + "value": "1" + }, + { + "label": "Layer 2", + "value": "2" + }, + { + "label": "Layer 3", + "value": "3" + }, + { + "label": "Layer 4", + "value": "4" + } + ], + "name": "CliffLayer" + }, + { + "value": "45", + "type": "Number", + "unit": "DegreeAngle", + "label": "Steep slope angle", + "group": "Layers", + "name": "CliffAngle" + }, + { + "value": "true", + "type": "Boolean", + "label": "Cast shadows", + "group": "Rendering", + "name": "CastShadow" + }, + { + "value": "true", + "type": "Boolean", + "label": "Receive shadows", + "group": "Rendering", + "name": "ReceiveShadow" + }, + { + "value": "", + "type": "String", + "label": "Sculpted and painted data", + "description": "Written by the sculpt and paint tools of the scene editor, in a compact format not meant to be read or written by hand: use Edits instead. Clear it to go back to the relief, heightmap and edits.", + "advanced": true, + "name": "SculptData" + } + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Shape", + "children": [ + { + "propertyName": "Relief" + }, + { + "propertyName": "Seed" + }, + { + "propertyName": "HeightmapImage" + }, + { + "propertyName": "Edits" + }, + { + "propertyName": "Resolution" + } + ] + }, + { + "folderName": "Layers", + "children": [ + { + "propertyName": "Layer1Color" + }, + { + "propertyName": "Layer1Texture" + }, + { + "propertyName": "Layer1NormalMap" + }, + { + "propertyName": "Layer2Color" + }, + { + "propertyName": "Layer2Texture" + }, + { + "propertyName": "Layer2NormalMap" + }, + { + "propertyName": "Layer3Color" + }, + { + "propertyName": "Layer3Texture" + }, + { + "propertyName": "Layer3NormalMap" + }, + { + "propertyName": "Layer4Color" + }, + { + "propertyName": "Layer4Texture" + }, + { + "propertyName": "Layer4NormalMap" + }, + { + "propertyName": "NormalMapStrength" + }, + { + "propertyName": "TextureSize" + }, + { + "propertyName": "HeightBlending" + }, + { + "propertyName": "TextureVariation" + }, + { + "propertyName": "CliffLayer" + }, + { + "propertyName": "CliffAngle" + } + ] + }, + { + "folderName": "Wetness", + "children": [ + { + "propertyName": "WetShore" + }, + { + "propertyName": "WetShoreZ" + }, + { + "propertyName": "WetShoreFade" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "CastShadow" + }, + { + "propertyName": "ReceiveShadow" + } + ] + }, + { + "propertyName": "SculptData" + } + ] + }, + "variants": [] + }, + { + "areaMaxX": 1000, + "areaMaxY": 1000, + "areaMaxZ": 40, + "areaMinX": 0, + "areaMinY": 0, + "areaMinZ": 0, + "defaultName": "Grass", + "description": "A field of grass blades swaying in the wind and pushed aside by characters. They grow in the area of the object, on the highest 3D terrain at each point (one field can cover several terrains), or at the Z position of the object where there is no terrain.", + "fullName": "Terrain grass", + "helpPath": "", + "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGQ9Ik0zIDIxYzEtNSAxLTkgMC0xMyAyIDMgMyA3IDMgMTN6bTQgMGMwLTYgMS0xMSA0LTE2LTEgNS0xIDEwIDAgMTZ6bTUgMGMwLTUgMi05IDUtMTItMiA0LTIgOC0xIDEyem01IDBjMS00IDItNiA0LTgtMSAzLTEgNS0xIDh6Ii8+PC9zdmc+", + "is3D": true, + "isUsingLegacyInstancesRenderer": true, + "name": "TerrainGrass", + "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/732ef90b7fcf5dd9171fe95d0cf262e09159b487304915a4693baa893d9ce16c_grass.svg", + "objects": [ + { + "assetStoreId": "", + "name": "AreaPlaceholder", + "type": "Scene3D::Cube3DObject", + "variables": [], + "effects": [], + "behaviors": [], + "content": { + "width": 1000, + "height": 1000, + "depth": 40, + "enableTextureTransparency": false, + "facesOrientation": "Y", + "backFaceUpThroughWhichAxisRotation": "X", + "materialType": "Basic", + "tint": "255;255;255", + "frontFaceResourceName": "", + "frontFaceVisible": false, + "frontFaceResourceRepeat": false, + "backFaceResourceName": "", + "backFaceVisible": false, + "backFaceResourceRepeat": false, + "leftFaceResourceName": "", + "leftFaceVisible": false, + "leftFaceResourceRepeat": false, + "rightFaceResourceName": "", + "rightFaceVisible": false, + "rightFaceResourceRepeat": false, + "topFaceResourceName": "", + "topFaceVisible": false, + "topFaceResourceRepeat": false, + "bottomFaceResourceName": "", + "bottomFaceVisible": false, + "bottomFaceResourceRepeat": false + } + } + ], + "objectsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "objectName": "AreaPlaceholder" + } + ] + }, + "objectsGroups": [], + "layers": [ + { + "ambientLightColorB": 200, + "ambientLightColorG": 200, + "ambientLightColorR": 200, + "camera2DPlaneMaxDrawingDistance": 5000, + "camera3DFarPlaneDistance": 10000, + "camera3DFieldOfView": 45, + "camera3DNearPlaneDistance": 3, + "cameraType": "", + "followBaseLayerCamera": false, + "isLightingLayer": false, + "isLocked": false, + "name": "", + "renderingType": "", + "visibility": true, + "cameras": [ + { + "defaultSize": true, + "defaultViewport": true, + "height": 0, + "viewportBottom": 1, + "viewportLeft": 0, + "viewportRight": 1, + "viewportTop": 0, + "width": 0 + } + ], + "effects": [] + } + ], + "instances": [ + { + "angle": 0, + "customSize": true, + "depth": 40, + "height": 1000, + "keepRatio": true, + "layer": "", + "name": "AreaPlaceholder", + "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-001000001000", + "width": 1000, + "x": 0, + "y": 0, + "zOrder": 1, + "numberProperties": [], + "stringProperties": [], + "initialVariables": [] + } + ], + "editionSettings": {}, + "eventsFunctions": [ + { + "fullName": "", + "functionType": "Action", + "name": "onCreated", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Terrain3D::DefineHelperClasses" + }, + "parameters": [ + "", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Terrain3D::DefineGrassHelperClasses" + }, + "parameters": [ + "", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "for (const child of object.getChildrenContainer().getAdhocListOfAllInstances()) {", + " child.deleteFromScene();", + "}", + "if (object.__terrainGrass) object.__terrainGrass.dispose();", + "object.__terrainGrass = new gdjs.__terrainGrassExtension.Grass(object);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "onDestroy", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrainGrass.dispose();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "doStepPostEvents", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const game = runtimeScene.getGame();", + "// Time doesn't pass in the scene editor: real time is used to show the wind.", + "const isInGameEdition = game.isInGameEdition && game.isInGameEdition();", + "const elapsedTime = isInGameEdition ? 1 / 60 : object.getElapsedTime() / 1000;", + "const time = isInGameEdition", + " ? performance.now() / 1000", + " : object.getRuntimeScene().getTimeManager().getTimeFromStart() / 1000;", + "const layer = object.getInstanceContainer().getLayer(object.getLayer());", + "object.__terrainGrass.update(elapsedTime, time, layer.getRenderer().getThreeCamera());" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "onHotReloading", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrainGrass.updateFromProperties();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" + } + ], + "objectGroups": [] + } + ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + } + ] + }, + "propertyDescriptors": [ + { + "value": "60", + "type": "Number", + "label": "Density", + "description": "The number of blades in a square of 100 x 100 pixels.", + "group": "Blades", + "name": "Density" + }, + { + "value": "36", + "type": "Number", + "unit": "Pixel", + "label": "Blade height", + "description": "The average height of blades (each blade is between 60% and 140% of it).", + "group": "Blades", + "name": "BladeHeight" + }, + { + "value": "5", + "type": "Number", + "unit": "Pixel", + "label": "Blade width", + "group": "Blades", + "name": "BladeWidth" + }, + { + "value": "52;104;36", + "type": "Color", + "label": "Base color", + "group": "Blades", + "name": "BaseColor" + }, + { + "value": "150;196;82", + "type": "Color", + "label": "Tip color", + "group": "Blades", + "name": "TipColor" + }, + { + "value": "1", + "type": "Number", + "label": "Seed", + "description": "Each seed places blades differently.", + "group": "Blades", + "name": "Seed" + }, + { + "value": "1", + "type": "Choice", + "label": "Grows on terrain layer", + "description": "On a 3D terrain, blades only grow where this layer is painted. A new terrain has layer 1 painted everywhere.", + "group": "Ground", + "choices": [ + { + "label": "Any", + "value": "0" + }, + { + "label": "Layer 1", + "value": "1" + }, + { + "label": "Layer 2", + "value": "2" + }, + { + "label": "Layer 3", + "value": "3" + }, + { + "label": "Layer 4", + "value": "4" + } + ], + "name": "GroundLayer" + }, + { + "value": "40", + "type": "Number", + "unit": "DegreeAngle", + "label": "Maximum slope", + "description": "No blades grow on steeper ground.", + "group": "Ground", + "name": "MaxSlope" + }, + { + "value": "1", + "type": "Number", + "label": "Wind strength", + "description": "0 for no wind.", + "group": "Wind", + "name": "WindStrength" + }, + { + "value": "2", + "type": "Number", + "label": "Wind speed", + "group": "Wind", + "name": "WindSpeed" + }, + { + "value": "", + "type": "String", + "label": "Object bending the grass", + "description": "The name of an object pushing blades aside when walking through the grass, like the player. The 8 first instances are used.", + "group": "Wind", + "name": "BendingObject" + }, + { + "value": "2500", + "type": "Number", + "unit": "Pixel", + "label": "Fade distance", + "description": "Blades are less and less dense up to this distance from the camera, and hidden after.", + "group": "Rendering", + "name": "FadeDistance" + } + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Blades", + "children": [ + { + "propertyName": "Density" + }, + { + "propertyName": "BladeHeight" + }, + { + "propertyName": "BladeWidth" + }, + { + "propertyName": "BaseColor" + }, + { + "propertyName": "TipColor" + }, + { + "propertyName": "Seed" + } + ] + }, + { + "folderName": "Ground", + "children": [ + { + "propertyName": "GroundLayer" + }, + { + "propertyName": "MaxSlope" + } + ] + }, + { + "folderName": "Wind", + "children": [ + { + "propertyName": "WindStrength" + }, + { + "propertyName": "WindSpeed" + }, + { + "propertyName": "BendingObject" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "FadeDistance" + } + ] + } + ] + }, + "variants": [] + }, + { + "areaMaxX": 2000, + "areaMaxY": 2000, + "areaMaxZ": 200, + "areaMinX": 0, + "areaMinY": 0, + "areaMinZ": 0, + "assetStoreTag": "3d scattered models", + "defaultName": "ScatteredModels", + "description": "Many copies of a 3D model (trees, rocks, bushes, flowers...) placed in the area of the object and drawn all at once, which is much faster than an object for each one. They stand on the highest 3D terrain at each point, or at the Z position of the object where there is no terrain. Paint where they grow with the brushes of the scene editor.", + "fullName": "Scattered models (forests, rocks...)", + "helpPath": "", + "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGQ9Ik0xMCAyMXYtM0gzbDUtNUg1bDUtNUg3bDUtNSA1IDVoLTNsNSA1aC0zbDUgNWgtN3YzeiIvPjwvc3ZnPg==", + "is3D": true, + "isUsingLegacyInstancesRenderer": true, + "name": "ScatteredModels", + "previewIconUrl": "", + "objects": [ + { + "assetStoreId": "", + "name": "AreaPlaceholder", + "type": "Scene3D::Cube3DObject", + "variables": [], + "effects": [], + "behaviors": [], + "content": { + "width": 2000, + "height": 2000, + "depth": 200, + "enableTextureTransparency": false, + "facesOrientation": "Y", + "backFaceUpThroughWhichAxisRotation": "X", + "materialType": "Basic", + "tint": "255;255;255", + "frontFaceResourceName": "", + "frontFaceVisible": false, + "frontFaceResourceRepeat": false, + "backFaceResourceName": "", + "backFaceVisible": false, + "backFaceResourceRepeat": false, + "leftFaceResourceName": "", + "leftFaceVisible": false, + "leftFaceResourceRepeat": false, + "rightFaceResourceName": "", + "rightFaceVisible": false, + "rightFaceResourceRepeat": false, + "topFaceResourceName": "", + "topFaceVisible": false, + "topFaceResourceRepeat": false, + "bottomFaceResourceName": "", + "bottomFaceVisible": false, + "bottomFaceResourceRepeat": false + } + } + ], + "objectsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "objectName": "AreaPlaceholder" + } + ] + }, + "objectsGroups": [], + "layers": [ + { + "ambientLightColorB": 200, + "ambientLightColorG": 200, + "ambientLightColorR": 200, + "camera2DPlaneMaxDrawingDistance": 5000, + "camera3DFarPlaneDistance": 10000, + "camera3DFieldOfView": 45, + "camera3DNearPlaneDistance": 3, + "cameraType": "", + "followBaseLayerCamera": false, + "isLightingLayer": false, + "isLocked": false, + "name": "", + "renderingType": "", + "visibility": true, + "cameras": [ + { + "defaultSize": true, + "defaultViewport": true, + "height": 0, + "viewportBottom": 1, + "viewportLeft": 0, + "viewportRight": 1, + "viewportTop": 0, + "width": 0 + } + ], + "effects": [] + } + ], + "instances": [ + { + "angle": 0, + "customSize": true, + "depth": 200, + "height": 2000, + "keepRatio": true, + "layer": "", + "name": "AreaPlaceholder", + "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-002000002000", + "width": 2000, + "x": 0, + "y": 0, + "zOrder": 1, + "numberProperties": [], + "stringProperties": [], + "initialVariables": [] + } + ], + "editionSettings": {}, + "eventsFunctions": [ + { + "fullName": "", + "functionType": "Action", + "name": "onCreated", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Terrain3D::DefineHelperClasses" + }, + "parameters": [ + "", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Terrain3D::DefineScatterHelperClasses" + }, + "parameters": [ + "", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "for (const child of object.getChildrenContainer().getAdhocListOfAllInstances()) {", + " child.deleteFromScene();", + "}", + "if (object.__terrainScatter) object.__terrainScatter.dispose();", + "object.__terrainScatter = new gdjs.__terrainScatterExtension.Scatter(object);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "onDestroy", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrainScatter.dispose();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "doStepPostEvents", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const game = runtimeScene.getGame();", + "const isInGameEdition = game.isInGameEdition && game.isInGameEdition();", + "const elapsedTime = isInGameEdition ? 1 / 60 : object.getElapsedTime() / 1000;", + "const layer = object.getInstanceContainer().getLayer(object.getLayer());", + "object.__terrainScatter.update(elapsedTime, layer.getRenderer().getThreeCamera());" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "onHotReloading", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrainScatter.onHotReloading();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "description": "Paint the density of copies around a position, up to the density of the object (copies removed with \"Erase copies\" come back). The effect fades out up to the radius.", + "fullName": "Add copies", + "functionType": "Action", + "group": "Paint", + "name": "AddCopies", + "sentence": "Add copies of _PARAM0_ around _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "object.__terrainScatter.addCopies(x, y, radius);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "name": "Radius", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Remove the copies standing in a circle, for example a tree cut by the player (use the position of the nearest copy and a small radius) or a forest cleared for a building. They come back when the scene is restarted.", + "fullName": "Erase copies", + "functionType": "Action", + "group": "Paint", + "name": "EraseCopies", + "sentence": "Erase the copies of _PARAM0_ in a circle at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "object.__terrainScatter.removeCopies(x, y, radius);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "name": "Radius", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "the number of copies placed in the area.", + "fullName": "Number of copies", + "functionType": "ExpressionAndCondition", + "group": "Copies", + "name": "CopyCount", + "sentence": "the number of copies", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "eventsFunctionContext.returnValue = object.__terrainScatter.getCopyCount();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "description": "The X position of the base of the copy nearest to a position (the position itself if there is no copy).", + "fullName": "X position of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyX", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? copy.x : x;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "The Y position of the base of the copy nearest to a position (the position itself if there is no copy).", + "fullName": "Y position of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyY", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? copy.y : y;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "The Z position of the base of the copy nearest to a position (on the ground).", + "fullName": "Z position of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyZ", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? copy.z : object.getZ();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "the distance (on X and Y) from a position to the base of the nearest copy. It's a very big number when there is no copy.", + "fullName": "Distance to the nearest copy", + "functionType": "ExpressionAndCondition", + "group": "Copies", + "name": "NearestCopyDistance", + "sentence": "the distance from _PARAM1_; _PARAM2_ to the nearest copy", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? Math.hypot(copy.x - x, copy.y - y) : Number.MAX_VALUE;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + } + ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + }, + { + "folderName": "Paint", + "children": [ + { + "functionName": "AddCopies" + }, + { + "functionName": "EraseCopies" + } + ] + }, + { + "folderName": "Copies", + "children": [ + { + "functionName": "CopyCount" + }, + { + "functionName": "NearestCopyX" + }, + { + "functionName": "NearestCopyY" + }, + { + "functionName": "NearestCopyZ" + }, + { + "functionName": "NearestCopyDistance" + } + ] + } + ] + }, + "propertyDescriptors": [ + { + "value": "", + "type": "Resource", + "label": "3D model", + "description": "The model copied everywhere (a .glb file). Models standing on the Y axis, like most models of the asset store, are stood up along Z.", + "group": "Model", + "extraInformation": [ + "model3D" + ], + "choices": [], + "name": "Model" + }, + { + "value": "300", + "type": "Number", + "unit": "Pixel", + "label": "Height of a copy", + "description": "Copies keep the proportions of the model.", + "group": "Model", + "name": "Size" + }, + { + "value": "0.8", + "type": "Number", + "label": "Smallest random scale", + "description": "Each copy is scaled at random between the smallest and the largest scale.", + "group": "Model", + "name": "MinScale" + }, + { + "value": "1.2", + "type": "Number", + "label": "Largest random scale", + "group": "Model", + "name": "MaxScale" + }, + { + "value": "true", + "type": "Boolean", + "label": "Random rotation", + "description": "Turn each copy around the Z axis at random.", + "group": "Model", + "name": "RandomRotation" + }, + { + "value": "255;255;255", + "type": "Color", + "label": "Random tint", + "description": "Each copy is tinted between white and this color, at random. White for no tint.", + "group": "Model", + "name": "RandomTint" + }, + { + "value": "0.3", + "type": "Number", + "label": "Density", + "description": "The number of copies in a square of 100 x 100 pixels where the density is fully painted (0.3 is about one copy every 180 pixels). Copies never overlap their neighbors much: they are placed at random in a grid.", + "group": "Placement", + "name": "Density" + }, + { + "value": "1", + "type": "Number", + "label": "Seed", + "description": "Each seed places copies differently.", + "group": "Placement", + "name": "Seed" + }, + { + "value": "Everywhere", + "type": "Choice", + "label": "Copies grow", + "description": "Everywhere in the area, or only where they are painted with the \"Add copies\" brush (or the edits).", + "group": "Placement", + "choices": [ + { + "label": "Everywhere (erase them with the brush)", + "value": "Everywhere" + }, + { + "label": "Only where painted", + "value": "Painted" + } + ], + "name": "Coverage" + }, + { + "value": "1", + "type": "Choice", + "label": "Grows on terrain layer", + "description": "On a 3D terrain, copies only grow where this layer is painted, away from its borders (so that trees don't stand on roads painted with another layer). A new terrain has layer 1 painted everywhere.", + "group": "Ground", + "choices": [ + { + "label": "Any", + "value": "0" + }, + { + "label": "Layer 1", + "value": "1" + }, + { + "label": "Layer 2", + "value": "2" + }, + { + "label": "Layer 3", + "value": "3" + }, + { + "label": "Layer 4", + "value": "4" + } + ], + "name": "GroundLayer" + }, + { + "value": "30", + "type": "Number", + "unit": "DegreeAngle", + "label": "Maximum slope", + "description": "No copies grow on steeper ground.", + "group": "Ground", + "name": "MaxSlope" + }, + { + "value": "-100000", + "type": "Number", + "unit": "Pixel", + "label": "Minimum ground Z", + "description": "No copies grow where the ground is lower (for example, under the water level).", + "group": "Ground", + "name": "MinGroundZ" + }, + { + "value": "100000", + "type": "Number", + "unit": "Pixel", + "label": "Maximum ground Z", + "description": "No copies grow where the ground is higher (for example, above the tree line).", + "group": "Ground", + "name": "MaxGroundZ" + }, + { + "value": "false", + "type": "Boolean", + "label": "Trunk collisions", + "description": "Give each copy a static collision shape around its trunk, for the 3D physics behavior (add it to this object, with the \"Static\" type) and navigation meshes. Without them, these behaviors have no effect on this object.", + "group": "Collisions", + "name": "TrunkCollisions" + }, + { + "value": "30", + "type": "Number", + "unit": "Pixel", + "label": "Trunk diameter", + "description": "The diameter of the collision shape of a copy at the height given in \"Height of a copy\" (it is scaled with the copy).", + "group": "Collisions", + "name": "TrunkDiameter" + }, + { + "value": "8000", + "type": "Number", + "unit": "Pixel", + "label": "Draw distance", + "description": "Copies shrink and are hidden at this distance from the camera.", + "group": "Rendering", + "name": "DrawDistance" + }, + { + "value": "true", + "type": "Boolean", + "label": "Cast shadows", + "group": "Rendering", + "name": "CastShadow" + }, + { + "value": "true", + "type": "Boolean", + "label": "Receive shadows", + "group": "Rendering", + "name": "ReceiveShadow" + }, + { + "value": "", + "type": "MultilineString", + "label": "Edits", + "description": "Where copies are added or erased, applied in order, as a JSON list, for example [{\"tool\": \"add\", \"x\": 0.3, \"y\": 0.4, \"radius\": 0.15}, {\"tool\": \"erase\", \"x\": 0.1, \"y\": 0.9, \"toX\": 0.8, \"toY\": 0.2, \"radius\": 0.02}]. Tools: add (density: from 0 to 1, 1 by default, how dense copies are compared to the Density property) and erase. strength (from 0 to 1) makes a stroke lighter. x, y and radius are fractions (0 to 1) of the object size (x is (scene X - object X) / object width). Add toX and toY to paint along a line (a row of trees, a path through a forest). With \"Copies grow: only where painted\", only the added areas have copies. Shown in the scene editor.", + "group": "Placement", + "advanced": true, + "name": "Edits" + }, + { + "value": "", + "type": "String", + "label": "Painted data", + "description": "Written by the brushes of the scene editor, in a compact format not meant to be read or written by hand: use Edits instead. Clear it to go back to the coverage and the edits.", + "advanced": true, + "name": "PaintData" + } + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Model", + "children": [ + { + "propertyName": "Model" + }, + { + "propertyName": "Size" + }, + { + "propertyName": "MinScale" + }, + { + "propertyName": "MaxScale" + }, + { + "propertyName": "RandomRotation" + }, + { + "propertyName": "RandomTint" + } + ] + }, + { + "folderName": "Placement", + "children": [ + { + "propertyName": "Density" + }, + { + "propertyName": "Seed" + }, + { + "propertyName": "Coverage" + }, + { + "propertyName": "Edits" + } + ] + }, + { + "folderName": "Ground", + "children": [ + { + "propertyName": "GroundLayer" + }, + { + "propertyName": "MaxSlope" + }, + { + "propertyName": "MinGroundZ" + }, + { + "propertyName": "MaxGroundZ" + } + ] + }, + { + "folderName": "Collisions", + "children": [ + { + "propertyName": "TrunkCollisions" + }, + { + "propertyName": "TrunkDiameter" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "DrawDistance" + }, + { + "propertyName": "CastShadow" + }, + { + "propertyName": "ReceiveShadow" + } + ] + }, + { + "propertyName": "PaintData" + } + ] + }, + "variants": [] + } + ] } ], "externalLayouts": [] diff --git a/examples/starting-first-person/assets/StartingWall.png b/examples/starting-first-person/assets/StartingWall.png index b3a097887..06278d6e5 100644 Binary files a/examples/starting-first-person/assets/StartingWall.png and b/examples/starting-first-person/assets/StartingWall.png differ diff --git a/examples/starting-first-person/starting-first-person.json b/examples/starting-first-person/starting-first-person.json index 036046876..09b576c93 100644 --- a/examples/starting-first-person/starting-first-person.json +++ b/examples/starting-first-person/starting-first-person.json @@ -1,7 +1,7 @@ { "firstLayout": "", "gdVersion": { - "build": 283, + "build": 284, "major": 5, "minor": 6, "revision": 0 @@ -214,6 +214,22 @@ "name": "assets/Skybox_Right.png", "smoothed": true, "userAdded": false + }, + { + "file": "assets/StartingGround.png", + "kind": "image", + "metadata": "", + "name": "assets/StartingGround.png", + "smoothed": true, + "userAdded": false + }, + { + "file": "assets/StartingWall.png", + "kind": "image", + "metadata": "", + "name": "assets/StartingWall.png", + "smoothed": true, + "userAdded": false } ] }, @@ -230,12 +246,13 @@ "mangledName": "Game_32Scene", "name": "Game Scene", "r": 255, + "renderer3DWorldScale": 100, "standardSortMethod": true, "stopSoundsOnStartup": true, "title": "", "v": 255, "uiSettings": { - "grid": false, + "grid": true, "gridType": "rectangular", "gridWidth": 64, "gridHeight": 64, @@ -245,8 +262,8 @@ "gridOffsetZ": 0, "gridColor": 10401023, "gridAlpha": 0.8, - "snap": false, - "zoomFactor": 0.3437520241612461, + "snap": true, + "zoomFactor": 0.28172599320796804, "windowMask": false, "selectedLayer": "", "gameEditorMode": "embedded-game" @@ -261,24 +278,6 @@ } ], "instances": [ - { - "angle": 0, - "customSize": true, - "depth": 30, - "height": 1728, - "layer": "", - "locked": true, - "name": "Ground", - "persistentUuid": "6a14e5ab-5c31-4692-8cd5-c50937fd60c6", - "width": 1920, - "x": -320, - "y": -576, - "z": -30, - "zOrder": 1, - "numberProperties": [], - "stringProperties": [], - "initialVariables": [] - }, { "angle": 0, "customSize": true, @@ -527,77 +526,25 @@ "numberProperties": [], "stringProperties": [], "initialVariables": [] + }, + { + "angle": 0, + "customSize": true, + "height": 1728, + "layer": "", + "locked": true, + "name": "Terrain", + "persistentUuid": "5f63df8b-0768-4f0a-9390-8623ae59b4bb", + "width": 1920, + "x": -320, + "y": -576, + "zOrder": 0, + "numberProperties": [], + "stringProperties": [], + "initialVariables": [] } ], "objects": [ - { - "assetStoreId": "", - "name": "Ground", - "persistentUuid": "e50356d8-e24c-4e12-8435-9684026b40ef", - "type": "Scene3D::Cube3DObject", - "variables": [], - "effects": [], - "behaviors": [ - { - "name": "Physics3D", - "type": "Physics3D::Physics3DBehavior", - "object3D": "Object3D", - "bodyType": "Static", - "bullet": false, - "fixedRotation": false, - "shape": "Box", - "shapeOrientation": "Z", - "shapeDimensionA": 0, - "shapeDimensionB": 0, - "shapeDimensionC": 0, - "density": 1, - "friction": 0.3, - "restitution": 0.1, - "linearDamping": 0.1, - "angularDamping": 0.1, - "gravityScale": 1, - "layers": 17, - "masks": 17, - "shapeOffsetX": 0, - "shapeOffsetY": 0, - "shapeOffsetZ": 0, - "massCenterOffsetX": 0, - "massCenterOffsetY": 0, - "massCenterOffsetZ": 0, - "massOverride": 0 - } - ], - "content": { - "width": 1000, - "height": 1000, - "depth": 40, - "enableTextureTransparency": false, - "facesOrientation": "Y", - "frontFaceResourceName": "assets\\StartingGround.png", - "backFaceResourceName": "", - "backFaceUpThroughWhichAxisRotation": "X", - "leftFaceResourceName": "assets\\StartingWall.png", - "rightFaceResourceName": "assets\\StartingWall.png", - "topFaceResourceName": "assets\\StartingWall.png", - "bottomFaceResourceName": "assets\\StartingWall.png", - "frontFaceVisible": true, - "backFaceVisible": false, - "leftFaceVisible": true, - "rightFaceVisible": true, - "topFaceVisible": true, - "bottomFaceVisible": true, - "frontFaceResourceRepeat": true, - "backFaceResourceRepeat": false, - "leftFaceResourceRepeat": false, - "rightFaceResourceRepeat": false, - "topFaceResourceRepeat": false, - "bottomFaceResourceRepeat": false, - "materialType": "StandardWithoutMetalness", - "isCastingShadow": true, - "isReceivingShadow": true, - "tileScale": 0.2 - } - }, { "assetStoreId": "", "name": "Obstacle", @@ -1085,6 +1032,59 @@ "content": { "IsChecked": false } + }, + { + "assetStoreId": "", + "name": "Terrain", + "persistentUuid": "59e9680b-78cd-49d1-b317-ef849e7e0ffc", + "type": "Terrain3D::Terrain3D", + "variant": "", + "variables": [], + "effects": [], + "behaviors": [ + { + "name": "Physics3D", + "type": "Physics3D::Physics3DBehavior", + "object3D": "Object3D", + "bodyType": "Static", + "bullet": false, + "fixedRotation": false, + "shape": "Box", + "meshShapeResourceName": "", + "shapeOrientation": "Z", + "shapeDimensionA": 0, + "shapeDimensionB": 0, + "shapeDimensionC": 0, + "shapeOffsetX": 0, + "shapeOffsetY": 0, + "shapeOffsetZ": 0, + "massCenterOffsetX": 0, + "massCenterOffsetY": 0, + "massCenterOffsetZ": 0, + "massOverride": 0, + "density": 1, + "friction": 0.3, + "restitution": 0.1, + "linearDamping": 0.1, + "angularDamping": 0.1, + "gravityScale": 1, + "layers": 17, + "masks": 17 + } + ], + "content": { + "SculptData": "2;256;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AQABAQIBAgIAAQEAAQEAAQIBAAICAQEEAQEAVQIAAQIAAQIBAgIDAgIAAQIAARAAAQEAAQECAQABAQIAAQECAQEFBAACAgABAQADAQABAQACAQAFAgAEAQIAAQICAgEDAgIAAQECBgABAwABCAMAAQMCBAMGAwACAQEMAQMHAAICAAEBGhkEAwIABAECAQYDAQAEAQIABAEGAwABAgMCAAECAwIAAgEAAgQFAgQAAQEDAgAEAgMEAAEDAgIAAQUCAggHAAIBAgEAAgQBAQEAVwICAAEBAgIAAgQFAgIAAQ4AAQEAAwEAAQQDAAIFAQICAAECAAEBAQQDAQEAFgIEAQADAgEGAgcCCAEBAAEBBgECAAECBQACDAEJAAEDAgACAR4ZAgUCAgEBAAEBAgADAgECAQQAAQUEAQAEAQABBAMCAAEBAAMBBAEBBgMAAgIAAQMEAQEEAwIBAgABAgMAAQQDAgAFAwIAAgEAXAQAAwEGAwQBAAEEDgECAQABAwQCBQQHAQIGAwIBAQADAwAdAgIAAgEGBgcAAQYBAAEBAAECAgABAQABBAUAAQoDBQEAAwIAAQEEAQABAwIAAQIBAAMBAgACAgMAAQIBAgABAQEEAAQBAgMEAQIAAgEAAwECAgABAQACAQABBAMAAQIAAQMCAgEABgIAAQEBAF8EAgACAQABAgACAgwAAgECAgMAAQEFAgIAAwEAAgEBACICAgABAgQFAgYFBAEAAgIAAQIDAAIEAgMBAwABAgACHBsBAgACAgMEAAIDAAECAAEBAgACAgMAAQICAQACAwIBBgMEAwQFAgACAQABAgEEAQABAQABBgMBAgIDAAECAAEBAgECAAEBAAMBAGICAAIEAgEBAgACDgABBQQBAQQHAQQAAQIAAQMCAQEBACgECAUAAQYDAAEDCAMAAQQDCAUDDAEFAAEDAQIBAhgVAwQBAgEBAAICAAEBAgEAAQIBAAICAQIBAAICAwIBBAMAAQIAAgECAAECAgEDAgEEAwABAgEBBAEABwQFAGcCAgECAAECAAEBAgoBBAABAwIFAAECAAIBAAIDAC0GAQABCAUGBwQCAQYJBAEAAQwCDRAFBQUCBAEBBAwCBAkJAQQCBgcBAgMCAw4PAgECAQICAAIDBgABAQMCAQABAgABAQABAgECAgMCAAEBAgEAAQQDBgMCBQIBAGkCAAECAQQBAAEEAQgEAQABAQMAAQIMDQIAAQMBAC8EAgICAgYJBBQbCAABDg8IFAEkPygBKzoHKwMOHQEEJBcGAw02AysIEhECAgMCEBIjHAgFHx4GHxgBFRgbHBUOERYVEA0KDxQDDQwJCA0KAQMAAQQFAGkEAQIAAgIAAQIMAwIFAQQAAgUIAwMAMgIBBgUIAwABBQoUGQUKBgkDAgQWChkAARliRwUAAQoFEBEFEAsRDhYGIQwLNCMFEgcOCwcGHCMBAAEGAgMAAQIFAQACAQIBBAIKDwIKCwACBAMAAQQAAQMAawIAAQIBAAMIAgUBBAABAQABAwA3AgYHEgABBxQRHgMIDwABFDE+OQNAOzw3QhMaDQgpKAMREhsHDgsOCAUrJgQjCxIRAAICAQABIB8AARoZFhUWERATEA8MCwIBAAEEAwgHAgEAAQIBAGwEAQECAgECCgUBBAQFAgEDAPIBBAACAQIAAQQBBAIFAgABAQEA8gECAgIAAQEAAQIEAgMAAgMA9QECAAICAAEGAAEDAgABAwD2AQIAAQECAgYBAAIDAPgBBAEAAgICAgUA+gEEAAEDBAEBAPoBAgACAgQAAQMA+wECAAICAQD8AQIBAgIBAPwBAgIBAP8BAgCBAgIA/wECAQD/AQIAgQICAIACAgCBBAIAjBoBAIIGAQCABAECAP8BAgCCBAEA/wEBAgD/AQIDAIACAQD/AQMEAP8BAQD/AQEAggICAP0BAQEEAQCAAgEA/gECAQUA/gEBAwgA/AEBAAEDBgD9AQIDAgEBAPwBAQICAwD9AQMEAwYA/AEBBAMCAAECAPsBAgMCAgABBwD7AQECAgIHBgD7AQICAwUEAQD8AQEDBgEBAP0BBAkEBAD7AQEAAQEIAwD8AQMAAgICAgD6AQUEAAEEAwABAQD6AQIBAAEDAgEBAPoBAgIDAAEDAAEEAPoBAwABBAABAQYDAPoBAgMDAggBAwD6AQMAAQYCBwEGAPoBAgQDBwYBAPsBBAUCAgECAwD6AQECAQYCBQEA+gEDBAQDBwgGAPoBBAEFAAEIAQD8AQMEAgEBAPsBAwYAAQEAAgMA/gEBAAEEAPoBBAEDAAECBAEA+gEDAAEEAgIFAQD9AQEBAgD+AQEBAgQDAgIA+AEBAwIEAAIHAgYA9wEBAAEEAAMCAAEBAAECAPYBAQQAAgIBAQECAwQA9gEEAAEBAgEAAgECAQMGAPUBAgECAAEBBAMAAQECAAEDBAD0AQIAAQIDBAMEAwIAAQIBAAECAPQBAgECAQIAAQQDAQIAAQEAAQIA8gECAAIBAgEAAQIBAAEBAgD4AQIDAgEAAQECAwYBAPQBAgEBAgAEAwQAAQMEAAEBAgDzAQIAAQEAAwQDAAEBAgD2AQIDBAABAgABAgEAAQIDAgD0AQEAAQIAAQICAgABAQIBAPcBAQICAQEA+wECAAIDAgIBAgIAAQECAQD1AQIAAQIBAgACAgEEAQEA9AEEBQICAgMAAQIBAgABAQIAAQEA8gEBBgABAQECAAECAAIBAAEBAgDzAQICAwABAgEAAQMAAQYBAAECAQD1AQICAQECAAIFBAIBAPYBAgMCAgIAAwEBAgD3AQIAAgIBAgABAQIBAPYBAgADBAEAAQEA+wECAAICAQABAgEA+AECAQQCAAEBAgEA+QECAAEBAAICAQD7AQIDAgABAQD9AQIAgAIBAIACAgCAAgIAgAICAP8BAgMAgAICAIEEAgCDCAIA5hoDAAICAQABAgIA+QEBAAMCAgEBAAECAAEDBAD0AQQAAgMBAwACAgABAgIBAAICAPUBAgABAQABAwEBAgABBgABAQIA8AEBAgECAQQCAQACAQACBwABAgQAAQIBAgDtAQMCAgIFAAECBAMCAAECAgEDAwABBAQBAOwBAwABBAUEBgEAAQEEAAEBAQABAgABAwABBQABCADrAQcCBAEEAQUEAAICAQYDAgECAgABAQMFAgYCAOgBAwIBAQABAQYCAAIBAgMEAwQCAQACAgMBAwIGAOgBAgEAAwEBAAEBAgQAAQMCAAEBBAEEAAECAQMDAQYCAOYBBAACAQACAQABAgEFAAEEAAEBAAICAAECAAECAAEBAQMGAAEBAgDjAQECAAEEAQAFAgEBAAQBAQABAgABAgIDAQMCBgECAOYBAgABAQAEAgEABAIBAQACAgABAgEDAgMGAQIA4QEBAAECAwICAAYCAQACAQICAQAFAgEBAgcIAQIA4gEBBAEAAQIABAECAgABAQABAQABBAMABAICAwEGCQgBAOABAgACAwADAgABAgECAQACAgEAAgECAAMCAQADAwQHCADfAQIAAQEAAQIBAAIDAgQEAQIAAgICAAECAAMBAAEBAAICAAIBAAEHAN8BCgABAgACAgEAAQIBBQECAQACAQACAQDinQEBAIACAQD/AQUA/wEDAAEBAP0BAQMAAQMA/AEBAwACAgECAPkBAwABAwQAAgECAPkBAwECAQABAQMEAPcBAQMCAQIBAwQDAQYA9QEBAQEAAQIDBAEBAwABBAD1AQUAAgIDBAMBAAEDAAECAgDyAQEFBAACAQABAwMEBQUEAQQA8QEDAQQAAQEBAAEHAgADAwEBAgYA7wEDAgABAwEDAAIEAQIBAAEJBAICAgDtAQEBAAEBAAEBAQIAAQUGAQEAAQIFAAQGAOwBAQMBAAEBAQQBAgIHAgIBAwcCAAEDBgICAOgBAQEDAAEBAgABAQYDAgcCBAEBAQEJAAEEAgMCBAIA5gEBAwEAAgIAAQEEAwEDBgEAAgUAAQoA6aECBAACAQIAAQEBAPkBAgADAQECAgABAQABBAMA9AEDAAIEAgQAAgEAAQEBAgACAQD1AQEAAQIAAQQCAgEAAQUAAQIBAPABAgECAQIDAQIAAgIAAggAAQEDAAEBAgEA7QEEAQEBBgABAQMEAQABAQECBAQAAQMDAgDsAQQAAQMGAwUCAAECAwABAgIAAQEAAQQAAQYAAQcA6wEIAQMCAwIGAwACAQIFBAECAQEAAQIEBgEFAQDoAQQBAgIAAQIFAQACAgEEAwQDAQIAAgEEAgQBBQDoAQECAAMCAgABAgEDAAEEAQABAgMCAwABAQIEBAIFAQDmAQMAAgIAAgIAAQECBgABAwABAgACAQABAQABAQABAgIEBQABAgEA4wECAQABAwIABQECAgAEAgIAAQEAAQEBBAIEAQUCAQDmAQEAAQIABAECAAQBAgIAAgEAAQECBAEEBQIBAOEBAgABAQQBAQAGAQIAAgIBAQIABQECAgEIBwIBAOIBAgMCAAEBAAQCAQEAAQIAAQIAAQMEAAQBAQQCBQoHAgDgAQEAAgQAAwEAAQECAQIAAgECAAICAQADAQIAAwQDCAcA3wEBAAECAAEBAgACBAEDAwIBAAIBAQABAQADAgABAgACAQACAgABCADfAQkAAQEAAgECAAEBAgYCAQIAAgIAAgIA9L8BqgERBw8JCQkHCQcJAwUFBQUDBQACAQEBAQEBAQABAQABAQEABAIAAQIAAgIBAgABAgEAAQQEAQIGBAYCAgoECAIGAgoKBAgGAgIODgABBAMDAQEBIAUCAAIHAQcCAQUBBQABAQUBAAIDAAEDAwEDAQEAAQMDAgMAAQMAAQEBAwIFAgUCAwMCAwABBQACAwABAQABBQABAQMBAAEBAwABAgEHAQACAwEBAgABAwIDAQBVAgIEBAYGCggICggKDhATBgUGAQIAAwICAQABAgACBAQBAQACAQADAgECAAICAAYBAAIDBAECAAEEAAEDAwQAAQUAAgQCCQYBBAUEAQUGAQABBAABCwABDAIBAgABAgIlIAECAAECAQQDAgEBBgIDAQQAAQEAAQEEAAEBAAECAAQBAgECAgMCAgMCAQIBAgIDBAACAwIEBQIAAQIBAgABAgECAQEAAQIAAQIAAQECAAIBAgIDAgIAVQEAAQEAAQECAQEEAQEAAQEAAQ8AAQIAAQIBAgABAgEAAQIBAgIGAwACAQABAgADAgABAgACAgAFAQAEAgEAAQEBAQIEAQEAAQIBBQABBAABBwQAAQQBAwQFBAACAgILAgQIAAIBAAECGRoDBAEABAIBAgUEAgAEAgEABAIFBAABAQQBAAEBBAEAAgIAAgMGAQMAAQIEAQAEAQQDAAEEAQEAAQYBAQcIAAICAQIAAgMCAgIAVwEBAAECAQEAAgMGAQEAAQ0AAQIAAwIAAQMEAAIGAgEBAAEBAAECAgMEAgIAFgEDAgADAQIFAQgBBwICAAECBQIBAAEBBgACCwIKAAEEAQACAh0aAQYBAQICAAECAQADAQIBAgMAAQYDAgAEAgABAwQBAAECAAMCAwICBQQAAgEAAQQDAgIDBAECAQABAQQAAQMEAQAFBAEAAgIAXAMAAwIFBAMCAAEDDQIBAgABBAMBBgMIAgEFBAECAgADBAAdAQEAAgIFBQgAAQUCAAECAAEBAQABAgABAwYAAQkEBgIAAwEAAQIDAgABBAEAAQECAAMCAQACAQQAAQECAQABAgIDAAQCAQQDAgEAAgIAAwIBAQABAgACAgABAwQAAQEAAQQBAQIABgEAAQICAF8DAQACAgABAQACAQsAAgIBAQQAAQIGAQEAAwIAAgICACIBAQABAQMGAQUGAwIAAgEAAQEEAAIDAQQCBAABAQACGxwCAQACAQQDAAIEAAEBAAECAQACAQQAAQEBAgACBAECBQQDBAMGAQACAgABAQIDAgABAgABBQQCAQEEAAEBAAECAQIBAAECAAMCAGIBAAIDAQICAQACDQABBgMCAgMIAgMAAQEAAQQBAgICACgDBwYAAQUEAAEEBwQAAQMEBwYECwIGAAEEAgECARcWBAMCAQICAAIBAAECAQIAAQECAAIBAgECAAIBBAECAwQAAQEAAgIBAAEBAQIEAQIDBAABAQICAwIABwMGAGcBAQIBAAEBAAECAQkCAwABBAEGAAEBAAICAAIEAC0FAgABBwYFCAMBAgUKAwIAAQsBDg8GBgYBAwICAwsBAwoKAgMBBQgCAQQBBA0QAQIBAgEBAAIEBQABAgQBAgABAQABAgABAQIBAQQBAAECAQIAAQMEBQQBBgECAGkBAAEBAgMCAAEDAgcDAgABAgQAAQELDgEAAQQCAC8DAQEBAQUKAxMcBwABDRAHEwIjQCcCLDkILAQNHgIDIxgFBA41BCwHERIBAQQBDxEkGwcGIB0FIBcCFhccGxYNEhUWDw4JEBMEDgsKBw4JAgQAAQMGAGkDAgEAAgEAAQELBAEGAgMAAgYHBAQAMgECBQYHBAABBgkTGgYJBQoEAQMVCRoAARphSAYAAQkGDxIGDwwSDRUFIgsMMyQGEQgNDAgFGyQCAAEFAQQAAQEGAgACAgECAwEJEAEJDAACAwQAAQMAAQQAawEAAQECAAMHAQYCAwABAgABBAA3AQUIEQABCBMSHQQHEAABEzI9OgQ/PDs4QRQZDgcqJwQSERwIDQwNBwYsJQMkDBESAAIBAgABHyAAARkaFRYVEg8UDxALDAECAAEDBAcIAQIAAQECAGwDAgIBAQIBCQYCAwMGAQIEAPIBAwACAgEAAQMCAwEGAQABAgIA8gEBAQEAAQIAAQEDAQQAAgQA9QEBAAIBAAEFAAEEAQABBAD2AQEAAQIBAQUCAAIEAPgBAwIAAgEBAQYA+gEDAAEEAwICAPoBAQACAQMAAQQA+wEBAAIBAgD8AQECAQECAPwBAQECAP8BAQCBAgEA/wEBAgD/AQEAgQIBAIACAQCBBAEAjBoCAIIGAgCABAIBAP8BAQCCBAIA/wECAQD/AQEEAIACAgD/AQQDAP8BAgD/AQIAggIBAP0BAgIDAgCAAgIA/gEBAgYA/gECBAcA/AECAAEEBQD9AQEEAQICAPwBAgEBBAD9AQQDBAUA/AECAwQBAAEBAPsBAQQBAQABCAD7AQIBAQEIBQD7AQEBBAYDAgD8AQIEBQICAP0BAwoDAwD7AQIAAQIHBAD8AQQAAgEBAQD6AQYDAAEDBAABAgD6AQECAAEEAQICAPoBAQEEAAEEAAEDAPoBBAABAwABAgUEAPoBAQQEAQcCBAD6AQQAAQUBCAIFAPoBAQMECAUCAPsBAwYBAQIBBAD6AQIBAgUBBgIA+gEEAwMECAcFAPoBAwIGAAEHAgD8AQQDAQICAPsBBAUAAQIAAgQA/gECAAEDAPoBAwIEAAEBAwIA+gEEAAEDAQEGAgD9AQICAQD+AQICAQMEAQEA+AECBAEDAAIIAQUA9wECAAEDAAMBAAECAAEBAPYBAgMAAgECAgIBBAMA9gEDAAECAQIAAgIBAgQFAPUBAQIBAAECAwQAAQIBAAEEAwD0AQEAAQEEAwQDBAEAAQECAAEBAPQBAQIBAgEAAQMEAgEAAQIAAQEA8gEBAAICAQIAAQECAAECAQD4AQEEAQIAAQIBBAUCAPQBAQICAQAEBAMAAQQDAAECAQDzAQEAAQIAAwMEAAECAQD2AQEEAwABAQABAQIAAQEEAQD0AQIAAQEAAQEBAQABAgECAPcBAgEBAgIA+wEBAAIEAQECAQEAAQIBAgD1AQEAAQECAQACAQIDAgIA9AEDBgEBAQQAAQECAQABAgEAAQIA8gECBQABAgIBAAEBAAICAAECAQDzAQEBBAABAQIAAQQAAQUCAAEBAgD1AQEBAgIBAAIGAwECAPYBAQQBAQEAAwICAQD3AQEAAgECAQABAgECAPYBAQADAwIAAQIA+wEBAAIBAgABAQIA+AEBAgMBAAECAQIA+QEBAAECAAIBAgD7AQEEAQABAgD9AQEAgAICAIACAQCAAgEAgAIBAP8BAQQAgAIBAIEEAQCDCAEA7NoBAgCAAgIA/wEGAP8BBAABAgD9AQIEAAEEAPwBAgQAAgECAQD5AQQAAQQDAAICAQD5AQQCAQIAAQIEAwD3AQIEAQIBAgQDBAIFAPUBAgICAAEBBAMCAgQAAQMA9QEGAAIBBAMEAgABBAABAQEA8gECBgMAAgIAAQQEAwYGAwIDAPEBBAIDAAECAgABCAEAAwQCAgEFAO8BBAEAAQQCBAACAwIBAgABCgMBAQEA7QECAgABAgABAgIBAAEGBQICAAEBBgAEBQDsAQIEAgABAgIDAgEBCAEBAgQIAQABBAUBAQDoAQICBAABAgEAAQIFBAEIAQMCAgICCgABAwEEAQMBAOYBAgQCAAIBAAECAwQCBAUCAAIGAAEJAIGEBACBhAQAgYQEAIGEBACBhAQ=", + "Layer1Texture": "assets/StartingGround.png", + "Layer1Color": "255;255;255", + "HeightBlending": 0.5, + "TextureVariation": 0, + "TextureSize": 128, + "CliffLayer": "3", + "Layer2Texture": "", + "Layer3Texture": "", + "CliffAngle": 45, + "CastShadow": false + } } ], "objectsFolderStructure": { @@ -1100,7 +1100,7 @@ "objectName": "Obstacle" }, { - "objectName": "Ground" + "objectName": "Terrain" }, { "objectName": "ControlsToggle" @@ -6793,7 +6793,6 @@ "name": "PlatformerMultitouchMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -7047,7 +7046,6 @@ "name": "Platformer3DMultitouchMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -7222,7 +7220,6 @@ "name": "Shooter3DMultitouchMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -7386,7 +7383,6 @@ "name": "FirstPersonMultitouchMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -8591,7 +8587,6 @@ "name": "PhysicsCar3DMultitouchMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -8804,7 +8799,6 @@ "name": "TopDownMultitouchMapper", "objectType": "", "previewIconUrl": "", - "quickCustomizationVisibility": "hidden", "eventsFunctions": [ { "fullName": "", @@ -23224,6 +23218,7802 @@ } ], "eventsBasedObjects": [] + }, + { + "author": "", + "category": "General", + "dimension": "3D", + "extensionNamespace": "", + "fullName": "3D terrain", + "gdevelopVersion": ">=5.5.222", + "helpPath": "", + "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLXRlcnJhaW4iIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTQsNkwxMC4yNSwxMUwxMy4xLDE0LjhMMTEuNSwxNkM5LjgxLDEzLjc1IDcsMTAgNywxMEwxLDE4SDIzTDE0LDZaIiAvPjwvc3ZnPg==", + "name": "Terrain3D", + "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", + "shortDescription": "A ground with hills and valleys that can be sculpted and painted in the editor, with collisions.", + "version": "0.1.0", + "description": [ + "A 3D terrain object for open worlds, adventure or racing games: hills, mountains, islands or valleys, with painted layers of grass, dirt, rock or sand.", + "", + "- **Sculpt and paint in the scene editor**: select a terrain to show the tools (raise, lower, smooth, flatten and paint 4 layers).", + "- **Start from a relief** (hills, mountains, island) or **from a heightmap image**.", + "- **Steep slopes automatically show rock** (or any layer).", + "- **Natural borders between layers**: stones and tufts of a texture cover the gaps of the other where layers meet (height blending), and the repetition of textures is made less visible (texture variation).", + "- **Physics**: add the **3D physics** behavior to the terrain. Characters and objects collide with the ground exactly. The terrain is always static.", + "- **Pathfinding**: add the **floor/obstacle for pathfinding (navmesh based)** behavior to the terrain, so that characters with the navmesh pathfinding behavior walk on its hills and around its cliffs (in recent GDevelop versions).", + "- **Shape it with the Edits property**: a list of raise, flatten, smooth and paint strokes written in JSON, shown in the scene editor. It's readable and can be changed by hand or by an AI agent, like the relief, the heightmap and the other properties. Actions only change the terrain during the game.", + "- **Change it during the game** with actions (raise, lower, flatten, smooth, paint, along a line for paths and rivers) and read the ground height, slope or layer with expressions.", + "- **Grass**: add a **terrain grass** object over the terrain. Thousands of blades sway in the wind, grow where a layer (like grass) is painted and where the ground is not too steep, and are pushed aside by characters. Sculpt or paint the terrain and the grass follows.", + "- **Forests, rocks and bushes**: add a **scattered models** object over the terrain. It places hundreds or thousands of copies of a 3D model (trees, rocks, bushes, flowers...) on the ground and draws them all at once, which is much faster than an object for each. Copies grow where a layer is painted, away from steep slopes, and follow the ground when it's sculpted. Add or erase them with the brushes of the scene editor or with the Edits property, give trees trunk collisions, and cut them down with actions. Ready-made forests, rocks and bushes are in the asset store.", + "- **Tunnels and caves**: make holes in the ground (and fill them again) with the brushes, the Edits property or actions, and put a 3D model of the tunnel or the cave (with a mesh collision shape) under the ground. Nothing is drawn in holes, and characters, cars and rays go through them.", + "- **Made for large worlds**: the terrain is split in chunks that are hidden when off-screen and simplified when far away, and collisions are updated only where the ground changes." + ], + "origin": { + "identifier": "Terrain3D", + "name": "gdevelop-extension-store" + }, + "tags": [ + "3d", + "terrain", + "ground", + "heightmap", + "landscape", + "open world", + "grass", + "foliage", + "forest", + "trees", + "tunnel", + "cave" + ], + "authorIds": [], + "dependencies": [], + "globalVariables": [], + "sceneVariables": [], + "eventsFunctions": [ + { + "description": "Define helper classes JavaScript code.", + "fullName": "Define helper classes", + "functionType": "Action", + "name": "DefineHelperClasses", + "private": true, + "sentence": "Define helper classes JavaScript code", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "if (gdjs.__terrain3DExtension) {", + " return;", + "}", + "", + "// Tested by the gameplay tests of the \"3D island terrain\" example (GDevelop-examples repository).", + "", + "const groundsByScene = new WeakMap();", + "/**", + " * The grounds of a scene (the terrains), followed by the terrain grass.", + " *", + " * A ground has: `getVersion()` (changing when the ground changes),", + " * `containsPoint(x, y)`, `getHeightAt(x, y)` (Z position of the ground),", + " * `getSlopeAt(x, y)` (in degrees), `getLayerWeightAt(x, y, layer)` (how", + " * much a painted layer, from 1 to 4, is there, from 0 to 1) and optionally", + " * `getTintAt(x, y)` (the painted tint, as red, green and blue from 0 to 255).", + " * @param {gdjs.RuntimeScene} runtimeScene", + " * @returns {Set}", + " */", + "const getGrounds = (runtimeScene) => {", + " let grounds = groundsByScene.get(runtimeScene);", + " if (!grounds) {", + " grounds = new Set();", + " groundsByScene.set(runtimeScene, grounds);", + " }", + " return grounds;", + "};", + "", + "/**", + " * @param {number} seed", + " * @returns {() => number} Random numbers between 0 (included) and 1 (excluded), the same for the same seed.", + " */", + "const createRandom = (seed) => {", + " let state = seed >>> 0 || 1;", + " return () => {", + " state ^= state << 13;", + " state ^= state >>> 17;", + " state ^= state << 5;", + " return (state >>> 0) / 4294967296;", + " };", + "};", + "", + "/**", + " * Find the highest ground at a position: grounds can overlap (like an island over a sea floor).", + " * @param {Iterable} grounds", + " * @param {{ground: object | null, z: number}} result Set to the ground (null if there is none) and its Z position.", + " */", + "const findHighestGround = (grounds, x, y, result) => {", + " result.ground = null;", + " for (const ground of grounds) {", + " if (!ground.containsPoint(x, y)) continue;", + " const z = ground.getHeightAt(x, y);", + " if (!result.ground || z > result.z) {", + " result.ground = ground;", + " result.z = z;", + " }", + " }", + " return result;", + "};", + "", + "// The grounds or the objects placed on them can change every frame (while", + "// sculpting or moving them in the editor): things are placed again at most this often.", + "const PLACEMENT_DELAY = 0.3;", + "", + "/**", + " * Tells when things placed on the grounds of a scene (grass blades, scattered", + " * models) must be placed again: after the grounds or the given properties", + " * changed, at most every `PLACEMENT_DELAY` seconds.", + " */", + "class GroundPlacement {", + " /** @param {gdjs.RuntimeScene} runtimeScene */", + " constructor(runtimeScene) {", + " this.grounds = getGrounds(runtimeScene);", + " this.placedFrom = '';", + " this.timeSincePlacement = 0;", + " }", + "", + " /**", + " * @param {number} elapsedSeconds", + " * @param {string} propertiesKey Changes when the things must be placed again.", + " */", + " shouldPlaceAgain(elapsedSeconds, propertiesKey) {", + " this.timeSincePlacement += elapsedSeconds;", + " if (this.placedFrom && this.timeSincePlacement < PLACEMENT_DELAY) return false;", + " let placementKey = propertiesKey;", + " for (const ground of this.grounds) placementKey += '|' + ground.getVersion();", + " if (placementKey === this.placedFrom) return false;", + " this.placedFrom = placementKey;", + " this.timeSincePlacement = 0;", + " return true;", + " }", + "", + " placeAgain() {", + " this.placedFrom = '';", + " }", + "}", + "", + "/**", + " * Replace parts of the shader code of a Three.js material, with an error if", + " * a part is missing (for example after an update of Three.js).", + " * @param {string} shaderCode", + " * @param {Array<[string, string]>} replacements The searched code and its replacement.", + " * @param {string} materialName", + " */", + "const patchShaderCode = (shaderCode, replacements, materialName) => {", + " for (const [searchedCode, newCode] of replacements) {", + " if (!shaderCode.includes(searchedCode)) {", + " console.error(`${materialName}: \"${searchedCode}\" was not found in the shader, which won't look as expected.`);", + " }", + " shaderCode = shaderCode.replace(searchedCode, newCode);", + " }", + " return shaderCode;", + "};", + "", + "// Size of the object inner area. Instances are scaled from it, so these only", + "// define the default size of a new terrain.", + "const AREA_SIZE = 4096;", + "const AREA_DEPTH = 600;", + "", + "// Terrains have at most 8 x 8 chunks (so at most 64 draw calls), of at least", + "// 32 x 32 cells.", + "const MIN_CHUNK_CELLS = 32;", + "const MAX_CHUNKS_PER_SIDE = 8;", + "/** @param {number} resolution */", + "const getChunkCells = (resolution) => Math.max(MIN_CHUNK_CELLS, resolution / MAX_CHUNKS_PER_SIDE);", + "// Grid steps of each level of detail, and the distance (in chunk sizes)", + "// from which each one is used.", + "const LOD_STEPS = [1, 2, 4];", + "const LOD_DISTANCES = [0, 3, 6];", + "", + "const LAYER_COUNT = 4;", + "const RESOLUTIONS = [64, 128, 256, 512, 1024];", + "const DEFAULT_RESOLUTION = 256;", + "// Each undo step is a copy of the data: fewer are kept for big terrains.", + "const UNDO_STEPS_SAMPLES = 20 * 257 * 257;", + "", + "const clamp01 = (value) => (value < 0 ? 0 : value > 1 ? 1 : value);", + "const clampInteger = (value, min, max) =>", + " value < min ? min : value > max ? max : Math.floor(value);", + "", + "/**", + " * A rectangle of grid samples, inclusive. Empty when `minI > maxI`.", + " */", + "class SampleRectangle {", + " constructor() {", + " this.clear();", + " }", + "", + " clear() {", + " this.minI = Number.MAX_SAFE_INTEGER;", + " this.minJ = Number.MAX_SAFE_INTEGER;", + " this.maxI = -1;", + " this.maxJ = -1;", + " }", + "", + " isEmpty() {", + " return this.minI > this.maxI;", + " }", + "", + " /** @param {SampleRectangle} other */", + " add(other) {", + " if (other.isEmpty()) return;", + " this.minI = Math.min(this.minI, other.minI);", + " this.minJ = Math.min(this.minJ, other.minJ);", + " this.maxI = Math.max(this.maxI, other.maxI);", + " this.maxJ = Math.max(this.maxJ, other.maxJ);", + " }", + "", + " set(minI, minJ, maxI, maxJ) {", + " this.minI = minI;", + " this.minJ = minJ;", + " this.maxI = maxI;", + " this.maxJ = maxJ;", + " return this;", + " }", + "}", + "", + "/**", + " * The heights (between 0 and 1) and the painted layers weights of a square", + " * grid of `size * size` samples.", + " */", + "class TerrainData {", + " /** @param {number} resolution The number of cells on each side. */", + " constructor(resolution) {", + " this.resolution = resolution;", + " this.size = resolution + 1;", + " this.heights = new Float32Array(this.size * this.size);", + " /** 4 weights (0-255) per sample, one for each layer. */", + " this.splat = new Uint8Array(this.size * this.size * LAYER_COUNT);", + " this.fillLayer(0);", + " /**", + " * 4 bytes per sample: a tint multiplying the layers (red, green and", + " * blue, white for none) and the wetness (0 for dry).", + " */", + " this.colors = new Uint8Array(this.size * this.size * 4);", + " this.clearColors();", + " }", + "", + " clearColors() {", + " this.colors.fill(255);", + " for (let index = 3; index < this.colors.length; index += 4) {", + " this.colors[index] = 0;", + " }", + " }", + "", + " /** @param {number} layer */", + " fillLayer(layer) {", + " this.splat.fill(0);", + " for (let index = layer; index < this.splat.length; index += LAYER_COUNT) {", + " this.splat[index] = 255;", + " }", + " }", + "", + " getHeight(i, j) {", + " const last = this.resolution;", + " const clampedI = i < 0 ? 0 : i > last ? last : i;", + " const clampedJ = j < 0 ? 0 : j > last ? last : j;", + " return this.heights[clampedJ * this.size + clampedI];", + " }", + "", + " /**", + " * @param {number} gridX", + " * @param {number} gridY", + " * @returns {number} The height interpolated on the triangles that are drawn.", + " */", + " sampleHeight(gridX, gridY) {", + " const x = Math.min(Math.max(gridX, 0), this.resolution);", + " const y = Math.min(Math.max(gridY, 0), this.resolution);", + " const i = Math.min(Math.floor(x), this.resolution - 1);", + " const j = Math.min(Math.floor(y), this.resolution - 1);", + " const fx = x - i;", + " const fy = y - j;", + " const topRight = this.getHeight(i + 1, j);", + " const bottomLeft = this.getHeight(i, j + 1);", + " if (fx + fy <= 1) {", + " const topLeft = this.getHeight(i, j);", + " return topLeft + fx * (topRight - topLeft) + fy * (bottomLeft - topLeft);", + " }", + " const bottomRight = this.getHeight(i + 1, j + 1);", + " return (", + " bottomRight +", + " (1 - fx) * (bottomLeft - bottomRight) +", + " (1 - fy) * (topRight - bottomRight)", + " );", + " }", + "", + " /** @returns {number} The weight (0-1) of a layer, interpolated between samples. */", + " sampleLayerWeight(gridX, gridY, layer) {", + " const x = Math.min(Math.max(gridX, 0), this.resolution);", + " const y = Math.min(Math.max(gridY, 0), this.resolution);", + " const i = Math.min(Math.floor(x), this.resolution - 1);", + " const j = Math.min(Math.floor(y), this.resolution - 1);", + " const fx = x - i;", + " const fy = y - j;", + " const weight = (i, j) => this.splat[(j * this.size + i) * LAYER_COUNT + layer];", + " const top = weight(i, j) + fx * (weight(i + 1, j) - weight(i, j));", + " const bottom = weight(i, j + 1) + fx * (weight(i + 1, j + 1) - weight(i, j + 1));", + " return (top + fy * (bottom - top)) / 255;", + " }", + "", + " /**", + " * @param {number} channel 0, 1 and 2 for the tint, 3 for the wetness.", + " * @returns {number} The channel (0-1), interpolated between samples.", + " */", + " sampleColor(gridX, gridY, channel) {", + " const x = Math.min(Math.max(gridX, 0), this.resolution);", + " const y = Math.min(Math.max(gridY, 0), this.resolution);", + " const i = Math.min(Math.floor(x), this.resolution - 1);", + " const j = Math.min(Math.floor(y), this.resolution - 1);", + " const fx = x - i;", + " const fy = y - j;", + " const value = (i, j) => this.colors[(j * this.size + i) * 4 + channel];", + " const top = value(i, j) + fx * (value(i + 1, j) - value(i, j));", + " const bottom = value(i, j + 1) + fx * (value(i + 1, j + 1) - value(i, j + 1));", + " return (top + fy * (bottom - top)) / 255;", + " }", + "", + " /**", + " * @param {number} index The index of a sample.", + " * @returns {boolean} true if no layer is painted on the sample: it's a hole.", + " */", + " isHole(index) {", + " const offset = index * LAYER_COUNT;", + " return (this.splat[offset] | this.splat[offset + 1] | this.splat[offset + 2] | this.splat[offset + 3]) === 0;", + " }", + "", + " /**", + " * @returns {number} The sum of the layer weights (0-1), interpolated between samples: there is a", + " * hole where it's under 0.5, like what is drawn.", + " */", + " sampleSolidity(gridX, gridY) {", + " let solidity = 0;", + " for (let layer = 0; layer < LAYER_COUNT; layer++) solidity += this.sampleLayerWeight(gridX, gridY, layer);", + " return solidity;", + " }", + "", + " /** @returns {number} The index (from 0) of the layer painted the most. */", + " getDominantLayer(gridX, gridY) {", + " const i = clampInteger(Math.round(gridX), 0, this.resolution);", + " const j = clampInteger(Math.round(gridY), 0, this.resolution);", + " const offset = (j * this.size + i) * LAYER_COUNT;", + " let dominantLayer = 0;", + " for (let layer = 1; layer < LAYER_COUNT; layer++) {", + " if (this.splat[offset + layer] > this.splat[offset + dominantLayer]) {", + " dominantLayer = layer;", + " }", + " }", + " return dominantLayer;", + " }", + "", + " /** @param {TerrainData} other */", + " copyFrom(other) {", + " this.heights.set(other.heights);", + " this.splat.set(other.splat);", + " this.colors.set(other.colors);", + " }", + "", + " /**", + " * @param {number} resolution", + " * @returns {TerrainData} A copy of this data with another resolution.", + " */", + " resampled(resolution) {", + " const data = new TerrainData(resolution);", + " const ratio = this.resolution / resolution;", + " for (let j = 0; j < data.size; j++) {", + " for (let i = 0; i < data.size; i++) {", + " const index = j * data.size + i;", + " data.heights[index] = this.sampleHeight(i * ratio, j * ratio);", + " const sourceOffset =", + " (Math.round(j * ratio) * this.size + Math.round(i * ratio)) * LAYER_COUNT;", + " for (let layer = 0; layer < LAYER_COUNT; layer++) {", + " data.splat[index * LAYER_COUNT + layer] = this.splat[sourceOffset + layer];", + " }", + " for (let channel = 0; channel < 4; channel++) {", + " data.colors[index * 4 + channel] = this.colors[sourceOffset + channel];", + " }", + " }", + " }", + " return data;", + " }", + "}", + "", + "/**", + " * Brushes change the samples around a segment, with an effect going smoothly", + " * from full on the segment to none at `radius`. A point is a segment of", + " * length 0. Positions are in scene units relative to the terrain origin.", + " */", + "const brushes = {", + " /**", + " * @param {TerrainData} data", + " * @param {{cellWidth: number, cellHeight: number, ax: number, ay: number, bx: number, by: number, radius: number}} stroke", + " * @param {(sampleIndex: number, weight: number, t: number) => void} apply Called with the weight of", + " * the brush and the position (from 0 to 1) of the nearest point of the segment.", + " * @param {SampleRectangle} changedSamples", + " */", + " forEachSample(data, stroke, apply, changedSamples) {", + " const { cellWidth, cellHeight, ax, ay, bx, by, radius } = stroke;", + " const last = data.resolution;", + " const minI = clampInteger(Math.floor((Math.min(ax, bx) - radius) / cellWidth), 0, last);", + " const maxI = clampInteger(Math.ceil((Math.max(ax, bx) + radius) / cellWidth), 0, last);", + " const minJ = clampInteger(Math.floor((Math.min(ay, by) - radius) / cellHeight), 0, last);", + " const maxJ = clampInteger(Math.ceil((Math.max(ay, by) + radius) / cellHeight), 0, last);", + " changedSamples.set(minI, minJ, maxI, maxJ);", + " if (radius <= 0) return;", + "", + " const abX = bx - ax;", + " const abY = by - ay;", + " const abLengthSquared = abX * abX + abY * abY;", + " const radiusSquared = radius * radius;", + " for (let j = minJ; j <= maxJ; j++) {", + " const y = j * cellHeight;", + " for (let i = minI; i <= maxI; i++) {", + " const x = i * cellWidth;", + " const t =", + " abLengthSquared > 0", + " ? clamp01(((x - ax) * abX + (y - ay) * abY) / abLengthSquared)", + " : 0;", + " const dx = x - (ax + t * abX);", + " const dy = y - (ay + t * abY);", + " const distanceSquared = dx * dx + dy * dy;", + " if (distanceSquared >= radiusSquared) continue;", + " const falloff = 1 - distanceSquared / radiusSquared;", + " apply(j * data.size + i, falloff * falloff, t);", + " }", + " }", + " },", + "", + " raise(data, stroke, amount, changedSamples) {", + " const { heights } = data;", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index, weight) => {", + " heights[index] = clamp01(heights[index] + amount * weight);", + " },", + " changedSamples", + " );", + " },", + "", + " /**", + " * @param {number} targetHeight", + " * @param {number} [endTargetHeight] The target at the end of the segment, for a slope (like a road", + " * going up). The same target as at its start by default.", + " */", + " flatten(data, stroke, targetHeight, strength, changedSamples, endTargetHeight = targetHeight) {", + " const { heights } = data;", + " const startTarget = clamp01(targetHeight);", + " const endTarget = clamp01(endTargetHeight);", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index, weight, t) => {", + " const target = startTarget + (endTarget - startTarget) * t;", + " heights[index] += (target - heights[index]) * clamp01(weight * strength);", + " },", + " changedSamples", + " );", + " },", + "", + " smooth(data, stroke, strength, changedSamples) {", + " const { heights, size } = data;", + " const changes = [];", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index, weight) => {", + " const i = index % size;", + " const j = (index - i) / size;", + " let sum = 0;", + " for (let dj = -1; dj <= 1; dj++) {", + " for (let di = -1; di <= 1; di++) {", + " sum += data.getHeight(i + di, j + dj);", + " }", + " }", + " changes.push(index, heights[index] + (sum / 9 - heights[index]) * clamp01(weight * strength));", + " },", + " changedSamples", + " );", + " // Written after all averages are computed, so that the result", + " // doesn't depend on the order in which samples are visited.", + " for (let index = 0; index < changes.length; index += 2) {", + " heights[changes[index]] = changes[index + 1];", + " }", + " },", + "", + " paint(data, stroke, layer, strength, changedSamples) {", + " const { splat } = data;", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index, weight) => {", + " const amount = clamp01(weight * strength);", + " const offset = index * LAYER_COUNT;", + " for (let otherLayer = 0; otherLayer < LAYER_COUNT; otherLayer++) {", + " const target = otherLayer === layer ? 255 : 0;", + " const value = splat[offset + otherLayer];", + " // Rounded towards the target, so that light strokes still paint.", + " const change = (target - value) * amount;", + " splat[offset + otherLayer] = value + (change > 0 ? Math.ceil(change) : Math.floor(change));", + " }", + " },", + " changedSamples", + " );", + " },", + "", + " /** Remove all the layers, making a hole (`fill` or painting a layer fills it again). */", + " hole(data, stroke, changedSamples) {", + " const { splat } = data;", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index) => {", + " splat.fill(0, index * LAYER_COUNT, (index + 1) * LAYER_COUNT);", + " },", + " changedSamples", + " );", + " },", + "", + " /**", + " * Fill the holes in the radius with the layers of the ground around them:", + " * each pass fills the holes next to the ground with the average of their", + " * neighbors, from the edges of the holes inward.", + " */", + " fill(data, stroke, changedSamples) {", + " const { splat, size, resolution } = data;", + " /** @type {number[]} */", + " let holes = [];", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index) => {", + " if (data.isHole(index)) holes.push(index);", + " },", + " changedSamples", + " );", + " const filledWeights = new Float32Array(holes.length * LAYER_COUNT);", + " while (holes.length > 0) {", + " /** @type {number[]} */", + " const remainingHoles = [];", + " /** @type {number[]} */", + " const filledHoles = [];", + " for (const index of holes) {", + " const i = index % size;", + " const j = (index - i) / size;", + " const weights = filledWeights.subarray(filledHoles.length * LAYER_COUNT, (filledHoles.length + 1) * LAYER_COUNT);", + " weights.fill(0);", + " for (let neighborJ = Math.max(j - 1, 0); neighborJ <= Math.min(j + 1, resolution); neighborJ++) {", + " for (let neighborI = Math.max(i - 1, 0); neighborI <= Math.min(i + 1, resolution); neighborI++) {", + " const neighbor = neighborJ * size + neighborI;", + " if (data.isHole(neighbor)) continue;", + " for (let layer = 0; layer < LAYER_COUNT; layer++) weights[layer] += splat[neighbor * LAYER_COUNT + layer];", + " }", + " }", + " if (weights[0] + weights[1] + weights[2] + weights[3] > 0) filledHoles.push(index);", + " else remainingHoles.push(index);", + " }", + " if (filledHoles.length === 0) {", + " // No ground around (a hole bigger than the brush): the first layer is used.", + " for (const index of remainingHoles) splat[index * LAYER_COUNT] = 255;", + " break;", + " }", + " // Filled after the pass, so that the holes are filled from the edges inward.", + " filledHoles.forEach((index, filledIndex) => {", + " const weights = filledWeights.subarray(filledIndex * LAYER_COUNT, (filledIndex + 1) * LAYER_COUNT);", + " const total = weights[0] + weights[1] + weights[2] + weights[3];", + " for (let layer = 0; layer < LAYER_COUNT; layer++) {", + " splat[index * LAYER_COUNT + layer] = Math.round((weights[layer] / total) * 255);", + " }", + " });", + " holes = remainingHoles;", + " }", + " },", + "", + " /** @param {number[]} color The red, green and blue (0-255) of the tint: white removes it. */", + " tint(data, stroke, color, strength, changedSamples) {", + " brushes.paintColors(data, stroke, 0, color, strength, changedSamples);", + " },", + "", + " /** @param {number} wetness From 0 (dry) to 1. */", + " wet(data, stroke, wetness, strength, changedSamples) {", + " brushes.paintColors(data, stroke, 3, [Math.round(clamp01(wetness) * 255)], strength, changedSamples);", + " },", + "", + " /**", + " * Bring channels of the colors (see `TerrainData.colors`) towards targets.", + " * @param {number} firstChannel", + " * @param {number[]} targets From 0 to 255, for the channels from the first one.", + " */", + " paintColors(data, stroke, firstChannel, targets, strength, changedSamples) {", + " const { colors } = data;", + " brushes.forEachSample(", + " data,", + " stroke,", + " (index, weight) => {", + " const amount = clamp01(weight * strength);", + " for (let channel = 0; channel < targets.length; channel++) {", + " const offset = index * 4 + firstChannel + channel;", + " // Rounded towards the target, so that light strokes still paint.", + " const change = (targets[channel] - colors[offset]) * amount;", + " colors[offset] += change > 0 ? Math.ceil(change) : Math.floor(change);", + " }", + " },", + " changedSamples", + " );", + " },", + "};", + "", + "const relief = (() => {", + " const hash = (x, y, seed) => {", + " let h = Math.imul(x, 374761393) + Math.imul(y, 668265263) + Math.imul(seed, 982451653);", + " h = Math.imul(h ^ (h >>> 13), 1274126177);", + " return ((h ^ (h >>> 16)) >>> 0) / 4294967296;", + " };", + " const valueNoise = (x, y, seed) => {", + " const x0 = Math.floor(x);", + " const y0 = Math.floor(y);", + " const fx = x - x0;", + " const fy = y - y0;", + " const sx = fx * fx * (3 - 2 * fx);", + " const sy = fy * fy * (3 - 2 * fy);", + " const top = hash(x0, y0, seed) + sx * (hash(x0 + 1, y0, seed) - hash(x0, y0, seed));", + " const bottom =", + " hash(x0, y0 + 1, seed) + sx * (hash(x0 + 1, y0 + 1, seed) - hash(x0, y0 + 1, seed));", + " return top + sy * (bottom - top);", + " };", + " /** Fractal noise between 0 and 1. */", + " const fractalNoise = (x, y, seed) => {", + " let value = 0;", + " let amplitude = 0.5;", + " let frequency = 1;", + " for (let octave = 0; octave < 5; octave++) {", + " value += amplitude * valueNoise(x * frequency, y * frequency, seed + octave);", + " amplitude *= 0.5;", + " frequency *= 2;", + " }", + " return value / 0.96875;", + " };", + " const smoothstep = (edge0, edge1, x) => {", + " const t = clamp01((x - edge0) / (edge1 - edge0));", + " return t * t * (3 - 2 * t);", + " };", + " const reliefHeights = {", + " Flat: () => 0,", + " Hills: (u, v, seed) => 0.05 + 0.5 * Math.pow(fractalNoise(u * 3, v * 3, seed), 2),", + " Mountains: (u, v, seed) => {", + " const ridge = 1 - Math.abs(2 * fractalNoise(u * 2.5, v * 2.5, seed) - 1);", + " return 0.9 * Math.pow(ridge, 2.5);", + " },", + " Island: (u, v, seed) => {", + " const distanceToCenter = 2 * Math.hypot(u - 0.5, v - 0.5);", + " const coast = 0.2 * (fractalNoise(u * 4, v * 4, seed + 7) - 0.5);", + " const land = smoothstep(0.95, 0.55, distanceToCenter + coast);", + " return land * (0.12 + 0.6 * Math.pow(fractalNoise(u * 3, v * 3, seed), 1.5));", + " },", + " };", + " const beachLayer = 3;", + "", + " return {", + " /**", + " * @param {TerrainData} data", + " * @param {string} reliefName Flat, Hills, Mountains or Island.", + " * @param {number} seed", + " */", + " generate(data, reliefName, seed) {", + " const getHeight = reliefHeights[reliefName] || reliefHeights.Flat;", + " const integerSeed = Math.floor(seed) | 0;", + " data.fillLayer(0);", + " for (let j = 0; j < data.size; j++) {", + " for (let i = 0; i < data.size; i++) {", + " const index = j * data.size + i;", + " const height = clamp01(", + " getHeight(i / data.resolution, j / data.resolution, integerSeed)", + " );", + " data.heights[index] = height;", + " if (reliefName === 'Island') {", + " const sand = Math.round(255 * smoothstep(0.16, 0.1, height));", + " const offset = index * LAYER_COUNT;", + " data.splat[offset] = 255 - sand;", + " data.splat[offset + beachLayer] = sand;", + " }", + " }", + " }", + " },", + " };", + "})();", + "", + "/**", + " * Edits written in the Edits property: a JSON list of brush strokes, like", + " * `[{\"tool\": \"raise\", \"x\": 0.5, \"y\": 0.5, \"radius\": 0.2, \"height\": 0.3}]`,", + " * applied on the relief or the heightmap. Positions and radius are fractions", + " * of the terrain size, heights and Z positions fractions of its depth: they", + " * are the same for all instances, whatever their size. They are made to be", + " * read and written by people and by AI agents.", + " */", + "const edits = {", + " /**", + " * @param {TerrainData} data", + " * @param {string} text", + " */", + " apply(data, text) {", + " if (!text.trim()) return;", + " let editList;", + " try {", + " editList = JSON.parse(text);", + " } catch (error) {", + " console.warn('The edits of a terrain are not valid JSON: ' + error.message);", + " return;", + " }", + " if (!Array.isArray(editList)) {", + " console.warn('The edits of a terrain must be a JSON list (like [{\"tool\": \"raise\", ...}]).');", + " return;", + " }", + " const numberOr = (value, defaultValue) => (typeof value === 'number' && isFinite(value) ? value : defaultValue);", + " const changedSamples = new SampleRectangle();", + " for (const edit of editList) {", + " if (!edit || typeof edit !== 'object') continue;", + " const x = numberOr(edit.x, 0.5);", + " const y = numberOr(edit.y, 0.5);", + " const stroke = {", + " cellWidth: 1 / data.resolution,", + " cellHeight: 1 / data.resolution,", + " ax: x,", + " ay: y,", + " bx: numberOr(edit.toX, x),", + " by: numberOr(edit.toY, y),", + " radius: numberOr(edit.radius, 0.1),", + " };", + " const strength = clamp01(numberOr(edit.strength, 1));", + " if (edit.tool === 'raise') {", + " brushes.raise(data, stroke, numberOr(edit.height, 0.1), changedSamples);", + " } else if (edit.tool === 'flatten') {", + " const z = numberOr(edit.z, 0);", + " brushes.flatten(data, stroke, z, strength, changedSamples, numberOr(edit.toZ, z));", + " } else if (edit.tool === 'smooth') {", + " // Smoothing the same fraction of the terrain at any resolution.", + " const passes = Math.max(Math.round(((1 + 9 * strength) * data.resolution) / 256), 1);", + " for (let pass = 0; pass < passes; pass++) brushes.smooth(data, stroke, 1, changedSamples);", + " } else if (edit.tool === 'paint') {", + " const layer = clampInteger(numberOr(edit.layer, 1) - 1, 0, LAYER_COUNT - 1);", + " brushes.paint(data, stroke, layer, strength, changedSamples);", + " } else if (edit.tool === 'tint') {", + " const color = gdjs.rgbOrHexToRGBColor(typeof edit.color === 'string' ? edit.color : '255;255;255');", + " brushes.tint(data, stroke, color, strength, changedSamples);", + " } else if (edit.tool === 'wetness') {", + " brushes.wet(data, stroke, numberOr(edit.amount, 1), strength, changedSamples);", + " } else if (edit.tool === 'hole') {", + " brushes.hole(data, stroke, changedSamples);", + " } else if (edit.tool === 'fill') {", + " brushes.fill(data, stroke, changedSamples);", + " } else {", + " console.warn('Unknown tool in the edits of a terrain: \"' + edit.tool + '\" (use raise, flatten, smooth, paint, tint, wetness, hole or fill).');", + " }", + " }", + " },", + "};", + "", + "/**", + " * @param {gdjs.RuntimeGame} game", + " * @param {string} imageResourceName", + " * @returns {CanvasImageSource | null} The image, or null if it's not loaded (yet).", + " */", + "const getLoadedImage = (game, imageResourceName) => {", + " const imageManager = game.getImageManager();", + " if (typeof imageManager.getImageSource === 'function') return imageManager.getImageSource(imageResourceName);", + " // Older GDevelop versions: a 192 x 192 placeholder is given for images that are not loaded.", + " const texture = imageManager.getPIXITexture(imageResourceName);", + " return texture && texture !== imageManager.getInvalidPIXITexture() ? texture.baseTexture.getDrawableSource() : null;", + "};", + "", + "/**", + " * Loads heights from an image: black is the lowest, white the highest.", + " * Heights have 16 bits of precision when the red channel holds the most", + " * significant byte and the green one the least significant (a gray image", + " * works as expected too).", + " * @param {TerrainData} data", + " * @param {gdjs.RuntimeGame} game", + " * @param {string} imageResourceName", + " * @returns {boolean} true if the image could be read.", + " */", + "const loadHeightsFromImage = (data, game, imageResourceName) => {", + " const source = getLoadedImage(game, imageResourceName);", + " if (!source) return false;", + "", + " const canvas = document.createElement('canvas');", + " canvas.width = source.width;", + " canvas.height = source.height;", + " const context = canvas.getContext('2d', { willReadFrequently: true });", + " if (!context) return false;", + " context.drawImage(source, 0, 0);", + " const pixels = context.getImageData(0, 0, source.width, source.height).data;", + " // Pixels are decoded before being interpolated, to keep the 16 bits.", + " const pixelHeight = (x, y) => {", + " const offset = (y * source.width + x) * 4;", + " return (256 * pixels[offset] + pixels[offset + 1]) / 65535;", + " };", + " for (let j = 0; j < data.size; j++) {", + " const y = (j / data.resolution) * (source.height - 1);", + " const y0 = Math.floor(y);", + " const y1 = Math.min(y0 + 1, source.height - 1);", + " for (let i = 0; i < data.size; i++) {", + " const x = (i / data.resolution) * (source.width - 1);", + " const x0 = Math.floor(x);", + " const x1 = Math.min(x0 + 1, source.width - 1);", + " const top = pixelHeight(x0, y0) + (x - x0) * (pixelHeight(x1, y0) - pixelHeight(x0, y0));", + " const bottom = pixelHeight(x0, y1) + (x - x0) * (pixelHeight(x1, y1) - pixelHeight(x0, y1));", + " data.heights[j * data.size + i] = top + (y - y0) * (bottom - top);", + " }", + " }", + " return true;", + "};", + "", + "/**", + " * Stores the data in a string. Only the differences with the base data (from", + " * the relief or the heightmap) are stored: heights on 16 bits, layer weights", + " * on 8 bits, then (since format 2) the tint and wetness on 8 bits. Each value is predicted from its neighbors and the", + " * prediction errors are written as variable length integers, with runs of", + " * zeros collapsed. Untouched or smoothly changed areas take almost no space.", + " */", + "const codec = (() => {", + " const formatVersion = '2';", + " // Format 1 has no tint and wetness.", + " const readableFormatVersions = ['1', '2'];", + "", + " class ByteWriter {", + " constructor() {", + " this.bytes = new Uint8Array(1024);", + " this.length = 0;", + " }", + " writeVarUint(value) {", + " if (this.length + 5 > this.bytes.length) {", + " const bytes = new Uint8Array(this.bytes.length * 2);", + " bytes.set(this.bytes);", + " this.bytes = bytes;", + " }", + " while (value >= 0x80) {", + " this.bytes[this.length++] = (value & 0x7f) | 0x80;", + " value >>>= 7;", + " }", + " this.bytes[this.length++] = value;", + " }", + " }", + "", + " class ByteReader {", + " /** @param {Uint8Array} bytes */", + " constructor(bytes) {", + " this.bytes = bytes;", + " this.position = 0;", + " }", + " readVarUint() {", + " let value = 0;", + " let shift = 0;", + " let byte;", + " do {", + " if (this.position >= this.bytes.length) throw new Error('Truncated terrain data.');", + " byte = this.bytes[this.position++];", + " value += (byte & 0x7f) * Math.pow(2, shift);", + " shift += 7;", + " } while (byte & 0x80);", + " return value;", + " }", + " }", + "", + " /** Predicts a value from the left, top and top-left ones. */", + " const predict = (values, size, index) => {", + " const i = index % size;", + " if (index < size) return i === 0 ? 0 : values[index - 1];", + " if (i === 0) return values[index - size];", + " return values[index - 1] + values[index - size] - values[index - size - 1];", + " };", + "", + " /**", + " * @param {ByteWriter} writer", + " * @param {Int32Array} values A grid of `size * size` values.", + " */", + " const writeGrid = (writer, values, size) => {", + " let zeroRun = 0;", + " for (let index = 0; index < values.length; index++) {", + " const error = values[index] - predict(values, size, index);", + " if (error === 0) {", + " zeroRun++;", + " continue;", + " }", + " if (zeroRun > 0) {", + " writer.writeVarUint(0);", + " writer.writeVarUint(zeroRun);", + " zeroRun = 0;", + " }", + " // Zigzag: small negative and positive numbers get small codes.", + " writer.writeVarUint(error > 0 ? error * 2 : -error * 2 - 1);", + " }", + " if (zeroRun > 0) {", + " writer.writeVarUint(0);", + " writer.writeVarUint(zeroRun);", + " }", + " };", + "", + " /** @param {ByteReader} reader */", + " const readGrid = (reader, size) => {", + " const values = new Int32Array(size * size);", + " let zeroRun = 0;", + " for (let index = 0; index < values.length; index++) {", + " let error = 0;", + " if (zeroRun > 0) {", + " zeroRun--;", + " } else {", + " const code = reader.readVarUint();", + " if (code === 0) {", + " zeroRun = reader.readVarUint() - 1;", + " } else {", + " error = code % 2 === 0 ? code / 2 : -(code + 1) / 2;", + " }", + " }", + " values[index] = predict(values, size, index) + error;", + " }", + " return values;", + " };", + "", + " const toBase64 = (bytes, length) => {", + " let binary = '';", + " for (let offset = 0; offset < length; offset += 0x8000) {", + " binary += String.fromCharCode.apply(null, bytes.subarray(offset, Math.min(offset + 0x8000, length)));", + " }", + " return btoa(binary);", + " };", + "", + " const fromBase64 = (base64) => {", + " const binary = atob(base64);", + " const bytes = new Uint8Array(binary.length);", + " for (let index = 0; index < binary.length; index++) {", + " bytes[index] = binary.charCodeAt(index);", + " }", + " return bytes;", + " };", + "", + " return {", + " /**", + " * @param {TerrainData} data", + " * @param {TerrainData} baseData The data without changes, with the same resolution.", + " */", + " encode(data, baseData) {", + " const writer = new ByteWriter();", + " const values = new Int32Array(data.heights.length);", + " for (let index = 0; index < values.length; index++) {", + " values[index] = Math.round((data.heights[index] - baseData.heights[index]) * 65535);", + " }", + " writeGrid(writer, values, data.size);", + " for (let layer = 0; layer < LAYER_COUNT; layer++) {", + " for (let index = 0; index < values.length; index++) {", + " const offset = index * LAYER_COUNT + layer;", + " values[index] = data.splat[offset] - baseData.splat[offset];", + " }", + " writeGrid(writer, values, data.size);", + " }", + " // Not painted, these grids are zeros: a few bytes each.", + " for (let channel = 0; channel < 4; channel++) {", + " for (let index = 0; index < values.length; index++) {", + " const offset = index * 4 + channel;", + " values[index] = data.colors[offset] - baseData.colors[offset];", + " }", + " writeGrid(writer, values, data.size);", + " }", + " return formatVersion + ';' + data.resolution + ';' + toBase64(writer.bytes, writer.length);", + " },", + "", + " /**", + " * Store grids of integers in a string, like the terrain data. Used by other objects (the scattered models).", + " * @param {Int32Array[]} grids Grids of `size * size` values.", + " * @param {number} size", + " */", + " encodeGrids(grids, size) {", + " const writer = new ByteWriter();", + " for (const grid of grids) writeGrid(writer, grid, size);", + " return toBase64(writer.bytes, writer.length);", + " },", + "", + " /**", + " * @param {string} base64 A string from `encodeGrids`.", + " * @param {number} size", + " * @param {number} count The number of grids.", + " * @returns {Int32Array[]} The grids (an error is thrown if the string is not valid).", + " */", + " decodeGrids(base64, size, count) {", + " const reader = new ByteReader(fromBase64(base64));", + " return Array.from({ length: count }, () => readGrid(reader, size));", + " },", + "", + " /**", + " * @param {string} text", + " * @param {(resolution: number) => TerrainData} createBaseData", + " * @returns {TerrainData | null} null if the text is empty or not valid.", + " */", + " decode(text, createBaseData) {", + " if (!text) return null;", + " const parts = text.split(';');", + " const resolution = parseInt(parts[1], 10);", + " if (readableFormatVersions.indexOf(parts[0]) === -1 || RESOLUTIONS.indexOf(resolution) === -1) {", + " return null;", + " }", + " try {", + " const reader = new ByteReader(fromBase64(parts[2]));", + " const data = createBaseData(resolution);", + " const heightChanges = readGrid(reader, data.size);", + " for (let index = 0; index < heightChanges.length; index++) {", + " data.heights[index] = clamp01(data.heights[index] + heightChanges[index] / 65535);", + " }", + " for (let layer = 0; layer < LAYER_COUNT; layer++) {", + " const weightChanges = readGrid(reader, data.size);", + " for (let index = 0; index < weightChanges.length; index++) {", + " const offset = index * LAYER_COUNT + layer;", + " // The base may have changed since the data was saved.", + " data.splat[offset] = Math.min(Math.max(data.splat[offset] + weightChanges[index], 0), 255);", + " }", + " }", + " if (parts[0] !== '1') {", + " for (let channel = 0; channel < 4; channel++) {", + " const colorChanges = readGrid(reader, data.size);", + " for (let index = 0; index < colorChanges.length; index++) {", + " const offset = index * 4 + channel;", + " data.colors[offset] = Math.min(Math.max(data.colors[offset] + colorChanges[index], 0), 255);", + " }", + " }", + " }", + " return data;", + " } catch (error) {", + " console.error('Terrain data could not be read:', error);", + " return null;", + " }", + " },", + " };", + "})();", + "", + "const lodIndices = new Map();", + "/**", + " * The triangles of a chunk, shared by all chunks of the same size. Chunk", + " * borders have a skirt going down, hiding the gaps between chunks drawn", + " * with different levels of detail.", + " * @param {number} chunkCells", + " * @param {number} step", + " */", + "const getLodIndex = (chunkCells, step) => {", + " const key = chunkCells + ';' + step;", + " const existingIndex = lodIndices.get(key);", + " if (existingIndex) return existingIndex;", + "", + " const sideVertices = chunkCells + 1;", + " const gridVertices = sideVertices * sideVertices;", + " const indices = [];", + " const grid = (i, j) => j * sideVertices + i;", + " for (let j = 0; j < chunkCells; j += step) {", + " for (let i = 0; i < chunkCells; i += step) {", + " const topLeft = grid(i, j);", + " const topRight = grid(i + step, j);", + " const bottomLeft = grid(i, j + step);", + " const bottomRight = grid(i + step, j + step);", + " indices.push(topLeft, topRight, bottomLeft, topRight, bottomRight, bottomLeft);", + " }", + " }", + " const borders = [", + " (k) => grid(k, 0),", + " (k) => grid(k, chunkCells),", + " (k) => grid(0, k),", + " (k) => grid(chunkCells, k),", + " ];", + " borders.forEach((borderVertex, border) => {", + " const skirtVertex = (k) => gridVertices + border * sideVertices + k;", + " for (let k = 0; k < chunkCells; k += step) {", + " const a = borderVertex(k);", + " const b = borderVertex(k + step);", + " const skirtA = skirtVertex(k);", + " const skirtB = skirtVertex(k + step);", + " // Both faces, as a skirt can be seen from either side.", + " indices.push(a, b, skirtB, a, skirtB, skirtA, a, skirtB, b, a, skirtA, skirtB);", + " }", + " });", + " const index = new THREE.BufferAttribute(new Uint16Array(indices), 1);", + " lodIndices.set(key, index);", + " return index;", + "};", + "", + "const vertexShaderDeclarations = `", + "varying vec2 vTerrainLocalXY;", + "varying float vTerrainLocalZ;", + "uniform float terrainWorldScale;", + "varying vec2 vTerrainWorldXY;", + "varying float vTerrainUp;", + "`;", + "const vertexShaderCode = `", + "vTerrainLocalXY = position.xy;", + "vTerrainLocalZ = position.z;", + "vTerrainWorldXY = (modelMatrix * vec4(transformed, 1.0)).xy * terrainWorldScale;", + "vTerrainUp = normalize(transpose(inverse(mat3(modelMatrix))) * objectNormal).z;", + "`;", + "const fragmentShaderDeclarations = `", + "uniform sampler2D terrainSplatMap;", + "uniform vec2 terrainSplatUvScaleAndOffset;", + "uniform vec3 terrainLayerColors[4];", + "uniform sampler2D terrainLayerMap0;", + "uniform sampler2D terrainLayerMap1;", + "uniform sampler2D terrainLayerMap2;", + "uniform sampler2D terrainLayerMap3;", + "uniform sampler2D terrainLayerNormalMap0;", + "uniform sampler2D terrainLayerNormalMap1;", + "uniform sampler2D terrainLayerNormalMap2;", + "uniform sampler2D terrainLayerNormalMap3;", + "uniform float terrainHasNormalMaps;", + "uniform float terrainNormalMapStrength;", + "uniform float terrainTextureSize;", + "uniform float terrainHeightBlending;", + "uniform float terrainTextureVariation;", + "uniform vec4 terrainCliffLayer;", + "uniform vec2 terrainCliffCosines;", + "uniform vec4 terrainBrush;", + "uniform sampler2D terrainColorMap;", + "uniform vec3 terrainWetShore;", + "varying vec2 vTerrainLocalXY;", + "varying float vTerrainLocalZ;", + "varying vec2 vTerrainWorldXY;", + "varying float vTerrainUp;", + "", + "float terrainHash(vec2 p) {", + " return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);", + "}", + "// The rotation of the copies of textures used to make repetition less visible.", + "const mat2 terrainCopyRotation = mat2(0.8, -0.6, 0.6, 0.8);", + "/** A bump of a normal map (from -1 to 1), mixed with its rotated copy in patches. */", + "vec3 terrainSampleNormal(sampler2D normalMap, vec2 uv, vec2 uv2, float patches) {", + " vec3 bump = texture2D(normalMap, uv).xyz * 2.0 - 1.0;", + " if (patches <= 0.0) return bump;", + " vec3 copyBump = texture2D(normalMap, uv2).xyz * 2.0 - 1.0;", + " // The bumps of the copy are rotated back with the inverse rotation.", + " copyBump.xy = transpose(terrainCopyRotation) * copyBump.xy;", + " return mix(bump, copyBump, patches);", + "}", + "float terrainNoise(vec2 p) {", + " vec2 i = floor(p);", + " vec2 f = fract(p);", + " vec2 u = f * f * (3.0 - 2.0 * f);", + " return mix(", + " mix(terrainHash(i), terrainHash(i + vec2(1.0, 0.0)), u.x),", + " mix(terrainHash(i + vec2(0.0, 1.0)), terrainHash(i + vec2(1.0, 1.0)), u.x),", + " u.y", + " );", + "}", + "`;", + "// Holes are samples without any painted layer: the interpolated sum of the", + "// weights gives smooth edges. Only compiled for terrains having holes, as", + "// discard makes drawing slower on some GPUs.", + "const holeShaderCode = `", + "#ifdef TERRAIN_HOLES", + "if (dot(texture2D(terrainSplatMap, vTerrainLocalXY * terrainSplatUvScaleAndOffset.x + terrainSplatUvScaleAndOffset.y), vec4(1.0)) < 0.5) discard;", + "#endif", + "`;", + "// For the shadows: the depth of holes is not drawn.", + "const holeDepthShaderDeclarations = `", + "uniform sampler2D terrainSplatMap;", + "uniform vec2 terrainSplatUvScaleAndOffset;", + "varying vec2 vTerrainLocalXY;", + "`;", + "", + "const fragmentShaderCode = holeShaderCode + `", + "// Borders between layers are made irregular and sharper, to look painted.", + "vec2 terrainNoisePosition = vTerrainWorldXY / 24.0;", + "vec4 terrainJitter = vec4(", + " terrainNoise(terrainNoisePosition),", + " terrainNoise(terrainNoisePosition + 19.1),", + " terrainNoise(terrainNoisePosition + 37.7),", + " terrainNoise(terrainNoisePosition + 53.3)", + ");", + "// Layer weights are in a texture, so that painted details stay the same", + "// whatever the level of detail of the ground.", + "vec2 terrainSplatUv = vTerrainLocalXY * terrainSplatUvScaleAndOffset.x + terrainSplatUvScaleAndOffset.y;", + "vec4 terrainWeights = texture2D(terrainSplatMap, terrainSplatUv) * (0.7 + 0.6 * terrainJitter);", + "terrainWeights = terrainWeights * terrainWeights;", + "terrainWeights /= max(dot(terrainWeights, vec4(1.0)), 0.00001);", + "float terrainCliff = 1.0 - smoothstep(terrainCliffCosines.x, terrainCliffCosines.y, vTerrainUp);", + "terrainWeights = mix(terrainWeights, terrainCliffLayer, terrainCliff * dot(terrainCliffLayer, vec4(1.0)));", + "", + "vec2 terrainUv = vTerrainWorldXY / terrainTextureSize;", + "vec3 terrainTexture0 = texture2D(terrainLayerMap0, terrainUv).rgb;", + "vec3 terrainTexture1 = texture2D(terrainLayerMap1, terrainUv).rgb;", + "vec3 terrainTexture2 = texture2D(terrainLayerMap2, terrainUv).rgb;", + "vec3 terrainTexture3 = texture2D(terrainLayerMap3, terrainUv).rgb;", + "", + "// Less visible repetition: in irregular patches, textures are replaced by a", + "// rotated and shifted copy of themselves, and colors slowly vary.", + "float terrainPatches = 0.0;", + "vec2 terrainUv2 = terrainUv;", + "float terrainColorVariation = 1.0;", + "if (terrainTextureVariation > 0.0) {", + " // Up to half of the ground is covered by the copies, each patch showing", + " // only one of them (mixing them would make them blurry).", + " float terrainPatchThreshold = 1.0 - 0.5 * terrainTextureVariation;", + " terrainPatches = smoothstep(terrainPatchThreshold - 0.05, terrainPatchThreshold + 0.05, terrainNoise(terrainUv * 0.23 + 7.3));", + " terrainUv2 = terrainCopyRotation * terrainUv + vec2(0.31, 0.73);", + " terrainTexture0 = mix(terrainTexture0, texture2D(terrainLayerMap0, terrainUv2).rgb, terrainPatches);", + " terrainTexture1 = mix(terrainTexture1, texture2D(terrainLayerMap1, terrainUv2).rgb, terrainPatches);", + " terrainTexture2 = mix(terrainTexture2, texture2D(terrainLayerMap2, terrainUv2).rgb, terrainPatches);", + " terrainTexture3 = mix(terrainTexture3, texture2D(terrainLayerMap3, terrainUv2).rgb, terrainPatches);", + " terrainColorVariation += terrainTextureVariation * 0.3 *", + " (terrainNoise(terrainUv * 0.11) + 0.5 * terrainNoise(terrainUv * 0.37 + 3.1) - 0.75);", + "}", + "", + "// Height-based blending: the brightness of a texture, compared to its", + "// average, is used as its height. Where layers meet, the high parts (stones,", + "// tufts) cover the low parts (sand, gaps) instead of fading into each other.", + "if (terrainHeightBlending > 0.0) {", + " const vec3 terrainLuminance = vec3(0.299, 0.587, 0.114);", + " vec4 terrainHeights = vec4(", + " dot(terrainTexture0 - textureLod(terrainLayerMap0, vec2(0.5), 16.0).rgb, terrainLuminance),", + " dot(terrainTexture1 - textureLod(terrainLayerMap1, vec2(0.5), 16.0).rgb, terrainLuminance),", + " dot(terrainTexture2 - textureLod(terrainLayerMap2, vec2(0.5), 16.0).rgb, terrainLuminance),", + " dot(terrainTexture3 - textureLod(terrainLayerMap3, vec2(0.5), 16.0).rgb, terrainLuminance)", + " );", + " vec4 terrainHeightWeights = terrainWeights + terrainHeights;", + " float terrainTop = max(max(terrainHeightWeights.x, terrainHeightWeights.y), max(terrainHeightWeights.z, terrainHeightWeights.w));", + " // Only painted layers (and the cliff layer) can show.", + " vec4 terrainHeightBlend = max(terrainHeightWeights - terrainTop + 0.1, 0.0) * step(0.004, terrainWeights);", + " terrainHeightBlend /= max(dot(terrainHeightBlend, vec4(1.0)), 0.00001);", + " terrainWeights = mix(terrainWeights, terrainHeightBlend, terrainHeightBlending);", + "}", + "", + "vec3 terrainColor = terrainColorVariation * (", + " terrainWeights.x * terrainLayerColors[0] * terrainTexture0 +", + " terrainWeights.y * terrainLayerColors[1] * terrainTexture1 +", + " terrainWeights.z * terrainLayerColors[2] * terrainTexture2 +", + " terrainWeights.w * terrainLayerColors[3] * terrainTexture3);", + "", + "// The painted tint multiplies all layers. Wet ground is darker (and", + "// shinier, see the roughness).", + "vec4 terrainTintAndWetness = texture2D(terrainColorMap, terrainSplatUv);", + "float terrainWetness = max(", + " terrainTintAndWetness.a,", + " terrainWetShore.z * (1.0 - smoothstep(terrainWetShore.x, terrainWetShore.x + terrainWetShore.y, vTerrainLocalZ))", + ");", + "terrainColor *= terrainTintAndWetness.rgb * (1.0 - 0.4 * terrainWetness);", + "diffuseColor.rgb *= terrainColor;", + "", + "if (terrainBrush.w > 0.5) {", + " float brushDistance = distance(vTerrainWorldXY, terrainBrush.xy);", + " float ringWidth = max(terrainBrush.z * 0.04, 1.5);", + " float ring = 1.0 - smoothstep(0.0, ringWidth, abs(brushDistance - terrainBrush.z));", + " float disc = brushDistance < terrainBrush.z ? 0.12 : 0.0;", + " diffuseColor.rgb = mix(diffuseColor.rgb, vec3(1.0), max(ring * 0.85, disc));", + "}", + "`;", + "", + "const fragmentShaderRoughnessCode = `", + "roughnessFactor = mix(roughnessFactor, 0.3, terrainWetness);", + "`;", + "", + "const fragmentShaderNormalCode = `", + "if (terrainHasNormalMaps > 0.5) {", + " vec3 terrainMapNormal =", + " terrainWeights.x * terrainSampleNormal(terrainLayerNormalMap0, terrainUv, terrainUv2, terrainPatches) +", + " terrainWeights.y * terrainSampleNormal(terrainLayerNormalMap1, terrainUv, terrainUv2, terrainPatches) +", + " terrainWeights.z * terrainSampleNormal(terrainLayerNormalMap2, terrainUv, terrainUv2, terrainPatches) +", + " terrainWeights.w * terrainSampleNormal(terrainLayerNormalMap3, terrainUv, terrainUv2, terrainPatches);", + " terrainMapNormal.xy *= terrainNormalMapStrength;", + " // The X and Y of textures follow the X and Y of the 3D world, projected on", + " // the ground (the camera space of the normal is used).", + " vec3 terrainTangent = (viewMatrix * vec4(1.0, 0.0, 0.0, 0.0)).xyz;", + " terrainTangent -= normal * dot(normal, terrainTangent);", + " // A ground facing the X axis (a rotated terrain) uses the Y axis instead.", + " if (dot(terrainTangent, terrainTangent) < 0.000001) {", + " terrainTangent = (viewMatrix * vec4(0.0, 1.0, 0.0, 0.0)).xyz;", + " terrainTangent -= normal * dot(normal, terrainTangent);", + " }", + " terrainTangent = normalize(terrainTangent);", + " vec3 terrainBitangent = cross(normal, terrainTangent);", + " normal = normalize(mat3(terrainTangent, terrainBitangent, normal) * terrainMapNormal);", + "}", + "`;", + "", + "let whiteTexture = null;", + "const getWhiteTexture = () => {", + " if (!whiteTexture) {", + " whiteTexture = new THREE.DataTexture(new Uint8Array([255, 255, 255, 255]), 1, 1);", + " whiteTexture.needsUpdate = true;", + " }", + " return whiteTexture;", + "};", + "", + "let flatNormalTexture = null;", + "const getFlatNormalTexture = () => {", + " if (!flatNormalTexture) {", + " flatNormalTexture = new THREE.DataTexture(new Uint8Array([128, 128, 255, 255]), 1, 1);", + " flatNormalTexture.needsUpdate = true;", + " }", + " return flatNormalTexture;", + "};", + "", + "const layerTextures = new WeakMap();", + "const layerNormalMaps = new WeakMap();", + "/**", + " * Textures of the image manager have no mipmaps: they would shimmer when", + " * repeated in the distance.", + " * @param {gdjs.RuntimeGame} game", + " * @param {string} resourceName", + " * @param {boolean} isNormalMap Normal maps are directions, not colors.", + " */", + "const getLayerTexture = (game, resourceName, isNormalMap) => {", + " if (!resourceName) return isNormalMap ? getFlatNormalTexture() : getWhiteTexture();", + " const imageTexture = game.getImageManager().getThreeTexture(resourceName);", + " const textures = isNormalMap ? layerNormalMaps : layerTextures;", + " let texture = textures.get(imageTexture);", + " if (!texture) {", + " texture = imageTexture.clone();", + " if (isNormalMap) texture.colorSpace = THREE.NoColorSpace;", + " texture.wrapS = THREE.RepeatWrapping;", + " texture.wrapT = THREE.RepeatWrapping;", + " texture.minFilter = THREE.LinearMipmapLinearFilter;", + " texture.generateMipmaps = true;", + " texture.anisotropy = 4;", + " texture.needsUpdate = true;", + " textures.set(imageTexture, texture);", + " }", + " return texture;", + "};", + "", + "/**", + " * A texture of 4 bytes per sample of a grid of `size * size` samples (layer", + " * weights, tint and wetness...), interpolated between samples.", + " * @param {Uint8Array} bytes", + " * @param {number} size", + " */", + "const createGridTexture = (bytes, size) => {", + " const texture = new THREE.DataTexture(bytes, size, size);", + " texture.magFilter = THREE.LinearFilter;", + " texture.minFilter = THREE.LinearFilter;", + " texture.userData.isUploaded = false;", + " texture.onUpdate = () => {", + " texture.userData.isUploaded = true;", + " };", + " return texture;", + "};", + "", + "/**", + " * Send the rows of the changed samples to the GPU, or the whole texture", + " * before its first upload (and in older GDevelop versions).", + " * @param {THREE.DataTexture} texture", + " * @param {SampleRectangle} samples", + " */", + "const uploadGridTextureRows = (texture, samples) => {", + " if (texture.userData.isUploaded && typeof texture.addUpdateRange === 'function') {", + " const size = texture.image.width;", + " for (let j = samples.minJ; j <= samples.maxJ; j++) {", + " texture.addUpdateRange((j * size + samples.minI) * 4, (samples.maxI - samples.minI + 1) * 4);", + " }", + " }", + " texture.needsUpdate = true;", + "};", + "", + "class TerrainRenderer {", + " /** @param {Terrain} terrain */", + " constructor(terrain) {", + " this.terrain = terrain;", + " this.group = new THREE.Group();", + " terrain.object.get3DRendererObject().add(this.group);", + " /** @type {Array<{mesh: THREE.Mesh, geometry: THREE.BufferGeometry, firstI: number, firstJ: number, center: THREE.Vector3, lod: number}>} */", + " this.chunks = [];", + " this.chunksPerSide = 0;", + " this.chunkCells = 0;", + " this.chunkSideVertices = 0;", + " this.chunkGridVertices = 0;", + " this.chunkVertices = 0;", + " /** @type {THREE.DataTexture | null} */", + " this.splatTexture = null;", + " /** @type {THREE.DataTexture | null} */", + " this.colorTexture = null;", + " this.uniforms = {", + " terrainSplatMap: { value: getWhiteTexture() },", + " terrainSplatUvScaleAndOffset: { value: new THREE.Vector2() },", + " terrainLayerColors: { value: [new THREE.Color(), new THREE.Color(), new THREE.Color(), new THREE.Color()] },", + " terrainLayerMap0: { value: getWhiteTexture() },", + " terrainLayerMap1: { value: getWhiteTexture() },", + " terrainLayerMap2: { value: getWhiteTexture() },", + " terrainLayerMap3: { value: getWhiteTexture() },", + " terrainLayerNormalMap0: { value: getFlatNormalTexture() },", + " terrainLayerNormalMap1: { value: getFlatNormalTexture() },", + " terrainLayerNormalMap2: { value: getFlatNormalTexture() },", + " terrainLayerNormalMap3: { value: getFlatNormalTexture() },", + " terrainHasNormalMaps: { value: 0 },", + " terrainNormalMapStrength: { value: 1 },", + " terrainTextureSize: { value: 256 },", + " terrainHeightBlending: { value: 0 },", + " terrainTextureVariation: { value: 0 },", + " terrainCliffLayer: { value: new THREE.Vector4() },", + " terrainCliffCosines: { value: new THREE.Vector2() },", + " terrainBrush: { value: new THREE.Vector4() },", + " terrainColorMap: { value: getWhiteTexture() },", + " // Z (in the terrain group) under which the ground is wet, fade height, and 1 if it's used.", + " terrainWetShore: { value: new THREE.Vector3() },", + " // Scene units by 3D world unit.", + " terrainWorldScale: { value: 1 },", + " };", + " this.material = new THREE.MeshStandardMaterial({ roughness: 1, metalness: 0 });", + " this.material.onBeforeCompile = (shader) => {", + " Object.assign(shader.uniforms, this.uniforms);", + " shader.vertexShader = patchShaderCode(shader.vertexShader, [", + " ['#include ', '#include \\n' + vertexShaderDeclarations],", + " ['#include ', '#include \\n' + vertexShaderCode],", + " ], 'Terrain3D');", + " shader.fragmentShader = patchShaderCode(shader.fragmentShader, [", + " ['#include ', '#include \\n' + fragmentShaderDeclarations],", + " ['#include ', fragmentShaderCode],", + " ['#include ', '#include \\n' + fragmentShaderRoughnessCode],", + " ['#include ', '#include \\n' + fragmentShaderNormalCode],", + " ], 'Terrain3D');", + " };", + " this.material.customProgramCacheKey = () => (this.hasHoles ? 'Terrain3DWithHoles' : 'Terrain3D');", + " this.hasHoles = false;", + " // Without them, holes would cast shadows.", + " this.depthMaterial = this._createHoleDepthMaterial(new THREE.MeshDepthMaterial());", + " this.distanceMaterial = this._createHoleDepthMaterial(new THREE.MeshDistanceMaterial());", + " this._cameraPosition = new THREE.Vector3();", + " this._chunkPosition = new THREE.Vector3();", + " this._scale = new THREE.Vector3();", + " }", + "", + " /**", + " * @param {THREE.MeshDepthMaterial | THREE.MeshDistanceMaterial} material", + " * @returns The material, drawing nothing in holes.", + " */", + " _createHoleDepthMaterial(material) {", + " material.defines = { TERRAIN_HOLES: '' };", + " material.onBeforeCompile = (shader) => {", + " Object.assign(shader.uniforms, this.uniforms);", + " shader.vertexShader = patchShaderCode(shader.vertexShader, [", + " ['#include ', '#include \\nvarying vec2 vTerrainLocalXY;'],", + " ['#include ', '#include \\nvTerrainLocalXY = position.xy;'],", + " ], 'Terrain3D shadows');", + " shader.fragmentShader = patchShaderCode(shader.fragmentShader, [", + " ['#include ', '#include \\n' + holeDepthShaderDeclarations],", + " ['#include ', holeShaderCode + '#include '],", + " ], 'Terrain3D shadows');", + " };", + " material.customProgramCacheKey = () => 'Terrain3DHoles';", + " return material;", + " }", + "", + " /**", + " * Use the shaders drawing holes, or the faster ones without them.", + " * @param {boolean} hasHoles", + " */", + " setHasHoles(hasHoles) {", + " if (hasHoles === this.hasHoles) return;", + " this.hasHoles = hasHoles;", + " // The material has its own defines (STANDARD).", + " if (hasHoles) this.material.defines.TERRAIN_HOLES = '';", + " else delete this.material.defines.TERRAIN_HOLES;", + " this.material.needsUpdate = true;", + " for (const chunk of this.chunks) this._setChunkShadowMaterials(chunk.mesh);", + " }", + "", + " /** @param {THREE.Mesh} mesh */", + " _setChunkShadowMaterials(mesh) {", + " mesh.customDepthMaterial = this.hasHoles ? this.depthMaterial : undefined;", + " mesh.customDistanceMaterial = this.hasHoles ? this.distanceMaterial : undefined;", + " }", + "", + " /** Recreate all the chunks, for example after the resolution changed. */", + " rebuild() {", + " this.disposeChunks();", + " const { data } = this.terrain;", + " this.chunkCells = getChunkCells(data.resolution);", + " this.chunkSideVertices = this.chunkCells + 1;", + " this.chunkGridVertices = this.chunkSideVertices * this.chunkSideVertices;", + " this.chunkVertices = this.chunkGridVertices + 4 * this.chunkSideVertices;", + " this.chunksPerSide = data.resolution / this.chunkCells;", + " const cellSize = AREA_SIZE / data.resolution;", + " // Sample centers are at the texel centers.", + " this.splatTexture = createGridTexture(data.splat, data.size);", + " this.uniforms.terrainSplatMap.value = this.splatTexture;", + " this.colorTexture = createGridTexture(data.colors, data.size);", + " // Tints are colors chosen like the layer colors.", + " this.colorTexture.colorSpace = THREE.SRGBColorSpace;", + " this.uniforms.terrainColorMap.value = this.colorTexture;", + " this.uniforms.terrainSplatUvScaleAndOffset.value.set(1 / (cellSize * data.size), 0.5 / data.size);", + " for (let chunkJ = 0; chunkJ < this.chunksPerSide; chunkJ++) {", + " for (let chunkI = 0; chunkI < this.chunksPerSide; chunkI++) {", + " const firstI = chunkI * this.chunkCells;", + " const firstJ = chunkJ * this.chunkCells;", + " const positions = new Float32Array(this.chunkVertices * 3);", + " for (let j = 0; j < this.chunkSideVertices; j++) {", + " for (let i = 0; i < this.chunkSideVertices; i++) {", + " const vertex = j * this.chunkSideVertices + i;", + " positions[vertex * 3] = (firstI + i) * cellSize;", + " positions[vertex * 3 + 1] = (firstJ + j) * cellSize;", + " }", + " }", + " // Skirt vertices are below border vertices.", + " this._forEachSkirtVertex((skirtVertex, borderVertex) => {", + " positions[skirtVertex * 3] = positions[borderVertex * 3];", + " positions[skirtVertex * 3 + 1] = positions[borderVertex * 3 + 1];", + " });", + " const geometry = new THREE.BufferGeometry();", + " geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));", + " geometry.setAttribute('normal', new THREE.BufferAttribute(new Float32Array(this.chunkVertices * 3), 3));", + " geometry.setIndex(getLodIndex(this.chunkCells, LOD_STEPS[0]));", + " geometry.boundingBox = new THREE.Box3();", + " geometry.boundingSphere = new THREE.Sphere();", + " const mesh = new THREE.Mesh(geometry, this.material);", + " mesh.matrixAutoUpdate = false;", + " // Rays hit the ground at its full resolution, whatever the level of", + " // detail drawn, and go through holes.", + " const chunkCells = this.chunkCells;", + " const terrainData = () => this.terrain.data;", + " const localPoint = new THREE.Vector3();", + " mesh.raycast = function (raycaster, intersects) {", + " const drawnIndex = geometry.index;", + " const firstIntersection = intersects.length;", + " geometry.setIndex(getLodIndex(chunkCells, LOD_STEPS[0]));", + " THREE.Mesh.prototype.raycast.call(this, raycaster, intersects);", + " geometry.setIndex(drawnIndex);", + " const data = terrainData();", + " const cellSize = AREA_SIZE / data.resolution;", + " for (let index = intersects.length - 1; index >= firstIntersection; index--) {", + " this.worldToLocal(localPoint.copy(intersects[index].point));", + " if (data.sampleSolidity(localPoint.x / cellSize, localPoint.y / cellSize) < 0.5) {", + " intersects.splice(index, 1);", + " }", + " }", + " };", + " this._setChunkShadowMaterials(mesh);", + " this.group.add(mesh);", + " this.chunks.push({", + " mesh,", + " geometry,", + " firstI,", + " firstJ,", + " center: new THREE.Vector3(", + " (firstI + this.chunkCells / 2) * cellSize,", + " (firstJ + this.chunkCells / 2) * cellSize,", + " 0", + " ),", + " lod: 0,", + " });", + " }", + " }", + " this.updateSamples(new SampleRectangle().set(0, 0, data.resolution, data.resolution), true);", + " this.updateMaterial();", + " }", + "", + " _forEachSkirtVertex(callback) {", + " for (let k = 0; k < this.chunkSideVertices; k++) {", + " callback(this.chunkGridVertices + k, k);", + " callback(this.chunkGridVertices + this.chunkSideVertices + k, this.chunkCells * this.chunkSideVertices + k);", + " callback(this.chunkGridVertices + 2 * this.chunkSideVertices + k, k * this.chunkSideVertices);", + " callback(", + " this.chunkGridVertices + 3 * this.chunkSideVertices + k,", + " k * this.chunkSideVertices + this.chunkCells", + " );", + " }", + " }", + "", + " /**", + " * Skirts go down only to the lowest point a coarser neighbor chunk can", + " * draw along the border, so that they can't be seen through holes.", + " * @param {Float32Array} positions", + " */", + " _updateSkirtHeights(positions) {", + " const coarsestStep = LOD_STEPS[LOD_STEPS.length - 1];", + " const borderVertices = [", + " (k) => k,", + " (k) => this.chunkCells * this.chunkSideVertices + k,", + " (k) => k * this.chunkSideVertices,", + " (k) => k * this.chunkSideVertices + this.chunkCells,", + " ];", + " borderVertices.forEach((borderVertex, border) => {", + " for (let k = 0; k < this.chunkSideVertices; k++) {", + " let minZ = Infinity;", + " const from = Math.max(k - coarsestStep, 0);", + " const to = Math.min(k + coarsestStep, this.chunkCells);", + " for (let other = from; other <= to; other++) {", + " minZ = Math.min(minZ, positions[borderVertex(other) * 3 + 2]);", + " }", + " const skirtVertex = this.chunkGridVertices + border * this.chunkSideVertices + k;", + " positions[skirtVertex * 3 + 2] = minZ;", + " }", + " });", + " }", + "", + " /**", + " * Update the vertices of the chunks sharing the given samples.", + " * @param {SampleRectangle} samples", + " * @param {boolean} hasPaintChanged", + " */", + " updateSamples(samples, hasPaintChanged) {", + " if (samples.isEmpty()) return;", + " const { data } = this.terrain;", + " const cellSize = AREA_SIZE / data.resolution;", + " const slopeFactor = AREA_DEPTH / cellSize;", + " // Normals of samples next to changed ones change too.", + " const minI = Math.max(samples.minI - 1, 0);", + " const minJ = Math.max(samples.minJ - 1, 0);", + " const maxI = Math.min(samples.maxI + 1, data.resolution);", + " const maxJ = Math.min(samples.maxJ + 1, data.resolution);", + " const firstChunkI = Math.max(Math.ceil(minI / this.chunkCells) - 1, 0);", + " const firstChunkJ = Math.max(Math.ceil(minJ / this.chunkCells) - 1, 0);", + " const lastChunkI = Math.min(Math.floor(maxI / this.chunkCells), this.chunksPerSide - 1);", + " const lastChunkJ = Math.min(Math.floor(maxJ / this.chunkCells), this.chunksPerSide - 1);", + "", + " for (let chunkJ = firstChunkJ; chunkJ <= lastChunkJ; chunkJ++) {", + " for (let chunkI = firstChunkI; chunkI <= lastChunkI; chunkI++) {", + " const chunk = this.chunks[chunkJ * this.chunksPerSide + chunkI];", + " const positions = chunk.geometry.attributes.position.array;", + " const normals = chunk.geometry.attributes.normal.array;", + " const fromI = Math.max(minI - chunk.firstI, 0);", + " const toI = Math.min(maxI - chunk.firstI, this.chunkCells);", + " const fromJ = Math.max(minJ - chunk.firstJ, 0);", + " const toJ = Math.min(maxJ - chunk.firstJ, this.chunkCells);", + " for (let j = fromJ; j <= toJ; j++) {", + " const gridJ = chunk.firstJ + j;", + " for (let i = fromI; i <= toI; i++) {", + " const gridI = chunk.firstI + i;", + " const vertex = j * this.chunkSideVertices + i;", + " const sample = gridJ * data.size + gridI;", + " positions[vertex * 3 + 2] = data.heights[sample] * AREA_DEPTH;", + "", + " const leftI = Math.max(gridI - 1, 0);", + " const rightI = Math.min(gridI + 1, data.resolution);", + " const upJ = Math.max(gridJ - 1, 0);", + " const downJ = Math.min(gridJ + 1, data.resolution);", + " const nx =", + " ((data.getHeight(leftI, gridJ) - data.getHeight(rightI, gridJ)) * slopeFactor) /", + " (rightI - leftI);", + " const ny =", + " ((data.getHeight(gridI, upJ) - data.getHeight(gridI, downJ)) * slopeFactor) /", + " (downJ - upJ);", + " const inverseLength = 1 / Math.sqrt(nx * nx + ny * ny + 1);", + " normals[vertex * 3] = nx * inverseLength;", + " normals[vertex * 3 + 1] = ny * inverseLength;", + " normals[vertex * 3 + 2] = inverseLength;", + " }", + " }", + " this._forEachSkirtVertex((skirtVertex, borderVertex) => {", + " for (let axis = 0; axis < 3; axis++) {", + " normals[skirtVertex * 3 + axis] = normals[borderVertex * 3 + axis];", + " }", + " });", + " this._updateSkirtHeights(positions);", + " let maxZ = 0;", + " for (let vertex = 0; vertex < this.chunkGridVertices; vertex++) {", + " maxZ = Math.max(maxZ, positions[vertex * 3 + 2]);", + " }", + " const chunkSize = this.chunkCells * cellSize;", + " chunk.geometry.boundingBox.min.set(chunk.firstI * cellSize, chunk.firstJ * cellSize, 0);", + " chunk.geometry.boundingBox.max.set(", + " chunk.firstI * cellSize + chunkSize,", + " chunk.firstJ * cellSize + chunkSize,", + " maxZ", + " );", + " chunk.geometry.boundingBox.getBoundingSphere(chunk.geometry.boundingSphere);", + " chunk.center.z = maxZ / 2;", + " chunk.geometry.attributes.position.needsUpdate = true;", + " chunk.geometry.attributes.normal.needsUpdate = true;", + " }", + " }", + " if (hasPaintChanged) {", + " // The data may have been replaced by one with the same resolution.", + " this.splatTexture.image.data = data.splat;", + " uploadGridTextureRows(this.splatTexture, samples);", + " this.colorTexture.image.data = data.colors;", + " uploadGridTextureRows(this.colorTexture, samples);", + " }", + " }", + "", + " updateMaterial() {", + " const { object } = this.terrain;", + " const game = object.getRuntimeScene().getGame();", + " const colors = [object._getLayer1Color(), object._getLayer2Color(), object._getLayer3Color(), object._getLayer4Color()];", + " const textures = [", + " object._getLayer1Texture(),", + " object._getLayer2Texture(),", + " object._getLayer3Texture(),", + " object._getLayer4Texture(),", + " ];", + " const normalMaps = [", + " object._getLayer1NormalMap(),", + " object._getLayer2NormalMap(),", + " object._getLayer3NormalMap(),", + " object._getLayer4NormalMap(),", + " ];", + " for (let layer = 0; layer < LAYER_COUNT; layer++) {", + " this.uniforms.terrainLayerColors.value[layer].set(gdjs.rgbOrHexStringToNumber(colors[layer]));", + " this.uniforms['terrainLayerMap' + layer].value = getLayerTexture(game, textures[layer], false);", + " this.uniforms['terrainLayerNormalMap' + layer].value = getLayerTexture(game, normalMaps[layer], true);", + " }", + " this.uniforms.terrainHasNormalMaps.value = normalMaps.some((normalMap) => !!normalMap) ? 1 : 0;", + " this.uniforms.terrainNormalMapStrength.value = Math.max(object._getNormalMapStrength(), 0);", + " this.uniforms.terrainTextureSize.value = Math.max(object._getTextureSize(), 1);", + " this.uniforms.terrainHeightBlending.value = clamp01(object._getHeightBlending());", + " this.uniforms.terrainTextureVariation.value = clamp01(object._getTextureVariation());", + "", + " const cliffLayer = parseInt(object._getCliffLayer(), 10);", + " const cliffAngle = gdjs.toRad(object._getCliffAngle());", + " const cliffBlendAngle = gdjs.toRad(4);", + " this.uniforms.terrainCliffLayer.value.set(", + " cliffLayer === 1 ? 1 : 0,", + " cliffLayer === 2 ? 1 : 0,", + " cliffLayer === 3 ? 1 : 0,", + " cliffLayer === 4 ? 1 : 0", + " );", + " this.uniforms.terrainCliffCosines.value.set(", + " Math.cos(cliffAngle + cliffBlendAngle),", + " Math.cos(cliffAngle - cliffBlendAngle)", + " );", + "", + " const castShadow = object._getCastShadow();", + " const receiveShadow = object._getReceiveShadow();", + " for (const chunk of this.chunks) {", + " chunk.mesh.castShadow = castShadow;", + " chunk.mesh.receiveShadow = receiveShadow;", + " }", + " }", + "", + " /**", + " * Show the brush circle of the editor.", + " * @param {THREE.Vector3 | null} localPosition", + " * @param {number} radius In scene units.", + " */", + " setBrush(localPosition, radius) {", + " const brush = this.uniforms.terrainBrush.value;", + " if (!localPosition) {", + " brush.w = 0;", + " return;", + " }", + " this.group.updateWorldMatrix(true, false);", + " const worldPosition = this._chunkPosition.copy(localPosition).applyMatrix4(this.group.matrixWorld);", + " const worldScale = this.uniforms.terrainWorldScale.value;", + " brush.set(worldPosition.x * worldScale, worldPosition.y * worldScale, radius, 1);", + " }", + "", + " /** @param {THREE.Camera | null} camera */", + " updateLevelsOfDetail(camera) {", + " if (!camera || !this.group.visible) return;", + " const matrixWorld = this.group.matrixWorld;", + " this._cameraPosition.setFromMatrixPosition(camera.matrixWorld);", + " this._scale.setFromMatrixScale(matrixWorld);", + " const cellSize = AREA_SIZE / this.terrain.data.resolution;", + " const chunkWorldSize = this.chunkCells * cellSize * Math.max(this._scale.x, this._scale.y);", + " for (const chunk of this.chunks) {", + " const distance =", + " this._chunkPosition.copy(chunk.center).applyMatrix4(matrixWorld).distanceTo(this._cameraPosition) /", + " chunkWorldSize;", + " let lod = LOD_DISTANCES.length - 1;", + " while (lod > 0 && distance < LOD_DISTANCES[lod]) lod--;", + " if (lod !== chunk.lod) {", + " chunk.lod = lod;", + " chunk.geometry.setIndex(getLodIndex(this.chunkCells, LOD_STEPS[lod]));", + " }", + " }", + " }", + "", + " disposeChunks() {", + " for (const chunk of this.chunks) {", + " chunk.mesh.removeFromParent();", + " // The index is shared by all chunks: it must stay on the GPU.", + " chunk.geometry.setIndex(null);", + " chunk.geometry.dispose();", + " }", + " this.chunks.length = 0;", + " if (this.splatTexture) this.splatTexture.dispose();", + " this.splatTexture = null;", + " if (this.colorTexture) this.colorTexture.dispose();", + " this.colorTexture = null;", + " }", + "", + " dispose() {", + " this.disposeChunks();", + " this.material.dispose();", + " this.depthMaterial.dispose();", + " this.distanceMaterial.dispose();", + " this.group.removeFromParent();", + " }", + "}", + "", + "/**", + " * Makes a Physics3D behavior use a height field shape following the terrain.", + " * Changes of heights are applied to the shape without recreating it.", + " * @implements {gdjs.Physics3DRuntimeBehavior.BodyUpdater}", + " */", + "class TerrainBodyUpdater {", + " /**", + " * @param {Terrain} terrain", + " * @param {gdjs.Physics3DRuntimeBehavior} physics", + " */", + " constructor(terrain, physics) {", + " this.terrain = terrain;", + " this.physics = physics;", + " this.defaultBodyUpdater = physics.bodyUpdater;", + " this.changedSamples = new SampleRectangle();", + " /** @type {Jolt.HeightFieldShape | null} */", + " this.heightField = null;", + " this.heightScale = 1;", + " this.heightsBufferPointer = 0;", + " this.heightsBufferLength = 0;", + " this.bodyX = 0;", + " this.bodyY = 0;", + " this.bodyZ = 0;", + " this.bodyHeight = 0;", + " physics.bodyType = 'Static';", + " physics.bodyUpdater = this;", + " }", + "", + " createShape() {", + " const { data, object } = this.terrain;", + " const worldInvScale = this.physics._sharedData.worldInvScale;", + " const { size } = data;", + " this.heightScale = object.getDepth() * worldInvScale;", + "", + " // Changes of heights (SetHeights) break collisions unless the sample", + " // count is a power of 2: extra samples have no collision.", + " const sampleCount = Math.pow(2, Math.ceil(Math.log2(size)));", + " const settings = new Jolt.HeightFieldShapeSettings();", + " settings.mScale.Set(", + " (object.getWidth() / data.resolution) * worldInvScale,", + " 1,", + " (object.getHeight() / data.resolution) * worldInvScale", + " );", + " settings.mSampleCount = sampleCount;", + " settings.mBlockSize = 4;", + " // Allows later changes of heights on the whole depth of the object.", + " settings.mMinHeightValue = 0;", + " settings.mMaxHeightValue = this.heightScale;", + " settings.mHeightSamples.resize(sampleCount * sampleCount);", + " const samples = new Float32Array(", + " Jolt.HEAPF32.buffer,", + " Jolt.getPointer(settings.mHeightSamples.data()),", + " sampleCount * sampleCount", + " );", + " const noCollision = Jolt.HeightFieldShapeConstantValues.prototype.cNoCollisionValue;", + " samples.fill(noCollision);", + " // Height fields go along X and Z: rows are flipped so that, once", + " // rotated to have heights along Z, rows go along Y.", + " for (let j = 0; j < size; j++) {", + " const row = (size - 1 - j) * sampleCount;", + " for (let i = 0; i < size; i++) {", + " samples[row + i] = this._getSampleHeight(j * size + i, noCollision);", + " }", + " }", + " const shape = settings.Create().Get();", + " Jolt.destroy(settings);", + " this.heightField = Jolt.castObject(shape, Jolt.HeightFieldShape);", + " this.changedSamples.clear();", + " return shape;", + " }", + "", + " /** @returns {number} The height of a sample in the shape, or no collision in a hole. */", + " _getSampleHeight(index, noCollision) {", + " const height = this.terrain.surfaceHeights[index];", + " return Number.isNaN(height) ? noCollision : height * this.heightScale;", + " }", + "", + " _getBodyPosition() {", + " const { object } = this.terrain;", + " const worldInvScale = this.physics._sharedData.worldInvScale;", + " this.bodyX = object.getX();", + " this.bodyY = object.getY();", + " this.bodyZ = object.getZ();", + " // The origin of the height field is at the bottom of the terrain (see createShape).", + " this.bodyHeight = object.getHeight();", + " return this.physics._sharedData.getRVec3(", + " object.getX() * worldInvScale,", + " (object.getY() + object.getHeight()) * worldInvScale,", + " object.getZ() * worldInvScale", + " );", + " }", + "", + " _getBodyRotation() {", + " // A quarter turn around X: heights go along Z.", + " return this.physics._sharedData.getQuat(Math.SQRT1_2, 0, 0, Math.SQRT1_2);", + " }", + "", + " createAndAddBody() {", + " const { physics } = this;", + " const { bodyInterface } = physics._sharedData;", + " const shape = this.createShape();", + " const settings = new Jolt.BodyCreationSettings(", + " shape,", + " this._getBodyPosition(),", + " this._getBodyRotation(),", + " Jolt.EMotionType_Static,", + " physics.getBodyLayer()", + " );", + " settings.mFriction = physics.friction;", + " settings.mRestitution = physics.restitution;", + " const body = bodyInterface.CreateBody(settings);", + " Jolt.destroy(settings);", + " bodyInterface.AddBody(body.GetID(), Jolt.EActivation_DontActivate);", + " return body;", + " }", + "", + " updateObjectFromBody() {", + " // The terrain is static: the body never moves it.", + " }", + "", + " updateBodyFromObject() {", + " const { physics } = this;", + " const body = physics._body;", + " if (!body) return;", + " const { object } = this.terrain;", + " const { bodyInterface } = physics._sharedData;", + " if (", + " object.getX() !== this.bodyX ||", + " object.getY() !== this.bodyY ||", + " object.getZ() !== this.bodyZ ||", + " object.getHeight() !== this.bodyHeight", + " ) {", + " bodyInterface.SetPositionAndRotation(", + " body.GetID(),", + " this._getBodyPosition(),", + " this._getBodyRotation(),", + " Jolt.EActivation_DontActivate", + " );", + " }", + " if (!this.changedSamples.isEmpty()) {", + " this._applyChangedHeights(body);", + " }", + " }", + "", + " _applyChangedHeights(body) {", + " const { data } = this.terrain;", + " const { size } = data;", + " const { minI, minJ, maxI, maxJ } = this.changedSamples;", + " // Jolt only changes blocks of samples. Rows are flipped and extra", + " // samples have no collision (see createShape).", + " const blockSize = this.heightField.GetBlockSize();", + " const sampleCount = this.heightField.GetSampleCount();", + " const alignDown = (value) => Math.floor(value / blockSize) * blockSize;", + " const alignUp = (value) => Math.min(Math.ceil(value / blockSize) * blockSize, sampleCount);", + " const firstX = alignDown(minI);", + " const firstRow = alignDown(size - 1 - maxJ);", + " const width = alignUp(maxI + 1) - firstX;", + " const height = alignUp(size - minJ) - firstRow;", + " if (width * height > this.heightsBufferLength) {", + " if (this.heightsBufferPointer) Jolt._webidl_free(this.heightsBufferPointer);", + " this.heightsBufferLength = width * height;", + " this.heightsBufferPointer = Jolt._webidl_malloc(this.heightsBufferLength * 4);", + " }", + " const heights = new Float32Array(Jolt.HEAPF32.buffer, this.heightsBufferPointer, width * height);", + " const noCollision = Jolt.HeightFieldShapeConstantValues.prototype.cNoCollisionValue;", + " for (let row = 0; row < height; row++) {", + " const j = size - 1 - (firstRow + row);", + " for (let column = 0; column < width; column++) {", + " const i = firstX + column;", + " heights[row * width + column] =", + " i < size && j >= 0 ? this._getSampleHeight(j * size + i, noCollision) : noCollision;", + " }", + " }", + " const sharedData = this.physics._sharedData;", + " this.heightField.SetHeights(", + " firstX,", + " firstRow,", + " width,", + " height,", + " this.heightsBufferPointer,", + " width,", + " sharedData.jolt.GetTempAllocator()", + " );", + " sharedData.bodyInterface.NotifyShapeChanged(", + " body.GetID(),", + " sharedData.getVec3(0, 0, 0),", + " false,", + " Jolt.EActivation_DontActivate", + " );", + " this._wakeUpBodiesOnChangedSamples();", + " this.changedSamples.clear();", + " }", + "", + " /** Bodies sleeping on the changed area would otherwise float or sink. */", + " _wakeUpBodiesOnChangedSamples() {", + " const { object, data } = this.terrain;", + " const worldInvScale = this.physics._sharedData.worldInvScale;", + " const cellWidth = object.getWidth() / data.resolution;", + " const cellHeight = object.getHeight() / data.resolution;", + " const { minI, minJ, maxI, maxJ } = this.changedSamples;", + " const min = new Jolt.Vec3(", + " (object.getX() + (minI - 1) * cellWidth) * worldInvScale,", + " (object.getY() + (minJ - 1) * cellHeight) * worldInvScale,", + " (object.getZ() - 1) * worldInvScale", + " );", + " const max = new Jolt.Vec3(", + " (object.getX() + (maxI + 1) * cellWidth) * worldInvScale,", + " (object.getY() + (maxJ + 1) * cellHeight) * worldInvScale,", + " (object.getZ() + object.getDepth() + 1) * worldInvScale", + " );", + " const box = new Jolt.AABox(min, max);", + " const broadPhaseLayerFilter = new Jolt.BroadPhaseLayerFilter();", + " const objectLayerFilter = new Jolt.ObjectLayerFilter();", + " this.physics._sharedData.bodyInterface.ActivateBodiesInAABox(box, broadPhaseLayerFilter, objectLayerFilter);", + " Jolt.destroy(min);", + " Jolt.destroy(max);", + " Jolt.destroy(box);", + " Jolt.destroy(broadPhaseLayerFilter);", + " Jolt.destroy(objectLayerFilter);", + " }", + "", + " recreateShape() {", + " const body = this.physics._body;", + " if (!body) return;", + " this.physics._sharedData.bodyInterface.SetShape(", + " body.GetID(),", + " this.createShape(),", + " false,", + " Jolt.EActivation_DontActivate", + " );", + " const { resolution } = this.terrain.data;", + " this.changedSamples.set(0, 0, resolution, resolution);", + " this._wakeUpBodiesOnChangedSamples();", + " this.changedSamples.clear();", + " }", + "", + " destroyBody() {", + " this.defaultBodyUpdater.destroyBody();", + " this.heightField = null;", + " }", + "", + " dispose() {", + " if (this.heightsBufferPointer) Jolt._webidl_free(this.heightsBufferPointer);", + " this.heightsBufferPointer = 0;", + " this.heightsBufferLength = 0;", + " }", + "}", + "", + "/** @param {TerrainData} data */", + "const copyTerrainData = (data) => {", + " const copy = new TerrainData(data.resolution);", + " copy.copyFrom(data);", + " return copy;", + "};", + "", + "// Generating a relief or reading an image is long for big terrains, and done", + "// for every instance and every hot-reload: the results are kept.", + "const baseDataCache = new Map();", + "const MAX_CACHED_BASE_DATA = 8;", + "// Changes of the shape of a terrain kept for consumers of its surface that", + "// missed some versions: older changes update the whole surface.", + "const MAX_SURFACE_CHANGES = 64;", + "// Unique among all terrains: a terrain replaced by another one never has its version.", + "let lastGroundVersion = 0;", + "", + "/**", + " * The terrain of a Terrain3D object: its data, how it's drawn and its", + " * surface (for collisions and navigation meshes). Positions are in the scene.", + " */", + "class Terrain {", + " /** @param {gdjs.CustomRuntimeObject3D} object */", + " constructor(object) {", + " this.object = object;", + " /** @type {TerrainData} */", + " this.data = new TerrainData(DEFAULT_RESOLUTION);", + " /** The data before any sculpting or painting, from the relief or heightmap. */", + " this.baseData = this.data;", + " this.renderer = new TerrainRenderer(this);", + " /** @type {TerrainBodyUpdater[]} */", + " this.bodyUpdaters = [];", + " this.changedSamples = new SampleRectangle();", + " this.hasPaintChanged = false;", + " /** Increased each time the ground changes, for objects following it (like grass). */", + " this.version = 0;", + " /** Increased each time the shape of the ground changes (not its paint or position). */", + " this.surfaceVersion = 0;", + " /**", + " * The last changes of the shape of the ground, for `getChangedArea`.", + " * @type {Array<{version: number, samples: SampleRectangle}>}", + " */", + " this.surfaceChanges = [];", + " /** The changes since this version are all in `surfaceChanges`. */", + " this.surfaceChangesStartVersion = 0;", + " this._stroke = { cellWidth: 1, cellHeight: 1, ax: 0, ay: 0, bx: 0, by: 0, radius: 0 };", + " this._strokeChangedSamples = new SampleRectangle();", + " /** The properties the data was loaded from, to reload it only if they change. */", + " this.loadedFrom = '';", + " /** The heightmap image being loaded: the relief is used until it is. */", + " this.loadingHeightmapImage = '';", + " this.isDisposed = false;", + " /** The last value of the sculpt data property read or saved. */", + " this.sculptData = '';", + " /**", + " * Values saved by the editor tools: the editor sends them back with a", + " * hot-reload, possibly after newer ones were saved.", + " * @type {string[]}", + " */", + " this.savedSculptData = [];", + " /** The heights of the surface: the heights of the data, and NaN in holes. */", + " this.surfaceHeights = new Float32Array(0);", + " this.loadFromProperties();", + " this._transform = this._getTransform();", + " getGrounds(object.getRuntimeScene()).add(this);", + " /**", + " * The shape of the ground, for the physics engine and navigation meshes.", + " * @type {gdjs.Surface}", + " */", + " this.surface = {", + " getVersion: () => this.surfaceVersion,", + " getChangedArea: (sinceVersion) => this.getChangedArea(sinceVersion),", + " getTriangles: () => null,", + " getHeightField: () => ({ columns: this.data.size, rows: this.data.size, heights: this.surfaceHeights }),", + " };", + " if (typeof object.setSurface === 'function') {", + " object.setSurface(this.surface);", + " } else if (gdjs.Physics3DRuntimeBehavior) {", + " // Older GDevelop versions don't use surfaces.", + " for (const behavior of object._behaviors) {", + " if (behavior instanceof gdjs.Physics3DRuntimeBehavior) {", + " this.bodyUpdaters.push(new TerrainBodyUpdater(this, behavior));", + " }", + " }", + " }", + " }", + "", + " _getSourceProperties() {", + " const { object } = this;", + " return [", + " object._getResolution(),", + " object._getRelief(),", + " object._getSeed(),", + " object._getHeightmapImage(),", + " object._getEdits(),", + " ].join('|');", + " }", + "", + " loadFromProperties() {", + " const { object } = this;", + " const resolution = parseInt(object._getResolution(), 10);", + " const validResolution = RESOLUTIONS.indexOf(resolution) !== -1 ? resolution : DEFAULT_RESOLUTION;", + " this.sculptData = object._getSculptData();", + " this.loadedFrom = this._getSourceProperties();", + "", + " this.baseData = this._getBaseData(validResolution);", + " let data = codec.decode(this.sculptData, (resolution) => copyTerrainData(this._getBaseData(resolution)));", + " if (data && data.resolution !== validResolution) {", + " data = data.resampled(validResolution);", + " }", + " this.setData(data || copyTerrainData(this.baseData));", + " }", + "", + " /**", + " * @param {number} resolution", + " * @returns {TerrainData} The data from the relief or the heightmap, shared: it must not be changed.", + " */", + " _getBaseData(resolution) {", + " const { object } = this;", + " const key = [resolution, object._getRelief(), object._getSeed(), object._getHeightmapImage(), object._getEdits()].join('|');", + " const cachedData = baseDataCache.get(key);", + " if (cachedData) return cachedData;", + "", + " const data = new TerrainData(resolution);", + " const heightmapImage = object._getHeightmapImage();", + " const isHeightmapLoaded =", + " !!heightmapImage && loadHeightsFromImage(data, object.getRuntimeScene().getGame(), heightmapImage);", + " if (!isHeightmapLoaded) {", + " relief.generate(data, object._getRelief(), object._getSeed());", + " if (heightmapImage) this._reloadOnceLoaded(heightmapImage);", + " }", + " edits.apply(data, object._getEdits());", + " // Not kept if the image is missing: it may be loaded later.", + " if (isHeightmapLoaded || !heightmapImage) baseDataCache.set(key, data);", + " if (baseDataCache.size > MAX_CACHED_BASE_DATA) {", + " baseDataCache.delete(baseDataCache.keys().next().value);", + " }", + " return data;", + " }", + "", + " /** @param {TerrainData} data */", + " setData(data) {", + " this.version = ++lastGroundVersion;", + " this.surfaceVersion = this.version;", + " this.surfaceChanges.length = 0;", + " this.surfaceChangesStartVersion = this.version;", + " const hasSameResolution = data.resolution === this.data.resolution && this.renderer.chunks.length > 0;", + " this.data = data;", + " const allSamples = new SampleRectangle().set(0, 0, data.resolution, data.resolution);", + " this.surfaceHeights = new Float32Array(data.heights.length);", + " this.renderer.setHasHoles(false);", + " this._updateSurfaceHeights(allSamples);", + " if (hasSameResolution) {", + " this.renderer.updateSamples(allSamples, true);", + " } else {", + " this.renderer.rebuild();", + " }", + " for (const bodyUpdater of this.bodyUpdaters) {", + " bodyUpdater.recreateShape();", + " }", + " }", + "", + " onHotReloading() {", + " const sculptData = this.object._getSculptData();", + " const savedIndex = this.savedSculptData.indexOf(sculptData);", + " if (savedIndex !== -1 && this._getSourceProperties() === this.loadedFrom) {", + " // The terrain already has this data, or newer changes.", + " this.savedSculptData.splice(0, savedIndex + 1);", + " } else if (sculptData !== this.sculptData || this._getSourceProperties() !== this.loadedFrom) {", + " this.savedSculptData.length = 0;", + " this.loadFromProperties();", + " }", + " this.renderer.updateMaterial();", + " }", + "", + " /** @returns {string} The data to save in the sculpt data property. */", + " save() {", + " this.sculptData = codec.encode(this.data, this.baseData);", + " this.savedSculptData.push(this.sculptData);", + " return this.sculptData;", + " }", + "", + " getCellWidth() {", + " return this.object.getWidth() / this.data.resolution;", + " }", + "", + " getCellHeight() {", + " return this.object.getHeight() / this.data.resolution;", + " }", + "", + " _toGridX(x) {", + " return (x - this.object.getX()) / this.getCellWidth();", + " }", + "", + " _toGridY(y) {", + " return (y - this.object.getY()) / this.getCellHeight();", + " }", + "", + " /** @returns {number} The Z position of the ground. */", + " getHeightAt(x, y) {", + " return this.object.getZ() + this.data.sampleHeight(this._toGridX(x), this._toGridY(y)) * this.object.getDepth();", + " }", + "", + " /** @returns {number} The angle of the ground with the horizontal, in degrees. */", + " getSlopeAt(x, y) {", + " const gridX = this._toGridX(x);", + " const gridY = this._toGridY(y);", + " const { data } = this;", + " const depth = this.object.getDepth();", + " const slopeX = ((data.sampleHeight(gridX + 0.5, gridY) - data.sampleHeight(gridX - 0.5, gridY)) * depth) / this.getCellWidth();", + " const slopeY = ((data.sampleHeight(gridX, gridY + 0.5) - data.sampleHeight(gridX, gridY - 0.5)) * depth) / this.getCellHeight();", + " return gdjs.toDegrees(Math.atan(Math.hypot(slopeX, slopeY)));", + " }", + "", + " /** @returns {number} The layer painted the most, from 1 to 4. */", + " getLayerAt(x, y) {", + " return this.data.getDominantLayer(this._toGridX(x), this._toGridY(y)) + 1;", + " }", + "", + " /**", + " * @param {number} layer From 1 to 4.", + " * @returns {number} How much the layer is painted, from 0 to 1.", + " */", + " getLayerWeightAt(x, y, layer) {", + " const layerIndex = clampInteger(layer - 1, 0, LAYER_COUNT - 1);", + " return this.data.sampleLayerWeight(this._toGridX(x), this._toGridY(y), layerIndex);", + " }", + "", + " /** @returns {number[]} The painted tint: red, green and blue, from 0 to 255 (white when there is none). */", + " getTintAt(x, y) {", + " const gridX = this._toGridX(x);", + " const gridY = this._toGridY(y);", + " return [0, 1, 2].map((channel) => Math.round(this.data.sampleColor(gridX, gridY, channel) * 255));", + " }", + "", + " /** @returns {number} How wet the ground is, from 0 (dry) to 1, painted or near the water (see the wet shore). */", + " getWetnessAt(x, y) {", + " const paintedWetness = this.data.sampleColor(this._toGridX(x), this._toGridY(y), 3);", + " const { object } = this;", + " if (!object._getWetShore()) return paintedWetness;", + " const fade = Math.max(object._getWetShoreFade(), 0.001);", + " const shoreWetness = clamp01((object._getWetShoreZ() + fade - this.getHeightAt(x, y)) / fade);", + " // Like the smoothstep of the shader.", + " return Math.max(paintedWetness, shoreWetness * shoreWetness * (3 - 2 * shoreWetness));", + " }", + "", + " _isInArea(x, y) {", + " const { object } = this;", + " return (", + " x >= object.getX() &&", + " x <= object.getX() + object.getWidth() &&", + " y >= object.getY() &&", + " y <= object.getY() + object.getHeight()", + " );", + " }", + "", + " /** @returns {boolean} true if the position is above or under the terrain, and not in a hole. */", + " containsPoint(x, y) {", + " return this._isInArea(x, y) && this.data.sampleSolidity(this._toGridX(x), this._toGridY(y)) >= 0.5;", + " }", + "", + " /** @returns {boolean} true if there is a hole in the terrain at the position, like what is drawn. */", + " isHoleAt(x, y) {", + " return this._isInArea(x, y) && this.data.sampleSolidity(this._toGridX(x), this._toGridY(y)) < 0.5;", + " }", + "", + " _getStroke(x1, y1, x2, y2, radius) {", + " const stroke = this._stroke;", + " stroke.cellWidth = this.getCellWidth();", + " stroke.cellHeight = this.getCellHeight();", + " stroke.ax = x1 - this.object.getX();", + " stroke.ay = y1 - this.object.getY();", + " stroke.bx = x2 - this.object.getX();", + " stroke.by = y2 - this.object.getY();", + " stroke.radius = Math.max(radius, 0);", + " return stroke;", + " }", + "", + " /**", + " * @param {SampleRectangle} samples", + " * @param {boolean} haveHeightsChanged false when only the paint changed.", + " */", + " _onSamplesChanged(samples, haveHeightsChanged) {", + " if (samples.isEmpty()) return;", + " this.version = ++lastGroundVersion;", + " this.changedSamples.add(samples);", + " // Painting changes the surface when it makes holes or fills them.", + " const hasSurfaceChanged = this._updateSurfaceHeights(samples);", + " if (!haveHeightsChanged && !hasSurfaceChanged) return;", + " this.surfaceVersion = this.version;", + " this.surfaceChanges.push({ version: this.version, samples: new SampleRectangle().set(samples.minI, samples.minJ, samples.maxI, samples.maxJ) });", + " if (this.surfaceChanges.length > MAX_SURFACE_CHANGES) {", + " this.surfaceChangesStartVersion = this.surfaceChanges.shift().version;", + " }", + " for (const bodyUpdater of this.bodyUpdaters) {", + " bodyUpdater.changedSamples.add(samples);", + " }", + " }", + "", + " /**", + " * @param {SampleRectangle} samples", + " * @returns {boolean} true if heights of the surface changed, or holes were made or filled.", + " */", + " _updateSurfaceHeights(samples) {", + " const { data, surfaceHeights } = this;", + " let hasChanged = false;", + " let hasHoles = false;", + " for (let j = samples.minJ; j <= samples.maxJ; j++) {", + " for (let i = samples.minI; i <= samples.maxI; i++) {", + " const index = j * data.size + i;", + " const isHole = data.isHole(index);", + " const height = isHole ? NaN : data.heights[index];", + " const previousHeight = surfaceHeights[index];", + " if (isHole) {", + " hasHoles = true;", + " if (Number.isNaN(previousHeight)) continue;", + " } else if (height === previousHeight) {", + " continue;", + " }", + " surfaceHeights[index] = height;", + " hasChanged = true;", + " }", + " }", + " // Filled holes keep the shaders drawing holes until the data is loaded again.", + " if (hasHoles) this.renderer.setHasHoles(true);", + " return hasChanged;", + " }", + "", + " /** @param {number} height In scene units, negative to lower the ground. */", + " raise(x1, y1, x2, y2, radius, height) {", + " const samples = this._strokeChangedSamples;", + " brushes.raise(this.data, this._getStroke(x1, y1, x2, y2, radius), height / this.object.getDepth(), samples);", + " this._onSamplesChanged(samples, true);", + " }", + "", + " /** @param {number} z The Z position to flatten the ground to. */", + " /** @param {number} [toZ] The Z position at the end of the segment, for a slope. The same as z by default. */", + " flatten(x1, y1, x2, y2, radius, z, strength, toZ = z) {", + " const samples = this._strokeChangedSamples;", + " const toHeight = (value) => (value - this.object.getZ()) / this.object.getDepth();", + " brushes.flatten(this.data, this._getStroke(x1, y1, x2, y2, radius), toHeight(z), strength, samples, toHeight(toZ));", + " this._onSamplesChanged(samples, true);", + " }", + "", + " smooth(x1, y1, x2, y2, radius, strength) {", + " const samples = this._strokeChangedSamples;", + " brushes.smooth(this.data, this._getStroke(x1, y1, x2, y2, radius), strength, samples);", + " this._onSamplesChanged(samples, true);", + " }", + "", + " /** @param {number} layer From 1 to 4. */", + " paint(x1, y1, x2, y2, radius, layer, strength) {", + " const layerIndex = clampInteger(layer - 1, 0, LAYER_COUNT - 1);", + " const samples = this._strokeChangedSamples;", + " brushes.paint(this.data, this._getStroke(x1, y1, x2, y2, radius), layerIndex, strength, samples);", + " this.hasPaintChanged = true;", + " this._onSamplesChanged(samples, false);", + " }", + "", + " /** @param {number[]} color The red, green and blue (0-255) of the tint: white removes it. */", + " tint(x1, y1, x2, y2, radius, color, strength) {", + " const samples = this._strokeChangedSamples;", + " brushes.tint(this.data, this._getStroke(x1, y1, x2, y2, radius), color, strength, samples);", + " this.hasPaintChanged = true;", + " this._onSamplesChanged(samples, false);", + " }", + "", + " /** @param {number} wetness From 0 (dry) to 1. */", + " wet(x1, y1, x2, y2, radius, wetness, strength) {", + " const samples = this._strokeChangedSamples;", + " brushes.wet(this.data, this._getStroke(x1, y1, x2, y2, radius), wetness, strength, samples);", + " this.hasPaintChanged = true;", + " this._onSamplesChanged(samples, false);", + " }", + "", + " /** Make a hole around a segment (`fillHoles` fills it again). */", + " makeHole(x1, y1, x2, y2, radius) {", + " const samples = this._strokeChangedSamples;", + " brushes.hole(this.data, this._getStroke(x1, y1, x2, y2, radius), samples);", + " this.hasPaintChanged = true;", + " this._onSamplesChanged(samples, false);", + " }", + "", + " /** Fill the holes around a segment with the layers of the ground around them. */", + " fillHoles(x1, y1, x2, y2, radius) {", + " const samples = this._strokeChangedSamples;", + " brushes.fill(this.data, this._getStroke(x1, y1, x2, y2, radius), samples);", + " this.hasPaintChanged = true;", + " this._onSamplesChanged(samples, false);", + " }", + "", + " /**", + " * Make the entrance of a tunnel or a cave: a hole, with the ground in", + " * front of it flattened to the floor of the tunnel.", + " * @param {number} angle The direction going into the ground, in degrees.", + " * @param {number} floorZ", + " * @param {number} length The length of the flat ground in front of the entrance.", + " */", + " carveEntrance(x, y, radius, angle, floorZ, length) {", + " const directionX = Math.cos(gdjs.toRad(angle));", + " const directionY = Math.sin(gdjs.toRad(angle));", + " this.flatten(x - directionX * length, y - directionY * length, x, y, radius * 1.5, floorZ, 1);", + " this.makeHole(x, y, x + directionX * radius, y + directionY * radius, radius);", + " }", + "", + " generate(reliefName, seed) {", + " this.baseData = new TerrainData(this.data.resolution);", + " relief.generate(this.baseData, reliefName, seed);", + " this.setData(copyTerrainData(this.baseData));", + " }", + "", + " loadHeightmap(imageResourceName) {", + " const baseData = new TerrainData(this.data.resolution);", + " if (loadHeightsFromImage(baseData, this.object.getRuntimeScene().getGame(), imageResourceName)) {", + " baseData.splat.set(this.data.splat);", + " baseData.colors.set(this.data.colors);", + " this.baseData = baseData;", + " this.setData(copyTerrainData(baseData));", + " }", + " }", + "", + " /** @param {gdjs.RuntimeObject[]} objects */", + " placeOnGround(objects) {", + " for (const object of objects) {", + " if (object === this.object || !gdjs.Base3DHandler.is3D(object)) continue;", + " const groundZ = this.getHeightAt(object.getCenterXInScene(), object.getCenterYInScene());", + " // The origin of the object is not always at its bottom.", + " object.setZ(groundZ + object.getZ() - object.getUnrotatedAABBMinZ());", + " }", + " }", + "", + " /**", + " * @returns {THREE.Vector3 | null} The point of the ground under the cursor,", + " * in the terrain group coordinates. Holes are hit too, so that brushes can fill them.", + " */", + " raycastFromCursor(game, raycaster, ndc) {", + " const layer = this.object.getInstanceContainer().getLayer(this.object.getLayer());", + " const camera = layer.getRenderer().getThreeCamera();", + " if (!camera) return null;", + " const inputManager = game.getInputManager();", + " ndc.set(", + " (inputManager.getCursorX() / game.getGameResolutionWidth()) * 2 - 1,", + " -(inputManager.getCursorY() / game.getGameResolutionHeight()) * 2 + 1", + " );", + " raycaster.setFromCamera(ndc, camera);", + " this.renderer.group.updateWorldMatrix(true, false);", + " const ray = raycaster.ray.clone().applyMatrix4(this.renderer.group.matrixWorld.clone().invert());", + " ray.direction.normalize();", + "", + " const box = new THREE.Box3(new THREE.Vector3(0, 0, 0), new THREE.Vector3(AREA_SIZE, AREA_SIZE, AREA_DEPTH));", + " const entry = box.containsPoint(ray.origin) ? ray.origin.clone() : ray.intersectBox(box, new THREE.Vector3());", + " if (!entry) return null;", + " const cellSize = AREA_SIZE / this.data.resolution;", + " const groundZAt = (point) => this.data.sampleHeight(point.x / cellSize, point.y / cellSize) * AREA_DEPTH;", + " const step = cellSize / 2;", + " const point = entry.clone();", + " const previousPoint = entry.clone();", + " for (let distance = 0; distance < AREA_SIZE * 2; distance += step) {", + " if (point.z <= groundZAt(point)) {", + " // Refine between the last point above the ground and this one.", + " for (let iteration = 0; iteration < 8; iteration++) {", + " const middle = previousPoint.clone().lerp(point, 0.5);", + " if (middle.z <= groundZAt(middle)) point.copy(middle);", + " else previousPoint.copy(middle);", + " }", + " return point;", + " }", + " if (!box.containsPoint(point)) return null;", + " previousPoint.copy(point);", + " point.addScaledVector(ray.direction, step);", + " }", + " return null;", + " }", + "", + " /**", + " * @param {THREE.Vector3} localPosition In the terrain group coordinates (like from `raycastFromCursor`).", + " * @returns {{x: number, y: number, z: number}} The position in the scene.", + " */", + " toScenePosition(localPosition) {", + " const { object } = this;", + " return {", + " x: object.getX() + (localPosition.x / AREA_SIZE) * object.getWidth(),", + " y: object.getY() + (localPosition.y / AREA_SIZE) * object.getHeight(),", + " z: object.getZ() + (localPosition.z / AREA_DEPTH) * object.getDepth(),", + " };", + " }", + "", + " /** Draw the changes of the ground (collisions are updated by the physics engine). */", + " applyChanges() {", + " this.renderer.updateSamples(this.changedSamples, this.hasPaintChanged);", + " this.changedSamples.clear();", + " this.hasPaintChanged = false;", + " }", + "", + " /** @returns {number} A number changing each time the ground changes (see `getGrounds`). */", + " getVersion() {", + " return this.version;", + " }", + "", + " /**", + " * @param {number} sinceVersion", + " * @returns {gdjs.SurfaceArea | null} The part of the ground whose shape changed since the version (see `gdjs.Surface`).", + " */", + " getChangedArea(sinceVersion) {", + " if (sinceVersion < this.surfaceChangesStartVersion) return null;", + " const samples = new SampleRectangle();", + " for (const change of this.surfaceChanges) {", + " if (change.version > sinceVersion) samples.add(change.samples);", + " }", + " if (samples.isEmpty()) return { minX: 0, minY: 0, maxX: 0, maxY: 0 };", + " const { resolution } = this.data;", + " return {", + " minX: samples.minI / resolution,", + " minY: samples.minJ / resolution,", + " maxX: samples.maxI / resolution,", + " maxY: samples.maxJ / resolution,", + " };", + " }", + "", + " _getTransform() {", + " const { object } = this;", + " return [object.getX(), object.getY(), object.getZ(), object.getWidth(), object.getHeight(), object.getDepth()].join(',');", + " }", + "", + " /** @param {string} imageResourceName A heightmap image that is not loaded yet. */", + " _reloadOnceLoaded(imageResourceName) {", + " if (this.loadingHeightmapImage === imageResourceName) return;", + " this.loadingHeightmapImage = imageResourceName;", + " const game = this.object.getRuntimeScene().getGame();", + " game.getImageManager()", + " .loadResource(imageResourceName)", + " .then(() => {", + " if (this.isDisposed || this.loadingHeightmapImage !== imageResourceName) return;", + " this.loadingHeightmapImage = '';", + " if (getLoadedImage(game, imageResourceName)) this.loadFromProperties();", + " })", + " .catch(() => {});", + " }", + "", + " update() {", + " const transform = this._getTransform();", + " if (transform !== this._transform) {", + " this._transform = transform;", + " this.version = ++lastGroundVersion;", + " }", + " this.applyChanges();", + " this.renderer.uniforms.terrainWorldScale.value = getWorldScale(this.object);", + " this._updateWetShore();", + " const layer = this.object.getInstanceContainer().getLayer(this.object.getLayer());", + " this.renderer.updateLevelsOfDetail(layer.getRenderer().getThreeCamera());", + " }", + "", + " _updateWetShore() {", + " const { object } = this;", + " // In the coordinates of the terrain group, like the vertices.", + " const depthScale = AREA_DEPTH / Math.max(object.getDepth(), 0.0001);", + " this.renderer.uniforms.terrainWetShore.value.set(", + " (object._getWetShoreZ() - object.getZ()) * depthScale,", + " Math.max(object._getWetShoreFade(), 0.001) * depthScale,", + " object._getWetShore() ? 1 : 0", + " );", + " }", + "", + " dispose() {", + " this.isDisposed = true;", + " getGrounds(this.object.getRuntimeScene()).delete(this);", + " if (typeof this.object.setSurface === 'function' && this.object.getSurface() === this.surface) {", + " this.object.setSurface(null);", + " }", + " this.renderer.dispose();", + " for (const bodyUpdater of this.bodyUpdaters) {", + " bodyUpdater.dispose();", + " }", + " }", + "}", + "", + "/**", + " * @param {gdjs.RuntimeObject} object", + " * @returns {number} The number of scene units by 3D world unit (1 in GDevelop versions before the world scale).", + " */", + "const getWorldScale = (object) => {", + " const scene = object.getRuntimeScene();", + " return typeof scene.getRenderer3DWorldScale === 'function' ? scene.getRenderer3DWorldScale() : 1;", + "};", + "", + "const averageColors = new WeakMap();", + "let white = null;", + "/**", + " * @param {THREE.Texture} texture", + " * @returns {THREE.Color} The average color of the texture (white when it's not loaded).", + " */", + "const getAverageColor = (texture) => {", + " const cachedColor = averageColors.get(texture);", + " if (cachedColor) return cachedColor;", + " const image = texture.image;", + " const isDrawable =", + " typeof document !== 'undefined' &&", + " ((image instanceof HTMLImageElement && image.complete && image.naturalWidth > 0) ||", + " image instanceof HTMLCanvasElement ||", + " (typeof ImageBitmap !== 'undefined' && image instanceof ImageBitmap));", + " if (!isDrawable) return white || (white = new THREE.Color(1, 1, 1));", + " const size = 8;", + " const canvas = document.createElement('canvas');", + " canvas.width = size;", + " canvas.height = size;", + " const context = canvas.getContext('2d');", + " context.drawImage(image, 0, 0, size, size);", + " const pixels = context.getImageData(0, 0, size, size).data;", + " let red = 0;", + " let green = 0;", + " let blue = 0;", + " for (let index = 0; index < pixels.length; index += 4) {", + " red += pixels[index];", + " green += pixels[index + 1];", + " blue += pixels[index + 2];", + " }", + " const count = size * size * 255;", + " const color = new THREE.Color().setRGB(red / count, green / count, blue / count, THREE.SRGBColorSpace);", + " averageColors.set(texture, color);", + " return color;", + "};", + "", + "const svgIcon = (content) =>", + " 'data:image/svg+xml,' +", + " encodeURIComponent('' + content + '');", + "const strokes = 'fill=\"none\" stroke=\"#000\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"';", + "const toolIcons = {", + " Raise: svgIcon(''),", + " Lower: svgIcon(''),", + " Smooth: svgIcon(''),", + " Flatten: svgIcon(''),", + " Size: svgIcon(''),", + " Strength: svgIcon(''),", + " Undo: svgIcon(''),", + " Hole: svgIcon(''),", + " FillHoles: svgIcon(''),", + " Wetness: svgIcon(''),", + "};", + "// Tints for the color brush when the editor has no color picker (older GDevelop versions).", + "const tintPresets = ['#e2c879', '#a5c45a', '#8a6a48', '#c98a62', '#9a9a92', '#ffffff'];", + "", + "/**", + " * The sculpt and paint tools shown in the scene editor when a terrain is", + " * selected. Without the needed editor functions (older GDevelop versions),", + " * nothing is shown: the terrain can still be changed with events.", + " */", + "class TerrainEditorTools {", + " constructor() {", + " /** '' when no brush is used. */", + " this.toolName = '';", + " this.sizePercent = 30;", + " this.strengthPercent = 50;", + " /** @type {Terrain | null} */", + " this.terrain = null;", + " /** @type {gdjs.InGameEditor | null} */", + " this.editor = null;", + " this.isStroking = false;", + " this.flattenHeight = 0;", + " /** The color of the \"Tint\" brush, as \"#rrggbb\". */", + " this.tintColor = tintPresets[0];", + " this.wetnessPercent = 100;", + " this.lastStrokeTime = 0;", + " /** @type {{data: TerrainData, baseData: TerrainData}[]} */", + " this.undoSteps = [];", + " this.raycaster = new THREE.Raycaster();", + " this.ndc = new THREE.Vector2();", + " this._swatchColor = new THREE.Color();", + " }", + "", + " /** @param {gdjs.InGameEditor} editor */", + " update(editor) {", + " if (typeof editor.showToolbar !== 'function') {", + " // Older GDevelop versions: the terrain can only be changed with events.", + " return;", + " }", + " this.editor = editor;", + " const selectedObjects = editor.getSelectedObjects();", + " const terrain =", + " selectedObjects.length === 1 && selectedObjects[0].__terrain3D ? selectedObjects[0].__terrain3D : null;", + " if (terrain !== this.terrain) {", + " this._endStroke();", + " if (this.terrain) this.terrain.renderer.setBrush(null, 0);", + " this.terrain = terrain;", + " this.undoSteps.length = 0;", + " }", + " if (terrain) editor.showToolbar('Terrain3D', this._getToolbarItems(terrain));", + " const game = editor.getRuntimeGame();", + " if (!terrain || !this.toolName) {", + " if (terrain) terrain.renderer.setBrush(null, 0);", + " return;", + " }", + "", + " const inputManager = game.getInputManager();", + " if (inputManager.wasKeyJustPressed(gdjs.evtTools.input.keysNameToCode.Escape)) {", + " this._endStroke();", + " this.toolName = '';", + " terrain.renderer.setBrush(null, 0);", + " return;", + " }", + " editor.captureLeftMouseButton();", + " const hit = terrain.raycastFromCursor(game, this.raycaster, this.ndc);", + " const radius = this._getRadius();", + " terrain.renderer.setBrush(hit, radius);", + "", + " // Older GDevelop versions don't have it.", + " const isPressedForTool =", + " typeof editor.isCapturedLeftMouseButtonPressed === 'function'", + " ? editor.isCapturedLeftMouseButtonPressed()", + " : inputManager.isMouseButtonPressed(0);", + " // The button is released outside the canvas when the mouse is on a toolbar.", + " const isPressed = isPressedForTool && inputManager.isMouseInsideCanvas();", + " if (isPressed && hit && !this.isStroking) {", + " this._startStroke(terrain, hit);", + " }", + " if (this.isStroking && isPressed && hit) {", + " this._applyBrush(terrain, hit, radius);", + " }", + " if (!isPressed) {", + " this._endStroke();", + " }", + " }", + "", + " _getRadius() {", + " const { object } = this.terrain;", + " const maxRadius = Math.max(object.getWidth(), object.getHeight()) / 4;", + " return Math.max(maxRadius * Math.pow(this.sizePercent / 100, 2), this.terrain.getCellWidth());", + " }", + "", + " _startStroke(terrain, hit) {", + " this.isStroking = true;", + " this.lastStrokeTime = performance.now();", + " this.flattenHeight = terrain.toScenePosition(hit).z;", + " this.undoSteps.push({ data: copyTerrainData(terrain.data), baseData: terrain.baseData });", + " const maxUndoSteps = Math.max(Math.floor(UNDO_STEPS_SAMPLES / terrain.data.heights.length), 2);", + " while (this.undoSteps.length > maxUndoSteps) this.undoSteps.shift();", + " }", + "", + " _endStroke() {", + " if (!this.isStroking) return;", + " this.isStroking = false;", + " this._save();", + " }", + "", + " _applyBrush(terrain, hit, radius) {", + " const now = performance.now();", + " const elapsedSeconds = Math.min((now - this.lastStrokeTime) / 1000, 0.1);", + " this.lastStrokeTime = now;", + " const { x, y } = terrain.toScenePosition(hit);", + " const amount = (this.strengthPercent / 100) * elapsedSeconds;", + " const depth = terrain.object.getDepth();", + " if (this.toolName === 'Raise') terrain.raise(x, y, x, y, radius, amount * depth * 0.5);", + " else if (this.toolName === 'Lower') terrain.raise(x, y, x, y, radius, -amount * depth * 0.5);", + " else if (this.toolName === 'Smooth') terrain.smooth(x, y, x, y, radius, amount * 10);", + " else if (this.toolName === 'Flatten') terrain.flatten(x, y, x, y, radius, this.flattenHeight, amount * 10);", + " else if (this.toolName.startsWith('Paint')) {", + " terrain.paint(x, y, x, y, radius, parseInt(this.toolName.slice(5), 10), amount * 8);", + " } else if (this.toolName === 'Hole') {", + " terrain.makeHole(x, y, x, y, radius);", + " } else if (this.toolName === 'FillHoles') {", + " terrain.fillHoles(x, y, x, y, radius);", + " } else if (this.toolName === 'Tint') {", + " terrain.tint(x, y, x, y, radius, gdjs.rgbOrHexToRGBColor(this.tintColor), amount * 8);", + " } else if (this.toolName === 'Wetness') {", + " terrain.wet(x, y, x, y, radius, this.wetnessPercent / 100, amount * 8);", + " }", + " // Drawn now, as the terrain was updated earlier in this frame.", + " terrain.applyChanges();", + " }", + "", + " _save() {", + " const { terrain, editor } = this;", + " if (!terrain || !editor) return;", + " editor.updateObjectProperties(terrain.object.getName(), { SculptData: terrain.save() });", + " }", + "", + " _undo() {", + " const undoStep = this.undoSteps.pop();", + " // The relief, the edits or the resolution may have changed since the stroke.", + " if (!this.terrain || !undoStep || undoStep.baseData !== this.terrain.baseData) {", + " this.undoSteps.length = 0;", + " return;", + " }", + " this.terrain.setData(undoStep.data);", + " this._save();", + " }", + "", + " /** @returns {Array} The settings of the tint or wetness brush, when it's used. */", + " _getColorToolItems() {", + " const { editor } = this;", + " if (this.toolName === 'Tint') {", + " if (editor && typeof editor.isToolbarItemTypeSupported === 'function' && editor.isToolbarItemTypeSupported('color')) {", + " return [", + " {", + " type: 'color',", + " id: 'TintColor',", + " tooltip: 'Color of the tint',", + " value: this.tintColor,", + " onChange: (color) => (this.tintColor = color),", + " },", + " ];", + " }", + " return tintPresets.map((color) => ({", + " type: 'button',", + " id: 'TintPreset' + color,", + " tooltip: color === '#ffffff' ? 'No tint' : 'Tint with this color',", + " color,", + " isActive: this.tintColor === color,", + " onClick: () => (this.tintColor = color),", + " }));", + " }", + " if (this.toolName === 'Wetness') {", + " return [", + " {", + " type: 'slider',", + " id: 'WetnessAmount',", + " tooltip: 'Wetness',", + " iconUrl: toolIcons.Wetness,", + " min: 0,", + " max: 100,", + " value: this.wetnessPercent,", + " onChange: (value) => (this.wetnessPercent = value),", + " },", + " ];", + " }", + " return [];", + " }", + "", + " /**", + " * @param {Terrain} terrain", + " * @returns {Array} The toolbar for the current state (the editor only", + " * updates what changed).", + " */", + " _getToolbarItems(terrain) {", + " const { object } = terrain;", + " const toolButton = (toolName, tooltip, color) => ({", + " type: 'button',", + " id: toolName,", + " tooltip,", + " iconUrl: toolIcons[toolName],", + " color,", + " isActive: this.toolName === toolName,", + " onClick: () => (this.toolName = this.toolName === toolName ? '' : toolName),", + " });", + " const layerColors = [object._getLayer1Color(), object._getLayer2Color(), object._getLayer3Color(), object._getLayer4Color()];", + " // Layers are shown with their color, tinting their texture.", + " const swatchColors = layerColors.map((color, index) =>", + " this._swatchColor", + " .set(gdjs.rgbOrHexStringToNumber(color))", + " .multiply(getAverageColor(terrain.renderer.uniforms['terrainLayerMap' + index].value))", + " .getHexString()", + " );", + " return [", + " toolButton('Raise', 'Raise the ground (drag on the terrain)'),", + " toolButton('Lower', 'Lower the ground'),", + " toolButton('Smooth', 'Smooth the ground'),", + " toolButton('Flatten', 'Flatten the ground to the height where the drag starts'),", + " { type: 'divider', id: 'PaintDivider' },", + " ...swatchColors.map((swatchColor, index) =>", + " toolButton('Paint' + (index + 1), 'Paint layer ' + (index + 1), '#' + swatchColor)", + " ),", + " toolButton('Hole', 'Make a hole, for the entrance of a tunnel or a cave'),", + " toolButton('FillHoles', 'Fill holes, with the layers of the ground around them'),", + " { type: 'divider', id: 'ColorDivider' },", + " toolButton('Tint', 'Tint the ground (white removes the tint)', this.tintColor),", + " toolButton('Wetness', 'Make the ground wet, or dry with a wetness of 0'),", + " ...this._getColorToolItems(),", + " { type: 'divider', id: 'BrushDivider' },", + " {", + " type: 'slider',", + " id: 'Size',", + " tooltip: 'Brush size',", + " iconUrl: toolIcons.Size,", + " min: 1,", + " max: 100,", + " value: this.sizePercent,", + " onChange: (value) => (this.sizePercent = value),", + " },", + " {", + " type: 'slider',", + " id: 'Strength',", + " tooltip: 'Brush strength',", + " iconUrl: toolIcons.Strength,", + " min: 5,", + " max: 100,", + " value: this.strengthPercent,", + " onChange: (value) => (this.strengthPercent = value),", + " },", + " { type: 'button', id: 'Undo', tooltip: 'Undo the last stroke on this terrain', iconUrl: toolIcons.Undo, onClick: () => this._undo() },", + " ];", + " }", + "}", + "", + "const editorTools = new TerrainEditorTools();", + "if (gdjs.registerInGameEditorPostStepCallback) {", + " gdjs.registerInGameEditorPostStepCallback((editor) => editorTools.update(editor));", + "}", + "", + "gdjs.__terrain3DExtension = {", + " Terrain,", + " // Used by the terrain grass and the scattered models.", + " getWorldScale,", + " getGrounds,", + " patchShaderCode,", + " createRandom,", + " findHighestGround,", + " GroundPlacement,", + " svgIcon,", + " toolIcons,", + " // Exposed for tests.", + " TerrainBodyUpdater,", + " TerrainData,", + " SampleRectangle,", + " brushes,", + " edits,", + " codec,", + " relief,", + "};" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [], + "objectGroups": [] + }, + { + "description": "Define grass helper classes JavaScript code.", + "fullName": "Define grass helper classes", + "functionType": "Action", + "name": "DefineGrassHelperClasses", + "private": true, + "sentence": "Define grass helper classes JavaScript code", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "if (gdjs.__terrainGrassExtension) {", + " return;", + "}", + "// Blades grow on the grounds of the scene: the 3D terrains (see `getGrounds`", + "// in the terrain helper).", + "", + "// Size of the object inner area: instances are scaled from it.", + "const AREA_SIZE = 1000;", + "const AREA_DEPTH = 40;", + "const CHUNK_SIZE = 500;", + "const MAX_BENDING_OBJECTS = 8;", + "", + "/**", + " * A blade: a thin, tapered strip. Its width and height are 1: blades are", + " * scaled by the shader.", + " */", + "const createBladeGeometry = () => {", + " const halfWidths = [0.5, 0.42, 0.28, 0];", + " const heights = [0, 0.35, 0.7, 1];", + " const positions = [];", + " halfWidths.forEach((halfWidth, level) => {", + " positions.push(-halfWidth, 0, heights[level]);", + " if (halfWidth > 0) positions.push(halfWidth, 0, heights[level]);", + " });", + " const indices = [0, 1, 2, 1, 3, 2, 2, 3, 4, 3, 5, 4, 4, 5, 6];", + " const geometry = new THREE.InstancedBufferGeometry();", + " geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));", + " geometry.setAttribute('normal', new THREE.Float32BufferAttribute(new Array(positions.length).fill(0), 3));", + " geometry.setIndex(indices);", + " return geometry;", + "};", + "", + "const vertexShaderDeclarations = `", + "attribute vec4 grassRoot;", + "attribute vec2 grassShape;", + "// The tint of the ground at the root (linear, from 0 to 1).", + "attribute vec3 grassGroundTint;", + "uniform mat3 grassWorldToLocal;", + "uniform float grassWorldScale;", + "uniform float grassBladeWidth;", + "uniform float grassTime;", + "uniform float grassWindStrength;", + "uniform float grassWindSpeed;", + "uniform vec4 grassBenders[${MAX_BENDING_OBJECTS}];", + "uniform vec3 grassBaseColor;", + "uniform vec3 grassTipColor;", + "varying vec3 vGrassColor;", + "`;", + "const vertexShaderPosition = `", + "// Blades are shaped in scene units (with Y flipped like the 3D world), then", + "// moved back in the (scaled) object space.", + "float bladeHeight = position.z;", + "float grassAngle = grassRoot.w;", + "float bladeWidth = position.x * grassBladeWidth;", + "vec3 blade = vec3(", + " bladeWidth * cos(grassAngle),", + " bladeWidth * sin(grassAngle),", + " bladeHeight * grassShape.x", + ");", + "vec3 rootWorld = (modelMatrix * vec4(grassRoot.xyz, 1.0)).xyz * grassWorldScale;", + "// Tips bend more than the base.", + "float bendFactor = bladeHeight * bladeHeight * grassShape.x;", + "float gust = sin(grassTime * grassWindSpeed + rootWorld.x * 0.013 + rootWorld.y * 0.011);", + "float flutter = sin(grassTime * grassWindSpeed * 2.7 + grassShape.y * 40.0) * 0.3;", + "blade.xy += vec2(0.8, 0.5) * (gust + flutter + 0.6) * grassWindStrength * 0.35 * bendFactor;", + "for (int i = 0; i < ${MAX_BENDING_OBJECTS}; i++) {", + " vec4 bender = grassBenders[i];", + " if (bender.z <= 0.0) continue;", + " vec2 away = rootWorld.xy - bender.xy;", + " float distanceToBender = length(away);", + " float push = 1.0 - smoothstep(bender.z * 0.3, bender.z, distanceToBender);", + " if (push > 0.0) {", + " blade.xy += normalize(away + vec2(0.001)) * push * bendFactor * 0.9;", + " blade.z *= 1.0 - push * 0.5;", + " }", + "}", + "vec3 transformed = grassRoot.xyz + grassWorldToLocal * (blade / grassWorldScale);", + "vGrassColor = mix(grassBaseColor, grassTipColor, bladeHeight) * (0.85 + 0.3 * grassShape.y) * grassGroundTint;", + "`;", + "const vertexShaderNormal = `", + "// Blades are lit like the ground below them.", + "transformedNormal = normalize((viewMatrix * vec4(0.0, 0.0, 1.0, 0.0)).xyz);", + "`;", + "const fragmentShaderDeclarations = `", + "varying vec3 vGrassColor;", + "`;", + "const fragmentShaderColor = `", + "diffuseColor.rgb *= vGrassColor;", + "`;", + "// Both faces of blades are lit like the ground (not flipped for back faces).", + "const fragmentShaderNormal = `", + "#include ", + "normal = normalize(vNormal);", + "nonPerturbedNormal = normal;", + "`;", + "", + "const { getWorldScale, patchShaderCode, createRandom, findHighestGround, GroundPlacement } = gdjs.__terrain3DExtension;", + "", + "/**", + " * The grass of a terrain grass object: blades drawn with GPU instancing, in chunks", + " * that are hidden when off-screen and thinned out with the distance. They grow", + " * on the grounds below, like 3D terrains (only where a given layer is painted).", + " */", + "class Grass {", + " /** @param {gdjs.CustomRuntimeObject3D} object */", + " constructor(object) {", + " this.object = object;", + " this.group = new THREE.Group();", + " object.get3DRendererObject().add(this.group);", + " this.bladeGeometry = createBladeGeometry();", + " /** @type {Array<{mesh: THREE.Mesh, blades: number, center: THREE.Vector3}>} */", + " this.chunks = [];", + " this.placement = new GroundPlacement(object.getRuntimeScene());", + " this._groundAtBlade = { ground: null, z: 0 };", + " this.uniforms = {", + " grassWorldToLocal: { value: new THREE.Matrix3() },", + " // Scene units by 3D world unit.", + " grassWorldScale: { value: 1 },", + " grassBladeWidth: { value: 1 },", + " grassTime: { value: 0 },", + " grassWindStrength: { value: 1 },", + " grassWindSpeed: { value: 1 },", + " grassBenders: { value: Array.from({ length: MAX_BENDING_OBJECTS }, () => new THREE.Vector4()) },", + " grassBaseColor: { value: new THREE.Color() },", + " grassTipColor: { value: new THREE.Color() },", + " };", + " this.material = new THREE.MeshStandardMaterial({ roughness: 1, metalness: 0, side: THREE.DoubleSide });", + " this.material.onBeforeCompile = (shader) => {", + " Object.assign(shader.uniforms, this.uniforms);", + " shader.vertexShader = patchShaderCode(shader.vertexShader, [", + " ['#include ', '#include \\n' + vertexShaderDeclarations],", + " ['#include ', vertexShaderPosition],", + " ['#include ', '#include \\n' + vertexShaderNormal],", + " ], 'TerrainGrass');", + " shader.fragmentShader = patchShaderCode(shader.fragmentShader, [", + " ['#include ', '#include \\n' + fragmentShaderDeclarations],", + " ['#include ', '#include \\n' + fragmentShaderColor],", + " ['#include ', fragmentShaderNormal],", + " ], 'TerrainGrass');", + " };", + " this.material.customProgramCacheKey = () => 'TerrainGrass';", + "", + " this._matrix = new THREE.Matrix4();", + " this._cameraPosition = new THREE.Vector3();", + " this._chunkPosition = new THREE.Vector3();", + " this._scale = new THREE.Vector3();", + " this.updateFromProperties();", + " }", + "", + " updateFromProperties() {", + " const { object } = this;", + " this.uniforms.grassBaseColor.value.set(gdjs.rgbOrHexStringToNumber(object._getBaseColor()));", + " this.uniforms.grassTipColor.value.set(gdjs.rgbOrHexStringToNumber(object._getTipColor()));", + " this.uniforms.grassWindStrength.value = object._getWindStrength();", + " this.uniforms.grassWindSpeed.value = object._getWindSpeed();", + " this.uniforms.grassBladeWidth.value = Math.max(object._getBladeWidth(), 0);", + " this.material.needsUpdate = true;", + " this.placement.placeAgain();", + " }", + "", + " _getPlacementKey() {", + " const { object } = this;", + " return [", + " object.getX(), object.getY(), object.getZ(), object.getWidth(), object.getHeight(), object.getDepth(),", + " object._getDensity(), object._getBladeHeight(), object._getGroundLayer(), object._getMaxSlope(),", + " object._getSeed(),", + " ].join('|');", + " }", + "", + " /** Place the blades again, for example after the ground or the object changed. */", + " rebuild() {", + " const grounds = Array.from(this.placement.grounds);", + " for (const chunk of this.chunks) {", + " chunk.mesh.removeFromParent();", + " chunk.mesh.geometry.dispose();", + " }", + " this.chunks.length = 0;", + "", + " const { object } = this;", + " const width = object.getWidth();", + " const height = object.getHeight();", + " const scaleX = width / AREA_SIZE;", + " const scaleY = height / AREA_SIZE;", + " const scaleZ = Math.max(object.getDepth() / AREA_DEPTH, 0.0001);", + " const bladesPerChunk = Math.round((Math.max(object._getDensity(), 0) * CHUNK_SIZE * CHUNK_SIZE) / 10000);", + " const groundLayer = parseInt(object._getGroundLayer(), 10) || 0;", + " const maxSlope = object._getMaxSlope();", + " const chunksX = Math.max(Math.ceil(width / CHUNK_SIZE), 1);", + " const chunksY = Math.max(Math.ceil(height / CHUNK_SIZE), 1);", + " const nextRandom = createRandom(object._getSeed() * 7919 + 1);", + " const bladeHeight = Math.max(object._getBladeHeight(), 0);", + "", + " for (let chunkY = 0; chunkY < chunksY; chunkY++) {", + " for (let chunkX = 0; chunkX < chunksX; chunkX++) {", + " const roots = new Float32Array(bladesPerChunk * 4);", + " const shapes = new Float32Array(bladesPerChunk * 2);", + " const groundTints = new Uint8Array(bladesPerChunk * 3).fill(255);", + " let blades = 0;", + " let minZ = Infinity;", + " let maxZ = -Infinity;", + " for (let attempt = 0; attempt < bladesPerChunk; attempt++) {", + " // In scene units, relative to the object.", + " const x = (chunkX + nextRandom()) * CHUNK_SIZE;", + " const y = (chunkY + nextRandom()) * CHUNK_SIZE;", + " const angle = nextRandom() * Math.PI * 2;", + " const heightFactor = 0.6 + 0.8 * nextRandom();", + " const tint = nextRandom();", + " const growth = nextRandom();", + " if (x > width || y > height) continue;", + " const sceneX = object.getX() + x;", + " const sceneY = object.getY() + y;", + " const { ground, z } = findHighestGround(grounds, sceneX, sceneY, this._groundAtBlade);", + " const groundZ = ground ? z : object.getZ();", + " if (ground) {", + " if (groundLayer > 0 && growth > ground.getLayerWeightAt(sceneX, sceneY, groundLayer)) continue;", + " if (ground.getSlopeAt(sceneX, sceneY) > maxSlope) continue;", + " if (typeof ground.getTintAt === 'function') {", + " const tint = ground.getTintAt(sceneX, sceneY);", + " for (let channel = 0; channel < 3; channel++) {", + " groundTints[blades * 3 + channel] = Math.round(255 * Math.pow(tint[channel] / 255, 2.2));", + " }", + " }", + " }", + " const localZ = (groundZ - object.getZ()) / scaleZ;", + " roots[blades * 4] = x / scaleX;", + " roots[blades * 4 + 1] = y / scaleY;", + " roots[blades * 4 + 2] = localZ;", + " roots[blades * 4 + 3] = angle;", + " shapes[blades * 2] = heightFactor * bladeHeight;", + " shapes[blades * 2 + 1] = tint;", + " minZ = Math.min(minZ, localZ);", + " maxZ = Math.max(maxZ, localZ);", + " blades++;", + " }", + " if (blades === 0) continue;", + "", + " const geometry = new THREE.InstancedBufferGeometry();", + " geometry.index = this.bladeGeometry.index;", + " geometry.setAttribute('position', this.bladeGeometry.attributes.position);", + " geometry.setAttribute('normal', this.bladeGeometry.attributes.normal);", + " geometry.setAttribute('grassRoot', new THREE.InstancedBufferAttribute(roots.subarray(0, blades * 4), 4));", + " geometry.setAttribute('grassShape', new THREE.InstancedBufferAttribute(shapes.subarray(0, blades * 2), 2));", + " geometry.setAttribute('grassGroundTint', new THREE.InstancedBufferAttribute(groundTints.subarray(0, blades * 3), 3, true));", + " geometry.instanceCount = blades;", + " // The furthest tips can go with the wind and the bending objects (see the shader).", + " const maxBladeHeight = bladeHeight * 1.4;", + " const lean =", + " (0.35 * 1.9 * 0.943 * Math.max(object._getWindStrength(), 0) + 0.9) * maxBladeHeight +", + " Math.max(object._getBladeWidth(), 0) / 2;", + " const leanX = lean / scaleX;", + " const leanY = lean / scaleY;", + " geometry.boundingBox = new THREE.Box3(", + " new THREE.Vector3((chunkX * CHUNK_SIZE) / scaleX - leanX, (chunkY * CHUNK_SIZE) / scaleY - leanY, minZ),", + " new THREE.Vector3(", + " Math.min((chunkX + 1) * CHUNK_SIZE, width) / scaleX + leanX,", + " Math.min((chunkY + 1) * CHUNK_SIZE, height) / scaleY + leanY,", + " maxZ + (bladeHeight * 1.4) / scaleZ", + " )", + " );", + " geometry.boundingSphere = geometry.boundingBox.getBoundingSphere(new THREE.Sphere());", + " const mesh = new THREE.Mesh(geometry, this.material);", + " mesh.receiveShadow = true;", + " // Raycasts would only test one blade: the selection plane is used instead.", + " mesh.raycast = () => {};", + " this.group.add(mesh);", + " this.chunks.push({ mesh, blades, center: geometry.boundingSphere.center.clone() });", + " }", + " }", + " }", + "", + " _updateBendingObjects() {", + " const benders = this.uniforms.grassBenders.value;", + " const objectName = this.object._getBendingObject();", + " const bendingObjects = objectName ? this.object.getInstanceContainer().getObjects(objectName) || [] : [];", + " for (let index = 0; index < MAX_BENDING_OBJECTS; index++) {", + " const bendingObject = bendingObjects[index];", + " if (!bendingObject) {", + " benders[index].set(0, 0, 0, 0);", + " continue;", + " }", + " // Blades are pushed in the 3D scene, which is flipped on Y.", + " const radius = Math.max(bendingObject.getWidth(), bendingObject.getHeight()) * 1.2;", + " benders[index].set(bendingObject.getCenterXInScene(), -bendingObject.getCenterYInScene(), radius, 0);", + " }", + " }", + "", + " /**", + " * @param {number} elapsedSeconds", + " * @param {number} time In seconds, the same for all grass objects.", + " */", + " update(elapsedSeconds, time, camera) {", + " if (this.placement.shouldPlaceAgain(elapsedSeconds, this._getPlacementKey())) {", + " this.rebuild();", + " }", + "", + " this.uniforms.grassTime.value = time;", + " this.uniforms.grassWorldScale.value = getWorldScale(this.object);", + " // The object transformation is applied just before rendering, it's needed now.", + " this.object.getRenderer().ensureUpToDate();", + " this.group.updateWorldMatrix(true, false);", + " this.uniforms.grassWorldToLocal.value.setFromMatrix4(this._matrix.copy(this.group.matrixWorld).invert());", + " this._updateBendingObjects();", + " if (!camera) return;", + "", + " // Far chunks are hidden, and thinned out before (in 3D world units).", + " const fadeDistance = Math.max(this.object._getFadeDistance(), 1) / getWorldScale(this.object);", + " this._cameraPosition.setFromMatrixPosition(camera.matrixWorld);", + " for (const chunk of this.chunks) {", + " const distance = this._chunkPosition", + " .copy(chunk.center)", + " .applyMatrix4(this.group.matrixWorld)", + " .distanceTo(this._cameraPosition);", + " const density = gdjs.evtTools.common.clamp((fadeDistance - distance) / (fadeDistance * 0.5), 0, 1);", + " chunk.mesh.visible = density > 0;", + " chunk.mesh.geometry.instanceCount = Math.ceil(chunk.blades * density);", + " }", + " }", + "", + " dispose() {", + " for (const chunk of this.chunks) {", + " chunk.mesh.geometry.dispose();", + " }", + " this.bladeGeometry.dispose();", + " this.material.dispose();", + " this.group.removeFromParent();", + " }", + "}", + "", + "gdjs.__terrainGrassExtension = { Grass };" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [], + "objectGroups": [] + }, + { + "description": "Define scattered models helper classes JavaScript code.", + "fullName": "Define scattered models helper classes", + "functionType": "Action", + "name": "DefineScatterHelperClasses", + "private": true, + "sentence": "Define scattered models helper classes JavaScript code", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "if (gdjs.__terrainScatterExtension) {", + " return;", + "}", + "// Copies grow on the grounds of the scene, like the terrain grass (see", + "// `getGrounds` in the terrain helper).", + "", + "const {", + " getWorldScale,", + " getGrounds,", + " patchShaderCode,", + " createRandom,", + " findHighestGround,", + " GroundPlacement,", + " SampleRectangle,", + " brushes,", + " codec,", + " svgIcon,", + " toolIcons,", + "} = gdjs.__terrain3DExtension;", + "", + "// Size of the object inner area: instances are scaled from it.", + "const AREA_SIZE = 2000;", + "const AREA_DEPTH = 200;", + "// The painted density is stored in a grid of this many cells on each side,", + "// whatever the size of the object.", + "const MASK_RESOLUTION = 256;", + "const MASK_FORMAT_VERSION = '1';", + "// Copies are hidden in chunks: big chunks mean fewer draw calls, small ones", + "// hide more copies out of the screen.", + "const MIN_CHUNK_SIZE = 500;", + "const MAX_CHUNK_SIZE = 2000;", + "// Copies shrink to nothing over this part of the draw distance, instead of popping out.", + "const FADE_PART = 0.15;", + "const MAX_COPIES = 200000;", + "// Prisms of the trunk collisions.", + "const TRUNK_SIDES = 6;", + "", + "const clamp01 = (value) => (value < 0 ? 0 : value > 1 ? 1 : value);", + "", + "/**", + " * @returns {number} A number for a cell of the copies grid, changing with the seed.", + " */", + "const hashCell = (seed, i, j) => {", + " let h = Math.imul(i, 374761393) + Math.imul(j, 668265263) + Math.imul(seed, 982451653);", + " h = Math.imul(h ^ (h >>> 13), 1274126177);", + " return (h ^ (h >>> 16)) >>> 0;", + "};", + "", + "/**", + " * How dense copies are, from 0 to 1, in a grid covering the object: painted", + " * with the editor brushes, the edits or events.", + " */", + "class DensityMask {", + " /** @param {number} fillValue From 0 to 255. */", + " constructor(fillValue) {", + " // `resolution` and `size` let terrain brushes paint it.", + " this.resolution = MASK_RESOLUTION;", + " this.size = MASK_RESOLUTION + 1;", + " this.values = new Uint8Array(this.size * this.size).fill(fillValue);", + " }", + "", + " /**", + " * @param {number} u From 0 to 1, along the object width.", + " * @param {number} v From 0 to 1, along the object height.", + " * @returns {number} The density, from 0 to 1, interpolated between samples.", + " */", + " sample(u, v) {", + " const x = clamp01(u) * this.resolution;", + " const y = clamp01(v) * this.resolution;", + " const i = Math.min(Math.floor(x), this.resolution - 1);", + " const j = Math.min(Math.floor(y), this.resolution - 1);", + " const fx = x - i;", + " const fy = y - j;", + " const value = (i, j) => this.values[j * this.size + i];", + " const top = value(i, j) + fx * (value(i + 1, j) - value(i, j));", + " const bottom = value(i, j + 1) + fx * (value(i + 1, j + 1) - value(i, j + 1));", + " return (top + fy * (bottom - top)) / 255;", + " }", + "", + " /**", + " * Bring the density towards a target around a segment: fully up to 70%", + " * of the radius, then fading out (forests have dense centers and soft edges).", + " * @param {{cellWidth: number, cellHeight: number, ax: number, ay: number, bx: number, by: number, radius: number}} stroke", + " * @param {number} density The target, from 0 to 1.", + " * @param {number} strength From 0 to 1.", + " * @param {SampleRectangle} changedSamples", + " */", + " paint(stroke, density, strength, changedSamples) {", + " const { values } = this;", + " const target = Math.round(clamp01(density) * 255);", + " brushes.forEachSample(", + " this,", + " stroke,", + " (index, falloff) => {", + " // The falloff of terrain brushes is (1 - distance² / radius²)².", + " const weight = clamp01(2 * Math.sqrt(falloff));", + " // Rounded towards the target, so that light strokes still paint.", + " const change = (target - values[index]) * clamp01(weight * strength);", + " values[index] += change > 0 ? Math.ceil(change) : Math.floor(change);", + " },", + " changedSamples", + " );", + " }", + "", + " /** @returns {DensityMask} */", + " clone() {", + " const mask = new DensityMask(0);", + " mask.values.set(this.values);", + " return mask;", + " }", + "}", + "", + "/**", + " * Edits written in the Edits property: a JSON list of brush strokes, like", + " * `[{\"tool\": \"add\", \"x\": 0.5, \"y\": 0.5, \"radius\": 0.2}]`. Positions and", + " * radius are fractions of the object size, like the terrain edits.", + " */", + "const applyEdits = (mask, text) => {", + " if (!text.trim()) return;", + " let editList;", + " try {", + " editList = JSON.parse(text);", + " } catch (error) {", + " console.warn('The edits of scattered models are not valid JSON: ' + error.message);", + " return;", + " }", + " if (!Array.isArray(editList)) {", + " console.warn('The edits of scattered models must be a JSON list (like [{\"tool\": \"add\", ...}]).');", + " return;", + " }", + " const numberOr = (value, defaultValue) => (typeof value === 'number' && isFinite(value) ? value : defaultValue);", + " const changedSamples = new SampleRectangle();", + " for (const edit of editList) {", + " if (!edit || typeof edit !== 'object') continue;", + " const x = numberOr(edit.x, 0.5);", + " const y = numberOr(edit.y, 0.5);", + " const stroke = {", + " cellWidth: 1 / mask.resolution,", + " cellHeight: 1 / mask.resolution,", + " ax: x,", + " ay: y,", + " bx: numberOr(edit.toX, x),", + " by: numberOr(edit.toY, y),", + " radius: numberOr(edit.radius, 0.1),", + " };", + " const strength = clamp01(numberOr(edit.strength, 1));", + " if (edit.tool === 'add') {", + " mask.paint(stroke, numberOr(edit.density, 1), strength, changedSamples);", + " } else if (edit.tool === 'erase') {", + " mask.paint(stroke, 0, strength, changedSamples);", + " } else {", + " console.warn('Unknown tool in the edits of scattered models: \"' + edit.tool + '\" (use add or erase).');", + " }", + " }", + "};", + "", + "/**", + " * The density painted in the editor, stored as its difference with the", + " * density from the properties (the coverage and the edits), like the terrain", + " * sculpt data.", + " */", + "const maskCodec = {", + " /**", + " * @param {DensityMask} mask", + " * @param {DensityMask} baseMask", + " */", + " encode(mask, baseMask) {", + " const changes = new Int32Array(mask.values.length);", + " let hasChanges = false;", + " for (let index = 0; index < changes.length; index++) {", + " changes[index] = mask.values[index] - baseMask.values[index];", + " if (changes[index] !== 0) hasChanges = true;", + " }", + " if (!hasChanges) return '';", + " return MASK_FORMAT_VERSION + ';' + MASK_RESOLUTION + ';' + codec.encodeGrids([changes], mask.size);", + " },", + "", + " /**", + " * @param {string} text", + " * @param {DensityMask} baseMask", + " * @returns {DensityMask | null} null if the text is empty or not valid.", + " */", + " decode(text, baseMask) {", + " if (!text) return null;", + " const parts = text.split(';');", + " if (parts[0] !== MASK_FORMAT_VERSION || parseInt(parts[1], 10) !== MASK_RESOLUTION) return null;", + " try {", + " const mask = baseMask.clone();", + " const [changes] = codec.decodeGrids(parts[2], mask.size, 1);", + " for (let index = 0; index < changes.length; index++) {", + " // The base may have changed since the data was saved.", + " mask.values[index] = Math.min(Math.max(mask.values[index] + changes[index], 0), 255);", + " }", + " return mask;", + " } catch (error) {", + " console.error('The painted data of scattered models could not be read:', error);", + " return null;", + " }", + " },", + "};", + "", + "const fadeShaderDeclarations = `", + "uniform float scatterDrawDistance;", + "`;", + "// Instance matrices are applied after this: copies shrink towards their base.", + "const fadeShaderCode = `", + "#ifdef USE_INSTANCING", + "vec3 scatterCopyPosition = (modelMatrix * instanceMatrix * vec4(0.0, 0.0, 0.0, 1.0)).xyz;", + "transformed *= clamp(", + " (scatterDrawDistance - distance(scatterCopyPosition, cameraPosition)) / (scatterDrawDistance * ${FADE_PART}),", + " 0.0,", + " 1.0", + ");", + "#endif", + "`;", + "", + "const flipTriangles = (geometry) => {", + " const index = geometry.index;", + " for (let offset = 0; offset + 2 < index.count; offset += 3) {", + " const second = index.getX(offset + 1);", + " index.setX(offset + 1, index.getX(offset + 2));", + " index.setX(offset + 2, second);", + " }", + "};", + "", + "/**", + " * The parts of a model, ready to be drawn with instancing: each glTF", + " * primitive with its material. Geometries are moved so that the model base", + " * is at the origin, its height is 1 and it stands along Z (like 3D model", + " * objects with a 90° rotation around X).", + " * @param {THREE.Object3D} modelScene", + " * @param {object} uniforms Uniforms added to the materials.", + " * @returns {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}}", + " */", + "const createModelParts = (modelScene, uniforms) => {", + " modelScene.updateMatrixWorld(true);", + " const rootInverse = modelScene.matrixWorld.clone().invert();", + " const standUp = new THREE.Matrix4().makeRotationX(Math.PI / 2);", + " /** @type {Array<{mesh: THREE.Mesh, matrix: THREE.Matrix4}>} */", + " const meshes = [];", + " const box = new THREE.Box3();", + " modelScene.traverse((node) => {", + " if (!node.isMesh || !node.geometry || !node.geometry.attributes.position) return;", + " const matrix = standUp.clone().multiply(rootInverse).multiply(node.matrixWorld);", + " if (!node.geometry.boundingBox) node.geometry.computeBoundingBox();", + " box.union(node.geometry.boundingBox.clone().applyMatrix4(matrix));", + " meshes.push({ mesh: node, matrix });", + " });", + " if (box.isEmpty()) return { parts: [], width: 0, depth: 0 };", + "", + " const height = Math.max(box.max.z - box.min.z, 0.000001);", + " // The Y axis of the scene goes down: the model is flipped, like 3D model objects.", + " const normalization = new THREE.Matrix4()", + " .makeScale(1 / height, -1 / height, 1 / height)", + " .multiply(", + " new THREE.Matrix4().makeTranslation(", + " -(box.min.x + box.max.x) / 2,", + " -(box.min.y + box.max.y) / 2,", + " -box.min.z", + " )", + " );", + " const parts = meshes.map(({ mesh, matrix }) => {", + " let geometry = mesh.geometry.clone();", + " if (!geometry.index) {", + " const indices = Array.from({ length: geometry.attributes.position.count }, (_, index) => index);", + " geometry.setIndex(indices);", + " }", + " geometry.applyMatrix4(normalization.clone().multiply(matrix));", + " // The flip turned the faces inside out.", + " flipTriangles(geometry);", + " geometry.computeBoundingSphere();", + " geometry.computeBoundingBox();", + " const patchMaterial = (originalMaterial) => {", + " const material = originalMaterial.clone();", + " // Like 3D model objects (\"Standard without metalness\"), which look better without an environment map.", + " if (material.metalness) material.metalness = 0;", + " material.onBeforeCompile = (shader) => {", + " Object.assign(shader.uniforms, uniforms);", + " shader.vertexShader = patchShaderCode(shader.vertexShader, [", + " ['#include ', '#include \\n' + fadeShaderDeclarations],", + " ['#include ', '#include \\n' + fadeShaderCode],", + " ], 'ScatteredModels');", + " };", + " material.customProgramCacheKey = () => 'ScatteredModels';", + " return material;", + " };", + " const material = Array.isArray(mesh.material) ? mesh.material.map(patchMaterial) : patchMaterial(mesh.material);", + " return { geometry, material };", + " });", + " return {", + " parts,", + " width: (box.max.x - box.min.x) / height,", + " depth: (box.max.y - box.min.y) / height,", + " };", + "};", + "", + "/**", + " * A copy of the model: its base position in the scene, its scale (the", + " * height of the copy) and the key of its cell in the grid of copies.", + " * @typedef {{x: number, y: number, z: number, angle: number, height: number, key: number}} Copy", + " */", + "", + "/**", + " * The scattered models of an object: copies of a 3D model placed on the", + " * grounds of the scene, drawn with GPU instancing in chunks that are hidden", + " * when far or off-screen.", + " */", + "class Scatter {", + " /** @param {gdjs.CustomRuntimeObject3D} object */", + " constructor(object) {", + " this.object = object;", + " this.group = new THREE.Group();", + " object.get3DRendererObject().add(this.group);", + " /** @type {Array<{firstCellI: number, firstCellJ: number, meshes: THREE.InstancedMesh[], copies: Copy[], center: THREE.Vector3, radius: number}>} */", + " this.chunks = [];", + " /**", + " * The grid of copies: one copy at most in each cell, cells grouped in chunks.", + " * @type {{spacing: number, cellsPerChunk: number, cellsX: number, cellsY: number} | null}", + " */", + " this.grid = null;", + " this.placement = new GroundPlacement(object.getRuntimeScene());", + " this.uniforms = {", + " // In 3D world units.", + " scatterDrawDistance: { value: 1 },", + " };", + " /** @type {{parts: Array<{geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[]}>, width: number, depth: number}} */", + " this.model = { parts: [], width: 0, depth: 0 };", + " this.modelResourceName = '';", + " this.loadingModelResourceName = '';", + " /**", + " * Copies removed by events (for example, cut trees), by key.", + " * @type {Map}", + " */", + " this.removedCopies = new Map();", + " /** Increased each time copies change, for the collisions. */", + " this.version = 0;", + " this.isDisposed = false;", + " /** The last value of the painted data property read or saved. */", + " this.paintData = '';", + " /**", + " * Values saved by the editor tools: the editor sends them back with a", + " * hot-reload, possibly after newer ones were saved.", + " * @type {string[]}", + " */", + " this.savedPaintData = [];", + " this.maskSource = '';", + " /** @type {DensityMask} */", + " this.baseMask = new DensityMask(255);", + " /** @type {DensityMask} */", + " this.mask = this.baseMask;", + " this.changedSamples = new SampleRectangle();", + " this._stroke = { cellWidth: 1, cellHeight: 1, ax: 0, ay: 0, bx: 0, by: 0, radius: 0 };", + " this._groundAtCopy = { ground: null, z: 0 };", + " this._matrix = new THREE.Matrix4();", + " this._raycastSphere = new THREE.Sphere();", + " this._raycastRay = new THREE.Ray();", + " this._raycastBox = new THREE.Box3();", + " this._raycastInverseMatrix = new THREE.Matrix4();", + " this._raycastScale = new THREE.Vector3();", + " /** Placed on a copy to test the triangles of the model. */", + " this._raycastMesh = new THREE.Mesh();", + " /** @type {THREE.Intersection[]} */", + " this._raycastHits = [];", + " this._color = new THREE.Color();", + " this._tint = new THREE.Color();", + " this._cameraPosition = new THREE.Vector3();", + " this._chunkPosition = new THREE.Vector3();", + " /**", + " * Trunks, for the 3D physics behavior and navigation meshes. Always given,", + " * empty without trunk collisions: otherwise these behaviors would use the", + " * object box, covering the whole area.", + " * @type {gdjs.Surface}", + " */", + " this.surface = {", + " getVersion: () => this.version,", + " getChangedArea: () => null,", + " getTriangles: () => this._getTrunkTriangles(),", + " getHeightField: () => null,", + " };", + " /** The trunk collision properties, changing the surface. */", + " this.trunkKey = '';", + " this.loadMaskFromProperties();", + " this.updateFromProperties();", + " // Before the behaviors create their shapes.", + " this._updateSurface();", + " }", + "", + " _getMaskSource() {", + " return this.object._getCoverage() + '|' + this.object._getEdits();", + " }", + "", + " loadMaskFromProperties() {", + " const { object } = this;", + " this.maskSource = this._getMaskSource();", + " this.paintData = object._getPaintData();", + " this.baseMask = new DensityMask(object._getCoverage() === 'Painted' ? 0 : 255);", + " applyEdits(this.baseMask, object._getEdits());", + " this.mask = maskCodec.decode(this.paintData, this.baseMask) || this.baseMask.clone();", + " this.placement.placeAgain();", + " }", + "", + " onHotReloading() {", + " const paintData = this.object._getPaintData();", + " const savedIndex = this.savedPaintData.indexOf(paintData);", + " if (savedIndex !== -1 && this._getMaskSource() === this.maskSource) {", + " // The object already has this data, or newer changes.", + " this.savedPaintData.splice(0, savedIndex + 1);", + " } else if (paintData !== this.paintData || this._getMaskSource() !== this.maskSource) {", + " this.savedPaintData.length = 0;", + " this.loadMaskFromProperties();", + " }", + " this.updateFromProperties();", + " }", + "", + " /** @returns {string} The data to save in the painted data property. */", + " save() {", + " this.paintData = maskCodec.encode(this.mask, this.baseMask);", + " this.savedPaintData.push(this.paintData);", + " return this.paintData;", + " }", + "", + " updateFromProperties() {", + " const { object } = this;", + " const modelResourceName = object._getModel();", + " if (modelResourceName !== this.modelResourceName) {", + " this.modelResourceName = modelResourceName;", + " this._loadModel();", + " }", + " this.placement.placeAgain();", + " }", + "", + " _disposeModel() {", + " for (const part of this.model.parts) {", + " part.geometry.dispose();", + " for (const material of Array.isArray(part.material) ? part.material : [part.material]) {", + " material.dispose();", + " }", + " }", + " this.model = { parts: [], width: 0, depth: 0 };", + " }", + "", + " _loadModel() {", + " this._disposeChunks(this.chunks);", + " this._disposeModel();", + " const resourceName = this.modelResourceName;", + " if (!resourceName) return;", + " const game = this.object.getRuntimeScene().getGame();", + " const modelManager = game.getModel3DManager();", + " const gltf = modelManager.getModel(resourceName);", + " if (gltf === modelManager._invalidModel) {", + " // Not loaded yet, for example a model just chosen in the editor.", + " if (this.loadingModelResourceName === resourceName) return;", + " this.loadingModelResourceName = resourceName;", + " game.getResourceLoader()", + " .loadResource(resourceName)", + " .then(() => {", + " if (this.isDisposed || this.modelResourceName !== resourceName) return;", + " this.loadingModelResourceName = '';", + " if (modelManager.getModel(resourceName) !== modelManager._invalidModel) {", + " this._loadModel();", + " this.placement.placeAgain();", + " }", + " })", + " .catch(() => {});", + " return;", + " }", + " this.model = createModelParts(gltf.scene, this.uniforms);", + " }", + "", + " _getPlacementKey() {", + " const { object } = this;", + " return [", + " object.getX(), object.getY(), object.getZ(), object.getWidth(), object.getHeight(), object.getDepth(),", + " object._getDensity(), object._getSize(), object._getMinScale(), object._getMaxScale(),", + " object._getRandomRotation(), object._getRandomTint(), object._getGroundLayer(), object._getMaxSlope(),", + " object._getMinGroundZ(), object._getMaxGroundZ(), object._getSeed(), object._getDrawDistance(),", + " object._getCastShadow(), object._getReceiveShadow(), this.model.parts.length,", + " ].join('|');", + " }", + "", + " _getChunkSize() {", + " return Math.min(Math.max(this.object._getDrawDistance() / 4, MIN_CHUNK_SIZE), MAX_CHUNK_SIZE);", + " }", + "", + " /** @returns {number} The distance between copies (one copy per cell of this size at most). */", + " _getSpacing() {", + " const { object } = this;", + " const density = Math.max(object._getDensity(), 0);", + " if (density <= 0) return 0;", + " const spacing = 100 / Math.sqrt(density);", + " // Too many cells would freeze the game: the density is lowered.", + " const cellCount = (object.getWidth() / spacing) * (object.getHeight() / spacing);", + " return cellCount > MAX_COPIES ? spacing * Math.sqrt(cellCount / MAX_COPIES) : spacing;", + " }", + "", + " /**", + " * Find the copy of a cell of the grid, if there is one.", + " * @returns {Copy | null}", + " */", + " _placeCopy(cellI, cellJ, spacing, grounds) {", + " const { object } = this;", + " const width = object.getWidth();", + " const height = object.getHeight();", + " const nextRandom = createRandom(hashCell(Math.floor(object._getSeed()), cellI, cellJ));", + " // Always drawn in the same order, so that a copy stays the same when properties change.", + " const x = (cellI + nextRandom()) * spacing;", + " const y = (cellJ + nextRandom()) * spacing;", + " const presence = nextRandom();", + " const growth = nextRandom();", + " const scaleFactor = nextRandom();", + " const angle = nextRandom() * Math.PI * 2;", + " if (x > width || y > height) return null;", + " if (presence >= this.mask.sample(x / width, y / height)) return null;", + " const key = cellJ * 65536 + cellI;", + " if (this.removedCopies.has(key)) return null;", + "", + " const sceneX = object.getX() + x;", + " const sceneY = object.getY() + y;", + " const { ground, z } = findHighestGround(grounds, sceneX, sceneY, this._groundAtCopy);", + " const groundZ = ground ? z : object.getZ();", + " if (groundZ < object._getMinGroundZ() || groundZ > object._getMaxGroundZ()) return null;", + " const copyHeight =", + " Math.max(object._getSize(), 0) *", + " (object._getMinScale() + (object._getMaxScale() - object._getMinScale()) * scaleFactor);", + " if (ground) {", + " if (ground.getSlopeAt(sceneX, sceneY) > object._getMaxSlope()) return null;", + " const groundLayer = parseInt(object._getGroundLayer(), 10) || 0;", + " if (groundLayer > 0) {", + " if (growth > ground.getLayerWeightAt(sceneX, sceneY, groundLayer)) return null;", + " // Copies don't stand at the border of their layer, so that trees don't cover roads.", + " const margin = (copyHeight * Math.max(this.model.width, this.model.depth)) / 4;", + " for (const [offsetX, offsetY] of [[margin, 0], [-margin, 0], [0, margin], [0, -margin]]) {", + " if (ground.getLayerWeightAt(sceneX + offsetX, sceneY + offsetY, groundLayer) < 0.5) return null;", + " }", + " }", + " }", + " return {", + " x: sceneX,", + " y: sceneY,", + " z: groundZ,", + " angle: object._getRandomRotation() ? angle : 0,", + " height: copyHeight,", + " key,", + " };", + " }", + "", + " _disposeChunks(chunks) {", + " for (const chunk of chunks) {", + " for (const mesh of chunk.meshes) {", + " mesh.removeFromParent();", + " // Geometries and materials are the ones of the model.", + " mesh.dispose();", + " }", + " }", + " if (chunks === this.chunks) this.chunks = [];", + " }", + "", + " /** Place the copies again, for example after the ground or the object changed. */", + " rebuild() {", + " this._disposeChunks(this.chunks);", + " this.version++;", + " const { object } = this;", + " const spacing = this._getSpacing();", + " if (spacing <= 0 || this.model.parts.length === 0) {", + " this.grid = null;", + " return;", + " }", + " const cellsPerChunk = Math.max(Math.round(this._getChunkSize() / spacing), 1);", + " const grid = {", + " spacing,", + " cellsPerChunk,", + " cellsX: Math.ceil(object.getWidth() / spacing),", + " cellsY: Math.ceil(object.getHeight() / spacing),", + " };", + " this.grid = grid;", + " for (let firstCellJ = 0; firstCellJ < grid.cellsY; firstCellJ += cellsPerChunk) {", + " for (let firstCellI = 0; firstCellI < grid.cellsX; firstCellI += cellsPerChunk) {", + " this._createChunk(firstCellI, firstCellJ);", + " }", + " }", + " }", + "", + " /**", + " * Place again the copies of the chunks touching a rectangle of the scene,", + " * for example after painting.", + " */", + " _rebuildArea(minX, minY, maxX, maxY) {", + " const { grid, object } = this;", + " if (!grid) return;", + " const chunkSize = grid.spacing * grid.cellsPerChunk;", + " const firstChunkI = Math.max(Math.floor((minX - object.getX()) / chunkSize), 0);", + " const firstChunkJ = Math.max(Math.floor((minY - object.getY()) / chunkSize), 0);", + " const lastChunkI = Math.min(Math.floor((maxX - object.getX()) / chunkSize), Math.ceil(grid.cellsX / grid.cellsPerChunk) - 1);", + " const lastChunkJ = Math.min(Math.floor((maxY - object.getY()) / chunkSize), Math.ceil(grid.cellsY / grid.cellsPerChunk) - 1);", + " if (firstChunkI > lastChunkI || firstChunkJ > lastChunkJ) return;", + " const isInArea = (chunk) => {", + " const chunkI = chunk.firstCellI / grid.cellsPerChunk;", + " const chunkJ = chunk.firstCellJ / grid.cellsPerChunk;", + " return chunkI >= firstChunkI && chunkI <= lastChunkI && chunkJ >= firstChunkJ && chunkJ <= lastChunkJ;", + " };", + " this._disposeChunks(this.chunks.filter(isInArea));", + " this.chunks = this.chunks.filter((chunk) => !isInArea(chunk));", + " for (let chunkJ = firstChunkJ; chunkJ <= lastChunkJ; chunkJ++) {", + " for (let chunkI = firstChunkI; chunkI <= lastChunkI; chunkI++) {", + " this._createChunk(chunkI * grid.cellsPerChunk, chunkJ * grid.cellsPerChunk);", + " }", + " }", + " this.version++;", + " }", + "", + " _createChunk(firstCellI, firstCellJ) {", + " const { grid } = this;", + " const grounds = Array.from(this.placement.grounds);", + " /** @type {Copy[]} */", + " const copies = [];", + " for (let cellJ = firstCellJ; cellJ < Math.min(firstCellJ + grid.cellsPerChunk, grid.cellsY); cellJ++) {", + " for (let cellI = firstCellI; cellI < Math.min(firstCellI + grid.cellsPerChunk, grid.cellsX); cellI++) {", + " const copy = this._placeCopy(cellI, cellJ, grid.spacing, grounds);", + " if (copy) copies.push(copy);", + " }", + " }", + " if (copies.length === 0) return;", + "", + " const { object } = this;", + " // Copies are placed in scene units, and the object group is scaled", + " // from its inner area: they are scaled back to keep their proportions.", + " const scaleX = Math.max(object.getWidth() / AREA_SIZE, 0.0001);", + " const scaleY = Math.max(object.getHeight() / AREA_SIZE, 0.0001);", + " const scaleZ = Math.max(object.getDepth() / AREA_DEPTH, 0.0001);", + " const matrices = new Float32Array(copies.length * 16);", + " const tintColor = gdjs.rgbOrHexStringToNumber(object._getRandomTint());", + " const hasTint = tintColor !== 0xffffff;", + " const colors = hasTint ? new Float32Array(copies.length * 3) : null;", + " this._tint.setHex(tintColor);", + " const min = new THREE.Vector3(Infinity, Infinity, Infinity);", + " const max = new THREE.Vector3(-Infinity, -Infinity, -Infinity);", + " copies.forEach((copy, index) => {", + " const cos = Math.cos(copy.angle) * copy.height;", + " const sin = Math.sin(copy.angle) * copy.height;", + " const localX = (copy.x - object.getX()) / scaleX;", + " const localY = (copy.y - object.getY()) / scaleY;", + " const localZ = (copy.z - object.getZ()) / scaleZ;", + " this._matrix.set(", + " cos / scaleX, -sin / scaleX, 0, localX,", + " sin / scaleY, cos / scaleY, 0, localY,", + " 0, 0, copy.height / scaleZ, localZ,", + " 0, 0, 0, 1", + " );", + " this._matrix.toArray(matrices, index * 16);", + " if (colors) {", + " // The tint of each copy is between white and the tint color.", + " this._color.setRGB(1, 1, 1).lerp(this._tint, createRandom(copy.key + 1)());", + " this._color.toArray(colors, index * 3);", + " }", + " const reach = (copy.height * Math.max(this.model.width, this.model.depth)) / 2;", + " min.min(this._chunkPosition.set(copy.x - reach, copy.y - reach, copy.z));", + " max.max(this._chunkPosition.set(copy.x + reach, copy.y + reach, copy.z + copy.height));", + " });", + " const instanceMatrix = new THREE.InstancedBufferAttribute(matrices, 16);", + " const instanceColor = colors ? new THREE.InstancedBufferAttribute(colors, 3) : null;", + " const castShadow = object._getCastShadow();", + " const receiveShadow = object._getReceiveShadow();", + " const meshes = this.model.parts.map((part) => {", + " const mesh = new THREE.InstancedMesh(part.geometry, part.material, 0);", + " // All the parts of the model share the positions of the copies.", + " mesh.instanceMatrix = instanceMatrix;", + " mesh.instanceColor = instanceColor;", + " mesh.count = copies.length;", + " mesh.computeBoundingSphere();", + " mesh.castShadow = castShadow;", + " mesh.receiveShadow = receiveShadow;", + " // The first mesh tests all the parts of the copies.", + " mesh.raycast = () => {};", + " this.group.add(mesh);", + " return mesh;", + " });", + " const center = new THREE.Vector3(", + " ((min.x + max.x) / 2 - object.getX()) / scaleX,", + " ((min.y + max.y) / 2 - object.getY()) / scaleY,", + " ((min.z + max.z) / 2 - object.getZ()) / scaleZ", + " );", + " const chunk = { firstCellI, firstCellJ, meshes, copies, center, radius: min.distanceTo(max) / 2 };", + " // Used by the scene editor to select the object when a copy is clicked.", + " meshes[0].raycast = (raycaster, intersects) => this._raycastChunk(chunk, raycaster, intersects);", + " this.chunks.push(chunk);", + " }", + "", + " /**", + " * Add the hits of a ray on the copies of a chunk, like they are drawn. There", + " * can be thousands of copies: only the ones whose box is crossed by the ray", + " * are tested with the triangles of the model.", + " * @param {{meshes: THREE.InstancedMesh[], copies: Copy[], center: THREE.Vector3, radius: number}} chunk", + " * @param {THREE.Raycaster} raycaster", + " * @param {THREE.Intersection[]} intersects", + " */", + " _raycastChunk(chunk, raycaster, intersects) {", + " const { object, group } = this;", + " this._raycastSphere.center.copy(chunk.center).applyMatrix4(group.matrixWorld);", + " this._raycastSphere.radius = chunk.radius / getWorldScale(object);", + " if (!raycaster.ray.intersectsSphere(this._raycastSphere)) return;", + "", + " // The ray in the scene coordinates, like the copies.", + " const scale = this._raycastScale.set(", + " Math.max(object.getWidth() / AREA_SIZE, 0.0001),", + " Math.max(object.getHeight() / AREA_SIZE, 0.0001),", + " Math.max(object.getDepth() / AREA_DEPTH, 0.0001)", + " );", + " const ray = this._raycastRay.copy(raycaster.ray).applyMatrix4(this._raycastInverseMatrix.copy(group.matrixWorld).invert());", + " ray.origin.multiply(scale).add(this._chunkPosition.set(object.getX(), object.getY(), object.getZ()));", + " ray.direction.multiply(scale);", + "", + " const reachPerHeight = Math.max(this.model.width, this.model.depth) / 2;", + " const instanceMatrices = chunk.meshes[0].instanceMatrix.array;", + " chunk.copies.forEach((copy, index) => {", + " const reach = copy.height * reachPerHeight;", + " this._raycastBox.min.set(copy.x - reach, copy.y - reach, copy.z);", + " this._raycastBox.max.set(copy.x + reach, copy.y + reach, copy.z + copy.height);", + " if (!ray.intersectsBox(this._raycastBox)) return;", + " this._raycastMesh.matrixWorld.fromArray(instanceMatrices, index * 16).premultiply(group.matrixWorld);", + " for (const mesh of chunk.meshes) {", + " this._raycastMesh.geometry = mesh.geometry;", + " this._raycastMesh.material = mesh.material;", + " this._raycastHits.length = 0;", + " this._raycastMesh.raycast(raycaster, this._raycastHits);", + " for (const hit of this._raycastHits) {", + " hit.object = mesh;", + " hit.instanceId = index;", + " intersects.push(hit);", + " }", + " }", + " });", + " }", + "", + " /** @param {THREE.Camera | null} camera */", + " _hideFarChunks(camera) {", + " if (!camera) return;", + " const worldScale = getWorldScale(this.object);", + " const drawDistance = Math.max(this.object._getDrawDistance(), 1) / worldScale;", + " this.uniforms.scatterDrawDistance.value = drawDistance;", + " this._cameraPosition.setFromMatrixPosition(camera.matrixWorld);", + " for (const chunk of this.chunks) {", + " const distance = this._chunkPosition", + " .copy(chunk.center)", + " .applyMatrix4(this.group.matrixWorld)", + " .distanceTo(this._cameraPosition);", + " const isVisible = distance - chunk.radius / worldScale < drawDistance;", + " for (const mesh of chunk.meshes) mesh.visible = isVisible;", + " }", + " }", + "", + " /**", + " * @param {number} elapsedSeconds", + " * @param {THREE.Camera | null} camera", + " */", + " update(elapsedSeconds, camera) {", + " if (this.placement.shouldPlaceAgain(elapsedSeconds, this._getPlacementKey())) {", + " this.rebuild();", + " }", + " if (!this.changedSamples.isEmpty()) {", + " const { object } = this;", + " const sampleWidth = object.getWidth() / MASK_RESOLUTION;", + " const sampleHeight = object.getHeight() / MASK_RESOLUTION;", + " const { minI, minJ, maxI, maxJ } = this.changedSamples;", + " // Copies between samples use the density of the samples around them.", + " this._rebuildArea(", + " object.getX() + (minI - 1) * sampleWidth,", + " object.getY() + (minJ - 1) * sampleHeight,", + " object.getX() + (maxI + 1) * sampleWidth,", + " object.getY() + (maxJ + 1) * sampleHeight", + " );", + " this.changedSamples.clear();", + " }", + " // The object transformation is applied just before rendering, it's needed now.", + " this.object.getRenderer().ensureUpToDate();", + " this.group.updateWorldMatrix(true, false);", + " this._hideFarChunks(camera);", + " this._updateSurface();", + " }", + "", + " _updateSurface() {", + " const { object } = this;", + " if (typeof object.setSurface !== 'function') return;", + " const trunkKey = object._getTrunkCollisions() + '|' + object._getTrunkDiameter();", + " if (trunkKey !== this.trunkKey) {", + " this.trunkKey = trunkKey;", + " this.version++;", + " }", + " if (object.getSurface() !== this.surface) object.setSurface(this.surface);", + " }", + "", + " /** @returns {Copy[]} */", + " getCopies() {", + " const copies = [];", + " for (const chunk of this.chunks) copies.push(...chunk.copies);", + " return copies;", + " }", + "", + " /**", + " * @returns {Copy | null} The copy whose base is the nearest to a position (on X and Y), if any.", + " */", + " findNearestCopy(x, y) {", + " let nearestCopy = null;", + " let nearestDistanceSquared = Infinity;", + " for (const chunk of this.chunks) {", + " for (const copy of chunk.copies) {", + " const distanceSquared = (copy.x - x) * (copy.x - x) + (copy.y - y) * (copy.y - y);", + " if (distanceSquared < nearestDistanceSquared) {", + " nearestDistanceSquared = distanceSquared;", + " nearestCopy = copy;", + " }", + " }", + " }", + " return nearestCopy;", + " }", + "", + " getCopyCount() {", + " let count = 0;", + " for (const chunk of this.chunks) count += chunk.copies.length;", + " return count;", + " }", + "", + " /** Remove the copies whose base is in a circle (they come back when the scene is restarted). */", + " removeCopies(x, y, radius) {", + " let hasRemovedCopies = false;", + " for (const chunk of this.chunks) {", + " for (const copy of chunk.copies) {", + " if ((copy.x - x) * (copy.x - x) + (copy.y - y) * (copy.y - y) <= radius * radius) {", + " this.removedCopies.set(copy.key, copy);", + " hasRemovedCopies = true;", + " }", + " }", + " }", + " if (hasRemovedCopies) this._rebuildArea(x - radius, y - radius, x + radius, y + radius);", + " }", + "", + " /** Paint the full density around a position, and bring back the copies removed there. */", + " addCopies(x, y, radius) {", + " for (const [key, copy] of this.removedCopies) {", + " if ((copy.x - x) * (copy.x - x) + (copy.y - y) * (copy.y - y) <= radius * radius) {", + " this.removedCopies.delete(key);", + " }", + " }", + " this.paint(x, y, x, y, radius, 1, 1);", + " }", + "", + " _getStroke(x1, y1, x2, y2, radius) {", + " const { object } = this;", + " const stroke = this._stroke;", + " stroke.cellWidth = object.getWidth() / MASK_RESOLUTION;", + " stroke.cellHeight = object.getHeight() / MASK_RESOLUTION;", + " stroke.ax = x1 - object.getX();", + " stroke.ay = y1 - object.getY();", + " stroke.bx = x2 - object.getX();", + " stroke.by = y2 - object.getY();", + " stroke.radius = Math.max(radius, 0);", + " return stroke;", + " }", + "", + " /**", + " * Paint the density around a segment (copies are placed again at the next frame).", + " * @param {number} density From 0 (erase) to 1 (as dense as the Density property).", + " * @param {number} strength From 0 to 1.", + " */", + " paint(x1, y1, x2, y2, radius, density, strength) {", + " const changedSamples = new SampleRectangle();", + " this.mask.paint(this._getStroke(x1, y1, x2, y2, radius), density, strength, changedSamples);", + " this.changedSamples.add(changedSamples);", + " }", + "", + " /** @param {DensityMask} mask */", + " setMask(mask) {", + " this.mask = mask;", + " this.rebuild();", + " }", + "", + " /**", + " * Prisms around the trunks, from 0 to 1 in the object box (see `gdjs.Surface`).", + " * @returns {gdjs.SurfaceTriangles}", + " */", + " _getTrunkTriangles() {", + " const { object } = this;", + " const copies = object._getTrunkCollisions() ? this.getCopies() : [];", + " const verticesPerCopy = TRUNK_SIDES * 2 + 1;", + " const positions = new Float32Array(copies.length * verticesPerCopy * 3);", + " const indices = new Uint32Array(copies.length * TRUNK_SIDES * 3 * 3);", + " const width = object.getWidth() || 1;", + " const height = object.getHeight() || 1;", + " const depth = object.getDepth() || 1;", + " let indexOffset = 0;", + " copies.forEach((copy, copyIndex) => {", + " const radius = (Math.max(object._getTrunkDiameter(), 0) / 2) * (copy.height / Math.max(object._getSize(), 1));", + " const top = copy.z + copy.height * 0.8;", + " const firstVertex = copyIndex * verticesPerCopy;", + " const setVertex = (vertex, x, y, z) => {", + " positions[vertex * 3] = (x - object.getX()) / width;", + " positions[vertex * 3 + 1] = (y - object.getY()) / height;", + " positions[vertex * 3 + 2] = (z - object.getZ()) / depth;", + " };", + " for (let side = 0; side < TRUNK_SIDES; side++) {", + " const angle = ((side + 0.5) / TRUNK_SIDES) * Math.PI * 2;", + " const x = copy.x + Math.cos(angle) * radius;", + " const y = copy.y + Math.sin(angle) * radius;", + " // Starting a bit under the ground, which can be sloped.", + " setVertex(firstVertex + side * 2, x, y, copy.z - radius);", + " setVertex(firstVertex + side * 2 + 1, x, y, top);", + " }", + " const center = firstVertex + TRUNK_SIDES * 2;", + " setVertex(center, copy.x, copy.y, top);", + " for (let side = 0; side < TRUNK_SIDES; side++) {", + " const bottom = firstVertex + side * 2;", + " const nextBottom = firstVertex + ((side + 1) % TRUNK_SIDES) * 2;", + " // Triangles face out of the prism (vertices at odd indices are at the top).", + " indices.set([bottom, nextBottom, bottom + 1, nextBottom, nextBottom + 1, bottom + 1], indexOffset);", + " indices.set([center, bottom + 1, nextBottom + 1], indexOffset + 6);", + " indexOffset += 9;", + " }", + " });", + " if (copies.length === 0) {", + " // Without triangles, the physics engine and navigation meshes would use", + " // the object box: a tiny triangle under the object is used instead.", + " return { positions: new Float32Array([0, 0, -1, 0.0001, 0, -1, 0, 0.0001, -1]), indices: new Uint32Array([0, 2, 1]) };", + " }", + " return { positions, indices };", + " }", + "", + " dispose() {", + " this.isDisposed = true;", + " if (typeof this.object.setSurface === 'function' && this.object.getSurface() === this.surface) {", + " this.object.setSurface(null);", + " }", + " this._disposeChunks(this.chunks);", + " this._disposeModel();", + " this.group.removeFromParent();", + " }", + "}", + "", + "const scatterToolIcons = {", + " Add: svgIcon(", + " ''", + " ),", + " Erase: svgIcon(", + " ''", + " ),", + "};", + "", + "/**", + " * The brushes shown in the scene editor when scattered models are selected,", + " * to add or erase copies. Without the needed editor functions (older GDevelop", + " * versions), nothing is shown: copies can still be painted with events.", + " */", + "class ScatterEditorTools {", + " constructor() {", + " /** '' when no brush is used. */", + " this.toolName = '';", + " this.sizePercent = 30;", + " this.strengthPercent = 50;", + " /** @type {Scatter | null} */", + " this.scatter = null;", + " /** @type {gdjs.InGameEditor | null} */", + " this.editor = null;", + " this.isStroking = false;", + " this.lastStrokeTime = 0;", + " /** @type {Array<{mask: DensityMask, baseMask: DensityMask}>} */", + " this.undoSteps = [];", + " this.raycaster = new THREE.Raycaster();", + " this.ndc = new THREE.Vector2();", + " this._groundAtCursor = { ground: null, z: 0 };", + " /** A circle following the ground under the cursor. */", + " this.brushRing = new THREE.LineLoop(", + " new THREE.BufferGeometry().setAttribute('position', new THREE.BufferAttribute(new Float32Array(64 * 3), 3)),", + " new THREE.LineBasicMaterial({ color: 0xffffff, depthTest: false, transparent: true })", + " );", + " this.brushRing.renderOrder = 1000;", + " this.brushRing.frustumCulled = false;", + " this.brushRing.raycast = () => {};", + " }", + "", + " /** @param {gdjs.InGameEditor} editor */", + " update(editor) {", + " if (typeof editor.showToolbar !== 'function') return;", + " this.editor = editor;", + " const selectedObjects = editor.getSelectedObjects();", + " const scatter =", + " selectedObjects.length === 1 && selectedObjects[0].__terrainScatter ? selectedObjects[0].__terrainScatter : null;", + " if (scatter !== this.scatter) {", + " this._endStroke();", + " this.scatter = scatter;", + " this.undoSteps.length = 0;", + " }", + " if (scatter) editor.showToolbar('ScatteredModels', this._getToolbarItems());", + " const game = editor.getRuntimeGame();", + " if (!scatter || !this.toolName) {", + " this.brushRing.removeFromParent();", + " return;", + " }", + " const inputManager = game.getInputManager();", + " if (inputManager.wasKeyJustPressed(gdjs.evtTools.input.keysNameToCode.Escape)) {", + " this._endStroke();", + " this.toolName = '';", + " this.brushRing.removeFromParent();", + " return;", + " }", + " editor.captureLeftMouseButton();", + " const hit = this._getPositionUnderCursor(scatter, game);", + " const radius = this._getRadius();", + " this._showBrushRing(scatter, hit, radius);", + "", + " const isPressedForTool =", + " typeof editor.isCapturedLeftMouseButtonPressed === 'function'", + " ? editor.isCapturedLeftMouseButtonPressed()", + " : inputManager.isMouseButtonPressed(0);", + " // The button is released outside the canvas when the mouse is on a toolbar.", + " const isPressed = isPressedForTool && inputManager.isMouseInsideCanvas();", + " if (isPressed && hit && !this.isStroking) {", + " this._startStroke(scatter);", + " }", + " if (this.isStroking && isPressed && hit) {", + " this._applyBrush(scatter, hit, radius);", + " }", + " if (!isPressed) {", + " this._endStroke();", + " }", + " }", + "", + " _getRadius() {", + " const { object } = this.scatter;", + " const maxRadius = Math.max(object.getWidth(), object.getHeight()) / 4;", + " return Math.max(maxRadius * Math.pow(this.sizePercent / 100, 2), 10);", + " }", + "", + " /**", + " * @returns {{x: number, y: number} | null} The position of the scene under the cursor: on the", + " * highest ground hit, or on the plane at the object Z.", + " */", + " _getPositionUnderCursor(scatter, game) {", + " let hit = null;", + " for (const ground of getGrounds(scatter.object.getRuntimeScene())) {", + " if (typeof ground.raycastFromCursor !== 'function') continue;", + " const localHit = ground.raycastFromCursor(game, this.raycaster, this.ndc);", + " if (!localHit) continue;", + " const sceneHit = ground.toScenePosition(localHit);", + " if (!hit || sceneHit.z > hit.z) hit = sceneHit;", + " }", + " if (hit) return hit;", + "", + " const { object } = scatter;", + " const layer = object.getInstanceContainer().getLayer(object.getLayer());", + " const camera = layer.getRenderer().getThreeCamera();", + " if (!camera) return null;", + " const inputManager = game.getInputManager();", + " this.ndc.set(", + " (inputManager.getCursorX() / game.getGameResolutionWidth()) * 2 - 1,", + " -(inputManager.getCursorY() / game.getGameResolutionHeight()) * 2 + 1", + " );", + " this.raycaster.setFromCamera(this.ndc, camera);", + " scatter.group.updateWorldMatrix(true, false);", + " const ray = this.raycaster.ray.clone().applyMatrix4(scatter.group.matrixWorld.clone().invert());", + " const point = ray.intersectPlane(new THREE.Plane(new THREE.Vector3(0, 0, 1), 0), new THREE.Vector3());", + " if (!point) return null;", + " return {", + " x: object.getX() + (point.x / AREA_SIZE) * object.getWidth(),", + " y: object.getY() + (point.y / AREA_SIZE) * object.getHeight(),", + " };", + " }", + "", + " _showBrushRing(scatter, hit, radius) {", + " if (!hit) {", + " this.brushRing.removeFromParent();", + " return;", + " }", + " const { object } = scatter;", + " if (this.brushRing.parent !== scatter.group) scatter.group.add(this.brushRing);", + " const grounds = getGrounds(object.getRuntimeScene());", + " const positions = this.brushRing.geometry.attributes.position;", + " for (let index = 0; index < positions.count; index++) {", + " const angle = (index / positions.count) * Math.PI * 2;", + " const x = hit.x + Math.cos(angle) * radius;", + " const y = hit.y + Math.sin(angle) * radius;", + " const { ground, z } = findHighestGround(grounds, x, y, this._groundAtCursor);", + " positions.setXYZ(", + " index,", + " ((x - object.getX()) / object.getWidth()) * AREA_SIZE,", + " ((y - object.getY()) / object.getHeight()) * AREA_SIZE,", + " (((ground ? z : object.getZ()) + 2 - object.getZ()) / Math.max(object.getDepth(), 0.0001)) * AREA_DEPTH", + " );", + " }", + " positions.needsUpdate = true;", + " }", + "", + " _startStroke(scatter) {", + " this.isStroking = true;", + " this.lastStrokeTime = performance.now();", + " this.undoSteps.push({ mask: scatter.mask.clone(), baseMask: scatter.baseMask });", + " while (this.undoSteps.length > 30) this.undoSteps.shift();", + " }", + "", + " _endStroke() {", + " if (!this.isStroking) return;", + " this.isStroking = false;", + " this._save();", + " }", + "", + " _applyBrush(scatter, hit, radius) {", + " const now = performance.now();", + " const elapsedSeconds = Math.min((now - this.lastStrokeTime) / 1000, 0.1);", + " this.lastStrokeTime = now;", + " const strength = (this.strengthPercent / 100) * elapsedSeconds * 8;", + " scatter.paint(hit.x, hit.y, hit.x, hit.y, radius, this.toolName === 'Add' ? 1 : 0, strength);", + " }", + "", + " _save() {", + " const { scatter, editor } = this;", + " if (!scatter || !editor) return;", + " editor.updateObjectProperties(scatter.object.getName(), { PaintData: scatter.save() });", + " }", + "", + " _undo() {", + " const undoStep = this.undoSteps.pop();", + " // The coverage or the edits may have changed since the stroke.", + " if (!this.scatter || !undoStep || undoStep.baseMask !== this.scatter.baseMask) {", + " this.undoSteps.length = 0;", + " return;", + " }", + " this.scatter.setMask(undoStep.mask);", + " this._save();", + " }", + "", + " /** @returns {Array} */", + " _getToolbarItems() {", + " const toolButton = (toolName, tooltip) => ({", + " type: 'button',", + " id: toolName,", + " tooltip,", + " iconUrl: scatterToolIcons[toolName],", + " isActive: this.toolName === toolName,", + " onClick: () => (this.toolName = this.toolName === toolName ? '' : toolName),", + " });", + " return [", + " toolButton('Add', 'Add copies (drag on the ground)'),", + " toolButton('Erase', 'Erase copies'),", + " { type: 'divider', id: 'BrushDivider' },", + " {", + " type: 'slider',", + " id: 'Size',", + " tooltip: 'Brush size',", + " iconUrl: toolIcons.Size,", + " min: 1,", + " max: 100,", + " value: this.sizePercent,", + " onChange: (value) => (this.sizePercent = value),", + " },", + " {", + " type: 'slider',", + " id: 'Strength',", + " tooltip: 'Brush strength',", + " iconUrl: toolIcons.Strength,", + " min: 5,", + " max: 100,", + " value: this.strengthPercent,", + " onChange: (value) => (this.strengthPercent = value),", + " },", + " {", + " type: 'button',", + " id: 'Undo',", + " tooltip: 'Undo the last stroke on these scattered models',", + " iconUrl: toolIcons.Undo,", + " onClick: () => this._undo(),", + " },", + " ];", + " }", + "}", + "", + "const editorTools = new ScatterEditorTools();", + "if (gdjs.registerInGameEditorPostStepCallback) {", + " gdjs.registerInGameEditorPostStepCallback((editor) => editorTools.update(editor));", + "}", + "", + "gdjs.__terrainScatterExtension = {", + " Scatter,", + " // Exposed for tests.", + " DensityMask,", + " maskCodec,", + " applyEdits,", + "};", + "" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [], + "objectGroups": [] + } + ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "DefineHelperClasses" + }, + { + "functionName": "DefineGrassHelperClasses" + }, + { + "functionName": "DefineScatterHelperClasses" + } + ] + }, + "eventsBasedBehaviors": [], + "eventsBasedObjects": [ + { + "areaMaxX": 4096, + "areaMaxY": 4096, + "areaMaxZ": 600, + "areaMinX": 0, + "areaMinY": 0, + "areaMinZ": 0, + "assetStoreTag": "3d terrains", + "defaultName": "Terrain", + "description": "A ground with hills, mountains or valleys that can be sculpted and painted in the scene editor, or changed with events. Add the 3D physics behavior to it to make objects walk or roll on it.", + "fullName": "3D terrain", + "helpPath": "", + "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLXRlcnJhaW4iIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTQsNkwxMC4yNSwxMUwxMy4xLDE0LjhMMTEuNSwxNkM5LjgxLDEzLjc1IDcsMTAgNywxMEwxLDE4SDIzTDE0LDZaIiAvPjwvc3ZnPg==", + "is3D": true, + "isUsingLegacyInstancesRenderer": true, + "name": "Terrain3D", + "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/10e0a26c0d500830dfe23b94138beb0ef61607ba62bca05a5aa5c849777f1b06_terrain.svg", + "objects": [ + { + "assetStoreId": "", + "name": "AreaPlaceholder", + "type": "Scene3D::Cube3DObject", + "variables": [], + "effects": [], + "behaviors": [], + "content": { + "width": 4096, + "height": 4096, + "depth": 600, + "enableTextureTransparency": false, + "facesOrientation": "Y", + "backFaceUpThroughWhichAxisRotation": "X", + "materialType": "Basic", + "tint": "255;255;255", + "frontFaceResourceName": "", + "frontFaceVisible": false, + "frontFaceResourceRepeat": false, + "backFaceResourceName": "", + "backFaceVisible": false, + "backFaceResourceRepeat": false, + "leftFaceResourceName": "", + "leftFaceVisible": false, + "leftFaceResourceRepeat": false, + "rightFaceResourceName": "", + "rightFaceVisible": false, + "rightFaceResourceRepeat": false, + "topFaceResourceName": "", + "topFaceVisible": false, + "topFaceResourceRepeat": false, + "bottomFaceResourceName": "", + "bottomFaceVisible": false, + "bottomFaceResourceRepeat": false + } + } + ], + "objectsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "objectName": "AreaPlaceholder" + } + ] + }, + "objectsGroups": [], + "layers": [ + { + "ambientLightColorB": 200, + "ambientLightColorG": 200, + "ambientLightColorR": 200, + "camera2DPlaneMaxDrawingDistance": 5000, + "camera3DFarPlaneDistance": 10000, + "camera3DFieldOfView": 45, + "camera3DNearPlaneDistance": 3, + "cameraType": "", + "followBaseLayerCamera": false, + "isLightingLayer": false, + "isLocked": false, + "name": "", + "renderingType": "", + "visibility": true, + "cameras": [ + { + "defaultSize": true, + "defaultViewport": true, + "height": 0, + "viewportBottom": 1, + "viewportLeft": 0, + "viewportRight": 1, + "viewportTop": 0, + "width": 0 + } + ], + "effects": [] + } + ], + "instances": [ + { + "angle": 0, + "customSize": true, + "depth": 600, + "height": 4096, + "keepRatio": true, + "layer": "", + "name": "AreaPlaceholder", + "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-004096004096", + "width": 4096, + "x": 0, + "y": 0, + "zOrder": 1, + "numberProperties": [], + "stringProperties": [], + "initialVariables": [] + } + ], + "editionSettings": {}, + "eventsFunctions": [ + { + "fullName": "", + "functionType": "Action", + "name": "onCreated", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Terrain3D::DefineHelperClasses" + }, + "parameters": [ + "", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "for (const child of object.getChildrenContainer().getAdhocListOfAllInstances()) {", + " child.deleteFromScene();", + "}", + "if (object.__terrain3D) object.__terrain3D.dispose();", + "object.__terrain3D = new gdjs.__terrain3DExtension.Terrain(object);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "onDestroy", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrain3D.dispose();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "doStepPostEvents", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrain3D.update();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "onHotReloading", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrain3D.onHotReloading();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "description": "Raise the ground around a position (or lower it with a negative height). It's raised the most at the center and less and less up to the radius.", + "fullName": "Raise or lower the ground", + "functionType": "Action", + "group": "Sculpt", + "name": "RaiseGround", + "sentence": "Raise the ground of _PARAM0_ by _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const height = eventsFunctionContext.getArgument(\"Height\");", + "object.__terrain3D.raise(x, y, x, y, radius, height);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Height to add (negative to lower the ground)", + "name": "Height", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Raise the ground along a line, for example for a ridge, or lower it with a negative height to dig a river bed or a valley.", + "fullName": "Raise or lower the ground along a line", + "functionType": "Action", + "group": "Sculpt", + "name": "RaiseGroundAlongLine", + "sentence": "Raise the ground of _PARAM0_ by _PARAM6_ from _PARAM1_; _PARAM2_ to _PARAM3_; _PARAM4_ with a radius of _PARAM5_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x1 = eventsFunctionContext.getArgument(\"X1\");", + "const y1 = eventsFunctionContext.getArgument(\"Y1\");", + "const x2 = eventsFunctionContext.getArgument(\"X2\");", + "const y2 = eventsFunctionContext.getArgument(\"Y2\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const height = eventsFunctionContext.getArgument(\"Height\");", + "object.__terrain3D.raise(x1, y1, x2, y2, radius, height);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the start", + "name": "X1", + "type": "expression" + }, + { + "description": "Y position of the start", + "name": "Y1", + "type": "expression" + }, + { + "description": "X position of the end", + "name": "X2", + "type": "expression" + }, + { + "description": "Y position of the end", + "name": "Y2", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance from the line.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Height to add (negative to lower the ground)", + "name": "Height", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Flatten the ground around a position to a given height, for example to place a building.", + "fullName": "Flatten the ground", + "functionType": "Action", + "group": "Sculpt", + "name": "FlattenGround", + "sentence": "Flatten the ground of _PARAM0_ to a Z of _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const z = eventsFunctionContext.getArgument(\"Z\");", + "object.__terrain3D.flatten(x, y, x, y, radius, z, 1);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Z position of the flat ground", + "name": "Z", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Smooth the ground around a position, softening bumps and cliffs.", + "fullName": "Smooth the ground", + "functionType": "Action", + "group": "Sculpt", + "name": "SmoothGround", + "sentence": "Smooth the ground of _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_ (strength: _PARAM4_)", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const strength = eventsFunctionContext.getArgument(\"Strength\");", + "object.__terrain3D.smooth(x, y, x, y, radius, strength);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Strength (between 0 and 1)", + "name": "Strength", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Paint a layer (like grass, dirt, rock or sand) on the ground around a position.", + "fullName": "Paint a layer", + "functionType": "Action", + "group": "Paint", + "name": "PaintLayer", + "sentence": "Paint layer _PARAM4_ on _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const layer = eventsFunctionContext.getArgument(\"Layer\");", + "object.__terrain3D.paint(x, y, x, y, radius, layer, 1);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Layer (1 to 4)", + "name": "Layer", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Paint a layer on the ground along a line, for example for a path or a road.", + "fullName": "Paint a layer along a line", + "functionType": "Action", + "group": "Paint", + "name": "PaintLayerAlongLine", + "sentence": "Paint layer _PARAM6_ on _PARAM0_ from _PARAM1_; _PARAM2_ to _PARAM3_; _PARAM4_ with a radius of _PARAM5_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x1 = eventsFunctionContext.getArgument(\"X1\");", + "const y1 = eventsFunctionContext.getArgument(\"Y1\");", + "const x2 = eventsFunctionContext.getArgument(\"X2\");", + "const y2 = eventsFunctionContext.getArgument(\"Y2\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const layer = eventsFunctionContext.getArgument(\"Layer\");", + "object.__terrain3D.paint(x1, y1, x2, y2, radius, layer, 1);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the start", + "name": "X1", + "type": "expression" + }, + { + "description": "Y position of the start", + "name": "Y1", + "type": "expression" + }, + { + "description": "X position of the end", + "name": "X2", + "type": "expression" + }, + { + "description": "Y position of the end", + "name": "Y2", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance from the line.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Layer (1 to 4)", + "name": "Layer", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Tint the ground around a position with a color multiplying its layers, like dry grass or darker mud. White removes the tint.", + "fullName": "Tint the ground", + "functionType": "Action", + "group": "Paint", + "name": "TintGround", + "sentence": "Tint the ground of _PARAM0_ with _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const color = gdjs.rgbOrHexToRGBColor(eventsFunctionContext.getArgument(\"Color\"));", + "object.__terrain3D.tint(x, y, x, y, radius, color, 1);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Color", + "name": "Color", + "type": "color" + } + ], + "objectGroups": [] + }, + { + "description": "Make the ground around a position wet (darker and shiny, like after rain or near water) or dry.", + "fullName": "Change the wetness of the ground", + "functionType": "Action", + "group": "Paint", + "name": "WetGround", + "sentence": "Change the wetness of the ground of _PARAM0_ to _PARAM4_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const wetness = eventsFunctionContext.getArgument(\"Wetness\");", + "object.__terrain3D.wet(x, y, x, y, radius, wetness, 1);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "longDescription": "The effect fades out smoothly up to this distance.", + "name": "Radius", + "type": "expression" + }, + { + "description": "Wetness (from 0 for dry to 1)", + "name": "Wetness", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "the wetness of the ground at a position, from 0 (dry) to 1, painted or near the water (wet shore).", + "fullName": "Ground wetness", + "functionType": "ExpressionAndCondition", + "group": "Paint", + "name": "GroundWetness", + "sentence": "the wetness of the ground at _PARAM1_; _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "eventsFunctionContext.returnValue = object.__terrain3D.getWetnessAt(x, y);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "The tint painted on the ground at a position, like \"255;255;255\" (white when there is none).", + "fullName": "Ground tint", + "functionType": "StringExpression", + "group": "Paint", + "name": "GroundTint", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "eventsFunctionContext.returnValue = object.__terrain3D.getTintAt(x, y).join(\";\");" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "string" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Remove the ground in a circle, for the entrance of a tunnel, a cave or a well: there is nothing drawn and no collision there. Fill it again with the \"Fill holes\" action. A height field can't go under itself: put a 3D model (with a collision shape) for the tunnel or the cave under the ground. The physics engine removes the triangles touching a hole, so the hole in the collisions can be up to a cell larger than the one drawn: make the model go a bit further than the hole, or put rocks around the entrance.", + "fullName": "Make a hole", + "functionType": "Action", + "group": "Holes", + "name": "MakeHole", + "sentence": "Make a hole in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "object.__terrain3D.makeHole(x, y, x, y, radius);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "name": "Radius", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Fill the holes in a circle with the layers of the ground around them: the ground is drawn and has collisions again.", + "fullName": "Fill holes", + "functionType": "Action", + "group": "Holes", + "name": "FillHoles", + "sentence": "Fill the holes in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "object.__terrain3D.fillHoles(x, y, x, y, radius);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "name": "Radius", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Make the entrance of a tunnel or a cave on a slope: a hole, with the ground in front of it flattened to the floor of the tunnel.", + "fullName": "Carve an entrance", + "functionType": "Action", + "group": "Holes", + "name": "CarveEntrance", + "sentence": "Carve an entrance in _PARAM0_ at _PARAM1_; _PARAM2_ with a radius of _PARAM3_, going in at _PARAM4_° with its floor at Z _PARAM5_ and _PARAM6_ pixels of flat ground in front", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "const angle = eventsFunctionContext.getArgument(\"Angle\");", + "const floorZ = eventsFunctionContext.getArgument(\"FloorZ\");", + "const length = eventsFunctionContext.getArgument(\"Length\");", + "object.__terrain3D.carveEntrance(x, y, radius, angle, floorZ, length);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "name": "Radius", + "type": "expression" + }, + { + "description": "Direction going into the ground (in degrees)", + "name": "Angle", + "type": "expression" + }, + { + "description": "Z position of the floor", + "name": "FloorZ", + "type": "expression" + }, + { + "description": "Length of the flat ground in front of the entrance (in pixels)", + "name": "Length", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Check if there is a hole in the ground at a position.", + "fullName": "Hole", + "functionType": "Condition", + "group": "Holes", + "name": "IsHole", + "sentence": "There is a hole in _PARAM0_ at _PARAM1_; _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "eventsFunctionContext.returnValue = object.__terrain3D.isHoleAt(x, y);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "The Z position of the ground at a position, or the given value where there is a hole.", + "fullName": "Ground Z position, with holes", + "functionType": "Expression", + "group": "Holes", + "name": "GroundZWithHoles", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const terrain = object.__terrain3D;", + "eventsFunctionContext.returnValue = terrain.isHoleAt(x, y) ? eventsFunctionContext.getArgument(\"ZInHoles\") : terrain.getHeightAt(x, y);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + }, + { + "description": "Value in holes", + "longDescription": "For example, a Z position far under the ground.", + "name": "ZInHoles", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Replace the ground with a generated relief. Painted layers are reset.", + "fullName": "Generate a relief", + "functionType": "Action", + "group": "Generation", + "name": "GenerateRelief", + "sentence": "Generate a relief of _PARAM1_ on _PARAM0_ with seed _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const relief = eventsFunctionContext.getArgument(\"Relief\");", + "const seed = eventsFunctionContext.getArgument(\"Seed\");", + "object.__terrain3D.generate(relief, seed);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "Relief", + "name": "Relief", + "supplementaryInformation": "[\"Flat\", \"Hills\", \"Mountains\", \"Island\"]", + "type": "stringWithSelector" + }, + { + "description": "Seed", + "longDescription": "Each seed gives a different terrain with the same relief.", + "name": "Seed", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Replace the ground with the heights of an image. Painted layers are kept.", + "fullName": "Load a heightmap", + "functionType": "Action", + "group": "Generation", + "name": "LoadHeightmap", + "sentence": "Load the heights of _PARAM0_ from _PARAM1_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const image = eventsFunctionContext.getArgument(\"Image\");", + "object.__terrain3D.loadHeightmap(image);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "Heightmap image", + "longDescription": "Black is the lowest, white the highest. For more precision, store the height on 16 bits in the red (most significant byte) and green channels.", + "name": "Image", + "type": "imageResource" + } + ], + "objectGroups": [] + }, + { + "description": "Move objects up or down so that their bottom is on the ground, under their center. Useful to place trees, rocks or characters on a terrain.", + "fullName": "Put objects on the ground", + "functionType": "Action", + "group": "Position", + "name": "PutOnGround", + "sentence": "Put _PARAM1_ on the ground of _PARAM0_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrain3D.placeOnGround(eventsFunctionContext.getObjects(\"Objects\"));" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "Objects to put on the ground", + "name": "Objects", + "type": "objectList" + } + ], + "objectGroups": [] + }, + { + "description": "the Z position of the ground at a position of the scene.", + "fullName": "Ground Z position", + "functionType": "ExpressionAndCondition", + "group": "Position", + "name": "GroundZ", + "sentence": "the ground Z position at _PARAM1_; _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "eventsFunctionContext.returnValue = object.__terrain3D.getHeightAt(x, y);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "the slope of the ground (angle with the horizontal, in degrees) at a position of the scene.", + "fullName": "Ground slope", + "functionType": "ExpressionAndCondition", + "group": "Position", + "name": "GroundSlope", + "sentence": "the ground slope at _PARAM1_; _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "eventsFunctionContext.returnValue = object.__terrain3D.getSlopeAt(x, y);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "the layer (from 1 to 4) painted the most at a position of the scene. Useful to play footstep sounds or to slow down characters in water or mud.", + "fullName": "Ground layer", + "functionType": "ExpressionAndCondition", + "group": "Paint", + "name": "GroundLayer", + "sentence": "the ground layer at _PARAM1_; _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "eventsFunctionContext.returnValue = object.__terrain3D.getLayerAt(x, y);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::Terrain3D", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + } + ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + }, + { + "folderName": "Sculpt", + "children": [ + { + "functionName": "RaiseGround" + }, + { + "functionName": "RaiseGroundAlongLine" + }, + { + "functionName": "FlattenGround" + }, + { + "functionName": "SmoothGround" + } + ] + }, + { + "folderName": "Paint", + "children": [ + { + "functionName": "PaintLayer" + }, + { + "functionName": "PaintLayerAlongLine" + }, + { + "functionName": "TintGround" + }, + { + "functionName": "WetGround" + }, + { + "functionName": "GroundWetness" + }, + { + "functionName": "GroundTint" + }, + { + "functionName": "GroundLayer" + } + ] + }, + { + "folderName": "Holes", + "children": [ + { + "functionName": "MakeHole" + }, + { + "functionName": "FillHoles" + }, + { + "functionName": "CarveEntrance" + }, + { + "functionName": "IsHole" + }, + { + "functionName": "GroundZWithHoles" + } + ] + }, + { + "folderName": "Generation", + "children": [ + { + "functionName": "GenerateRelief" + }, + { + "functionName": "LoadHeightmap" + } + ] + }, + { + "folderName": "Position", + "children": [ + { + "functionName": "PutOnGround" + }, + { + "functionName": "GroundZ" + }, + { + "functionName": "GroundSlope" + } + ] + } + ] + }, + "propertyDescriptors": [ + { + "value": "Hills", + "type": "Choice", + "label": "Relief", + "description": "The initial shape, before sculpting.", + "group": "Shape", + "choices": [ + { + "label": "Flat", + "value": "Flat" + }, + { + "label": "Hills", + "value": "Hills" + }, + { + "label": "Mountains", + "value": "Mountains" + }, + { + "label": "Island", + "value": "Island" + } + ], + "name": "Relief" + }, + { + "value": "1", + "type": "Number", + "label": "Seed", + "description": "Each seed gives a different terrain with the same relief.", + "group": "Shape", + "name": "Seed" + }, + { + "value": "", + "type": "Resource", + "label": "Heightmap image", + "description": "Optional. Replaces the relief: black is the lowest, white the highest (the object depth).", + "group": "Shape", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "HeightmapImage" + }, + { + "value": "", + "type": "MultilineString", + "label": "Edits", + "description": "Changes of the relief or heightmap, applied in order, as a JSON list, for example [{\"tool\": \"raise\", \"x\": 0.3, \"y\": 0.4, \"radius\": 0.15, \"height\": 0.4}, {\"tool\": \"paint\", \"x\": 0.1, \"y\": 0.9, \"toX\": 0.8, \"toY\": 0.2, \"radius\": 0.02, \"layer\": 2}]. Tools: raise (height added to the ground, negative to lower), flatten (z: the ground is brought to this height, fully at the center, cutting what is above it and filling what is below; with toX and toY, toZ gives the height at the end of the line, for a road going up), smooth (strength), paint (layer, from 1 to 4, and strength), tint (color, like \"120;140;60\", multiplying the layers, white removing the tint, and strength), wetness (amount, from 0 for dry to 1 for wet, and strength), hole (no ground in the radius, for the entrances of tunnels and caves) and fill (the holes in the radius are filled with the layers of the ground around them). x, y and radius are fractions (0 to 1) of the terrain size (x is (scene X - terrain X) / terrain width). height and z are fractions of its depth: the ground at z is at Z = terrain Z + z * terrain depth. Add toX and toY to change the ground along a line (paths, rivers, ridges). Shown in the scene editor.", + "group": "Shape", + "advanced": true, + "name": "Edits" + }, + { + "value": "256", + "type": "Choice", + "label": "Resolution", + "description": "The number of cells on each side of the terrain. Higher is more detailed but slower.", + "group": "Shape", + "choices": [ + { + "label": "64 x 64", + "value": "64" + }, + { + "label": "128 x 128", + "value": "128" + }, + { + "label": "256 x 256", + "value": "256" + }, + { + "label": "512 x 512", + "value": "512" + }, + { + "label": "1024 x 1024", + "value": "1024" + } + ], + "name": "Resolution" + }, + { + "value": "96;160;64", + "type": "Color", + "label": "Layer 1 color (grass)", + "group": "Layers", + "name": "Layer1Color" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 1 texture", + "description": "Optional. Tinted by the layer color.", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer1Texture" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 1 normal map", + "description": "Optional. The bumps of the texture, lit by the lights (an OpenGL normal map, like the textures of ambientCG).", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer1NormalMap" + }, + { + "value": "150;112;72", + "type": "Color", + "label": "Layer 2 color (dirt)", + "group": "Layers", + "name": "Layer2Color" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 2 texture", + "description": "Optional. Tinted by the layer color.", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer2Texture" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 2 normal map", + "description": "Optional. The bumps of the texture, lit by the lights (an OpenGL normal map, like the textures of ambientCG).", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer2NormalMap" + }, + { + "value": "124;118;112", + "type": "Color", + "label": "Layer 3 color (rock)", + "group": "Layers", + "name": "Layer3Color" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 3 texture", + "description": "Optional. Tinted by the layer color.", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer3Texture" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 3 normal map", + "description": "Optional. The bumps of the texture, lit by the lights (an OpenGL normal map, like the textures of ambientCG).", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer3NormalMap" + }, + { + "value": "224;204;148", + "type": "Color", + "label": "Layer 4 color (sand)", + "group": "Layers", + "name": "Layer4Color" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 4 texture", + "description": "Optional. Tinted by the layer color.", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer4Texture" + }, + { + "value": "", + "type": "Resource", + "label": "Layer 4 normal map", + "description": "Optional. The bumps of the texture, lit by the lights (an OpenGL normal map, like the textures of ambientCG).", + "group": "Layers", + "extraInformation": [ + "image" + ], + "choices": [], + "name": "Layer4NormalMap" + }, + { + "value": "1", + "type": "Number", + "label": "Normal map strength", + "description": "How much the normal maps bump the ground: 0 for none, 1 as they are, more to exaggerate them.", + "group": "Layers", + "name": "NormalMapStrength" + }, + { + "value": "256", + "type": "Number", + "unit": "Pixel", + "label": "Texture size", + "description": "The size, in the scene, of one repetition of the layer textures.", + "group": "Layers", + "name": "TextureSize" + }, + { + "value": "0.5", + "type": "Number", + "label": "Height blending", + "description": "Where layers meet, the bright parts of textures (stones, tufts) cover the dark ones (gaps, sand) instead of fading into each other: 0 for none, 1 for the most.", + "group": "Layers", + "name": "HeightBlending" + }, + { + "value": "0.5", + "type": "Number", + "label": "Texture variation", + "description": "Makes the repetition of textures less visible, with patches of bigger and rotated textures and colors slowly varying: 0 for none, 1 for the most.", + "group": "Layers", + "name": "TextureVariation" + }, + { + "value": "false", + "type": "Boolean", + "label": "Wet shore", + "description": "Make the ground wet (darker and shiny) under a Z position, like the sand of a beach near the water.", + "group": "Wetness", + "name": "WetShore" + }, + { + "value": "0", + "type": "Number", + "unit": "Pixel", + "label": "Wet below Z", + "description": "The ground is wet under this Z position (for example, the Z of a 3D water object).", + "group": "Wetness", + "name": "WetShoreZ" + }, + { + "value": "60", + "type": "Number", + "unit": "Pixel", + "label": "Wet shore fade", + "description": "The ground becomes dry over this height above \"Wet below Z\".", + "group": "Wetness", + "name": "WetShoreFade" + }, + { + "value": "3", + "type": "Choice", + "label": "Layer on steep slopes", + "description": "This layer is shown where the ground is steep, whatever is painted.", + "group": "Layers", + "choices": [ + { + "label": "None", + "value": "0" + }, + { + "label": "Layer 1", + "value": "1" + }, + { + "label": "Layer 2", + "value": "2" + }, + { + "label": "Layer 3", + "value": "3" + }, + { + "label": "Layer 4", + "value": "4" + } + ], + "name": "CliffLayer" + }, + { + "value": "45", + "type": "Number", + "unit": "DegreeAngle", + "label": "Steep slope angle", + "group": "Layers", + "name": "CliffAngle" + }, + { + "value": "true", + "type": "Boolean", + "label": "Cast shadows", + "group": "Rendering", + "name": "CastShadow" + }, + { + "value": "true", + "type": "Boolean", + "label": "Receive shadows", + "group": "Rendering", + "name": "ReceiveShadow" + }, + { + "value": "", + "type": "String", + "label": "Sculpted and painted data", + "description": "Written by the sculpt and paint tools of the scene editor, in a compact format not meant to be read or written by hand: use Edits instead. Clear it to go back to the relief, heightmap and edits.", + "advanced": true, + "name": "SculptData" + } + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Shape", + "children": [ + { + "propertyName": "Relief" + }, + { + "propertyName": "Seed" + }, + { + "propertyName": "HeightmapImage" + }, + { + "propertyName": "Edits" + }, + { + "propertyName": "Resolution" + } + ] + }, + { + "folderName": "Layers", + "children": [ + { + "propertyName": "Layer1Color" + }, + { + "propertyName": "Layer1Texture" + }, + { + "propertyName": "Layer1NormalMap" + }, + { + "propertyName": "Layer2Color" + }, + { + "propertyName": "Layer2Texture" + }, + { + "propertyName": "Layer2NormalMap" + }, + { + "propertyName": "Layer3Color" + }, + { + "propertyName": "Layer3Texture" + }, + { + "propertyName": "Layer3NormalMap" + }, + { + "propertyName": "Layer4Color" + }, + { + "propertyName": "Layer4Texture" + }, + { + "propertyName": "Layer4NormalMap" + }, + { + "propertyName": "NormalMapStrength" + }, + { + "propertyName": "TextureSize" + }, + { + "propertyName": "HeightBlending" + }, + { + "propertyName": "TextureVariation" + }, + { + "propertyName": "CliffLayer" + }, + { + "propertyName": "CliffAngle" + } + ] + }, + { + "folderName": "Wetness", + "children": [ + { + "propertyName": "WetShore" + }, + { + "propertyName": "WetShoreZ" + }, + { + "propertyName": "WetShoreFade" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "CastShadow" + }, + { + "propertyName": "ReceiveShadow" + } + ] + }, + { + "propertyName": "SculptData" + } + ] + }, + "variants": [] + }, + { + "areaMaxX": 1000, + "areaMaxY": 1000, + "areaMaxZ": 40, + "areaMinX": 0, + "areaMinY": 0, + "areaMinZ": 0, + "defaultName": "Grass", + "description": "A field of grass blades swaying in the wind and pushed aside by characters. They grow in the area of the object, on the highest 3D terrain at each point (one field can cover several terrains), or at the Z position of the object where there is no terrain.", + "fullName": "Terrain grass", + "helpPath": "", + "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGQ9Ik0zIDIxYzEtNSAxLTkgMC0xMyAyIDMgMyA3IDMgMTN6bTQgMGMwLTYgMS0xMSA0LTE2LTEgNS0xIDEwIDAgMTZ6bTUgMGMwLTUgMi05IDUtMTItMiA0LTIgOC0xIDEyem01IDBjMS00IDItNiA0LTgtMSAzLTEgNS0xIDh6Ii8+PC9zdmc+", + "is3D": true, + "isUsingLegacyInstancesRenderer": true, + "name": "TerrainGrass", + "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/732ef90b7fcf5dd9171fe95d0cf262e09159b487304915a4693baa893d9ce16c_grass.svg", + "objects": [ + { + "assetStoreId": "", + "name": "AreaPlaceholder", + "type": "Scene3D::Cube3DObject", + "variables": [], + "effects": [], + "behaviors": [], + "content": { + "width": 1000, + "height": 1000, + "depth": 40, + "enableTextureTransparency": false, + "facesOrientation": "Y", + "backFaceUpThroughWhichAxisRotation": "X", + "materialType": "Basic", + "tint": "255;255;255", + "frontFaceResourceName": "", + "frontFaceVisible": false, + "frontFaceResourceRepeat": false, + "backFaceResourceName": "", + "backFaceVisible": false, + "backFaceResourceRepeat": false, + "leftFaceResourceName": "", + "leftFaceVisible": false, + "leftFaceResourceRepeat": false, + "rightFaceResourceName": "", + "rightFaceVisible": false, + "rightFaceResourceRepeat": false, + "topFaceResourceName": "", + "topFaceVisible": false, + "topFaceResourceRepeat": false, + "bottomFaceResourceName": "", + "bottomFaceVisible": false, + "bottomFaceResourceRepeat": false + } + } + ], + "objectsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "objectName": "AreaPlaceholder" + } + ] + }, + "objectsGroups": [], + "layers": [ + { + "ambientLightColorB": 200, + "ambientLightColorG": 200, + "ambientLightColorR": 200, + "camera2DPlaneMaxDrawingDistance": 5000, + "camera3DFarPlaneDistance": 10000, + "camera3DFieldOfView": 45, + "camera3DNearPlaneDistance": 3, + "cameraType": "", + "followBaseLayerCamera": false, + "isLightingLayer": false, + "isLocked": false, + "name": "", + "renderingType": "", + "visibility": true, + "cameras": [ + { + "defaultSize": true, + "defaultViewport": true, + "height": 0, + "viewportBottom": 1, + "viewportLeft": 0, + "viewportRight": 1, + "viewportTop": 0, + "width": 0 + } + ], + "effects": [] + } + ], + "instances": [ + { + "angle": 0, + "customSize": true, + "depth": 40, + "height": 1000, + "keepRatio": true, + "layer": "", + "name": "AreaPlaceholder", + "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-001000001000", + "width": 1000, + "x": 0, + "y": 0, + "zOrder": 1, + "numberProperties": [], + "stringProperties": [], + "initialVariables": [] + } + ], + "editionSettings": {}, + "eventsFunctions": [ + { + "fullName": "", + "functionType": "Action", + "name": "onCreated", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Terrain3D::DefineHelperClasses" + }, + "parameters": [ + "", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Terrain3D::DefineGrassHelperClasses" + }, + "parameters": [ + "", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "for (const child of object.getChildrenContainer().getAdhocListOfAllInstances()) {", + " child.deleteFromScene();", + "}", + "if (object.__terrainGrass) object.__terrainGrass.dispose();", + "object.__terrainGrass = new gdjs.__terrainGrassExtension.Grass(object);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "onDestroy", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrainGrass.dispose();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "doStepPostEvents", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const game = runtimeScene.getGame();", + "// Time doesn't pass in the scene editor: real time is used to show the wind.", + "const isInGameEdition = game.isInGameEdition && game.isInGameEdition();", + "const elapsedTime = isInGameEdition ? 1 / 60 : object.getElapsedTime() / 1000;", + "const time = isInGameEdition", + " ? performance.now() / 1000", + " : object.getRuntimeScene().getTimeManager().getTimeFromStart() / 1000;", + "const layer = object.getInstanceContainer().getLayer(object.getLayer());", + "object.__terrainGrass.update(elapsedTime, time, layer.getRenderer().getThreeCamera());" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "onHotReloading", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrainGrass.updateFromProperties();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::TerrainGrass", + "type": "object" + } + ], + "objectGroups": [] + } + ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + } + ] + }, + "propertyDescriptors": [ + { + "value": "60", + "type": "Number", + "label": "Density", + "description": "The number of blades in a square of 100 x 100 pixels.", + "group": "Blades", + "name": "Density" + }, + { + "value": "36", + "type": "Number", + "unit": "Pixel", + "label": "Blade height", + "description": "The average height of blades (each blade is between 60% and 140% of it).", + "group": "Blades", + "name": "BladeHeight" + }, + { + "value": "5", + "type": "Number", + "unit": "Pixel", + "label": "Blade width", + "group": "Blades", + "name": "BladeWidth" + }, + { + "value": "52;104;36", + "type": "Color", + "label": "Base color", + "group": "Blades", + "name": "BaseColor" + }, + { + "value": "150;196;82", + "type": "Color", + "label": "Tip color", + "group": "Blades", + "name": "TipColor" + }, + { + "value": "1", + "type": "Number", + "label": "Seed", + "description": "Each seed places blades differently.", + "group": "Blades", + "name": "Seed" + }, + { + "value": "1", + "type": "Choice", + "label": "Grows on terrain layer", + "description": "On a 3D terrain, blades only grow where this layer is painted. A new terrain has layer 1 painted everywhere.", + "group": "Ground", + "choices": [ + { + "label": "Any", + "value": "0" + }, + { + "label": "Layer 1", + "value": "1" + }, + { + "label": "Layer 2", + "value": "2" + }, + { + "label": "Layer 3", + "value": "3" + }, + { + "label": "Layer 4", + "value": "4" + } + ], + "name": "GroundLayer" + }, + { + "value": "40", + "type": "Number", + "unit": "DegreeAngle", + "label": "Maximum slope", + "description": "No blades grow on steeper ground.", + "group": "Ground", + "name": "MaxSlope" + }, + { + "value": "1", + "type": "Number", + "label": "Wind strength", + "description": "0 for no wind.", + "group": "Wind", + "name": "WindStrength" + }, + { + "value": "2", + "type": "Number", + "label": "Wind speed", + "group": "Wind", + "name": "WindSpeed" + }, + { + "value": "", + "type": "String", + "label": "Object bending the grass", + "description": "The name of an object pushing blades aside when walking through the grass, like the player. The 8 first instances are used.", + "group": "Wind", + "name": "BendingObject" + }, + { + "value": "2500", + "type": "Number", + "unit": "Pixel", + "label": "Fade distance", + "description": "Blades are less and less dense up to this distance from the camera, and hidden after.", + "group": "Rendering", + "name": "FadeDistance" + } + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Blades", + "children": [ + { + "propertyName": "Density" + }, + { + "propertyName": "BladeHeight" + }, + { + "propertyName": "BladeWidth" + }, + { + "propertyName": "BaseColor" + }, + { + "propertyName": "TipColor" + }, + { + "propertyName": "Seed" + } + ] + }, + { + "folderName": "Ground", + "children": [ + { + "propertyName": "GroundLayer" + }, + { + "propertyName": "MaxSlope" + } + ] + }, + { + "folderName": "Wind", + "children": [ + { + "propertyName": "WindStrength" + }, + { + "propertyName": "WindSpeed" + }, + { + "propertyName": "BendingObject" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "FadeDistance" + } + ] + } + ] + }, + "variants": [] + }, + { + "areaMaxX": 2000, + "areaMaxY": 2000, + "areaMaxZ": 200, + "areaMinX": 0, + "areaMinY": 0, + "areaMinZ": 0, + "assetStoreTag": "3d scattered models", + "defaultName": "ScatteredModels", + "description": "Many copies of a 3D model (trees, rocks, bushes, flowers...) placed in the area of the object and drawn all at once, which is much faster than an object for each one. They stand on the highest 3D terrain at each point, or at the Z position of the object where there is no terrain. Paint where they grow with the brushes of the scene editor.", + "fullName": "Scattered models (forests, rocks...)", + "helpPath": "", + "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGQ9Ik0xMCAyMXYtM0gzbDUtNUg1bDUtNUg3bDUtNSA1IDVoLTNsNSA1aC0zbDUgNWgtN3YzeiIvPjwvc3ZnPg==", + "is3D": true, + "isUsingLegacyInstancesRenderer": true, + "name": "ScatteredModels", + "previewIconUrl": "", + "objects": [ + { + "assetStoreId": "", + "name": "AreaPlaceholder", + "type": "Scene3D::Cube3DObject", + "variables": [], + "effects": [], + "behaviors": [], + "content": { + "width": 2000, + "height": 2000, + "depth": 200, + "enableTextureTransparency": false, + "facesOrientation": "Y", + "backFaceUpThroughWhichAxisRotation": "X", + "materialType": "Basic", + "tint": "255;255;255", + "frontFaceResourceName": "", + "frontFaceVisible": false, + "frontFaceResourceRepeat": false, + "backFaceResourceName": "", + "backFaceVisible": false, + "backFaceResourceRepeat": false, + "leftFaceResourceName": "", + "leftFaceVisible": false, + "leftFaceResourceRepeat": false, + "rightFaceResourceName": "", + "rightFaceVisible": false, + "rightFaceResourceRepeat": false, + "topFaceResourceName": "", + "topFaceVisible": false, + "topFaceResourceRepeat": false, + "bottomFaceResourceName": "", + "bottomFaceVisible": false, + "bottomFaceResourceRepeat": false + } + } + ], + "objectsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "objectName": "AreaPlaceholder" + } + ] + }, + "objectsGroups": [], + "layers": [ + { + "ambientLightColorB": 200, + "ambientLightColorG": 200, + "ambientLightColorR": 200, + "camera2DPlaneMaxDrawingDistance": 5000, + "camera3DFarPlaneDistance": 10000, + "camera3DFieldOfView": 45, + "camera3DNearPlaneDistance": 3, + "cameraType": "", + "followBaseLayerCamera": false, + "isLightingLayer": false, + "isLocked": false, + "name": "", + "renderingType": "", + "visibility": true, + "cameras": [ + { + "defaultSize": true, + "defaultViewport": true, + "height": 0, + "viewportBottom": 1, + "viewportLeft": 0, + "viewportRight": 1, + "viewportTop": 0, + "width": 0 + } + ], + "effects": [] + } + ], + "instances": [ + { + "angle": 0, + "customSize": true, + "depth": 200, + "height": 2000, + "keepRatio": true, + "layer": "", + "name": "AreaPlaceholder", + "persistentUuid": "5d7a8b1e-3c2f-4e6a-9b1d-002000002000", + "width": 2000, + "x": 0, + "y": 0, + "zOrder": 1, + "numberProperties": [], + "stringProperties": [], + "initialVariables": [] + } + ], + "editionSettings": {}, + "eventsFunctions": [ + { + "fullName": "", + "functionType": "Action", + "name": "onCreated", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Terrain3D::DefineHelperClasses" + }, + "parameters": [ + "", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "Terrain3D::DefineScatterHelperClasses" + }, + "parameters": [ + "", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "for (const child of object.getChildrenContainer().getAdhocListOfAllInstances()) {", + " child.deleteFromScene();", + "}", + "if (object.__terrainScatter) object.__terrainScatter.dispose();", + "object.__terrainScatter = new gdjs.__terrainScatterExtension.Scatter(object);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "onDestroy", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrainScatter.dispose();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "doStepPostEvents", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const game = runtimeScene.getGame();", + "const isInGameEdition = game.isInGameEdition && game.isInGameEdition();", + "const elapsedTime = isInGameEdition ? 1 / 60 : object.getElapsedTime() / 1000;", + "const layer = object.getInstanceContainer().getLayer(object.getLayer());", + "object.__terrainScatter.update(elapsedTime, layer.getRenderer().getThreeCamera());" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "onHotReloading", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "object.__terrainScatter.onHotReloading();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "description": "Paint the density of copies around a position, up to the density of the object (copies removed with \"Erase copies\" come back). The effect fades out up to the radius.", + "fullName": "Add copies", + "functionType": "Action", + "group": "Paint", + "name": "AddCopies", + "sentence": "Add copies of _PARAM0_ around _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "object.__terrainScatter.addCopies(x, y, radius);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "name": "Radius", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Remove the copies standing in a circle, for example a tree cut by the player (use the position of the nearest copy and a small radius) or a forest cleared for a building. They come back when the scene is restarted.", + "fullName": "Erase copies", + "functionType": "Action", + "group": "Paint", + "name": "EraseCopies", + "sentence": "Erase the copies of _PARAM0_ in a circle at _PARAM1_; _PARAM2_ with a radius of _PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const radius = eventsFunctionContext.getArgument(\"Radius\");", + "object.__terrainScatter.removeCopies(x, y, radius);" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position of the center", + "name": "X", + "type": "expression" + }, + { + "description": "Y position of the center", + "name": "Y", + "type": "expression" + }, + { + "description": "Radius", + "name": "Radius", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "the number of copies placed in the area.", + "fullName": "Number of copies", + "functionType": "ExpressionAndCondition", + "group": "Copies", + "name": "CopyCount", + "sentence": "the number of copies", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "eventsFunctionContext.returnValue = object.__terrainScatter.getCopyCount();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + } + ], + "objectGroups": [] + }, + { + "description": "The X position of the base of the copy nearest to a position (the position itself if there is no copy).", + "fullName": "X position of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyX", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? copy.x : x;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "The Y position of the base of the copy nearest to a position (the position itself if there is no copy).", + "fullName": "Y position of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyY", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? copy.y : y;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "The Z position of the base of the copy nearest to a position (on the ground).", + "fullName": "Z position of the nearest copy", + "functionType": "Expression", + "group": "Copies", + "name": "NearestCopyZ", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? copy.z : object.getZ();" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "the distance (on X and Y) from a position to the base of the nearest copy. It's a very big number when there is no copy.", + "fullName": "Distance to the nearest copy", + "functionType": "ExpressionAndCondition", + "group": "Copies", + "name": "NearestCopyDistance", + "sentence": "the distance from _PARAM1_; _PARAM2_ to the nearest copy", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @type {gdjs.CustomRuntimeObject3D} */", + "const object = objects[0];", + "const x = eventsFunctionContext.getArgument(\"X\");", + "const y = eventsFunctionContext.getArgument(\"Y\");", + "const copy = object.__terrainScatter.findNearestCopy(x, y);", + "eventsFunctionContext.returnValue = copy ? Math.hypot(copy.x - x, copy.y - y) : Number.MAX_VALUE;" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Terrain3D::ScatteredModels", + "type": "object" + }, + { + "description": "X position", + "name": "X", + "type": "expression" + }, + { + "description": "Y position", + "name": "Y", + "type": "expression" + } + ], + "objectGroups": [] + } + ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "onHotReloading" + }, + { + "folderName": "Paint", + "children": [ + { + "functionName": "AddCopies" + }, + { + "functionName": "EraseCopies" + } + ] + }, + { + "folderName": "Copies", + "children": [ + { + "functionName": "CopyCount" + }, + { + "functionName": "NearestCopyX" + }, + { + "functionName": "NearestCopyY" + }, + { + "functionName": "NearestCopyZ" + }, + { + "functionName": "NearestCopyDistance" + } + ] + } + ] + }, + "propertyDescriptors": [ + { + "value": "", + "type": "Resource", + "label": "3D model", + "description": "The model copied everywhere (a .glb file). Models standing on the Y axis, like most models of the asset store, are stood up along Z.", + "group": "Model", + "extraInformation": [ + "model3D" + ], + "choices": [], + "name": "Model" + }, + { + "value": "300", + "type": "Number", + "unit": "Pixel", + "label": "Height of a copy", + "description": "Copies keep the proportions of the model.", + "group": "Model", + "name": "Size" + }, + { + "value": "0.8", + "type": "Number", + "label": "Smallest random scale", + "description": "Each copy is scaled at random between the smallest and the largest scale.", + "group": "Model", + "name": "MinScale" + }, + { + "value": "1.2", + "type": "Number", + "label": "Largest random scale", + "group": "Model", + "name": "MaxScale" + }, + { + "value": "true", + "type": "Boolean", + "label": "Random rotation", + "description": "Turn each copy around the Z axis at random.", + "group": "Model", + "name": "RandomRotation" + }, + { + "value": "255;255;255", + "type": "Color", + "label": "Random tint", + "description": "Each copy is tinted between white and this color, at random. White for no tint.", + "group": "Model", + "name": "RandomTint" + }, + { + "value": "0.3", + "type": "Number", + "label": "Density", + "description": "The number of copies in a square of 100 x 100 pixels where the density is fully painted (0.3 is about one copy every 180 pixels). Copies never overlap their neighbors much: they are placed at random in a grid.", + "group": "Placement", + "name": "Density" + }, + { + "value": "1", + "type": "Number", + "label": "Seed", + "description": "Each seed places copies differently.", + "group": "Placement", + "name": "Seed" + }, + { + "value": "Everywhere", + "type": "Choice", + "label": "Copies grow", + "description": "Everywhere in the area, or only where they are painted with the \"Add copies\" brush (or the edits).", + "group": "Placement", + "choices": [ + { + "label": "Everywhere (erase them with the brush)", + "value": "Everywhere" + }, + { + "label": "Only where painted", + "value": "Painted" + } + ], + "name": "Coverage" + }, + { + "value": "1", + "type": "Choice", + "label": "Grows on terrain layer", + "description": "On a 3D terrain, copies only grow where this layer is painted, away from its borders (so that trees don't stand on roads painted with another layer). A new terrain has layer 1 painted everywhere.", + "group": "Ground", + "choices": [ + { + "label": "Any", + "value": "0" + }, + { + "label": "Layer 1", + "value": "1" + }, + { + "label": "Layer 2", + "value": "2" + }, + { + "label": "Layer 3", + "value": "3" + }, + { + "label": "Layer 4", + "value": "4" + } + ], + "name": "GroundLayer" + }, + { + "value": "30", + "type": "Number", + "unit": "DegreeAngle", + "label": "Maximum slope", + "description": "No copies grow on steeper ground.", + "group": "Ground", + "name": "MaxSlope" + }, + { + "value": "-100000", + "type": "Number", + "unit": "Pixel", + "label": "Minimum ground Z", + "description": "No copies grow where the ground is lower (for example, under the water level).", + "group": "Ground", + "name": "MinGroundZ" + }, + { + "value": "100000", + "type": "Number", + "unit": "Pixel", + "label": "Maximum ground Z", + "description": "No copies grow where the ground is higher (for example, above the tree line).", + "group": "Ground", + "name": "MaxGroundZ" + }, + { + "value": "false", + "type": "Boolean", + "label": "Trunk collisions", + "description": "Give each copy a static collision shape around its trunk, for the 3D physics behavior (add it to this object, with the \"Static\" type) and navigation meshes. Without them, these behaviors have no effect on this object.", + "group": "Collisions", + "name": "TrunkCollisions" + }, + { + "value": "30", + "type": "Number", + "unit": "Pixel", + "label": "Trunk diameter", + "description": "The diameter of the collision shape of a copy at the height given in \"Height of a copy\" (it is scaled with the copy).", + "group": "Collisions", + "name": "TrunkDiameter" + }, + { + "value": "8000", + "type": "Number", + "unit": "Pixel", + "label": "Draw distance", + "description": "Copies shrink and are hidden at this distance from the camera.", + "group": "Rendering", + "name": "DrawDistance" + }, + { + "value": "true", + "type": "Boolean", + "label": "Cast shadows", + "group": "Rendering", + "name": "CastShadow" + }, + { + "value": "true", + "type": "Boolean", + "label": "Receive shadows", + "group": "Rendering", + "name": "ReceiveShadow" + }, + { + "value": "", + "type": "MultilineString", + "label": "Edits", + "description": "Where copies are added or erased, applied in order, as a JSON list, for example [{\"tool\": \"add\", \"x\": 0.3, \"y\": 0.4, \"radius\": 0.15}, {\"tool\": \"erase\", \"x\": 0.1, \"y\": 0.9, \"toX\": 0.8, \"toY\": 0.2, \"radius\": 0.02}]. Tools: add (density: from 0 to 1, 1 by default, how dense copies are compared to the Density property) and erase. strength (from 0 to 1) makes a stroke lighter. x, y and radius are fractions (0 to 1) of the object size (x is (scene X - object X) / object width). Add toX and toY to paint along a line (a row of trees, a path through a forest). With \"Copies grow: only where painted\", only the added areas have copies. Shown in the scene editor.", + "group": "Placement", + "advanced": true, + "name": "Edits" + }, + { + "value": "", + "type": "String", + "label": "Painted data", + "description": "Written by the brushes of the scene editor, in a compact format not meant to be read or written by hand: use Edits instead. Clear it to go back to the coverage and the edits.", + "advanced": true, + "name": "PaintData" + } + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "Model", + "children": [ + { + "propertyName": "Model" + }, + { + "propertyName": "Size" + }, + { + "propertyName": "MinScale" + }, + { + "propertyName": "MaxScale" + }, + { + "propertyName": "RandomRotation" + }, + { + "propertyName": "RandomTint" + } + ] + }, + { + "folderName": "Placement", + "children": [ + { + "propertyName": "Density" + }, + { + "propertyName": "Seed" + }, + { + "propertyName": "Coverage" + }, + { + "propertyName": "Edits" + } + ] + }, + { + "folderName": "Ground", + "children": [ + { + "propertyName": "GroundLayer" + }, + { + "propertyName": "MaxSlope" + }, + { + "propertyName": "MinGroundZ" + }, + { + "propertyName": "MaxGroundZ" + } + ] + }, + { + "folderName": "Collisions", + "children": [ + { + "propertyName": "TrunkCollisions" + }, + { + "propertyName": "TrunkDiameter" + } + ] + }, + { + "folderName": "Rendering", + "children": [ + { + "propertyName": "DrawDistance" + }, + { + "propertyName": "CastShadow" + }, + { + "propertyName": "ReceiveShadow" + } + ] + }, + { + "propertyName": "PaintData" + } + ] + }, + "variants": [] + } + ] } ], "externalLayouts": []