diff --git a/addons/zylann.hterrain/doc/docs/index.md b/addons/zylann.hterrain/doc/docs/index.md index fa0dbd4..ba4e4d8 100644 --- a/addons/zylann.hterrain/doc/docs/index.md +++ b/addons/zylann.hterrain/doc/docs/index.md @@ -11,6 +11,15 @@ It is entirely built on top of the `VisualServer` scripting API, which means it ![Screenshot of the editor with the plugin enabled and arrows showing where UIs are](images/overview.png) +### Video tutorials + +This written doc should be the most up to date and precise information, but video tutorials exist for a quick start. + +- [Kasper's tutorial](https://www.youtube.com/watch?v=Af1f2JPvSIs) about version 1.5.2 (16 Jan 2021) +- [GamesFromScratch presentation](https://www.youtube.com/watch?v=jYVO0-_sXZs), also featuring the [WaterWays](https://github.com/Arnklit/WaterGenGodot) plugin (23 dec 2020) +- [qubodupDev's Tutorial](https://www.youtube.com/watch?v=k_ISq6JyVSs) about version 1.3.3 (27 aug 2020) +- [Old tutorial](https://www.youtube.com/watch?v=eZuvfIHDeT4&) about version 0.8 (10 aug 2018! A lot is outdated in it but let's keep it here for the record) + ### How to install You will need to use Godot 3.1 or later. It is best to use latest stable 3.x version (Godot 4 is not supported yet). @@ -90,7 +99,7 @@ Sculpting ### Brush types -The default terrain is flat, but you may want to create hills and mountains. Because it uses a heightmap, editing this terrain is equivalent to editing an image. Because of this, the main tool is a brush with a configurable size and shape. You can see which area will be affected inside a 3D red circle appearing under your mouse, and you can choose how strong painting is by changing the `strength` slider. +The default terrain is flat, but you may want to create hills and mountains. Because it uses a heightmap, editing this terrain is equivalent to editing an image. Because of this, the main tool is a brush with a configurable size and shape. You can see which area will be affected inside a 3D red circle appearing under your mouse, and you can choose how strong painting is by changing the `Brush opacity` slider. ![Screenshot of the brush widget](images/brush_editor.png) @@ -118,7 +127,7 @@ As you sculpt, the plugin automatically recomputes normals of the terrain, and s You can enable or disable collisions by checking the `Collisions enabled` property in the inspector. Heightmap-based terrains usually implement collisions directly using the heightmap, which saves a lot of computations compared to a classic mesh collider. -This plugin depends on the **Bullet Physics** integration in Godot, which does have a height-field collider. **Godot Physics** does not support it, so you may want to make sure Bullet is enabled in your project settings: +This plugin depends on the **Bullet Physics** integration in Godot, which does have a height-field collider. **Godot Physics** does not support it until version 3.4, so if you use an older version, you may want to make sure Bullet is enabled in your project settings: ![Screenshot of the option to choose physics engines in project settings](images/choose_bullet_physics.png) @@ -180,7 +189,8 @@ This magic is done with a single shader, i.e a single `ShaderMaterial` in Godot' There are mainly 3 families of shaders this plugin supports: - `CLASSIC4`: simple shaders where each texture may be a separate resource. They are limited to 4 textures. -- `ARRAY`: more modern shader using texture arrays, which comes with a few constraints, but allows to paint a lot more different textures. +- `MULTISPLAT16`: more advanced shader using more splatmaps and texture arrays. It's expensive but supports up to 16 textures. +- `ARRAY`: experimental shader also using texture arrays, which comes with constraints, but allows to paint a lot more different textures. - Other shaders don't need textures, like `LOW_POLY`, which only uses colors. On the `HTerrain` node, there is a property called `shader_type`, which lets you choose among built-in shaders. The one you choose will define which workflow to follow: textures, or texture arrays. @@ -205,6 +215,8 @@ For each texture, you may find the following types of images, common in PBR shad You can find some of these textures for free at [cc0textures.com](http://cc0textures.com). +!!! note: Some shaders have a `Lite` and non-lite versions. One main difference between them is that `Lite` versions don't require normal maps, but the others require them. If you use a non-lite shader and forget to assign normal maps, shading will look wrong. + It is preferable to place those source images under a specific directory. Also, since the images will only serve as an input to generate the actual game resources, it is better to place a `.gdignore` file inside that directory. This way, Godot will not include those source files in the exported game: ``` @@ -267,7 +279,7 @@ If you use PBR textures, there might be a lot of files to assign. If you use a n #### Normal maps -As indicated in the [Godot documentation](https://docs.godotengine.org/en/stable/tutorials/3d/spatial_material.html#normal-map), normal maps are expected to use OpenGL convention (X+, Y-, Z+). So it is possible that normalmaps you find online use a different convention. +As indicated in the [Godot documentation](https://docs.godotengine.org/en/stable/tutorials/3d/spatial_material.html#normal-map), normal maps are expected to use OpenGL convention (X+, Y+, Z+). So it is possible that normalmaps you find online use a different convention. To help with this, the import tool allows you to flip Y, in case the normalmap uses DirectX convention. @@ -349,7 +361,7 @@ The `CLASSIC4` shader is a simple splatmap technique, where R, G, B, A match the It comes in two variants: -- `CLASSIC4`: full-featured shader, however it requires your textures to have normal maps. +- `CLASSIC4`: full-featured shader, however it requires your textures to have normal maps. If you don't assign them, shading will look wrong. - `CLASSIC4_LITE`: simpler shader with less features. It only requires albedo textures. @@ -362,7 +374,7 @@ It also comes in two variants: - `MULTISPLAT16`: full-featured shader, however it requires your texture arrays to have normal maps. - `MULTISPLAT16_LITE`: simpler shader with less features. It only requires albedo texture arrays. -It is the recommended choice if you need more than 4 textures, because it is much easier to use than the `ARRAY` shader and has produces less artifacts. +It is the recommended choice if you need more than 4 textures, because it is much easier to use than the `ARRAY` shader and produces less artifacts. One downside is performance: it is about twice slower than `CLASSIC4` (on an nVidia 1060, a fullscreen `CLASSIC4` is 0.8 ms, while `MULTISPLAT16` is 1.8 ms). Although, considering objects placed on the terrain should usually occlude ground pixels, the cost might be lower in a real game scenario. @@ -1137,6 +1149,7 @@ This issue happened a few times and had various causes so if the checks mentionn - Check the contents of your terrain's data folder. It must contain a `.hterrain` file and a few textures. - If they are present, make sure Godot has imported those textures. If it didn't, unfocus the editor, and focus it back (you should see a short progress bar as it does it) - Check if you used Ctrl+Z (undo) after a non-undoable action, like described in [issue #101](https://github.com/Zylann/godot_heightmap_plugin/issues/101) +- Make sure your `res://addons` folder is named `addons` *exactly lowercase*. It should not be named `Addons`. Plugins can fail if this convention is not respected. - If your problem relates to collisions in editor, update the collider using `Terrain -> Update Editor Collider`, because this one does not update automatically yet - Godot seems to randomly forget where the terrain saver is, but I need help to find out why because I could never reproduce it. See [issue #120](https://github.com/Zylann/godot_heightmap_plugin/issues/120) diff --git a/addons/zylann.hterrain/doc/mkdocs.yml b/addons/zylann.hterrain/doc/mkdocs.yml index 3ece331..9f01702 100644 --- a/addons/zylann.hterrain/doc/mkdocs.yml +++ b/addons/zylann.hterrain/doc/mkdocs.yml @@ -1,15 +1,11 @@ site_name: HTerrain plugin documentation theme: readthedocs -# I had to specify this even though it's supposed to be the default -# See https://github.com/mkdocs/mkdocs/issues/2145#issuecomment-735342512 -docs_dir: docs - markdown_extensions: - # Makes permalinks appear on headings - - toc: - permalink: True - # Makes boxes for notes and warnings - - admonition - # Better highlighter which supports GDScript - - codehilite + # Makes permalinks appear on headings + - toc: + permalink: True + # Makes boxes for notes and warnings + - admonition + # Better highlighter which supports GDScript + - codehilite diff --git a/addons/zylann.hterrain/hterrain.gd b/addons/zylann.hterrain/hterrain.gd index 163245b..a3bf731 100644 --- a/addons/zylann.hterrain/hterrain.gd +++ b/addons/zylann.hterrain/hterrain.gd @@ -1,7 +1,7 @@ tool extends Spatial -const QuadTreeLod = preload("./util/quad_tree_lod.gd") +const NativeFactory = preload("./native/factory.gd") const Mesher = preload("./hterrain_mesher.gd") const Grid = preload("./util/grid.gd") const HTerrainData = preload("./hterrain_data.gd") @@ -156,6 +156,8 @@ var _shader_uses_texture_array := false var _material := ShaderMaterial.new() var _material_params_need_update := false +var _render_layer_mask := 1 + # Actual number of textures supported by the shader currently selected var _ground_texture_count_cache = 0 @@ -168,7 +170,7 @@ var _texture_set_migration_textures = null var _data: HTerrainData = null var _mesher := Mesher.new() -var _lodder := QuadTreeLod.new() +var _lodder = NativeFactory.get_quad_tree_lod() var _viewer_pos_world := Vector3() # [lod][z][x] -> chunk @@ -277,7 +279,7 @@ func _get_property_list(): "hint": PROPERTY_HINT_LAYERS_3D_PHYSICS }, { - "name": "Shader", + "name": "Rendering", "type": TYPE_NIL, "usage": PROPERTY_USAGE_GROUP }, @@ -312,6 +314,12 @@ func _get_property_list(): # This triggers `ERROR: Cannot get class 'HTerrainTextureSet'` # See https://github.com/godotengine/godot/pull/41264 #"hint_string": "HTerrainTextureSet" + }, + { + "name": "render_layers", + "type": TYPE_INT, + "usage": PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_STORAGE, + "hint": PROPERTY_HINT_LAYERS_3D_RENDER } ] @@ -368,6 +376,9 @@ func _get(key: String): elif key == "collision_mask": return _collision_mask + + elif key == "render_layers": + return get_render_layer_mask() func _set(key: String, value): @@ -424,6 +435,9 @@ func _set(key: String, value): if _collider != null: _collider.set_collision_mask(value) + elif key == "render_layers": + return set_render_layer_mask(value) + func get_texture_set() -> HTerrainTextureSet: return _texture_set @@ -458,6 +472,15 @@ func set_shader_param(param_name: String, v): _material.set_shader_param(param_name, v) +func set_render_layer_mask(mask: int): + _render_layer_mask = mask + _for_all_chunks(SetRenderLayerMaskAction.new(mask)) + + +func get_render_layer_mask() -> int: + return _render_layer_mask + + func _set_data_directory(dirpath: String): if dirpath != _get_data_directory(): if dirpath == "": @@ -473,7 +496,7 @@ func _set_data_directory(dirpath: String): # Create new var d := HTerrainData.new() d.resource_path = fpath - set_data(d) + set_data(d) else: _logger.warn("Setting twice the same terrain directory??") @@ -1087,7 +1110,6 @@ func _process(delta: float): if not Engine.is_editor_hint(): # In editor, the camera is only accessible from an editor plugin _update_viewer_position(null) - var viewer_pos := _viewer_pos_world if has_data(): if _data.is_locked(): @@ -1096,9 +1118,10 @@ func _process(delta: float): if _data.get_resolution() != 0: var gt := get_internal_transform() - var local_viewer_pos := gt.affine_inverse() * viewer_pos + # Viewer position such that 1 unit == 1 pixel in the heightmap + var viewer_pos_heightmap_local := gt.affine_inverse() * _viewer_pos_world #var time_before = OS.get_ticks_msec() - _lodder.update(local_viewer_pos) + _lodder.update(viewer_pos_heightmap_local) #var time_elapsed = OS.get_ticks_msec() - time_before #if Engine.get_frames_drawn() % 60 == 0: # _logger.debug(str("Lodder time: ", time_elapsed)) @@ -1107,7 +1130,7 @@ func _process(delta: float): # Note: the detail system is not affected by map scale, # so we have to send viewer position in world space for layer in _detail_layers: - layer.process(delta, viewer_pos) + layer.process(delta, _viewer_pos_world) _updated_chunks = 0 @@ -1236,7 +1259,7 @@ func set_area_dirty(origin_in_cells_x: int, origin_in_cells_y: int, \ for lod in range(_lodder.get_lod_count()): # Get grid and chunk size var grid = _chunks[lod] - var s := _lodder.get_lod_size(lod) + var s : int = _lodder.get_lod_factor(lod) # Convert rect into this lod's coordinates: # Pick min and max (included), divide them, then add 1 to max so it's excluded again @@ -1261,7 +1284,7 @@ func _cb_make_chunk(cpos_x: int, cpos_y: int, lod: int): if chunk == null: # This is the first time this chunk is required at this lod, generate it - var lod_factor := _lodder.get_lod_size(lod) + var lod_factor : int = _lodder.get_lod_factor(lod) var origin_in_cells_x := cpos_x * _chunk_size * lod_factor var origin_in_cells_y := cpos_y * _chunk_size * lod_factor @@ -1276,6 +1299,8 @@ func _cb_make_chunk(cpos_x: int, cpos_y: int, lod: int): chunk = HTerrainChunk.new(self, origin_in_cells_x, origin_in_cells_y, material) chunk.parent_transform_changed(get_internal_transform()) + chunk.set_render_layer_mask(_render_layer_mask) + var grid = _chunks[lod] var row = grid[cpos_y] row[cpos_x] = chunk @@ -1294,7 +1319,7 @@ func _cb_recycle_chunk(chunk: HTerrainChunk, cx: int, cy: int, lod: int): func _cb_get_vertical_bounds(cpos_x: int, cpos_y: int, lod: int): - var chunk_size := _chunk_size * _lodder.get_lod_size(lod) + var chunk_size : int = _chunk_size * _lodder.get_lod_factor(lod) var origin_in_cells_x := cpos_x * chunk_size var origin_in_cells_y := cpos_y * chunk_size # This is a hack for speed, @@ -1546,3 +1571,10 @@ class SetMaterialAction: chunk.set_material(material) +class SetRenderLayerMaskAction: + var mask: int = 0 + func _init(m: int): + mask = m + func exec(chunk): + chunk.set_render_layer_mask(mask) + diff --git a/addons/zylann.hterrain/hterrain_chunk.gd b/addons/zylann.hterrain/hterrain_chunk.gd index 0cd9e7e..f0f7be9 100644 --- a/addons/zylann.hterrain/hterrain_chunk.gd +++ b/addons/zylann.hterrain/hterrain_chunk.gd @@ -113,3 +113,8 @@ func set_aabb(aabb: AABB): assert(_mesh_instance != RID()) VisualServer.instance_set_custom_aabb(_mesh_instance, aabb) + +func set_render_layer_mask(mask: int): + assert(_mesh_instance != RID()) + VisualServer.instance_set_layer_mask(_mesh_instance, mask) + diff --git a/addons/zylann.hterrain/hterrain_data.gd b/addons/zylann.hterrain/hterrain_data.gd index f745cb4..aeb111d 100644 --- a/addons/zylann.hterrain/hterrain_data.gd +++ b/addons/zylann.hterrain/hterrain_data.gd @@ -417,7 +417,7 @@ func get_all_heights() -> PoolRealArray: # modified area. # # map_type: -# which kind of map changed +# which kind of map changed, see CHANNEL_* constants # # index: # index of the map that changed @@ -1159,18 +1159,36 @@ func _load_map(dir: String, map_type: int, index: int) -> bool: # In this particular case, we can use Godot ResourceLoader directly, # if the texture got imported. - if Engine.editor_hint: - # But in the editor we want textures to be editable, - # so we have to automatically load the data also in RAM - if map.image == null: - map.image = Image.new() - map.image.load(fpath) - _ensure_map_format(map.image, map_type, index) - var tex = load(fpath) + + var must_load_image_in_editor := true + + if tex != null and tex is Image: + # The texture is imported as Image, + # perhaps the user wants it to be accessible from RAM in game. + _logger.debug("Map {0} is imported as Image. An ImageTexture will be generated." \ + .format([get_map_debug_name(map_type, index)])) + map.image = tex + tex = ImageTexture.new() + var map_type_info = _map_types[map_type] + tex.create_from_image(map.image, map_type_info.texture_flags) + must_load_image_in_editor = false + map.texture = tex + if Engine.editor_hint: + if must_load_image_in_editor: + # But in the editor we want textures to be editable, + # so we have to automatically load the data also in RAM + if map.image == null: + map.image = Image.new() + map.image.load(fpath) + _ensure_map_format(map.image, map_type, index) + else: + # The heightmap is different. + # It has often uses beyond graphics, so we always keep a RAM copy by default. + var im = _try_load_0_8_0_heightmap(fpath, map_type, map.image, _logger) if typeof(im) == TYPE_BOOL: return false diff --git a/addons/zylann.hterrain/hterrain_detail_layer.gd b/addons/zylann.hterrain/hterrain_detail_layer.gd index 76c39ff..d4ad4cf 100644 --- a/addons/zylann.hterrain/hterrain_detail_layer.gd +++ b/addons/zylann.hterrain/hterrain_detail_layer.gd @@ -47,10 +47,12 @@ export(Texture) var texture : Texture setget set_texture, get_texture # How far detail meshes can be seen. # TODO Improve speed of _get_chunk_aabb() so we can increase the limit # See https://github.com/Zylann/godot_heightmap_plugin/issues/155 -export(float, 1.0, 500.0) var view_distance := 100.0 setget set_view_distance, get_view_distance +export(float, 1.0, 500.0) \ + var view_distance := 100.0 setget set_view_distance, get_view_distance # Custom shader to replace the default one. -export(Shader) var custom_shader : Shader setget set_custom_shader, get_custom_shader +export(Shader) \ + var custom_shader : Shader setget set_custom_shader, get_custom_shader # Density modifier, to make more or less detail meshes appear overall. export(float, 0, 10) var density := 4.0 setget set_density, get_density @@ -60,6 +62,10 @@ export(float, 0, 10) var density := 4.0 setget set_density, get_density # I would have called it `mesh` but that's too broad and conflicts with local vars ._. export(Mesh) var instance_mesh : Mesh setget set_instance_mesh, get_instance_mesh +# Exposes rendering layers, similar to `VisualInstance.layers` +export(int, LAYERS_3D_RENDER) \ + var render_layers := 1 setget set_render_layer_mask, get_render_layer_mask + var _material: ShaderMaterial = null var _default_shader: Shader = null @@ -248,6 +254,17 @@ func get_instance_mesh() -> Mesh: return instance_mesh +func set_render_layer_mask(mask: int): + render_layers = mask + for k in _chunks: + var chunk = _chunks[k] + chunk.set_layer_mask(mask) + + +func get_render_layer_mask() -> int: + return render_layers + + func _get_used_mesh() -> Mesh: if instance_mesh == null: return DefaultMesh @@ -305,8 +322,10 @@ func _on_terrain_transform_changed(gt: Transform): if terrain == null: _logger.error("Detail layer is not child of a terrain!") return + + var terrain_transform : Transform = terrain.get_internal_transform() - # Update AABBs, because scale might have changed + # Update AABBs and transforms, because scale might have changed for k in _chunks: var mmi = _chunks[k] var aabb = _get_chunk_aabb(terrain, Vector3(k.x * CHUNK_SIZE, 0, k.y * CHUNK_SIZE)) @@ -314,6 +333,7 @@ func _on_terrain_transform_changed(gt: Transform): aabb.position.x = 0 aabb.position.z = 0 mmi.set_aabb(aabb) + mmi.set_transform(_get_chunk_transform(terrain_transform, k.x, k.y)) func process(delta: float, viewer_pos: Vector3): @@ -331,7 +351,7 @@ func process(delta: float, viewer_pos: Vector3): var mmi = _chunks[k] mmi.set_multimesh(_multimesh) - var local_viewer_pos = viewer_pos - terrain.translation + var local_viewer_pos = terrain.global_transform.affine_inverse() * viewer_pos var viewer_cx = local_viewer_pos.x / CHUNK_SIZE var viewer_cz = local_viewer_pos.z / CHUNK_SIZE @@ -367,6 +387,8 @@ func process(delta: float, viewer_pos: Vector3): for cx in range(cmin_x, cmax_x): _add_debug_cube(terrain, _get_chunk_aabb(terrain, Vector3(cx, 0, cz) * CHUNK_SIZE)) + var terrain_transform : Transform = terrain.get_internal_transform() + for cz in range(cmin_z, cmax_z): for cx in range(cmin_x, cmax_x): @@ -378,7 +400,7 @@ func process(delta: float, viewer_pos: Vector3): var d = (aabb.position + 0.5 * aabb.size).distance_to(local_viewer_pos) if d < view_distance: - _load_chunk(terrain, cx, cz, aabb) + _load_chunk(terrain_transform, cx, cz, aabb) var to_recycle = [] @@ -418,11 +440,14 @@ func _get_chunk_aabb(terrain, lpos: Vector3): return aabb -func _load_chunk(terrain, cx: int, cz: int, aabb: AABB): - var lpos = Vector3(cx, 0, cz) * CHUNK_SIZE +func _get_chunk_transform(terrain_transform: Transform, cx: int, cz: int) -> Transform: + var lpos := Vector3(cx, 0, cz) * CHUNK_SIZE # Terrain scale is not used on purpose. Rotation is not supported. - var trans = Transform(Basis(), terrain.get_internal_transform().origin + lpos) + var trans := Transform(Basis(), terrain_transform.origin + lpos) + return trans + +func _load_chunk(terrain_transform: Transform, cx: int, cz: int, aabb: AABB): # Nullify XZ translation because that's done by transform already aabb.position.x = 0 aabb.position.z = 0 @@ -433,12 +458,15 @@ func _load_chunk(terrain, cx: int, cz: int, aabb: AABB): _multimesh_instance_pool.pop_back() else: mmi = DirectMultiMeshInstance.new() - mmi.set_world(terrain.get_world()) + mmi.set_world(get_world()) mmi.set_multimesh(_multimesh) + var trans := _get_chunk_transform(terrain_transform, cx, cz) + mmi.set_material_override(_material) mmi.set_transform(trans) mmi.set_aabb(aabb) + mmi.set_layer_mask(render_layers) mmi.set_visible(visible) _chunks[Vector2(cx, cz)] = mmi diff --git a/addons/zylann.hterrain/native/SConstruct b/addons/zylann.hterrain/native/SConstruct index 3e9ece8..ba2f7e0 100644 --- a/addons/zylann.hterrain/native/SConstruct +++ b/addons/zylann.hterrain/native/SConstruct @@ -16,7 +16,7 @@ target_path = "bin/" TARGET_NAME = "hterrain_native" # Local dependency paths -godot_headers_path = "godot-cpp/godot_headers/" +godot_headers_path = "godot-cpp/godot-headers/" cpp_bindings_path = "godot-cpp/" cpp_bindings_library = "libgodot-cpp" diff --git a/addons/zylann.hterrain/native/bin/linux/libhterrain_native.so b/addons/zylann.hterrain/native/bin/linux/libhterrain_native.so index 0e2b925..8dca509 100644 Binary files a/addons/zylann.hterrain/native/bin/linux/libhterrain_native.so and b/addons/zylann.hterrain/native/bin/linux/libhterrain_native.so differ diff --git a/addons/zylann.hterrain/native/bin/win64/hterrain_native.dll b/addons/zylann.hterrain/native/bin/win64/hterrain_native.dll index c12bc7e..52a475e 100644 Binary files a/addons/zylann.hterrain/native/bin/win64/hterrain_native.dll and b/addons/zylann.hterrain/native/bin/win64/hterrain_native.dll differ diff --git a/addons/zylann.hterrain/native/factory.gd b/addons/zylann.hterrain/native/factory.gd index 801a357..a5d5e6b 100644 --- a/addons/zylann.hterrain/native/factory.gd +++ b/addons/zylann.hterrain/native/factory.gd @@ -2,8 +2,9 @@ const NATIVE_PATH = "res://addons/zylann.hterrain/native/" const ImageUtilsGeneric = preload("./image_utils_generic.gd") +const QuadTreeLodGeneric = preload("./quad_tree_lod_generic.gd") -# See https://docs.godotengine.org/en/3.2/classes/class_os.html#class-os-method-get-name +# See https://docs.godotengine.org/en/stable/classes/class_os.html#class-os-method-get-name const _supported_os = { "Windows": true, "X11": true, @@ -17,7 +18,7 @@ static func is_native_available() -> bool: return false # API changes can cause binary incompatibility var v = Engine.get_version_info() - return v.major == 3 and v.minor == 2 + return v.major == 3 and v.minor >= 2 and v.minor <= 5 static func get_image_utils(): @@ -27,3 +28,10 @@ static func get_image_utils(): return ImageUtilsNative.new() return ImageUtilsGeneric.new() + +static func get_quad_tree_lod(): + if is_native_available(): + var QuadTreeLod = load(NATIVE_PATH + "quad_tree_lod.gdns") + if QuadTreeLod != null: + return QuadTreeLod.new() + return QuadTreeLodGeneric.new() diff --git a/addons/zylann.hterrain/native/quad_tree_lod.gdns b/addons/zylann.hterrain/native/quad_tree_lod.gdns new file mode 100644 index 0000000..9eb6a48 --- /dev/null +++ b/addons/zylann.hterrain/native/quad_tree_lod.gdns @@ -0,0 +1,8 @@ +[gd_resource type="NativeScript" load_steps=2 format=2] + +[ext_resource path="res://addons/zylann.hterrain/native/hterrain.gdnlib" type="GDNativeLibrary" id=1] + +[resource] +resource_name = "quad_tree_lod" +class_name = "QuadTreeLod" +library = ExtResource( 1 ) diff --git a/addons/zylann.hterrain/util/quad_tree_lod.gd b/addons/zylann.hterrain/native/quad_tree_lod_generic.gd similarity index 80% rename from addons/zylann.hterrain/util/quad_tree_lod.gd rename to addons/zylann.hterrain/native/quad_tree_lod_generic.gd index 00f5fd3..76555f4 100644 --- a/addons/zylann.hterrain/util/quad_tree_lod.gd +++ b/addons/zylann.hterrain/native/quad_tree_lod_generic.gd @@ -3,8 +3,8 @@ tool class Quad: var children = null - var origin_x := 0 - var origin_y := 0 + var origin_x : int = 0 + var origin_y : int = 0 var data = null func _init(): @@ -22,9 +22,9 @@ class Quad: var _tree := Quad.new() -var _max_depth := 0 -var _base_size := 16 -var _split_scale := 2.0 +var _max_depth : int = 0 +var _base_size : int = 16 +var _split_scale : float = 2.0 var _make_func : FuncRef = null var _recycle_func : FuncRef = null @@ -44,7 +44,7 @@ func clear(): static func compute_lod_count(base_size: int, full_size: int) -> int: - var po = 0 + var po : int = 0 while full_size > base_size: full_size = full_size >> 1 po += 1 @@ -70,12 +70,7 @@ func set_split_scale(p_split_scale: float): # Split scale must be greater than a threshold, # otherwise lods will decimate too fast and it will look messy - if p_split_scale < MIN: - p_split_scale = MIN - if p_split_scale > MAX: - p_split_scale = MAX - - _split_scale = float(p_split_scale) + _split_scale = clamp(p_split_scale, MIN, MAX) func get_split_scale() -> float: @@ -91,16 +86,15 @@ func update(view_pos: Vector3): _tree.data = _make_chunk(_max_depth, 0, 0) -# TODO Should be renamed get_lod_factor -func get_lod_size(lod: int) -> int: +func get_lod_factor(lod: int) -> int: return 1 << lod func _update(quad: Quad, lod: int, view_pos: Vector3): # This function should be called regularly over frames. - var lod_factor := get_lod_size(lod) - var chunk_size := _base_size * lod_factor + var lod_factor : int = get_lod_factor(lod) + var chunk_size : int = _base_size * lod_factor var world_center := \ chunk_size * (Vector3(quad.origin_x, 0, quad.origin_y) + Vector3(0.5, 0, 0.5)) @@ -137,25 +131,20 @@ func _update(quad: Quad, lod: int, view_pos: Vector3): if no_split_child and world_center.distance_to(view_pos) > split_distance: # Join - if quad.has_children(): - for i in 4: - var child = quad.children[i] - _recycle_chunk(child.data, child.origin_x, child.origin_y, lod - 1) - quad.data = null - quad.clear_children() - - assert(quad.data == null) - quad.data = _make_chunk(lod, quad.origin_x, quad.origin_y) + for i in 4: + var child = quad.children[i] + _recycle_chunk(child.data, child.origin_x, child.origin_y, lod - 1) + quad.clear_children() + quad.data = _make_chunk(lod, quad.origin_x, quad.origin_y) func _join_all_recursively(quad: Quad, lod: int): if quad.has_children(): - for i in range(4): - var child = quad.children[i] - _join_all_recursively(child, lod - 1) + for i in 4: + _join_all_recursively(quad.children[i], lod - 1) quad.clear_children() - + elif quad.data != null: _recycle_chunk(quad.data, quad.origin_x, quad.origin_y, lod) quad.data = null @@ -180,15 +169,14 @@ func debug_draw_tree(ci: CanvasItem): func _debug_draw_tree_recursive(ci: CanvasItem, quad: Quad, lod_index: int, child_index: int): if quad.has_children(): - for i in range(0, quad.children.size()): - var child = quad.children[i] - _debug_draw_tree_recursive(ci, child, lod_index - 1, i) + for i in 4: + _debug_draw_tree_recursive(ci, quad.children[i], lod_index - 1, i) else: - var size := get_lod_size(lod_index) - var checker := 0 + var size : int = get_lod_factor(lod_index) + var checker : int = 0 if child_index == 1 or child_index == 2: checker = 1 - var chunk_indicator := 0 + var chunk_indicator : int = 0 if quad.data != null: chunk_indicator = 1 var r := Rect2(Vector2(quad.origin_x, quad.origin_y) * size, Vector2(size, size)) diff --git a/addons/zylann.hterrain/native/src/gd_library.cpp b/addons/zylann.hterrain/native/src/gd_library.cpp index d953903..2836ecd 100644 --- a/addons/zylann.hterrain/native/src/gd_library.cpp +++ b/addons/zylann.hterrain/native/src/gd_library.cpp @@ -1,4 +1,5 @@ #include "image_utils.h" +#include "quad_tree_lod.h" extern "C" { @@ -23,6 +24,7 @@ void GDN_EXPORT godot_nativescript_init(void *handle) { godot::Godot::nativescript_init(handle); godot::register_tool_class(); + godot::register_tool_class(); } } // extern "C" diff --git a/addons/zylann.hterrain/native/src/quad_tree_lod.cpp b/addons/zylann.hterrain/native/src/quad_tree_lod.cpp new file mode 100644 index 0000000..592375e --- /dev/null +++ b/addons/zylann.hterrain/native/src/quad_tree_lod.cpp @@ -0,0 +1,242 @@ +#include "quad_tree_lod.h" + +namespace godot { + +void QuadTreeLod::set_callbacks(Ref make_cb, Ref recycle_cb, Ref vbounds_cb) { + _make_func = make_cb; + _recycle_func = recycle_cb; + _vertical_bounds_func = vbounds_cb; +} + +int QuadTreeLod::get_lod_count() { + // TODO make this a count, not max + return _max_depth + 1; +} + +int QuadTreeLod::get_lod_factor(int lod) { + return 1 << lod; +} + +int QuadTreeLod::compute_lod_count(int base_size, int full_size) { + int po = 0; + while (full_size > base_size) { + full_size = full_size >> 1; + po += 1; + } + return po; +} + +// The higher, the longer LODs will spread and higher the quality. +// The lower, the shorter LODs will spread and lower the quality. +void QuadTreeLod::set_split_scale(real_t p_split_scale) { + real_t MIN = 2.0f; + real_t MAX = 5.0f; + + // Split scale must be greater than a threshold, + // otherwise lods will decimate too fast and it will look messy + if (p_split_scale < MIN) + p_split_scale = MIN; + if (p_split_scale > MAX) + p_split_scale = MAX; + + _split_scale = p_split_scale; +} + +real_t QuadTreeLod::get_split_scale() { + return _split_scale; +} + +void QuadTreeLod::clear() { + _join_all_recursively(ROOT, _max_depth); + _max_depth = 0; + _base_size = 0; +} + +void QuadTreeLod::create_from_sizes(int base_size, int full_size) { + clear(); + _base_size = base_size; + _max_depth = compute_lod_count(base_size, full_size); + + // Total qty of nodes is (N^L - 1) / (N - 1). -1 for root, where N=num children, L=levels including the root + int node_count = ((static_cast(pow(4, _max_depth+1)) - 1) / (4 - 1)) - 1; + _node_pool.resize(node_count); // e.g. ((4^6 -1) / 3 ) - 1 = 1364 excluding root + + _free_indices.resize((node_count / 4)); // 1364 / 4 = 341 + for (int i = 0; i < _free_indices.size(); i++) // i = 0 to 340, *4 = 0 to 1360 + _free_indices[i] = 4 * i; // _node_pool[4*0 + i0] is first child, [4*340 + i3] is last +} + +void QuadTreeLod::update(Vector3 view_pos) { + _update(ROOT, _max_depth, view_pos); + + // This makes sure we keep seeing the lowest LOD, + // if the tree is cleared while we are far away + Quad *root = _get_root(); + if (!root->has_children() && root->is_null()) + root->set_data(_make_chunk(_max_depth, 0, 0)); +} + +void QuadTreeLod::debug_draw_tree(CanvasItem *ci) { + if (ci != nullptr) + _debug_draw_tree_recursive(ci, ROOT, _max_depth, 0); +} + +// Intention is to only clear references to children +void QuadTreeLod::_clear_children(unsigned int index) { + Quad *quad = _get_node(index); + if (quad->has_children()) { + _recycle_children(quad->first_child); + quad->first_child = NO_CHILDREN; + } +} + +// Returns the index of the first_child. Allocates from _free_indices. +unsigned int QuadTreeLod::_allocate_children() { + if (_free_indices.size() == 0) { + return NO_CHILDREN; + } + + unsigned int i0 = _free_indices[_free_indices.size() - 1]; + _free_indices.pop_back(); + return i0; +} + +// Pass the first_child index, not the parent index. Stores back in _free_indices. +void QuadTreeLod::_recycle_children(unsigned int i0) { + // Debug check, there is no use case in recycling a node which is not a first child + CRASH_COND(i0 % 4 != 0); + + for (int i = 0; i < 4; ++i) { + _node_pool[i0 + i].init(); + } + + _free_indices.push_back(i0); +} + +Variant QuadTreeLod::_make_chunk(int lod, int origin_x, int origin_y) { + if (_make_func.is_valid()) { + return _make_func->call_func(origin_x, origin_y, lod); + } else { + return Variant(); + } +} + +void QuadTreeLod::_recycle_chunk(unsigned int quad_index, int lod) { + Quad *quad = _get_node(quad_index); + if (_recycle_func.is_valid()) { + _recycle_func->call_func(quad->get_data(), quad->origin_x, quad->origin_y, lod); + } +} + +void QuadTreeLod::_join_all_recursively(unsigned int quad_index, int lod) { + Quad *quad = _get_node(quad_index); + + if (quad->has_children()) { + for (int i = 0; i < 4; i++) { + _join_all_recursively(quad->first_child + i, lod - 1); + } + _clear_children(quad_index); + + } else if (quad->is_valid()) { + _recycle_chunk(quad_index, lod); + quad->clear_data(); + } +} + +void QuadTreeLod::_update(unsigned int quad_index, int lod, Vector3 view_pos) { + // This function should be called regularly over frames. + Quad *quad = _get_node(quad_index); + int lod_factor = get_lod_factor(lod); + int chunk_size = _base_size * lod_factor; + Vector3 world_center = static_cast(chunk_size) * (Vector3(static_cast(quad->origin_x), 0.f, static_cast(quad->origin_y)) + Vector3(0.5f, 0.f, 0.5f)); + + if (_vertical_bounds_func.is_valid()) { + Variant result = _vertical_bounds_func->call_func(quad->origin_x, quad->origin_y, lod); + ERR_FAIL_COND(result.get_type() != Variant::VECTOR2); + Vector2 vbounds = static_cast(result); + world_center.y = (vbounds.x + vbounds.y) / 2.0f; + } + + int split_distance = _base_size * lod_factor * static_cast(_split_scale); + + if (!quad->has_children()) { + if (lod > 0 && world_center.distance_to(view_pos) < split_distance) { + // Split + unsigned int new_idx = _allocate_children(); + ERR_FAIL_COND(new_idx == NO_CHILDREN); + quad->first_child = new_idx; + + for (int i = 0; i < 4; i++) { + Quad *child = _get_node(quad->first_child + i); + child->origin_x = quad->origin_x * 2 + (i & 1); + child->origin_y = quad->origin_y * 2 + ((i & 2) >> 1); + child->set_data(_make_chunk(lod - 1, child->origin_x, child->origin_y)); + // If the quad needs to split more, we'll ask more recycling... + } + + if (quad->is_valid()) { + _recycle_chunk(quad_index, lod); + quad->clear_data(); + } + } + } else { + bool no_split_child = true; + + for (int i = 0; i < 4; i++) { + _update(quad->first_child + i, lod - 1, view_pos); + + if (_get_node(quad->first_child + i)->has_children()) + no_split_child = false; + } + + if (no_split_child && world_center.distance_to(view_pos) > split_distance) { + // Join + for (int i = 0; i < 4; i++) { + _recycle_chunk(quad->first_child + i, lod - 1); + } + _clear_children(quad_index); + quad->set_data(_make_chunk(lod, quad->origin_x, quad->origin_y)); + } + } +} // _update + +void QuadTreeLod::_debug_draw_tree_recursive(CanvasItem *ci, unsigned int quad_index, int lod_index, int child_index) { + Quad *quad = _get_node(quad_index); + + if (quad->has_children()) { + int ch_index = quad->first_child; + for (int i = 0; i < 4; i++) { + _debug_draw_tree_recursive(ci, ch_index + i, lod_index - 1, i); + } + + } else { + real_t size = static_cast(get_lod_factor(lod_index)); + int checker = 0; + if (child_index == 1 || child_index == 2) + checker = 1; + + int chunk_indicator = 0; + if (quad->is_valid()) + chunk_indicator = 1; + + Rect2 rect2(Vector2(static_cast(quad->origin_x), static_cast(quad->origin_y)) * size, + Vector2(size, size)); + Color color(1.0f - static_cast(lod_index) * 0.2f, 0.2f * static_cast(checker), static_cast(chunk_indicator), 1.0f); + ci->draw_rect(rect2, color); + } +} + +void QuadTreeLod::_register_methods() { + register_method("set_callbacks", &QuadTreeLod::set_callbacks); + register_method("get_lod_count", &QuadTreeLod::get_lod_count); + register_method("get_lod_factor", &QuadTreeLod::get_lod_factor); + register_method("compute_lod_count", &QuadTreeLod::compute_lod_count); + register_method("set_split_scale", &QuadTreeLod::set_split_scale); + register_method("get_split_scale", &QuadTreeLod::get_split_scale); + register_method("clear", &QuadTreeLod::clear); + register_method("create_from_sizes", &QuadTreeLod::create_from_sizes); + register_method("update", &QuadTreeLod::update); + register_method("debug_draw_tree", &QuadTreeLod::debug_draw_tree); +} + +} // namespace godot diff --git a/addons/zylann.hterrain/native/src/quad_tree_lod.h b/addons/zylann.hterrain/native/src/quad_tree_lod.h new file mode 100644 index 0000000..a4132ec --- /dev/null +++ b/addons/zylann.hterrain/native/src/quad_tree_lod.h @@ -0,0 +1,121 @@ +#ifndef QUAD_TREE_LOD_H +#define QUAD_TREE_LOD_H + +#include +#include +#include + +#include + +namespace godot { + +class QuadTreeLod : public Reference { + GODOT_CLASS(QuadTreeLod, Reference) +public: + static void _register_methods(); + + QuadTreeLod() {} + ~QuadTreeLod() {} + + void _init() {} + + void set_callbacks(Ref make_cb, Ref recycle_cb, Ref vbounds_cb); + int get_lod_count(); + int get_lod_factor(int lod); + int compute_lod_count(int base_size, int full_size); + void set_split_scale(real_t p_split_scale); + real_t get_split_scale(); + void clear(); + void create_from_sizes(int base_size, int full_size); + void update(Vector3 view_pos); + void debug_draw_tree(CanvasItem *ci); + +private: + static const unsigned int NO_CHILDREN = -1; + static const unsigned int ROOT = -1; + + class Quad { + public: + unsigned int first_child = NO_CHILDREN; + int origin_x = 0; + int origin_y = 0; + + Quad() { + init(); + } + + ~Quad() { + } + + inline void init() { + first_child = NO_CHILDREN; + origin_x = 0; + origin_y = 0; + clear_data(); + } + + inline void clear_data() { + _data = Variant(); + } + + inline bool has_children() { + return first_child != NO_CHILDREN; + } + + inline bool is_null() { + return _data.get_type() == Variant::NIL; + } + + inline bool is_valid() { + return _data.get_type() != Variant::NIL; + } + + inline Variant get_data() { + return _data; + } + + inline void set_data(Variant p_data) { + _data = p_data; + } + + private: + Variant _data; // Type is HTerrainChunk.gd : Object + }; + + Quad _root; + std::vector _node_pool; + std::vector _free_indices; + + int _max_depth = 0; + int _base_size = 16; + real_t _split_scale = 2.0f; + + Ref _make_func; + Ref _recycle_func; + Ref _vertical_bounds_func; + + inline Quad *_get_root() { + return &_root; + } + + inline Quad *_get_node(unsigned int index) { + if (index == ROOT) { + return &_root; + } else { + return &_node_pool[index]; + } + } + + void _clear_children(unsigned int index); + unsigned int _allocate_children(); + void _recycle_children(unsigned int i0); + Variant _make_chunk(int lod, int origin_x, int origin_y); + void _recycle_chunk(unsigned int quad_index, int lod); + void _join_all_recursively(unsigned int quad_index, int lod); + void _update(unsigned int quad_index, int lod, Vector3 view_pos); + void _debug_draw_tree_recursive(CanvasItem *ci, unsigned int quad_index, int lod_index, int child_index); +}; // class QuadTreeLod + +} // namespace godot + +#endif // QUAD_TREE_LOD_H diff --git a/addons/zylann.hterrain/plugin.cfg b/addons/zylann.hterrain/plugin.cfg index fcecf6c..1fc9509 100644 --- a/addons/zylann.hterrain/plugin.cfg +++ b/addons/zylann.hterrain/plugin.cfg @@ -3,5 +3,5 @@ name="Heightmap Terrain" description="Heightmap-based terrain" author="Marc Gilleron" -version="1.5.2" +version="1.5.3 dev" script="tools/plugin.gd" diff --git a/addons/zylann.hterrain/tools/about/about_dialog.tscn b/addons/zylann.hterrain/tools/about/about_dialog.tscn index 367ec82..4c4136e 100644 --- a/addons/zylann.hterrain/tools/about/about_dialog.tscn +++ b/addons/zylann.hterrain/tools/about/about_dialog.tscn @@ -63,6 +63,18 @@ wacyym Sergey Lapin (slapin) Jonas (NoFr1ends) lenis0012 +Phyronnaz +RonanZe +furtherorbit +jp.owo.Manda (segfault-god) +hidemat +Jakub Buriánek (Buri) +Justin Swanhart (Greenlion) +Sebastian Clausen (sclausen) +MrGreaterThan +baals +Treer +stackdump.eth " text = "Version: {version} Author: Marc Gilleron @@ -74,6 +86,18 @@ wacyym Sergey Lapin (slapin) Jonas (NoFr1ends) lenis0012 +Phyronnaz +RonanZe +furtherorbit +jp.owo.Manda (segfault-god) +hidemat +Jakub Buriánek (Buri) +Justin Swanhart (Greenlion) +Sebastian Clausen (sclausen) +MrGreaterThan +baals +Treer +stackdump.eth " script = ExtResource( 3 ) diff --git a/addons/zylann.hterrain/tools/brush/brush.gd b/addons/zylann.hterrain/tools/brush/brush.gd new file mode 100644 index 0000000..69da939 --- /dev/null +++ b/addons/zylann.hterrain/tools/brush/brush.gd @@ -0,0 +1,184 @@ +tool + +# Brush properties (shape, transform, timing and opacity). +# Other attributes like color, height or texture index are tool-specific, +# while brush properties apply to all of them. +# This is separate from Painter because it could apply to multiple Painters at once. + +const Errors = preload("../../util/errors.gd") + +const SHAPES_DIR = "addons/zylann.hterrain/tools/brush/shapes" +const DEFAULT_BRUSH = "round2.exr" +# Reasonable size for sliders to be usable +const MAX_SIZE_FOR_SLIDERS = 500 +# Absolute size limit. Terrains can't be larger than that, and it will be very slow to paint +const MAX_SIZE = 4000 + +signal size_changed(new_size) +signal shapes_changed + +var _size := 32 +var _opacity := 1.0 +var _random_rotation := false +var _pressure_enabled := false +var _pressure_over_scale := 0.5 +var _pressure_over_opacity := 0.5 +# TODO Rename stamp_*? +var _frequency_distance := 0.0 +var _frequency_time_ms := 0 +# Array of greyscale Textures +var _shapes := [] + +var _shape_index := 0 +var _prev_position := Vector2(-999, -999) +var _prev_time_ms := 0 + + +func set_size(size: int): + if size < 1: + size = 1 + if size != _size: + _size = size + emit_signal("size_changed", _size) + + +func get_size() -> int: + return _size + + +func set_opacity(opacity: float): + _opacity = clamp(opacity, 0.0, 1.0) + + +func get_opacity() -> float: + return _opacity + + +func set_random_rotation_enabled(enabled: bool): + _random_rotation = enabled + + +func is_random_rotation_enabled() -> bool: + return _random_rotation + + +func set_pressure_enabled(enabled: bool): + _pressure_enabled = enabled + + +func is_pressure_enabled() -> bool: + return _pressure_enabled + + +func set_pressure_over_scale(amount: float): + _pressure_over_scale = clamp(amount, 0.0, 1.0) + + +func get_pressure_over_scale() -> float: + return _pressure_over_scale + + +func set_pressure_over_opacity(amount: float): + _pressure_over_opacity = clamp(amount, 0.0, 1.0) + + +func get_pressure_over_opacity() -> float: + return _pressure_over_opacity + + +func set_frequency_distance(d: float): + _frequency_distance = max(d, 0.0) + + +func get_frequency_distance() -> float: + return _frequency_distance + + +func set_frequency_time_ms(t: int): + if t < 0: + t = 0 + _frequency_time_ms = t + + +func get_frequency_time_ms() -> int: + return _frequency_time_ms + + +func set_shapes(shapes: Array): + assert(len(shapes) >= 1) + for s in shapes: + assert(s != null) + assert(s is Texture) + _shapes = shapes.duplicate(false) + if _shape_index >= len(_shapes): + _shape_index = len(_shapes) - 1 + emit_signal("shapes_changed") + + +func get_shapes() -> Array: + return _shapes.duplicate(false) + + +func get_shape(i: int) -> Texture: + return _shapes[i] + + +static func load_shape_from_image_file(fpath: String, logger) -> Texture: + var im := Image.new() + var err := im.load(fpath) + if err != OK: + logger.error("Could not load image at '{0}', error {1}" \ + .format([fpath, Errors.get_message(err)])) + return null + var tex := ImageTexture.new() + tex.create_from_image(im, Texture.FLAG_FILTER) + return tex + + +# Call this while handling mouse or pen input. +# If it returns false, painting should not run. +func configure_paint_input(painters: Array, position: Vector2, pressure: float) -> bool: + assert(len(_shapes) != 0) + + # DEBUG + #pressure = 0.5 + 0.5 * sin(OS.get_ticks_msec() / 200.0) + + if position.distance_to(_prev_position) < _frequency_distance: + return false + var now = OS.get_ticks_msec() + if (now - _prev_time_ms) < _frequency_time_ms: + return false + _prev_position = position + _prev_time_ms = now + + for painter in painters: + if _random_rotation: + painter.set_brush_rotation(rand_range(-PI, PI)) + else: + painter.set_brush_rotation(0.0) + + painter.set_brush_texture(_shapes[_shape_index]) + painter.set_brush_size(_size) + + if _pressure_enabled: + painter.set_brush_scale(lerp(1.0, pressure, _pressure_over_scale)) + painter.set_brush_opacity(_opacity * lerp(1.0, pressure, _pressure_over_opacity)) + else: + painter.set_brush_scale(1.0) + painter.set_brush_opacity(_opacity) + + #painter.paint_input(position) + + _shape_index += 1 + if _shape_index >= len(_shapes): + _shape_index = 0 + + return true + + +# Call this when the user releases the pen or mouse button +func on_paint_end(): + _prev_position = Vector2(-999, -999) + _prev_time_ms = 0 + + diff --git a/addons/zylann.hterrain/tools/brush/brush_editor.gd b/addons/zylann.hterrain/tools/brush/brush_editor.gd index 1834e9b..e90b2b7 100644 --- a/addons/zylann.hterrain/tools/brush/brush_editor.gd +++ b/addons/zylann.hterrain/tools/brush/brush_editor.gd @@ -1,13 +1,15 @@ tool extends Control -const Brush = preload("./terrain_painter.gd") +const TerrainPainter = preload("./terrain_painter.gd") +const Brush = preload("./brush.gd") const Errors = preload("../../util/errors.gd") #const NativeFactory = preload("../../native/factory.gd") const Logger = preload("../../util/logger.gd") -const SHAPES_DIR = "addons/zylann.hterrain/tools/brush/shapes" -const DEFAULT_BRUSH = "round2.exr" +const BrushSettingsDialogScene = preload("./settings_dialog/brush_settings_dialog.tscn") +const BrushSettingsDialog = preload("./settings_dialog/brush_settings_dialog.gd") + onready var _size_slider := $GridContainer/BrushSizeControl/Slider as Slider onready var _size_value_label := $GridContainer/BrushSizeControl/Label as Label @@ -37,8 +39,9 @@ onready var _slope_limit_control = $GridContainer/SlopeLimit onready var _shape_texture_rect = get_node("BrushShapeButton/TextureRect") -var _brush : Brush +var _terrain_painter : TerrainPainter var _load_image_dialog = null +var _brush_settings_dialog = null var _logger = Logger.get_for(self) # TODO This is an ugly workaround for https://github.com/godotengine/godot/issues/19479 @@ -62,7 +65,7 @@ func _ready(): _holes_checkbox.connect("toggled", self, "_on_holes_checkbox_toggled") _slope_limit_control.connect("changed", self, "_on_slope_limit_changed") - _size_slider.max_value = 200 + _size_slider.max_value = Brush.MAX_SIZE_FOR_SLIDERS #if NativeFactory.is_native_available(): # _size_slider.max_value = 200 #else: @@ -70,21 +73,19 @@ func _ready(): func setup_dialogs(base_control: Control): - assert(_load_image_dialog == null) - _load_image_dialog = EditorFileDialog.new() - _load_image_dialog.mode = EditorFileDialog.MODE_OPEN_FILE - _load_image_dialog.add_filter("*.exr ; EXR files") - _load_image_dialog.resizable = true - _load_image_dialog.access = EditorFileDialog.ACCESS_FILESYSTEM - _load_image_dialog.current_dir = SHAPES_DIR - _load_image_dialog.connect("file_selected", self, "_on_LoadImageDialog_file_selected") - base_control.add_child(_load_image_dialog) + assert(_brush_settings_dialog == null) + _brush_settings_dialog = BrushSettingsDialogScene.instance() + base_control.add_child(_brush_settings_dialog) + + # That dialog has sub-dialogs + _brush_settings_dialog.setup_dialogs(base_control) + _brush_settings_dialog.set_brush(_terrain_painter.get_brush()) func _exit_tree(): - if _load_image_dialog != null: - _load_image_dialog.queue_free() - _load_image_dialog = null + if _brush_settings_dialog != null: + _brush_settings_dialog.queue_free() + _brush_settings_dialog = null # Testing display modes #var mode = 0 @@ -96,48 +97,61 @@ func _exit_tree(): # if mode >= Brush.MODE_COUNT: # mode = 0 -func set_brush(brush: Brush): - if _brush != null: - _brush.disconnect("changed", self, "_on_brush_changed") +func set_terrain_painter(terrain_painter: TerrainPainter): + if _terrain_painter != null: + _terrain_painter.disconnect("flatten_height_changed", self, "_on_flatten_height_changed") + _terrain_painter.get_brush().disconnect("shapes_changed", self, "_on_brush_shapes_changed") - _brush = brush + _terrain_painter = terrain_painter - if brush != null: + if _terrain_painter != null: # TODO Had an issue in Godot 3.2.3 where mismatching type would silently cast to null... # It happens if the argument went through a Variant (for example if call_deferred is used) - assert(_brush != null) + assert(_terrain_painter != null) - if _brush != null: - # Initial params - _size_slider.value = brush.get_brush_size() - _opacity_slider.ratio = brush.get_opacity() - _flatten_height_box.value = brush.get_flatten_height() - _color_picker.get_picker().color = brush.get_color() - _density_slider.value = brush.get_detail_density() - _holes_checkbox.pressed = not brush.get_mask_flag() + if _terrain_painter != null: + # Initial brush params + _size_slider.value = _terrain_painter.get_brush().get_size() + _opacity_slider.ratio = _terrain_painter.get_brush().get_opacity() + # Initial specific params + _flatten_height_box.value = _terrain_painter.get_flatten_height() + _color_picker.get_picker().color = _terrain_painter.get_color() + _density_slider.value = _terrain_painter.get_detail_density() + _holes_checkbox.pressed = not _terrain_painter.get_mask_flag() - var low = rad2deg(brush.get_slope_limit_low_angle()) - var high = rad2deg(brush.get_slope_limit_high_angle()) + var low = rad2deg(_terrain_painter.get_slope_limit_low_angle()) + var high = rad2deg(_terrain_painter.get_slope_limit_high_angle()) _slope_limit_control.set_values(low, high) - set_display_mode(brush.get_mode()) - _set_brush_shape_from_file(SHAPES_DIR.plus_file(DEFAULT_BRUSH)) + set_display_mode(_terrain_painter.get_mode()) - _brush.connect("changed", self, "_on_brush_properties_changed") + # Load default brush + var brush := _terrain_painter.get_brush() + var default_shape_fpath := Brush.SHAPES_DIR.plus_file(Brush.DEFAULT_BRUSH) + var default_shape := Brush.load_shape_from_image_file(default_shape_fpath, _logger) + brush.set_shapes([default_shape]) + _shape_texture_rect.texture = brush.get_shape(0) + + _terrain_painter.connect("flatten_height_changed", self, "_on_flatten_height_changed") + brush.connect("shapes_changed", self, "_on_brush_shapes_changed") -func _on_brush_properties_changed(): - _flatten_height_box.value = _brush.get_flatten_height() +func _on_flatten_height_changed(): + _flatten_height_box.value = _terrain_painter.get_flatten_height() _flatten_height_pick_button.pressed = false +func _on_brush_shapes_changed(): + _shape_texture_rect.texture = _terrain_painter.get_brush().get_shape(0) + + func set_display_mode(mode: int): - var show_flatten := mode == Brush.MODE_FLATTEN - var show_color := mode == Brush.MODE_COLOR - var show_density := mode == Brush.MODE_DETAIL - var show_opacity := mode != Brush.MODE_MASK - var show_holes := mode == Brush.MODE_MASK - var show_slope_limit := mode == Brush.MODE_SPLAT + var show_flatten := mode == TerrainPainter.MODE_FLATTEN + var show_color := mode == TerrainPainter.MODE_COLOR + var show_density := mode == TerrainPainter.MODE_DETAIL + var show_opacity := mode != TerrainPainter.MODE_MASK + var show_holes := mode == TerrainPainter.MODE_MASK + var show_slope_limit := mode == TerrainPainter.MODE_SPLAT _set_visibility_of(_opacity_label, show_opacity) _set_visibility_of(_opacity_control, show_opacity) @@ -161,76 +175,47 @@ func set_display_mode(mode: int): func _on_size_slider_value_changed(v: float): - if _brush != null: - _brush.set_brush_size(int(v)) + if _terrain_painter != null: + _terrain_painter.set_brush_size(int(v)) _size_value_label.text = str(v) func _on_opacity_slider_value_changed(v: float): - if _brush != null: - _brush.set_opacity(_opacity_slider.ratio) + if _terrain_painter != null: + _terrain_painter.set_opacity(_opacity_slider.ratio) _opacity_value_label.text = str(v) func _on_flatten_height_box_value_changed(v: float): - if _brush != null: - _brush.set_flatten_height(v) + if _terrain_painter != null: + _terrain_painter.set_flatten_height(v) func _on_color_picker_color_changed(v: Color): - if _brush != null: - _brush.set_color(v) + if _terrain_painter != null: + _terrain_painter.set_color(v) func _on_density_slider_changed(v: float): - if _brush != null: - _brush.set_detail_density(v) + if _terrain_painter != null: + _terrain_painter.set_detail_density(v) func _on_holes_checkbox_toggled(v: bool): - if _brush != null: + if _terrain_painter != null: # When checked, we draw holes. When unchecked, we clear holes - _brush.set_mask_flag(not v) + _terrain_painter.set_mask_flag(not v) func _on_BrushShapeButton_pressed(): - _load_image_dialog.popup_centered_ratio(0.7) - - -func _on_LoadImageDialog_file_selected(path: String): - _set_brush_shape_from_file(path) - - -func _set_brush_shape_from_file(path: String): - var im := Image.new() - var err := im.load(path) - if err != OK: - _logger.error("Could not load image at '{0}', error {1}" \ - .format([path, Errors.get_message(err)])) - return - - var tex := ImageTexture.new() - tex.create_from_image(im, Texture.FLAG_FILTER) - - if _brush != null: - var im2 := im - var v := Engine.get_version_info() - if v.major == 3 and v.minor < 1: - # Forcing image brushes would ruin resized ones, - # due to https://github.com/godotengine/godot/issues/24244 - if path.find(SHAPES_DIR.plus_file(DEFAULT_BRUSH)) != -1: - im2 = null - - _brush.set_brush_texture(tex) - - _shape_texture_rect.texture = tex + _brush_settings_dialog.popup_centered() func _on_FlattenHeightPickButton_pressed(): - _brush.set_meta("pick_height", true) + _terrain_painter.set_meta("pick_height", true) func _on_slope_limit_changed(): var low = deg2rad(_slope_limit_control.get_low_value()) var high = deg2rad(_slope_limit_control.get_high_value()) - _brush.set_slope_limit_angles(low, high) + _terrain_painter.set_slope_limit_angles(low, high) diff --git a/addons/zylann.hterrain/tools/brush/brush_editor.tscn b/addons/zylann.hterrain/tools/brush/brush_editor.tscn index 286adef..8965d37 100644 --- a/addons/zylann.hterrain/tools/brush/brush_editor.tscn +++ b/addons/zylann.hterrain/tools/brush/brush_editor.tscn @@ -60,7 +60,7 @@ margin_bottom = 16.0 size_flags_horizontal = 3 size_flags_vertical = 1 min_value = 2.0 -max_value = 200.0 +max_value = 500.0 value = 2.0 exp_edit = true rounded = true @@ -120,6 +120,7 @@ margin_bottom = 24.0 size_flags_horizontal = 3 min_value = -500.0 max_value = 500.0 +step = 0.01 [node name="FlattenHeightPickButton" type="Button" parent="GridContainer/HB"] margin_left = 115.0 @@ -185,5 +186,6 @@ script = ExtResource( 3 ) range = Vector2( 0, 90 ) [node name="Temp" type="Node" parent="."] + [connection signal="pressed" from="BrushShapeButton" to="." method="_on_BrushShapeButton_pressed"] [connection signal="pressed" from="GridContainer/HB/FlattenHeightPickButton" to="." method="_on_FlattenHeightPickButton_pressed"] diff --git a/addons/zylann.hterrain/tools/brush/decal.gd b/addons/zylann.hterrain/tools/brush/decal.gd index 63d1dac..d1852fe 100644 --- a/addons/zylann.hterrain/tools/brush/decal.gd +++ b/addons/zylann.hterrain/tools/brush/decal.gd @@ -5,8 +5,8 @@ const DirectMeshInstance = preload("../../util/direct_mesh_instance.gd") const HTerrainData = preload("../../hterrain_data.gd") const Util = preload("../../util/util.gd") -var _mesh_instance = null -var _mesh = null +var _mesh_instance : DirectMeshInstance +var _mesh : PlaneMesh var _material = ShaderMaterial.new() #var _debug_mesh = CubeMesh.new() #var _debug_mesh_instance = null @@ -18,7 +18,7 @@ func _init(): _material.shader = load("res://addons/zylann.hterrain/tools/brush/decal.shader") _mesh_instance = DirectMeshInstance.new() _mesh_instance.set_material(_material) - + _mesh = PlaneMesh.new() _mesh_instance.set_mesh(_mesh) @@ -26,15 +26,13 @@ func _init(): #_debug_mesh_instance.set_mesh(_debug_mesh) -func set_size(size): +func set_size(size: float): _mesh.size = Vector2(size, size) # Must line up to terrain vertex policy, so must apply an off-by-one. # If I don't do that, the brush will appear to wobble above the ground var ss = size - 1 # Don't subdivide too much - if ss > 50: - ss /= 2 - if ss > 50: + while ss > 50: ss /= 2 _mesh.subdivide_width = ss _mesh.subdivide_depth = ss @@ -72,7 +70,7 @@ func set_terrain(terrain): update_visibility() -func set_position(p_local_pos): +func set_position(p_local_pos: Vector3): assert(_terrain != null) assert(typeof(p_local_pos) == TYPE_VECTOR3) diff --git a/addons/zylann.hterrain/tools/brush/no_blend.gdshader b/addons/zylann.hterrain/tools/brush/no_blend.gdshader new file mode 100644 index 0000000..8ae0f84 --- /dev/null +++ b/addons/zylann.hterrain/tools/brush/no_blend.gdshader @@ -0,0 +1,6 @@ +shader_type canvas_item; +render_mode blend_disabled; + +void fragment() { + COLOR = texture(TEXTURE, UV); +} diff --git a/addons/zylann.hterrain/tools/brush/painter.gd b/addons/zylann.hterrain/tools/brush/painter.gd index 4bec9fc..f25a8b1 100644 --- a/addons/zylann.hterrain/tools/brush/painter.gd +++ b/addons/zylann.hterrain/tools/brush/painter.gd @@ -1,7 +1,7 @@ # Core logic to paint a texture using shaders, with undo/redo support. # Operations are delayed so results are only available the next frame. -# This doesn't implement UI, only the painting logic. +# This doesn't implement UI or brush behavior, only rendering logic. # # Note: due to the absence of channel separation function in Image, # you may need to use multiple painters at once if your application exploits multiple channels. @@ -13,9 +13,20 @@ extends Node const Logger = preload("../../util/logger.gd") const Util = preload("../../util/util.gd") +const NoBlendShader = preload("./no_blend.gdshader") const UNDO_CHUNK_SIZE = 64 -const BRUSH_TEXTURE_SHADER_PARAM = "u_brush_texture" + +# All painting shaders can use these common parameters +const SHADER_PARAM_SRC_TEXTURE = "u_src_texture" +const SHADER_PARAM_SRC_RECT = "u_src_rect" +const SHADER_PARAM_OPACITY = "u_opacity" + +const _API_SHADER_PARAMS = [ + SHADER_PARAM_SRC_TEXTURE, + SHADER_PARAM_SRC_RECT, + SHADER_PARAM_OPACITY +] # Emitted when a region of the painted texture actually changed. # Note 1: the image might not have changed yet at this point. @@ -42,10 +53,20 @@ const _supported_formats = [ Image.FORMAT_RGBAH ] +# - Viewport (size of edited region + margin to allow quad rotation) +# |- Background +# | Fills pixels with unmodified source image. +# |- Brush sprite +# Size of actual brush, scaled/rotated, modifies source image. +# Assigned texture is the brush texture, src image is a shader param + var _viewport : Viewport -var _viewport_sprite : Sprite +var _viewport_bg_sprite : Sprite +var _viewport_brush_sprite : Sprite var _brush_size := 32 +var _brush_scale := 1.0 var _brush_position := Vector2() +var _brush_opacity := 1.0 var _brush_texture : Texture var _last_brush_position := Vector2() var _brush_material := ShaderMaterial.new() @@ -60,9 +81,7 @@ var _debug_display : TextureRect var _logger = Logger.get_for(self) -func _ready(): - if Util.is_in_edited_scene(self): - return +func _init(): _viewport = Viewport.new() _viewport.size = Vector2(_brush_size, _brush_size) _viewport.render_target_update_mode = Viewport.UPDATE_ONCE @@ -74,10 +93,19 @@ func _ready(): #_viewport.usage = Viewport.USAGE_2D #_viewport.keep_3d_linear - _viewport_sprite = Sprite.new() - _viewport_sprite.centered = false - _viewport_sprite.material = _brush_material - _viewport.add_child(_viewport_sprite) + # There is no "blend_disabled" option on standard CanvasItemMaterial... + var no_blend_material := ShaderMaterial.new() + no_blend_material.shader = NoBlendShader + _viewport_bg_sprite = Sprite.new() + _viewport_bg_sprite.centered = false + _viewport_bg_sprite.material = no_blend_material + _viewport.add_child(_viewport_bg_sprite) + + _viewport_brush_sprite = Sprite.new() + _viewport_brush_sprite.centered = true + _viewport_brush_sprite.material = _brush_material + _viewport_brush_sprite.position = _viewport.size / 2.0 + _viewport.add_child(_viewport_brush_sprite) add_child(_viewport) @@ -91,12 +119,16 @@ func set_image(image: Image, texture: ImageTexture): assert((image == null and texture == null) or (image != null and texture != null)) _image = image _texture = texture - _viewport_sprite.texture = _texture + _viewport_bg_sprite.texture = _texture + _brush_material.set_shader_param(SHADER_PARAM_SRC_TEXTURE, _texture) if image != null: _viewport.hdr = image.get_format() in _hdr_formats #print("PAINTER VIEWPORT HDR: ", _viewport.hdr) +# Sets the size of the brush in pixels. +# This will cause the internal viewport to resize, which is expensive. +# If you need to frequently change brush size during a paint stroke, prefer using scale instead. func set_brush_size(new_size: int): _brush_size = new_size @@ -105,8 +137,36 @@ func get_brush_size() -> int: return _brush_size +func set_brush_rotation(rotation: float): + _viewport_brush_sprite.rotation = rotation + + +func get_brush_rotation() -> float: + return _viewport_bg_sprite.rotation + + +# The difference between size and scale, is that size is in pixels, while scale is a multiplier. +# Scale is also a lot cheaper to change, so you may prefer changing it instead of size if that +# happens often during a painting stroke. +func set_brush_scale(s: float): + _brush_scale = clamp(s, 0.0, 1.0) + #_viewport_brush_sprite.scale = Vector2(s, s) + + +func get_brush_scale() -> float: + return _viewport_bg_sprite.scale.x + + +func set_brush_opacity(opacity: float): + _brush_opacity = clamp(opacity, 0.0, 1.0) + + +func get_brush_opacity() -> float: + return _brush_opacity + + func set_brush_texture(texture: Texture): - _brush_material.set_shader_param(BRUSH_TEXTURE_SHADER_PARAM, texture) + _viewport_brush_sprite.texture = texture func set_brush_shader(shader: Shader): @@ -115,6 +175,7 @@ func set_brush_shader(shader: Shader): func set_brush_shader_param(p: String, v): + assert(not _API_SHADER_PARAMS.has(p)) _modified_shader_params[p] = true _brush_material.set_shader_param(p, v) @@ -125,28 +186,53 @@ func clear_brush_shader_params(): _modified_shader_params.clear() +# If we want to be able to rotate the brush quad every frame, +# we must prepare a bigger viewport otherwise the quad will not fit inside +static func _get_size_fit_for_rotation(src_size: Vector2) -> Vector2: + var d = int(ceil(src_size.length())) + return Vector2(d, d) + + # You must call this from an `_input` function or similar. func paint_input(center_pos: Vector2): - var vp_size = Vector2(_brush_size, _brush_size) + var vp_size = _get_size_fit_for_rotation(Vector2(_brush_size, _brush_size)) if _viewport.size != vp_size: # Do this lazily so the brush slider won't lag while adjusting it # TODO An "sliding_ended" handling might produce better user experience _viewport.size = vp_size + _viewport_brush_sprite.position = _viewport.size / 2.0 # Need to floor the position in case the brush has an odd size - var brush_pos := (center_pos - Vector2(_brush_size, _brush_size) * 0.5).round() + var brush_pos := (center_pos - _viewport.size * 0.5).round() _viewport.render_target_update_mode = Viewport.UPDATE_ONCE - _viewport_sprite.position = -brush_pos + _viewport.render_target_clear_mode = Viewport.CLEAR_MODE_ONLY_NEXT_FRAME + _viewport_bg_sprite.position = -brush_pos _brush_position = brush_pos _cmd_paint = true + + # We want this quad to have a specific size, regardless of the texture assigned to it + _viewport_brush_sprite.scale = \ + _brush_scale * Vector2(_brush_size, _brush_size) / _viewport_brush_sprite.texture.get_size() # Using a Color because Godot doesn't understand vec4 var rect := Color() rect.r = brush_pos.x / _texture.get_width() rect.g = brush_pos.y / _texture.get_height() - rect.b = _brush_size / _texture.get_width() - rect.a = _brush_size / _texture.get_height() - _brush_material.set_shader_param("u_texture_rect", rect) + rect.b = _viewport.size.x / _texture.get_width() + rect.a = _viewport.size.y / _texture.get_height() + # In order to make sure that u_brush_rect is never bigger than the brush: + # 1. we ceil() the result of lower-left corner + # 2. we floor() the result of upper-right corner + # and then rederive width and height from the result +# var half_brush:Vector2 = Vector2(_brush_size, _brush_size) / 2 +# var brush_LL := (center_pos - half_brush).ceil() +# var brush_UR := (center_pos + half_brush).floor() +# rect.r = brush_LL.x / _texture.get_width() +# rect.g = brush_LL.y / _texture.get_height() +# rect.b = (brush_UR.x - brush_LL.x) / _texture.get_width() +# rect.a = (brush_UR.y - brush_LL.y) / _texture.get_height() + _brush_material.set_shader_param(SHADER_PARAM_SRC_RECT, rect) + _brush_material.set_shader_param(SHADER_PARAM_OPACITY, _brush_opacity) # Don't commit until this is false @@ -180,8 +266,8 @@ func _process(delta: float): var src_x : int = max(-brush_pos.x, 0) var src_y : int = max(-brush_pos.y, 0) - var src_w : int = min(max(_brush_size - src_x, 0), _texture.get_width() - dst_x) - var src_h : int = min(max(_brush_size - src_y, 0), _texture.get_height() - dst_y) + var src_w : int = min(max(_viewport.size.x - src_x, 0), _texture.get_width() - dst_x) + var src_h : int = min(max(_viewport.size.y - src_y, 0), _texture.get_height() - dst_y) if src_w != 0 and src_h != 0: _mark_modified_chunks(dst_x, dst_y, src_w, src_h) @@ -207,6 +293,7 @@ func _mark_modified_chunks(bx: int, by: int, bw: int, bh: int): for cy in range(cmin_y, cmax_y): for cx in range(cmin_x, cmax_x): + #print("Marking chunk ", Vector2(cx, cy)) _modified_chunks[Vector2(cx, cy)] = true diff --git a/addons/zylann.hterrain/tools/brush/settings_dialog/brush_settings_dialog.gd b/addons/zylann.hterrain/tools/brush/settings_dialog/brush_settings_dialog.gd new file mode 100644 index 0000000..8cceb4c --- /dev/null +++ b/addons/zylann.hterrain/tools/brush/settings_dialog/brush_settings_dialog.gd @@ -0,0 +1,259 @@ +tool +extends AcceptDialog + +const Util = preload("../../../util/util.gd") +const Brush = preload("../brush.gd") +const Logger = preload("../../../util/logger.gd") +const EditorUtil = preload("../../util/editor_util.gd") + +onready var _scratchpad = $VB/HB/VB3/PreviewScratchpad + +onready var _shape_list = $VB/HB/VB/ShapeList +onready var _remove_shape_button = $VB/HB/VB/HBoxContainer/RemoveShape +onready var _change_shape_button = $VB/HB/VB/ChangeShape + +onready var _size_slider = $VB/HB/VB2/Settings/Size +onready var _opacity_slider = $VB/HB/VB2/Settings/Opacity +onready var _pressure_enabled_checkbox = $VB/HB/VB2/Settings/PressureEnabled +onready var _pressure_over_size_slider = $VB/HB/VB2/Settings/PressureOverSize +onready var _pressure_over_opacity_slider = $VB/HB/VB2/Settings/PressureOverOpacity +onready var _frequency_distance_slider = $VB/HB/VB2/Settings/FrequencyDistance +onready var _frequency_time_slider = $VB/HB/VB2/Settings/FrequencyTime +onready var _random_rotation_checkbox = $VB/HB/VB2/Settings/RandomRotation + +var _brush : Brush +# This is a `EditorFileDialog`, +# but cannot type it because I want to be able to test it by running the scene. +# And when I run it, Godot does not allow to use `EditorFileDialog`. +var _load_image_dialog +# -1 means add, otherwise replace +var _load_image_index := -1 +var _logger = Logger.get_for(self) + + +func _ready(): + if Util.is_in_edited_scene(self): + return + + _size_slider.set_max_value(Brush.MAX_SIZE_FOR_SLIDERS) + _size_slider.set_greater_max_value(Brush.MAX_SIZE) + + # TESTING + if not Engine.editor_hint: + setup_dialogs(self) + call_deferred("popup") + + +func set_brush(brush : Brush): + assert(brush != null) + _brush = brush + _update_controls_from_brush() + + +func setup_dialogs(base_control: Control): + assert(_load_image_dialog == null) + _load_image_dialog = EditorUtil.create_open_file_dialog() + _load_image_dialog.mode = EditorFileDialog.MODE_OPEN_FILE + _load_image_dialog.add_filter("*.exr ; EXR files") + _load_image_dialog.resizable = true + _load_image_dialog.access = EditorFileDialog.ACCESS_FILESYSTEM + _load_image_dialog.current_dir = Brush.SHAPES_DIR + _load_image_dialog.connect("file_selected", self, "_on_LoadImageDialog_file_selected") + _load_image_dialog.connect("files_selected", self, "_on_LoadImageDialog_files_selected") + base_control.add_child(_load_image_dialog) + + +func _exit_tree(): + if _load_image_dialog != null: + _load_image_dialog.queue_free() + _load_image_dialog = null + + +func _get_shapes_from_gui() -> Array: + var shapes = [] + for i in _shape_list.get_item_count(): + var icon = _shape_list.get_item_icon(i) + assert(icon != null) + shapes.append(icon) + return shapes + + +func _update_shapes_gui(shapes: Array): + _shape_list.clear() + for shape in shapes: + assert(shape != null) + assert(shape is Texture) + _shape_list.add_icon_item(shape) + _update_shape_list_buttons() + + +func _on_AddShape_pressed(): + _load_image_index = -1 + _load_image_dialog.mode = EditorFileDialog.MODE_OPEN_FILES + _load_image_dialog.popup_centered_ratio(0.7) + + +func _on_RemoveShape_pressed(): + var selected_indices = _shape_list.get_selected_items() + if len(selected_indices) == 0: + return + + var index : int = selected_indices[0] + _shape_list.remove_item(index) + + var shapes = _get_shapes_from_gui() + for brush in _get_brushes(): + brush.set_shapes(shapes) + + _update_shape_list_buttons() + + +func _on_ShapeList_item_activated(index): + _request_modify_shape(index) + + +func _on_ChangeShape_pressed(): + var selected = _shape_list.get_selected_items() + if len(selected) == 0: + return + _request_modify_shape(selected[0]) + + +func _request_modify_shape(index: int): + _load_image_index = index + _load_image_dialog.mode = EditorFileDialog.MODE_OPEN_FILE + _load_image_dialog.popup_centered_ratio(0.7) + + +func _on_LoadImageDialog_files_selected(fpaths: PoolStringArray): + var shapes := _get_shapes_from_gui() + + for fpath in fpaths: + var tex := Brush.load_shape_from_image_file(fpath, _logger) + if tex == null: + # Failed + continue + shapes.append(tex) + + for brush in _get_brushes(): + brush.set_shapes(shapes) + + _update_shapes_gui(shapes) + + +func _on_LoadImageDialog_file_selected(fpath: String): + var tex := Brush.load_shape_from_image_file(fpath, _logger) + if tex == null: + # Failed + return + + var shapes := _get_shapes_from_gui() + if _load_image_index == -1 or _load_image_index >= len(shapes): + # Add + shapes.append(tex) + else: + # Replace + assert(_load_image_index >= 0) + shapes[_load_image_index] = tex + + for brush in _get_brushes(): + brush.set_shapes(shapes) + + _update_shapes_gui(shapes) + + +func _notification(what: int): + if what == NOTIFICATION_VISIBILITY_CHANGED: + if visible: + _update_controls_from_brush() + + +func _update_controls_from_brush(): + var brush := _brush + + if brush == null: + # To allow testing + brush = _scratchpad.get_painter().get_brush() + + _update_shapes_gui(brush.get_shapes()) + + _size_slider.set_value(brush.get_size(), false) + _opacity_slider.set_value(brush.get_opacity() * 100.0, false) + _pressure_enabled_checkbox.pressed = brush.is_pressure_enabled() + _pressure_over_size_slider.set_value(brush.get_pressure_over_scale() * 100.0, false) + _pressure_over_opacity_slider.set_value(brush.get_pressure_over_opacity() * 100.0, false) + _frequency_distance_slider.set_value(brush.get_frequency_distance(), false) + _frequency_time_slider.set_value(1000.0 / max(0.1, float(brush.get_frequency_time_ms())), false) + _random_rotation_checkbox.pressed = brush.is_random_rotation_enabled() + + +func _on_ClearScratchpad_pressed(): + _scratchpad.reset_image() + + +func _on_Size_value_changed(value: float): + for brush in _get_brushes(): + brush.set_size(value) + + +func _on_Opacity_value_changed(value): + for brush in _get_brushes(): + brush.set_opacity(value / 100.0) + + +func _on_PressureEnabled_toggled(button_pressed): + for brush in _get_brushes(): + brush.set_pressure_enabled(button_pressed) + + +func _on_PressureOverSize_value_changed(value): + for brush in _get_brushes(): + brush.set_pressure_over_scale(value / 100.0) + + +func _on_PressureOverOpacity_value_changed(value): + for brush in _get_brushes(): + brush.set_pressure_over_opacity(value / 100.0) + + +func _on_FrequencyDistance_value_changed(value): + for brush in _get_brushes(): + brush.set_frequency_distance(value) + + +func _on_FrequencyTime_value_changed(fps): + fps = max(1.0, fps) + var ms = 1000.0 / fps + if is_equal_approx(fps, 60.0): + ms = 0 + for brush in _get_brushes(): + brush.set_frequency_time_ms(ms) + + +func _on_RandomRotation_toggled(button_pressed: bool): + for brush in _get_brushes(): + brush.set_random_rotation_enabled(button_pressed) + + +func _get_brushes() -> Array: + if _brush != null: + # We edit both the preview brush and the terrain brush + # TODO Could we simply share the brush? + return [_brush, _scratchpad.get_painter().get_brush()] + # When testing the dialog in isolation, the edited brush might be null + return [_scratchpad.get_painter().get_brush()] + + +func _on_ShapeList_item_selected(index): + _update_shape_list_buttons() + + +func _on_ShapeList_nothing_selected(): + _update_shape_list_buttons() + + +func _update_shape_list_buttons(): + var selected_count = len(_shape_list.get_selected_items()) + # There must be at least one shape + _remove_shape_button.disabled = _shape_list.get_item_count() == 1 or selected_count == 0 + _change_shape_button.disabled = selected_count == 0 diff --git a/addons/zylann.hterrain/tools/brush/settings_dialog/brush_settings_dialog.tscn b/addons/zylann.hterrain/tools/brush/settings_dialog/brush_settings_dialog.tscn new file mode 100644 index 0000000..4dee24f --- /dev/null +++ b/addons/zylann.hterrain/tools/brush/settings_dialog/brush_settings_dialog.tscn @@ -0,0 +1,273 @@ +[gd_scene load_steps=5 format=2] + +[ext_resource path="res://addons/zylann.hterrain/tools/brush/shapes/round2.exr" type="Texture" id=1] +[ext_resource path="res://addons/zylann.hterrain/tools/util/spin_slider.tscn" type="PackedScene" id=2] +[ext_resource path="res://addons/zylann.hterrain/tools/brush/settings_dialog/brush_settings_dialog.gd" type="Script" id=3] +[ext_resource path="res://addons/zylann.hterrain/tools/brush/settings_dialog/preview_scratchpad.tscn" type="PackedScene" id=4] + +[node name="BrushSettingsDialog" type="AcceptDialog"] +visible = true +margin_left = 46.0 +margin_top = 65.0 +margin_right = 746.0 +margin_bottom = 465.0 +rect_min_size = Vector2( 700, 400 ) +window_title = "Brush settings" +resizable = true +script = ExtResource( 3 ) +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="VB" type="VBoxContainer" parent="."] +anchor_right = 1.0 +anchor_bottom = 1.0 +margin_left = 8.0 +margin_top = 8.0 +margin_right = -8.0 +margin_bottom = -36.0 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="HB" type="HBoxContainer" parent="VB"] +margin_right = 684.0 +margin_bottom = 356.0 +size_flags_vertical = 3 +custom_constants/separation = 8 + +[node name="VB" type="VBoxContainer" parent="VB/HB"] +margin_right = 120.0 +margin_bottom = 356.0 +size_flags_vertical = 3 + +[node name="Label" type="Label" parent="VB/HB/VB"] +margin_right = 120.0 +margin_bottom = 14.0 +text = "Shapes" + +[node name="ShapeList" type="ItemList" parent="VB/HB/VB"] +margin_top = 18.0 +margin_right = 120.0 +margin_bottom = 308.0 +rect_min_size = Vector2( 120, 0 ) +size_flags_vertical = 3 +items = [ "", ExtResource( 1 ), false ] +fixed_icon_size = Vector2( 100, 100 ) + +[node name="ChangeShape" type="Button" parent="VB/HB/VB"] +margin_top = 312.0 +margin_right = 120.0 +margin_bottom = 332.0 +disabled = true +text = "Change..." + +[node name="HBoxContainer" type="HBoxContainer" parent="VB/HB/VB"] +margin_top = 336.0 +margin_right = 120.0 +margin_bottom = 356.0 + +[node name="AddShape" type="Button" parent="VB/HB/VB/HBoxContainer"] +margin_right = 49.0 +margin_bottom = 20.0 +text = "Add..." + +[node name="RemoveShape" type="Button" parent="VB/HB/VB/HBoxContainer"] +margin_left = 53.0 +margin_right = 117.0 +margin_bottom = 20.0 +disabled = true +text = "Remove" + +[node name="VB2" type="VBoxContainer" parent="VB/HB"] +margin_left = 128.0 +margin_right = 476.0 +margin_bottom = 356.0 +size_flags_horizontal = 3 + +[node name="Label" type="Label" parent="VB/HB/VB2"] +margin_right = 348.0 +margin_bottom = 14.0 + +[node name="Settings" type="VBoxContainer" parent="VB/HB/VB2"] +margin_top = 18.0 +margin_right = 348.0 +margin_bottom = 262.0 +size_flags_horizontal = 3 + +[node name="Size" parent="VB/HB/VB2/Settings" instance=ExtResource( 2 )] +anchor_right = 0.0 +anchor_bottom = 0.0 +margin_right = 348.0 +margin_bottom = 28.0 +size_flags_horizontal = 3 +_value = 32.0 +_min_value = 2.0 +_max_value = 500.0 +_prefix = "Size:" +_suffix = "px" +_rounded = true +_centered = false +_allow_greater = true +_greater_max_value = 4000.0 + +[node name="Opacity" parent="VB/HB/VB2/Settings" instance=ExtResource( 2 )] +anchor_right = 0.0 +anchor_bottom = 0.0 +margin_top = 32.0 +margin_right = 348.0 +margin_bottom = 60.0 +size_flags_horizontal = 3 +_value = 100.0 +_prefix = "Opacity" +_suffix = "%" +_rounded = true +_centered = false + +[node name="PressureEnabled" type="CheckBox" parent="VB/HB/VB2/Settings"] +margin_top = 64.0 +margin_right = 348.0 +margin_bottom = 88.0 +text = "Enable pressure (pen tablets)" + +[node name="PressureOverSize" parent="VB/HB/VB2/Settings" instance=ExtResource( 2 )] +anchor_right = 0.0 +anchor_bottom = 0.0 +margin_top = 92.0 +margin_right = 348.0 +margin_bottom = 120.0 +_value = 50.0 +_prefix = "Pressure affects size:" +_suffix = "%" +_centered = false + +[node name="PressureOverOpacity" parent="VB/HB/VB2/Settings" instance=ExtResource( 2 )] +anchor_right = 0.0 +anchor_bottom = 0.0 +margin_top = 124.0 +margin_right = 348.0 +margin_bottom = 152.0 +_value = 50.0 +_prefix = "Pressure affects opacity:" +_suffix = "%" +_centered = false + +[node name="FrequencyTime" parent="VB/HB/VB2/Settings" instance=ExtResource( 2 )] +anchor_right = 0.0 +anchor_bottom = 0.0 +margin_top = 156.0 +margin_right = 348.0 +margin_bottom = 184.0 +_value = 60.0 +_min_value = 1.0 +_max_value = 60.0 +_prefix = "Frequency time:" +_suffix = "fps" +_centered = false + +[node name="FrequencyDistance" parent="VB/HB/VB2/Settings" instance=ExtResource( 2 )] +anchor_right = 0.0 +anchor_bottom = 0.0 +margin_top = 188.0 +margin_right = 348.0 +margin_bottom = 216.0 +_prefix = "Frequency distance:" +_suffix = "px" +_centered = false +_greater_max_value = 4000.0 + +[node name="RandomRotation" type="CheckBox" parent="VB/HB/VB2/Settings"] +margin_top = 220.0 +margin_right = 348.0 +margin_bottom = 244.0 +text = "Random rotation" + +[node name="HSeparator" type="HSeparator" parent="VB/HB/VB2/Settings"] +visible = false +margin_top = 124.0 +margin_right = 292.0 +margin_bottom = 128.0 + +[node name="SizeLimitHB" type="HBoxContainer" parent="VB/HB/VB2/Settings"] +visible = false +margin_top = 124.0 +margin_right = 142.0 +margin_bottom = 152.0 + +[node name="Label" type="Label" parent="VB/HB/VB2/Settings/SizeLimitHB"] +margin_top = 7.0 +margin_right = 64.0 +margin_bottom = 21.0 +hint_tooltip = "This allows to change the upper limit of the brush size slider. Bear in mind high values can slow down the editor." +mouse_filter = 0 +text = "Size limit:" + +[node name="SizeLimit" type="SpinBox" parent="VB/HB/VB2/Settings/SizeLimitHB"] +margin_left = 68.0 +margin_right = 142.0 +margin_bottom = 28.0 +size_flags_horizontal = 3 +min_value = 1.0 +max_value = 1000.0 +value = 200.0 + +[node name="HSeparator2" type="HSeparator" parent="VB/HB/VB2/Settings"] +visible = false +margin_top = 188.0 +margin_right = 292.0 +margin_bottom = 192.0 + +[node name="HB" type="HBoxContainer" parent="VB/HB/VB2/Settings"] +visible = false +margin_top = 248.0 +margin_right = 292.0 +margin_bottom = 268.0 + +[node name="Button" type="Button" parent="VB/HB/VB2/Settings/HB"] +margin_right = 99.0 +margin_bottom = 20.0 +text = "Load preset..." + +[node name="Button2" type="Button" parent="VB/HB/VB2/Settings/HB"] +margin_left = 103.0 +margin_right = 201.0 +margin_bottom = 20.0 +text = "Save preset..." + +[node name="VB3" type="VBoxContainer" parent="VB/HB"] +margin_left = 484.0 +margin_right = 684.0 +margin_bottom = 356.0 + +[node name="Label" type="Label" parent="VB/HB/VB3"] +margin_right = 200.0 +margin_bottom = 14.0 +text = "Scratchpad" + +[node name="PreviewScratchpad" parent="VB/HB/VB3" instance=ExtResource( 4 )] +margin_top = 18.0 +margin_right = 200.0 +margin_bottom = 318.0 +rect_min_size = Vector2( 200, 300 ) + +[node name="ClearScratchpad" type="Button" parent="VB/HB/VB3"] +margin_top = 322.0 +margin_right = 200.0 +margin_bottom = 342.0 +text = "Clear" + +[connection signal="item_activated" from="VB/HB/VB/ShapeList" to="." method="_on_ShapeList_item_activated"] +[connection signal="item_selected" from="VB/HB/VB/ShapeList" to="." method="_on_ShapeList_item_selected"] +[connection signal="nothing_selected" from="VB/HB/VB/ShapeList" to="." method="_on_ShapeList_nothing_selected"] +[connection signal="pressed" from="VB/HB/VB/ChangeShape" to="." method="_on_ChangeShape_pressed"] +[connection signal="pressed" from="VB/HB/VB/HBoxContainer/AddShape" to="." method="_on_AddShape_pressed"] +[connection signal="pressed" from="VB/HB/VB/HBoxContainer/RemoveShape" to="." method="_on_RemoveShape_pressed"] +[connection signal="value_changed" from="VB/HB/VB2/Settings/Size" to="." method="_on_Size_value_changed"] +[connection signal="value_changed" from="VB/HB/VB2/Settings/Opacity" to="." method="_on_Opacity_value_changed"] +[connection signal="toggled" from="VB/HB/VB2/Settings/PressureEnabled" to="." method="_on_PressureEnabled_toggled"] +[connection signal="value_changed" from="VB/HB/VB2/Settings/PressureOverSize" to="." method="_on_PressureOverSize_value_changed"] +[connection signal="value_changed" from="VB/HB/VB2/Settings/PressureOverOpacity" to="." method="_on_PressureOverOpacity_value_changed"] +[connection signal="value_changed" from="VB/HB/VB2/Settings/FrequencyTime" to="." method="_on_FrequencyTime_value_changed"] +[connection signal="value_changed" from="VB/HB/VB2/Settings/FrequencyDistance" to="." method="_on_FrequencyDistance_value_changed"] +[connection signal="toggled" from="VB/HB/VB2/Settings/RandomRotation" to="." method="_on_RandomRotation_toggled"] +[connection signal="pressed" from="VB/HB/VB3/ClearScratchpad" to="." method="_on_ClearScratchpad_pressed"] diff --git a/addons/zylann.hterrain/tools/brush/settings_dialog/preview_painter.gd b/addons/zylann.hterrain/tools/brush/settings_dialog/preview_painter.gd new file mode 100644 index 0000000..a27863c --- /dev/null +++ b/addons/zylann.hterrain/tools/brush/settings_dialog/preview_painter.gd @@ -0,0 +1,42 @@ +tool +extends Node + +const Painter = preload("./../painter.gd") +const Brush = preload("./../brush.gd") + +const ColorShader = preload("../shaders/color.shader") + +var _painter : Painter +var _brush : Brush + + +func _init(): + var p = Painter.new() + # The name is just for debugging + p.set_name("Painter") + add_child(p) + _painter = p + + _brush = Brush.new() + + +func set_image_texture(image: Image, texture: ImageTexture): + _painter.set_image(image, texture) + + +func get_brush() -> Brush: + return _brush + + +# This may be called from an `_input` callback +func paint_input(position: Vector2, pressure: float): + var p : Painter = _painter + + if not _brush.configure_paint_input([p], position, pressure): + return + + p.set_brush_shader(ColorShader) + #p.set_brush_shader_param("u_factor", _opacity) + p.set_brush_shader_param("u_color", Color(0,0,0,1)) + #p.set_image(_image, _texture) + p.paint_input(position) diff --git a/addons/zylann.hterrain/tools/brush/settings_dialog/preview_scratchpad.gd b/addons/zylann.hterrain/tools/brush/settings_dialog/preview_scratchpad.gd new file mode 100644 index 0000000..b4467a6 --- /dev/null +++ b/addons/zylann.hterrain/tools/brush/settings_dialog/preview_scratchpad.gd @@ -0,0 +1,51 @@ +tool +extends Control + +const PreviewPainter = preload("./preview_painter.gd") +const DefaultBrushTexture = preload("../shapes/round2.exr") + +onready var _texture_rect : TextureRect = $TextureRect +onready var _painter : PreviewPainter = $Painter + + +func _ready(): + reset_image() + # Default so it doesnt crash when painting and can be tested + _painter.get_brush().set_shapes([DefaultBrushTexture]) + + +func reset_image(): + var image = Image.new() + image.create(_texture_rect.rect_size.x, _texture_rect.rect_size.y, false, Image.FORMAT_RGB8) + image.fill(Color(1,1,1)) + var texture = ImageTexture.new() + texture.create_from_image(image) + _texture_rect.texture = texture + _painter.set_image_texture(image, texture) + + +func get_painter() -> PreviewPainter: + return _painter + + +func _gui_input(event): + if event is InputEventMouseMotion: + if Input.is_mouse_button_pressed(BUTTON_LEFT): + _painter.paint_input(event.position, event.pressure) + update() + + elif event is InputEventMouseButton: + if event.button_index == BUTTON_LEFT: + if event.pressed: + # TODO `pressure` is not available on button events + # So I have to assume zero... which means clicks do not paint anything? + _painter.paint_input(event.position, 0.0) + else: + _painter.get_brush().on_paint_end() + + +func _draw(): + var mpos = get_local_mouse_position() + var brush = _painter.get_brush() + draw_arc(mpos, 0.5 * brush.get_size(), -PI, PI, 32, Color(1, 0.2, 0.2), 2.0, true) + diff --git a/addons/zylann.hterrain/tools/brush/settings_dialog/preview_scratchpad.tscn b/addons/zylann.hterrain/tools/brush/settings_dialog/preview_scratchpad.tscn new file mode 100644 index 0000000..d9f2046 --- /dev/null +++ b/addons/zylann.hterrain/tools/brush/settings_dialog/preview_scratchpad.tscn @@ -0,0 +1,22 @@ +[gd_scene load_steps=3 format=2] + +[ext_resource path="res://addons/zylann.hterrain/tools/brush/settings_dialog/preview_scratchpad.gd" type="Script" id=1] +[ext_resource path="res://addons/zylann.hterrain/tools/brush/settings_dialog/preview_painter.gd" type="Script" id=2] + +[node name="PreviewScratchpad" type="Control"] +margin_right = 380.0 +margin_bottom = 383.0 +rect_clip_content = true +script = ExtResource( 1 ) +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="Painter" type="Node" parent="."] +script = ExtResource( 2 ) + +[node name="TextureRect" type="TextureRect" parent="."] +show_behind_parent = true +anchor_right = 1.0 +anchor_bottom = 1.0 +stretch_mode = 5 diff --git a/addons/zylann.hterrain/tools/brush/shaders/alpha.shader b/addons/zylann.hterrain/tools/brush/shaders/alpha.shader index a348a72..b166931 100644 --- a/addons/zylann.hterrain/tools/brush/shaders/alpha.shader +++ b/addons/zylann.hterrain/tools/brush/shaders/alpha.shader @@ -1,13 +1,20 @@ -shader_type canvas_item; -render_mode blend_disabled; - -uniform sampler2D u_brush_texture; -uniform float u_factor = 1.0; -uniform float u_value = 1.0; - -void fragment() { - float brush_value = texture(u_brush_texture, SCREEN_UV).r; - - vec4 src = texture(TEXTURE, UV); - COLOR = vec4(src.rgb, mix(src.a, u_value, u_factor * brush_value)); -} +shader_type canvas_item; +render_mode blend_disabled; + +uniform sampler2D u_src_texture; +uniform vec4 u_src_rect; +uniform float u_opacity = 1.0; +uniform float u_factor = 1.0; +uniform float u_value = 1.0; + +vec2 get_src_uv(vec2 screen_uv) { + vec2 uv = u_src_rect.xy + screen_uv * u_src_rect.zw; + return uv; +} + +void fragment() { + float brush_value = u_opacity * texture(TEXTURE, UV).r; + + vec4 src = texture(u_src_texture, get_src_uv(SCREEN_UV)); + COLOR = vec4(src.rgb, mix(src.a, u_value, u_factor * brush_value)); +} diff --git a/addons/zylann.hterrain/tools/brush/shaders/color.shader b/addons/zylann.hterrain/tools/brush/shaders/color.shader index 0333058..9ed1bbe 100644 --- a/addons/zylann.hterrain/tools/brush/shaders/color.shader +++ b/addons/zylann.hterrain/tools/brush/shaders/color.shader @@ -1,21 +1,28 @@ -shader_type canvas_item; -render_mode blend_disabled; - -uniform sampler2D u_brush_texture; -uniform float u_factor = 1.0; -uniform vec4 u_color = vec4(1.0); - -void fragment() { - float brush_value = texture(u_brush_texture, SCREEN_UV).r; - - vec4 src = texture(TEXTURE, UV); - - // Despite hints, albedo textures render darker. - // Trying to undo sRGB does not work because of 8-bit precision loss - // that would occur either in texture, or on the source image. - // So it's not possible to use viewports to paint albedo... - //src.rgb = pow(src.rgb, vec3(0.4545)); - - vec4 col = vec4(mix(src.rgb, u_color.rgb, brush_value * u_factor), src.a); - COLOR = col; -} +shader_type canvas_item; +render_mode blend_disabled; + +uniform sampler2D u_src_texture; +uniform vec4 u_src_rect; +uniform float u_opacity = 1.0; +uniform float u_factor = 1.0; +uniform vec4 u_color = vec4(1.0); + +vec2 get_src_uv(vec2 screen_uv) { + vec2 uv = u_src_rect.xy + screen_uv * u_src_rect.zw; + return uv; +} + +void fragment() { + float brush_value = u_opacity * texture(TEXTURE, UV).r; + + vec4 src = texture(u_src_texture, get_src_uv(SCREEN_UV)); + + // Despite hints, albedo textures render darker. + // Trying to undo sRGB does not work because of 8-bit precision loss + // that would occur either in texture, or on the source image. + // So it's not possible to use viewports to paint albedo... + //src.rgb = pow(src.rgb, vec3(0.4545)); + + vec4 col = vec4(mix(src.rgb, u_color.rgb, brush_value * u_factor), src.a); + COLOR = col; +} diff --git a/addons/zylann.hterrain/tools/brush/shaders/erode.shader b/addons/zylann.hterrain/tools/brush/shaders/erode.shader index 987a266..223c2ef 100644 --- a/addons/zylann.hterrain/tools/brush/shaders/erode.shader +++ b/addons/zylann.hterrain/tools/brush/shaders/erode.shader @@ -1,50 +1,58 @@ -shader_type canvas_item; - -uniform sampler2D u_brush_texture; -uniform float u_factor = 1.0; -uniform vec4 u_color = vec4(1.0); - -// float get_noise(vec2 pos) { -// return fract(sin(dot(pos.xy ,vec2(12.9898,78.233))) * 43758.5453); -// } - -float erode(sampler2D heightmap, vec2 uv, vec2 pixel_size, float weight) { - float r = 3.0; - - // Divide so the shader stays neighbor dependent 1 pixel across. - // For this to work, filtering must be enabled. - vec2 eps = pixel_size / (0.99 * r); - - float h = texture(heightmap, uv).r; - float eh = h; - //float dh = h; - - // Morphology with circular structuring element - for (float y = -r; y <= r; ++y) { - for (float x = -r; x <= r; ++x) { - - vec2 p = vec2(x, y); - float nh = texture(heightmap, uv + p * eps).r; - - float s = max(length(p) - r, 0); - eh = min(eh, nh + s); - - //s = min(r - length(p), 0); - //dh = max(dh, nh + s); - } - } - - eh = mix(h, eh, weight); - //dh = mix(h, dh, u_weight); - - float ph = eh;//mix(eh, dh, u_dilation); - - return ph; -} - -void fragment() { - float brush_value = texture(u_brush_texture, SCREEN_UV).r * u_factor; - float ph = erode(TEXTURE, UV, TEXTURE_PIXEL_SIZE, brush_value); - //ph += brush_value * 0.35; - COLOR = vec4(ph, ph, ph, 1.0); -} +shader_type canvas_item; + +uniform sampler2D u_src_texture; +uniform vec4 u_src_rect; +uniform float u_opacity = 1.0; +uniform float u_factor = 1.0; +uniform vec4 u_color = vec4(1.0); + +vec2 get_src_uv(vec2 screen_uv) { + vec2 uv = u_src_rect.xy + screen_uv * u_src_rect.zw; + return uv; +} + +// float get_noise(vec2 pos) { +// return fract(sin(dot(pos.xy ,vec2(12.9898,78.233))) * 43758.5453); +// } + +float erode(sampler2D heightmap, vec2 uv, vec2 pixel_size, float weight) { + float r = 3.0; + + // Divide so the shader stays neighbor dependent 1 pixel across. + // For this to work, filtering must be enabled. + vec2 eps = pixel_size / (0.99 * r); + + float h = texture(heightmap, uv).r; + float eh = h; + //float dh = h; + + // Morphology with circular structuring element + for (float y = -r; y <= r; ++y) { + for (float x = -r; x <= r; ++x) { + + vec2 p = vec2(x, y); + float nh = texture(heightmap, uv + p * eps).r; + + float s = max(length(p) - r, 0); + eh = min(eh, nh + s); + + //s = min(r - length(p), 0); + //dh = max(dh, nh + s); + } + } + + eh = mix(h, eh, weight); + //dh = mix(h, dh, u_weight); + + float ph = eh;//mix(eh, dh, u_dilation); + + return ph; +} + +void fragment() { + float brush_value = u_opacity * texture(TEXTURE, UV).r * u_factor; + vec2 src_pixel_size = 1.0 / vec2(textureSize(u_src_texture, 0)); + float ph = erode(u_src_texture, get_src_uv(SCREEN_UV), src_pixel_size, brush_value); + //ph += brush_value * 0.35; + COLOR = vec4(ph, ph, ph, 1.0); +} diff --git a/addons/zylann.hterrain/tools/brush/shaders/flatten.shader b/addons/zylann.hterrain/tools/brush/shaders/flatten.shader index 38f5044..54b1964 100644 --- a/addons/zylann.hterrain/tools/brush/shaders/flatten.shader +++ b/addons/zylann.hterrain/tools/brush/shaders/flatten.shader @@ -1,14 +1,21 @@ -shader_type canvas_item; -render_mode blend_disabled; - -uniform sampler2D u_brush_texture; -uniform float u_factor = 1.0; -uniform float u_flatten_value; - -void fragment() { - float brush_value = texture(u_brush_texture, SCREEN_UV).r; - - float src_h = texture(TEXTURE, UV).r; - float h = mix(src_h, u_flatten_value, u_factor * brush_value); - COLOR = vec4(h, 0.0, 0.0, 1.0); -} +shader_type canvas_item; +render_mode blend_disabled; + +uniform sampler2D u_src_texture; +uniform vec4 u_src_rect; +uniform float u_opacity = 1.0; +uniform float u_factor = 1.0; +uniform float u_flatten_value; + +vec2 get_src_uv(vec2 screen_uv) { + vec2 uv = u_src_rect.xy + screen_uv * u_src_rect.zw; + return uv; +} + +void fragment() { + float brush_value = u_opacity * texture(TEXTURE, UV).r; + + float src_h = texture(u_src_texture, get_src_uv(SCREEN_UV)).r; + float h = mix(src_h, u_flatten_value, u_factor * brush_value); + COLOR = vec4(h, 0.0, 0.0, 1.0); +} diff --git a/addons/zylann.hterrain/tools/brush/shaders/level.shader b/addons/zylann.hterrain/tools/brush/shaders/level.shader index 8d0b885..ad0eb9c 100644 --- a/addons/zylann.hterrain/tools/brush/shaders/level.shader +++ b/addons/zylann.hterrain/tools/brush/shaders/level.shader @@ -1,33 +1,39 @@ -shader_type canvas_item; -render_mode blend_disabled; - -uniform sampler2D u_brush_texture; -uniform float u_factor = 1.0; -uniform vec4 u_texture_rect; - -// TODO Could actually level to whatever height the brush was at the beginning of the stroke? - -void fragment() { - float brush_value = texture(u_brush_texture, SCREEN_UV).r; - - // The heightmap does not have mipmaps, - // so we need to use an approximation of average. - // This is not a very good one though... - float dst_h = 0.0; - vec2 uv_min = vec2(u_texture_rect.xy); - vec2 uv_max = vec2(u_texture_rect.xy + u_texture_rect.zw); - for (int i = 0; i < 5; ++i) { - for (int j = 0; j < 5; ++j) { - float x = mix(uv_min.x, uv_max.x, float(i) / 4.0); - float y = mix(uv_min.y, uv_max.y, float(j) / 4.0); - float h = texture(TEXTURE, vec2(x, y)).r; - dst_h += h; - } - } - dst_h /= (5.0 * 5.0); - - // TODO I have no idea if this will check out - float src_h = texture(TEXTURE, UV).r; - float h = mix(src_h, dst_h, u_factor * brush_value); - COLOR = vec4(h, 0.0, 0.0, 1.0); -} +shader_type canvas_item; +render_mode blend_disabled; + +uniform sampler2D u_src_texture; +uniform vec4 u_src_rect; +uniform float u_opacity = 1.0; +uniform float u_factor = 1.0; + +vec2 get_src_uv(vec2 screen_uv) { + vec2 uv = u_src_rect.xy + screen_uv * u_src_rect.zw; + return uv; +} + +// TODO Could actually level to whatever height the brush was at the beginning of the stroke? + +void fragment() { + float brush_value = u_opacity * texture(TEXTURE, UV).r; + + // The heightmap does not have mipmaps, + // so we need to use an approximation of average. + // This is not a very good one though... + float dst_h = 0.0; + vec2 uv_min = vec2(u_src_rect.xy); + vec2 uv_max = vec2(u_src_rect.xy + u_src_rect.zw); + for (int i = 0; i < 5; ++i) { + for (int j = 0; j < 5; ++j) { + float x = mix(uv_min.x, uv_max.x, float(i) / 4.0); + float y = mix(uv_min.y, uv_max.y, float(j) / 4.0); + float h = texture(u_src_texture, vec2(x, y)).r; + dst_h += h; + } + } + dst_h /= (5.0 * 5.0); + + // TODO I have no idea if this will check out + float src_h = texture(u_src_texture, get_src_uv(SCREEN_UV)).r; + float h = mix(src_h, dst_h, u_factor * brush_value); + COLOR = vec4(h, 0.0, 0.0, 1.0); +} diff --git a/addons/zylann.hterrain/tools/brush/shaders/raise.shader b/addons/zylann.hterrain/tools/brush/shaders/raise.shader index 6cbc41a..57c9619 100644 --- a/addons/zylann.hterrain/tools/brush/shaders/raise.shader +++ b/addons/zylann.hterrain/tools/brush/shaders/raise.shader @@ -1,13 +1,20 @@ shader_type canvas_item; render_mode blend_disabled; -uniform sampler2D u_brush_texture; +uniform sampler2D u_src_texture; +uniform vec4 u_src_rect; +uniform float u_opacity = 1.0; uniform float u_factor = 1.0; +vec2 get_src_uv(vec2 screen_uv) { + vec2 uv = u_src_rect.xy + screen_uv * u_src_rect.zw; + return uv; +} + void fragment() { - float brush_value = texture(u_brush_texture, SCREEN_UV).r; + float brush_value = u_opacity * texture(TEXTURE, UV).r; - float src_h = texture(TEXTURE, UV).r; + float src_h = texture(u_src_texture, get_src_uv(SCREEN_UV)).r; float h = src_h + u_factor * brush_value; COLOR = vec4(h, 0.0, 0.0, 1.0); } diff --git a/addons/zylann.hterrain/tools/brush/shaders/smooth.shader b/addons/zylann.hterrain/tools/brush/shaders/smooth.shader index c79dfdf..0123f99 100644 --- a/addons/zylann.hterrain/tools/brush/shaders/smooth.shader +++ b/addons/zylann.hterrain/tools/brush/shaders/smooth.shader @@ -1,19 +1,28 @@ -shader_type canvas_item; -render_mode blend_disabled; - -uniform sampler2D u_brush_texture; -uniform float u_factor = 1.0; - -void fragment() { - float brush_value = texture(u_brush_texture, SCREEN_UV).r; - - vec2 offset = TEXTURE_PIXEL_SIZE; - float src_nx = texture(TEXTURE, UV - vec2(offset.x, 0.0)).r; - float src_px = texture(TEXTURE, UV + vec2(offset.x, 0.0)).r; - float src_ny = texture(TEXTURE, UV - vec2(0.0, offset.y)).r; - float src_py = texture(TEXTURE, UV + vec2(0.0, offset.y)).r; - float src_h = texture(TEXTURE, UV).r; - float dst_h = (src_h + src_nx + src_px + src_ny + src_py) * 0.2; - float h = mix(src_h, dst_h, u_factor * brush_value); - COLOR = vec4(h, 0.0, 0.0, 1.0); -} +shader_type canvas_item; +render_mode blend_disabled; + +uniform sampler2D u_src_texture; +uniform vec4 u_src_rect; +uniform float u_opacity = 1.0; +uniform float u_factor = 1.0; + +vec2 get_src_uv(vec2 screen_uv) { + vec2 uv = u_src_rect.xy + screen_uv * u_src_rect.zw; + return uv; +} + +void fragment() { + float brush_value = u_opacity * texture(TEXTURE, UV).r; + + vec2 src_pixel_size = 1.0 / vec2(textureSize(u_src_texture, 0)); + vec2 src_uv = get_src_uv(SCREEN_UV); + vec2 offset = src_pixel_size; + float src_nx = texture(u_src_texture, src_uv - vec2(offset.x, 0.0)).r; + float src_px = texture(u_src_texture, src_uv + vec2(offset.x, 0.0)).r; + float src_ny = texture(u_src_texture, src_uv - vec2(0.0, offset.y)).r; + float src_py = texture(u_src_texture, src_uv + vec2(0.0, offset.y)).r; + float src_h = texture(u_src_texture, src_uv).r; + float dst_h = (src_h + src_nx + src_px + src_ny + src_py) * 0.2; + float h = mix(src_h, dst_h, u_factor * brush_value); + COLOR = vec4(h, 0.0, 0.0, 1.0); +} diff --git a/addons/zylann.hterrain/tools/brush/shaders/splat16.shader b/addons/zylann.hterrain/tools/brush/shaders/splat16.shader index 120b8c4..a32ae5e 100644 --- a/addons/zylann.hterrain/tools/brush/shaders/splat16.shader +++ b/addons/zylann.hterrain/tools/brush/shaders/splat16.shader @@ -1,68 +1,76 @@ -shader_type canvas_item; -render_mode blend_disabled; - -uniform sampler2D u_brush_texture; -uniform float u_factor = 1.0; -uniform vec4 u_splat = vec4(1.0, 0.0, 0.0, 0.0); -uniform sampler2D u_other_splatmap_1; -uniform sampler2D u_other_splatmap_2; -uniform sampler2D u_other_splatmap_3; -uniform sampler2D u_heightmap; -uniform float u_normal_min_y = 0.0; -uniform float u_normal_max_y = 1.0; - -float sum(vec4 v) { - return v.x + v.y + v.z + v.w; -} - -vec3 get_normal(sampler2D heightmap, vec2 pos) { - vec2 ps = vec2(1.0) / vec2(textureSize(heightmap, 0)); - float hnx = texture(heightmap, pos + vec2(-ps.x, 0.0)).r; - float hpx = texture(heightmap, pos + vec2(ps.x, 0.0)).r; - float hny = texture(heightmap, pos + vec2(0.0, -ps.y)).r; - float hpy = texture(heightmap, pos + vec2(0.0, ps.y)).r; - return normalize(vec3(hnx - hpx, 2.0, hpy - hny)); -} - -// Limits painting based on the slope, with a bit of falloff -float apply_slope_limit(float brush_value, vec3 normal, float normal_min_y, float normal_max_y) { - float normal_falloff = 0.2; - - // If an edge is at min/max, make sure it won't be affected by falloff - normal_min_y = normal_min_y <= 0.0 ? -2.0 : normal_min_y; - normal_max_y = normal_max_y >= 1.0 ? 2.0 : normal_max_y; - - brush_value *= 1.0 - smoothstep( - normal_max_y - normal_falloff, - normal_max_y + normal_falloff, normal.y); - - brush_value *= smoothstep( - normal_min_y - normal_falloff, - normal_min_y + normal_falloff, normal.y); - - return brush_value; -} - -void fragment() { - float brush_value = texture(u_brush_texture, SCREEN_UV).r * u_factor; - - vec3 normal = get_normal(u_heightmap, UV); - brush_value = apply_slope_limit(brush_value, normal, u_normal_min_y, u_normal_max_y); - - // It is assumed 3 other renders are done the same with the other 3 - vec4 src0 = texture(TEXTURE, UV); - vec4 src1 = texture(u_other_splatmap_1, UV); - vec4 src2 = texture(u_other_splatmap_2, UV); - vec4 src3 = texture(u_other_splatmap_3, UV); - float t = brush_value; - vec4 s0 = mix(src0, u_splat, t); - vec4 s1 = mix(src1, vec4(0.0), t); - vec4 s2 = mix(src2, vec4(0.0), t); - vec4 s3 = mix(src3, vec4(0.0), t); - float sum = sum(s0) + sum(s1) + sum(s2) + sum(s3); - s0 /= sum; - s1 /= sum; - s2 /= sum; - s3 /= sum; - COLOR = s0; -} +shader_type canvas_item; +render_mode blend_disabled; + +uniform sampler2D u_src_texture; +uniform vec4 u_src_rect; +uniform float u_opacity = 1.0; +uniform float u_factor = 1.0; +uniform vec4 u_splat = vec4(1.0, 0.0, 0.0, 0.0); +uniform sampler2D u_other_splatmap_1; +uniform sampler2D u_other_splatmap_2; +uniform sampler2D u_other_splatmap_3; +uniform sampler2D u_heightmap; +uniform float u_normal_min_y = 0.0; +uniform float u_normal_max_y = 1.0; + +vec2 get_src_uv(vec2 screen_uv) { + vec2 uv = u_src_rect.xy + screen_uv * u_src_rect.zw; + return uv; +} + +float sum(vec4 v) { + return v.x + v.y + v.z + v.w; +} + +vec3 get_normal(sampler2D heightmap, vec2 pos) { + vec2 ps = vec2(1.0) / vec2(textureSize(heightmap, 0)); + float hnx = texture(heightmap, pos + vec2(-ps.x, 0.0)).r; + float hpx = texture(heightmap, pos + vec2(ps.x, 0.0)).r; + float hny = texture(heightmap, pos + vec2(0.0, -ps.y)).r; + float hpy = texture(heightmap, pos + vec2(0.0, ps.y)).r; + return normalize(vec3(hnx - hpx, 2.0, hpy - hny)); +} + +// Limits painting based on the slope, with a bit of falloff +float apply_slope_limit(float brush_value, vec3 normal, float normal_min_y, float normal_max_y) { + float normal_falloff = 0.2; + + // If an edge is at min/max, make sure it won't be affected by falloff + normal_min_y = normal_min_y <= 0.0 ? -2.0 : normal_min_y; + normal_max_y = normal_max_y >= 1.0 ? 2.0 : normal_max_y; + + brush_value *= 1.0 - smoothstep( + normal_max_y - normal_falloff, + normal_max_y + normal_falloff, normal.y); + + brush_value *= smoothstep( + normal_min_y - normal_falloff, + normal_min_y + normal_falloff, normal.y); + + return brush_value; +} + +void fragment() { + float brush_value = u_opacity * texture(TEXTURE, UV).r * u_factor; + + vec2 src_uv = get_src_uv(SCREEN_UV); + vec3 normal = get_normal(u_heightmap, src_uv); + brush_value = apply_slope_limit(brush_value, normal, u_normal_min_y, u_normal_max_y); + + // It is assumed 3 other renders are done the same with the other 3 + vec4 src0 = texture(u_src_texture, src_uv); + vec4 src1 = texture(u_other_splatmap_1, src_uv); + vec4 src2 = texture(u_other_splatmap_2, src_uv); + vec4 src3 = texture(u_other_splatmap_3, src_uv); + float t = brush_value; + vec4 s0 = mix(src0, u_splat, t); + vec4 s1 = mix(src1, vec4(0.0), t); + vec4 s2 = mix(src2, vec4(0.0), t); + vec4 s3 = mix(src3, vec4(0.0), t); + float sum = sum(s0) + sum(s1) + sum(s2) + sum(s3); + s0 /= sum; + s1 /= sum; + s2 /= sum; + s3 /= sum; + COLOR = s0; +} diff --git a/addons/zylann.hterrain/tools/brush/shaders/splat4.shader b/addons/zylann.hterrain/tools/brush/shaders/splat4.shader index a5178a5..3a07061 100644 --- a/addons/zylann.hterrain/tools/brush/shaders/splat4.shader +++ b/addons/zylann.hterrain/tools/brush/shaders/splat4.shader @@ -1,7 +1,9 @@ shader_type canvas_item; render_mode blend_disabled; -uniform sampler2D u_brush_texture; +uniform sampler2D u_src_texture; +uniform vec4 u_src_rect; +uniform float u_opacity = 1.0; uniform float u_factor = 1.0; uniform vec4 u_splat = vec4(1.0, 0.0, 0.0, 0.0); uniform sampler2D u_heightmap; @@ -9,6 +11,11 @@ uniform float u_normal_min_y = 0.0; uniform float u_normal_max_y = 1.0; //uniform float u_normal_falloff = 0.0; +vec2 get_src_uv(vec2 screen_uv) { + vec2 uv = u_src_rect.xy + screen_uv * u_src_rect.zw; + return uv; +} + vec3 get_normal(sampler2D heightmap, vec2 pos) { vec2 ps = vec2(1.0) / vec2(textureSize(heightmap, 0)); float hnx = texture(heightmap, pos + vec2(-ps.x, 0.0)).r; @@ -38,12 +45,13 @@ float apply_slope_limit(float brush_value, vec3 normal, float normal_min_y, floa } void fragment() { - float brush_value = texture(u_brush_texture, SCREEN_UV).r * u_factor; + float brush_value = u_opacity * texture(TEXTURE, UV).r * u_factor; - vec3 normal = get_normal(u_heightmap, UV); + vec2 src_uv = get_src_uv(SCREEN_UV); + vec3 normal = get_normal(u_heightmap, src_uv); brush_value = apply_slope_limit(brush_value, normal, u_normal_min_y, u_normal_max_y); - vec4 src_splat = texture(TEXTURE, UV); + vec4 src_splat = texture(u_src_texture, src_uv); vec4 s = mix(src_splat, u_splat, brush_value); s = s / (s.r + s.g + s.b + s.a); COLOR = s; diff --git a/addons/zylann.hterrain/tools/brush/shaders/splat_indexed.shader b/addons/zylann.hterrain/tools/brush/shaders/splat_indexed.shader index c4d30b5..37c8b41 100644 --- a/addons/zylann.hterrain/tools/brush/shaders/splat_indexed.shader +++ b/addons/zylann.hterrain/tools/brush/shaders/splat_indexed.shader @@ -1,82 +1,90 @@ -shader_type canvas_item; -render_mode blend_disabled; - -uniform sampler2D u_brush_texture; -uniform float u_factor = 1.0; -uniform int u_texture_index; -uniform int u_mode; // 0: index, 1: weight -uniform sampler2D u_index_map; -uniform sampler2D u_weight_map; - -void fragment() { - float brush_value = texture(u_brush_texture, SCREEN_UV).r * clamp(u_factor, 0.0, 1.0); - - vec4 iv = texture(u_index_map, UV); - vec4 wv = texture(u_weight_map, UV); - - float i[3] = {iv.r, iv.g, iv.b}; - float w[3] = {wv.r, wv.g, wv.b}; - - if (brush_value > 0.0) { - float texture_index_f = float(u_texture_index) / 255.0; - int ci = u_texture_index % 3; - - float cm[3] = {-1.0, -1.0, -1.0}; - cm[ci] = 1.0; - - // Decompress third weight to make computations easier - w[2] = 1.0 - w[0] - w[1]; - - if (abs(i[ci] - texture_index_f) > 0.001) { - // Pixel does not have our texture index, - // transfer its weight to other components first - if (w[ci] > brush_value) { - w[0] -= cm[0] * brush_value; - w[1] -= cm[1] * brush_value; - w[2] -= cm[2] * brush_value; - - } else if (w[ci] >= 0.f) { - w[ci] = 0.f; - i[ci] = texture_index_f; - } - - } else { - // Pixel has our texture index, increase its weight - if (w[ci] + brush_value < 1.f) { - w[0] += cm[0] * brush_value; - w[1] += cm[1] * brush_value; - w[2] += cm[2] * brush_value; - - } else { - // Pixel weight is full, we can set all components to the same index. - // Need to nullify other weights because they would otherwise never reach - // zero due to normalization - w[0] = 0.0; - w[1] = 0.0; - w[2] = 0.0; - - w[ci] = 1.0; - - i[0] = texture_index_f; - i[1] = texture_index_f; - i[2] = texture_index_f; - } - } - - w[0] = clamp(w[0], 0.0, 1.0); - w[1] = clamp(w[1], 0.0, 1.0); - w[2] = clamp(w[2], 0.0, 1.0); - - // Renormalize - float sum = w[0] + w[1] + w[2]; - w[0] /= sum; - w[1] /= sum; - w[2] /= sum; - } - - if(u_mode == 0) { - COLOR = vec4(i[0], i[1], i[2], 1.0); - } else { - COLOR = vec4(w[0], w[1], w[2], 1.0); - } -} +shader_type canvas_item; +render_mode blend_disabled; + +uniform sampler2D u_src_texture; +uniform vec4 u_src_rect; +uniform float u_opacity = 1.0; +uniform float u_factor = 1.0; +uniform int u_texture_index; +uniform int u_mode; // 0: output index, 1: output weight +uniform sampler2D u_index_map; +uniform sampler2D u_weight_map; + +vec2 get_src_uv(vec2 screen_uv) { + vec2 uv = u_src_rect.xy + screen_uv * u_src_rect.zw; + return uv; +} + +void fragment() { + float brush_value = u_opacity * texture(TEXTURE, UV).r * clamp(u_factor, 0.0, 1.0); + + vec2 src_uv = get_src_uv(SCREEN_UV); + vec4 iv = texture(u_index_map, src_uv); + vec4 wv = texture(u_weight_map, src_uv); + + float i[3] = {iv.r, iv.g, iv.b}; + float w[3] = {wv.r, wv.g, wv.b}; + + if (brush_value > 0.0) { + float texture_index_f = float(u_texture_index) / 255.0; + int ci = u_texture_index % 3; + + float cm[3] = {-1.0, -1.0, -1.0}; + cm[ci] = 1.0; + + // Decompress third weight to make computations easier + w[2] = 1.0 - w[0] - w[1]; + + if (abs(i[ci] - texture_index_f) > 0.001) { + // Pixel does not have our texture index, + // transfer its weight to other components first + if (w[ci] > brush_value) { + w[0] -= cm[0] * brush_value; + w[1] -= cm[1] * brush_value; + w[2] -= cm[2] * brush_value; + + } else if (w[ci] >= 0.f) { + w[ci] = 0.f; + i[ci] = texture_index_f; + } + + } else { + // Pixel has our texture index, increase its weight + if (w[ci] + brush_value < 1.f) { + w[0] += cm[0] * brush_value; + w[1] += cm[1] * brush_value; + w[2] += cm[2] * brush_value; + + } else { + // Pixel weight is full, we can set all components to the same index. + // Need to nullify other weights because they would otherwise never reach + // zero due to normalization + w[0] = 0.0; + w[1] = 0.0; + w[2] = 0.0; + + w[ci] = 1.0; + + i[0] = texture_index_f; + i[1] = texture_index_f; + i[2] = texture_index_f; + } + } + + w[0] = clamp(w[0], 0.0, 1.0); + w[1] = clamp(w[1], 0.0, 1.0); + w[2] = clamp(w[2], 0.0, 1.0); + + // Renormalize + float sum = w[0] + w[1] + w[2]; + w[0] /= sum; + w[1] /= sum; + w[2] /= sum; + } + + if (u_mode == 0) { + COLOR = vec4(i[0], i[1], i[2], 1.0); + } else { + COLOR = vec4(w[0], w[1], w[2], 1.0); + } +} diff --git a/addons/zylann.hterrain/tools/brush/terrain_painter.gd b/addons/zylann.hterrain/tools/brush/terrain_painter.gd index 113dc3c..4e1058e 100644 --- a/addons/zylann.hterrain/tools/brush/terrain_painter.gd +++ b/addons/zylann.hterrain/tools/brush/terrain_painter.gd @@ -4,6 +4,7 @@ const Painter = preload("./painter.gd") const HTerrain = preload("../../hterrain.gd") const HTerrainData = preload("../../hterrain_data.gd") const Logger = preload("../../util/logger.gd") +const Brush = preload("./brush.gd") const RaiseShader = preload("./shaders/raise.shader") const SmoothShader = preload("./shaders/smooth.shader") @@ -33,12 +34,12 @@ class ModifiedMap: var map_index := 0 var painter_index := 0 -signal changed +signal flatten_height_changed var _painters := [] -var _brush_size := 32 -var _opacity := 1.0 +var _brush := Brush.new() + var _color := Color(1, 0, 0, 1) var _mask_flag := false var _mode := MODE_RAISE @@ -59,43 +60,45 @@ func _init(): var p = Painter.new() # The name is just for debugging p.set_name(str("Painter", i)) - p.set_brush_size(_brush_size) + #p.set_brush_size(_brush_size) p.connect("texture_region_changed", self, "_on_painter_texture_region_changed", [i]) add_child(p) _painters.append(p) +func get_brush() -> Brush: + return _brush + + func get_brush_size() -> int: - return _brush_size + return _brush.get_size() func set_brush_size(s: int): - if _brush_size == s: - return - _brush_size = s - for p in _painters: - p.set_brush_size(_brush_size) - emit_signal("changed") + _brush.set_size(s) +# for p in _painters: +# p.set_brush_size(_brush_size) func set_brush_texture(texture: Texture): - for p in _painters: - p.set_brush_texture(texture) + _brush.set_shapes([texture]) +# for p in _painters: +# p.set_brush_texture(texture) func get_opacity() -> float: - return _opacity + return _brush.get_opacity() func set_opacity(opacity: float): - _opacity = opacity + _brush.set_opacity(opacity) func set_flatten_height(h: float): if h == _flatten_height: return _flatten_height = h - emit_signal("changed") + emit_signal("flatten_height_changed") func get_flatten_height() -> float: @@ -177,7 +180,10 @@ func commit() -> Dictionary: var changes := [] var chunk_positions : Array + assert(len(_modified_maps) > 0) + for mm in _modified_maps: + #print("Flushing painter ", mm.painter_index) var painter : Painter = _painters[mm.painter_index] var info := painter.commit() @@ -198,6 +204,12 @@ func commit() -> Dictionary: # since the latter updates out of order for preview terrain_data.notify_region_change(rect, mm.map_type, mm.map_index, false, true) +# for i in len(_painters): +# var p = _painters[i] +# if p.has_modified_chunks(): +# print("Painter ", i, " has modified chunks") + + # `commit()` is supposed to consume these chunks, there should be none left assert(not has_modified_chunks()) return { @@ -227,14 +239,19 @@ func set_terrain(terrain: HTerrain): p.clear_brush_shader_params() -# This may be called from an `_input` callback -func paint_input(position: Vector2): +# This may be called from an `_input` callback. +# Returns `true` if any change was performed. +func paint_input(position: Vector2, pressure: float) -> bool: assert(_terrain.get_data() != null) var data = _terrain.get_data() assert(not data.is_locked()) - _modified_maps.clear() + if not _brush.configure_paint_input(_painters, position, pressure): + # Sometimes painting may not happen due to frequency options + return false + _modified_maps.clear() + match _mode: MODE_RAISE: _paint_height(data, position, 1.0) @@ -279,11 +296,12 @@ func paint_input(position: Vector2): MODE_DETAIL: _paint_detail(data, position) - + _: _logger.error("Unknown mode {0}".format([_mode])) assert(len(_modified_maps) > 0) + return true func _on_painter_texture_region_changed(rect: Rect2, painter_index: int): @@ -308,8 +326,8 @@ func _paint_height(data: HTerrainData, position: Vector2, factor: float): _modified_maps = [mm] # When using sculpting tools, make it dependent on brush size - var raise_strength := 10.0 + float(_brush_size) - var delta := factor * _opacity * (2.0 / 60.0) * raise_strength + var raise_strength := 10.0 + float(_brush.get_size()) + var delta := factor * (2.0 / 60.0) * raise_strength var p : Painter = _painters[0] @@ -332,7 +350,7 @@ func _paint_smooth(data: HTerrainData, position: Vector2): var p : Painter = _painters[0] p.set_brush_shader(SmoothShader) - p.set_brush_shader_param("u_factor", _opacity * (10.0 / 60.0)) + p.set_brush_shader_param("u_factor", (10.0 / 60.0)) p.set_image(image, texture) p.paint_input(position) @@ -350,7 +368,7 @@ func _paint_flatten(data: HTerrainData, position: Vector2): var p : Painter = _painters[0] p.set_brush_shader(FlattenShader) - p.set_brush_shader_param("u_factor", _opacity) + #p.set_brush_shader_param("u_factor", _opacity) p.set_brush_shader_param("u_flatten_value", _flatten_height) p.set_image(image, texture) p.paint_input(position) @@ -369,7 +387,7 @@ func _paint_level(data: HTerrainData, position: Vector2): var p : Painter = _painters[0] p.set_brush_shader(LevelShader) - p.set_brush_shader_param("u_factor", _opacity * (10.0 / 60.0)) + p.set_brush_shader_param("u_factor", (10.0 / 60.0)) p.set_image(image, texture) p.paint_input(position) @@ -387,7 +405,7 @@ func _paint_erode(data: HTerrainData, position: Vector2): var p : Painter = _painters[0] p.set_brush_shader(ErodeShader) - p.set_brush_shader_param("u_factor", _opacity) + #p.set_brush_shader_param("u_factor", _opacity) p.set_image(image, texture) p.paint_input(position) @@ -407,7 +425,7 @@ func _paint_splat4(data: HTerrainData, position: Vector2): var splat = Color(0.0, 0.0, 0.0, 0.0) splat[_texture_index] = 1.0; p.set_brush_shader(Splat4Shader) - p.set_brush_shader_param("u_factor", _opacity) + #p.set_brush_shader_param("u_factor", _opacity) p.set_brush_shader_param("u_splat", splat) p.set_brush_shader_param("u_normal_min_y", cos(_slope_limit_high_angle)) p.set_brush_shader_param("u_normal_max_y", cos(_slope_limit_low_angle) + 0.001) @@ -440,7 +458,7 @@ func _paint_splat_indexed(data: HTerrainData, position: Vector2): p.set_brush_shader(SplatIndexedShader) p.set_brush_shader_param("u_mode", mode) - p.set_brush_shader_param("u_factor", _opacity) + #p.set_brush_shader_param("u_factor", _opacity) p.set_brush_shader_param("u_index_map", textures[0]) p.set_brush_shader_param("u_weight_map", textures[1]) p.set_brush_shader_param("u_texture_index", _texture_index) @@ -482,7 +500,7 @@ func _paint_splat16(data: HTerrainData, position: Vector2): other_splatmaps.append(tex) p.set_brush_shader(Splat16Shader) - p.set_brush_shader_param("u_factor", _opacity) + #p.set_brush_shader_param("u_factor", _opacity) p.set_brush_shader_param("u_splat", splats[i]) p.set_brush_shader_param("u_other_splatmap_1", other_splatmaps[0]) p.set_brush_shader_param("u_other_splatmap_2", other_splatmaps[1]) @@ -510,7 +528,7 @@ func _paint_color(data: HTerrainData, position: Vector2): # https://github.com/Zylann/godot_heightmap_plugin/issues/17#issuecomment-734001879 p.set_brush_shader(ColorShader) - p.set_brush_shader_param("u_factor", _opacity) + #p.set_brush_shader_param("u_factor", _opacity) p.set_brush_shader_param("u_color", _color) p.set_image(image, texture) p.paint_input(position) @@ -529,7 +547,7 @@ func _paint_mask(data: HTerrainData, position: Vector2): var p : Painter = _painters[0] p.set_brush_shader(AlphaShader) - p.set_brush_shader_param("u_factor", _opacity) + #p.set_brush_shader_param("u_factor", _opacity) p.set_brush_shader_param("u_value", 1.0 if _mask_flag else 0.0) p.set_image(image, texture) p.paint_input(position) @@ -550,7 +568,7 @@ func _paint_detail(data: HTerrainData, position: Vector2): # TODO Don't use this shader p.set_brush_shader(ColorShader) - p.set_brush_shader_param("u_factor", _opacity) + #p.set_brush_shader_param("u_factor", _opacity) p.set_brush_shader_param("u_color", c) p.set_image(image, texture) p.paint_input(position) diff --git a/addons/zylann.hterrain/tools/exporter/export_image_dialog.tscn b/addons/zylann.hterrain/tools/exporter/export_image_dialog.tscn index 65da1cb..9f57677 100644 --- a/addons/zylann.hterrain/tools/exporter/export_image_dialog.tscn +++ b/addons/zylann.hterrain/tools/exporter/export_image_dialog.tscn @@ -1,6 +1,7 @@ -[gd_scene load_steps=2 format=2] +[gd_scene load_steps=3 format=2] [ext_resource path="res://addons/zylann.hterrain/tools/exporter/export_image_dialog.gd" type="Script" id=1] +[ext_resource path="res://addons/zylann.hterrain/tools/util/dialog_fitter.tscn" type="PackedScene" id=2] [node name="ExportImageDialog" type="WindowDialog"] margin_left = 77.0 @@ -156,6 +157,9 @@ margin_left = 257.0 margin_right = 311.0 margin_bottom = 20.0 text = "Cancel" + +[node name="DialogFitter" parent="." instance=ExtResource( 2 )] + [connection signal="text_changed" from="VB/Grid/OutputPath/HeightmapPathLineEdit" to="." method="_on_HeightmapPathLineEdit_text_changed"] [connection signal="pressed" from="VB/Grid/OutputPath/HeightmapPathBrowseButton" to="." method="_on_HeightmapPathBrowseButton_pressed"] [connection signal="item_selected" from="VB/Grid/FormatSelector" to="." method="_on_FormatSelector_item_selected"] diff --git a/addons/zylann.hterrain/tools/generate_mesh_dialog.tscn b/addons/zylann.hterrain/tools/generate_mesh_dialog.tscn index 230c6e7..b3face2 100644 --- a/addons/zylann.hterrain/tools/generate_mesh_dialog.tscn +++ b/addons/zylann.hterrain/tools/generate_mesh_dialog.tscn @@ -1,243 +1,84 @@ -[gd_scene load_steps=2 format=2] +[gd_scene load_steps=3 format=2] [ext_resource path="res://addons/zylann.hterrain/tools/generate_mesh_dialog.gd" type="Script" id=1] +[ext_resource path="res://addons/zylann.hterrain/tools/util/dialog_fitter.tscn" type="PackedScene" id=2] -[node name="GenerateMeshDialog" type="WindowDialog" index="0"] - -anchor_left = 0.0 -anchor_top = 0.0 -anchor_right = 0.0 -anchor_bottom = 0.0 +[node name="GenerateMeshDialog" type="WindowDialog"] margin_left = 57.0 margin_top = 83.0 margin_right = 505.0 margin_bottom = 269.0 rect_min_size = Vector2( 448, 186 ) -rect_pivot_offset = Vector2( 0, 0 ) -rect_clip_content = false -mouse_filter = 0 -mouse_default_cursor_shape = 0 -size_flags_horizontal = 1 -size_flags_vertical = 1 -popup_exclusive = false window_title = "Generate full mesh" -resizable = false script = ExtResource( 1 ) -_sections_unfolded = [ "Rect" ] -[node name="VBoxContainer" type="VBoxContainer" parent="." index="1"] - -anchor_left = 0.0 -anchor_top = 0.0 +[node name="VBoxContainer" type="VBoxContainer" parent="."] anchor_right = 1.0 anchor_bottom = 1.0 margin_left = 8.0 margin_top = 8.0 margin_right = -8.0 margin_bottom = -8.0 -rect_pivot_offset = Vector2( 0, 0 ) -rect_clip_content = false -mouse_filter = 1 -mouse_default_cursor_shape = 0 -size_flags_horizontal = 1 -size_flags_vertical = 1 -alignment = 0 -_sections_unfolded = [ "Margin" ] -[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer" index="0"] - -anchor_left = 0.0 -anchor_top = 0.0 -anchor_right = 0.0 -anchor_bottom = 0.0 +[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"] margin_right = 432.0 margin_bottom = 24.0 -rect_pivot_offset = Vector2( 0, 0 ) -rect_clip_content = false -mouse_filter = 1 -mouse_default_cursor_shape = 0 -size_flags_horizontal = 1 -size_flags_vertical = 1 -alignment = 0 -[node name="Label" type="Label" parent="VBoxContainer/HBoxContainer" index="0"] - -anchor_left = 0.0 -anchor_top = 0.0 -anchor_right = 0.0 -anchor_bottom = 0.0 +[node name="Label" type="Label" parent="VBoxContainer/HBoxContainer"] margin_top = 5.0 margin_right = 28.0 margin_bottom = 19.0 -rect_pivot_offset = Vector2( 0, 0 ) -rect_clip_content = false -mouse_filter = 2 -mouse_default_cursor_shape = 0 -size_flags_horizontal = 1 -size_flags_vertical = 4 text = "LOD" -percent_visible = 1.0 -lines_skipped = 0 -max_lines_visible = -1 -[node name="LODSpinBox" type="SpinBox" parent="VBoxContainer/HBoxContainer" index="1"] - -anchor_left = 0.0 -anchor_top = 0.0 -anchor_right = 0.0 -anchor_bottom = 0.0 +[node name="LODSpinBox" type="SpinBox" parent="VBoxContainer/HBoxContainer"] margin_left = 32.0 margin_right = 432.0 margin_bottom = 24.0 -rect_pivot_offset = Vector2( 0, 0 ) -rect_clip_content = false -mouse_filter = 0 -mouse_default_cursor_shape = 0 size_flags_horizontal = 3 -size_flags_vertical = 1 min_value = 1.0 max_value = 16.0 -step = 1.0 -page = 0.0 value = 1.0 -exp_edit = false -rounded = false -editable = true -prefix = "" -suffix = "" -_sections_unfolded = [ "Size Flags" ] -[node name="PreviewLabel" type="Label" parent="VBoxContainer" index="1"] - -anchor_left = 0.0 -anchor_top = 0.0 -anchor_right = 0.0 -anchor_bottom = 0.0 +[node name="PreviewLabel" type="Label" parent="VBoxContainer"] margin_top = 28.0 margin_right = 432.0 margin_bottom = 42.0 -rect_pivot_offset = Vector2( 0, 0 ) -rect_clip_content = false -mouse_filter = 2 -mouse_default_cursor_shape = 0 -size_flags_horizontal = 1 -size_flags_vertical = 4 text = "9999 vertices, 9999 triangles" -percent_visible = 1.0 -lines_skipped = 0 -max_lines_visible = -1 -[node name="Spacer" type="Control" parent="VBoxContainer" index="2"] - -anchor_left = 0.0 -anchor_top = 0.0 -anchor_right = 0.0 -anchor_bottom = 0.0 +[node name="Spacer" type="Control" parent="VBoxContainer"] margin_top = 46.0 margin_right = 432.0 margin_bottom = 54.0 rect_min_size = Vector2( 0, 8 ) -rect_pivot_offset = Vector2( 0, 0 ) -rect_clip_content = false -mouse_filter = 0 -mouse_default_cursor_shape = 0 -size_flags_horizontal = 1 -size_flags_vertical = 1 -_sections_unfolded = [ "Rect" ] -[node name="Label" type="Label" parent="VBoxContainer" index="3"] - -anchor_left = 0.0 -anchor_top = 0.0 -anchor_right = 0.0 -anchor_bottom = 0.0 +[node name="Label" type="Label" parent="VBoxContainer"] margin_top = 58.0 margin_right = 432.0 margin_bottom = 123.0 -rect_pivot_offset = Vector2( 0, 0 ) -rect_clip_content = false -mouse_filter = 2 -mouse_default_cursor_shape = 0 -size_flags_horizontal = 1 -size_flags_vertical = 4 text = "Note: generating a full mesh from the terrain may result in a huge amount of vertices for a single object. It is preferred to do this for small terrains, or as a temporary workaround to generate a navmesh." autowrap = true -percent_visible = 1.0 -lines_skipped = 0 -max_lines_visible = -1 -_sections_unfolded = [ "custom_colors" ] -[node name="Buttons" type="HBoxContainer" parent="VBoxContainer" index="4"] - -anchor_left = 0.0 -anchor_top = 0.0 -anchor_right = 0.0 -anchor_bottom = 0.0 +[node name="Buttons" type="HBoxContainer" parent="VBoxContainer"] margin_top = 127.0 margin_right = 432.0 margin_bottom = 147.0 -rect_pivot_offset = Vector2( 0, 0 ) -rect_clip_content = false -mouse_filter = 1 -mouse_default_cursor_shape = 0 -size_flags_horizontal = 1 -size_flags_vertical = 1 custom_constants/separation = 32 alignment = 1 -_sections_unfolded = [ "custom_constants" ] -[node name="Generate" type="Button" parent="VBoxContainer/Buttons" index="0"] - -anchor_left = 0.0 -anchor_top = 0.0 -anchor_right = 0.0 -anchor_bottom = 0.0 +[node name="Generate" type="Button" parent="VBoxContainer/Buttons"] margin_left = 137.0 margin_right = 208.0 margin_bottom = 20.0 -rect_pivot_offset = Vector2( 0, 0 ) -rect_clip_content = false -focus_mode = 2 -mouse_filter = 0 -mouse_default_cursor_shape = 0 -size_flags_horizontal = 1 -size_flags_vertical = 1 -toggle_mode = false -enabled_focus_mode = 2 -shortcut = null -group = null text = "Generate" -flat = false -align = 1 -[node name="Cancel" type="Button" parent="VBoxContainer/Buttons" index="1"] - -anchor_left = 0.0 -anchor_top = 0.0 -anchor_right = 0.0 -anchor_bottom = 0.0 +[node name="Cancel" type="Button" parent="VBoxContainer/Buttons"] margin_left = 240.0 margin_right = 294.0 margin_bottom = 20.0 -rect_pivot_offset = Vector2( 0, 0 ) -rect_clip_content = false -focus_mode = 2 -mouse_filter = 0 -mouse_default_cursor_shape = 0 -size_flags_horizontal = 1 -size_flags_vertical = 1 -toggle_mode = false -enabled_focus_mode = 2 -shortcut = null -group = null text = "Cancel" -flat = false -align = 1 + +[node name="DialogFitter" parent="." instance=ExtResource( 2 )] [connection signal="value_changed" from="VBoxContainer/HBoxContainer/LODSpinBox" to="." method="_on_LODSpinBox_value_changed"] - [connection signal="pressed" from="VBoxContainer/Buttons/Generate" to="." method="_on_Generate_pressed"] - [connection signal="pressed" from="VBoxContainer/Buttons/Cancel" to="." method="_on_Cancel_pressed"] - - diff --git a/addons/zylann.hterrain/tools/generator/generator_dialog.gd b/addons/zylann.hterrain/tools/generator/generator_dialog.gd index 513506e..c66b96a 100644 --- a/addons/zylann.hterrain/tools/generator/generator_dialog.gd +++ b/addons/zylann.hterrain/tools/generator/generator_dialog.gd @@ -1,6 +1,7 @@ tool extends WindowDialog +const HTerrain = preload("../../hterrain.gd") const HTerrainData = preload("../../hterrain_data.gd") const HTerrainMesher = preload("../../hterrain_mesher.gd") const Util = preload("../../util/util.gd") @@ -19,7 +20,7 @@ onready var _preview = $VBoxContainer/Editor/Preview/TerrainPreview onready var _progress_bar = $VBoxContainer/Editor/Preview/ProgressBar var _dummy_texture = load("res://addons/zylann.hterrain/tools/icons/empty.png") -var _terrain = null +var _terrain : HTerrain = null var _applying := false var _generator : TextureGenerator var _generated_textures := [null, null] @@ -106,6 +107,30 @@ func _ready(): "range": { "min": 0.0, "max": 1.0 }, "default_value": 0.0 }, + "island_weight": { + "type": TYPE_REAL, + "range": { "min": 0.0, "max": 1.0, "step": 0.01 }, + "default_value": 0.0 + }, + "island_sharpness": { + "type": TYPE_REAL, + "range": { "min": 0.0, "max": 1.0, "step": 0.01 }, + "default_value": 0.0 + }, + "island_height_ratio": { + "type": TYPE_REAL, + "range": { "min": -1.0, "max": 1.0, "step": 0.01 }, + "default_value": -1.0 + }, + "island_shape": { + "type": TYPE_REAL, + "range": { "min": 0.0, "max": 1.0, "step": 0.01 }, + "default_value": 0.0 + }, + "additive_heightmap": { + "type": TYPE_BOOL, + "default_value": false + }, "show_sea": { "type": TYPE_BOOL, "default_value": true @@ -212,11 +237,19 @@ func _update_generator(preview: bool): var preview_scale := 4.0 # As if 2049x2049 var sectors := [] + var terrain_size = 513 + + var additive_heightmap : Texture = null # Get preview scale and sectors to generate. # Allowing null terrain to make it testable. - if _terrain != null and _terrain.get_data() != null: - var terrain_size = _terrain.get_data().get_resolution() + var terrain_data := _terrain.get_data() + if _terrain != null and terrain_data != null: + terrain_size = terrain_data.get_resolution() + + if _inspector.get_value("additive_heightmap"): + additive_heightmap = \ + terrain_data.get_texture(HTerrainData.CHANNEL_HEIGHT) if preview: # When previewing the resolution does not span the entire terrain, @@ -225,6 +258,15 @@ func _update_generator(preview: bool): sectors.append(Vector2(0, 0)) else: + if additive_heightmap != null: + # We have to duplicate the heightmap because we are going to write + # into it during the generation process. + # It would be fine when we don't read outside of a generated tile, + # but we actually do that for erosion: neighboring pixels are read + # again, and if they were modified by a previous tile it will + # disrupt generation, so we need to use a copy of the original. + additive_heightmap = additive_heightmap.duplicate() + # When we get to generate it fully, sectors are used, # so the size or shape of the terrain doesn't matter preview_scale = 1.0 @@ -262,12 +304,21 @@ func _update_generator(preview: bool): p.params = { "u_octaves": _inspector.get_value("octaves"), "u_seed": _inspector.get_value("seed"), - "u_scale": scale * preview_scale, - "u_offset": base_offset_ndc / preview_scale, + "u_scale": scale, + "u_offset": base_offset_ndc, "u_base_height": _inspector.get_value("base_height") / preview_scale, "u_height_range": _inspector.get_value("height_range") / preview_scale, "u_roughness": _inspector.get_value("roughness"), - "u_curve": _inspector.get_value("curve") + "u_curve": _inspector.get_value("curve"), + "u_island_weight": _inspector.get_value("island_weight"), + "u_island_sharpness": _inspector.get_value("island_sharpness"), + "u_island_height_ratio": _inspector.get_value("island_height_ratio"), + "u_island_shape": _inspector.get_value("island_shape"), + "u_additive_heightmap": additive_heightmap, + "u_additive_heightmap_factor": \ + (1.0 if additive_heightmap != null else 0.0) / preview_scale, + "u_terrain_size": terrain_size / preview_scale, + "u_tile_size": _viewport_resolution } _generator.add_pass(p) diff --git a/addons/zylann.hterrain/tools/generator/generator_dialog.tscn b/addons/zylann.hterrain/tools/generator/generator_dialog.tscn index 3a69430..01a868b 100644 --- a/addons/zylann.hterrain/tools/generator/generator_dialog.tscn +++ b/addons/zylann.hterrain/tools/generator/generator_dialog.tscn @@ -1,14 +1,15 @@ -[gd_scene load_steps=4 format=2] +[gd_scene load_steps=5 format=2] [ext_resource path="res://addons/zylann.hterrain/tools/generator/generator_dialog.gd" type="Script" id=1] [ext_resource path="res://addons/zylann.hterrain/tools/inspector/inspector.tscn" type="PackedScene" id=2] [ext_resource path="res://addons/zylann.hterrain/tools/terrain_preview.tscn" type="PackedScene" id=3] +[ext_resource path="res://addons/zylann.hterrain/tools/util/dialog_fitter.tscn" type="PackedScene" id=4] [node name="GeneratorDialog" type="WindowDialog"] margin_left = 22.0 margin_top = 32.0 margin_right = 1122.0 -margin_bottom = 632.0 +margin_bottom = 666.0 rect_min_size = Vector2( 1100, 600 ) window_title = "Generate terrain" resizable = true @@ -25,27 +26,28 @@ margin_top = 8.0 margin_right = -8.0 margin_bottom = -8.0 custom_constants/separation = 16 +__meta__ = { +"_edit_use_anchors_": false +} [node name="Editor" type="HBoxContainer" parent="VBoxContainer"] margin_right = 1084.0 -margin_bottom = 548.0 +margin_bottom = 584.0 size_flags_vertical = 3 -[node name="Settings" type="Control" parent="VBoxContainer/Editor"] +[node name="Settings" type="VBoxContainer" parent="VBoxContainer/Editor"] margin_right = 420.0 -margin_bottom = 548.0 +margin_bottom = 584.0 rect_min_size = Vector2( 420, 0 ) [node name="Inspector" parent="VBoxContainer/Editor/Settings" instance=ExtResource( 2 )] -anchor_right = 1.0 -anchor_bottom = 1.0 -margin_right = 0.0 -margin_bottom = 0.0 +margin_right = 420.0 +margin_bottom = 584.0 [node name="Preview" type="Control" parent="VBoxContainer/Editor"] margin_left = 424.0 margin_right = 1084.0 -margin_bottom = 548.0 +margin_bottom = 584.0 size_flags_horizontal = 3 [node name="TerrainPreview" parent="VBoxContainer/Editor/Preview" instance=ExtResource( 3 )] @@ -66,9 +68,9 @@ margin_top = -16.0 step = 1.0 [node name="Choices" type="HBoxContainer" parent="VBoxContainer"] -margin_top = 564.0 +margin_top = 600.0 margin_right = 1084.0 -margin_bottom = 584.0 +margin_bottom = 620.0 custom_constants/separation = 32 alignment = 1 @@ -83,6 +85,9 @@ margin_left = 555.0 margin_right = 609.0 margin_bottom = 20.0 text = "Cancel" + +[node name="DialogFitter" parent="." instance=ExtResource( 4 )] + [connection signal="property_changed" from="VBoxContainer/Editor/Settings/Inspector" to="." method="_on_Inspector_property_changed"] [connection signal="dragged" from="VBoxContainer/Editor/Preview/TerrainPreview" to="." method="_on_TerrainPreview_dragged"] [connection signal="pressed" from="VBoxContainer/Choices/ApplyButton" to="." method="_on_ApplyButton_pressed"] diff --git a/addons/zylann.hterrain/tools/generator/shaders/perlin_noise.shader b/addons/zylann.hterrain/tools/generator/shaders/perlin_noise.shader index 6e67cbd..f92dd0f 100644 --- a/addons/zylann.hterrain/tools/generator/shaders/perlin_noise.shader +++ b/addons/zylann.hterrain/tools/generator/shaders/perlin_noise.shader @@ -8,9 +8,21 @@ uniform int u_seed; uniform int u_octaves = 5; uniform float u_roughness = 0.5; uniform float u_curve = 1.0; +uniform float u_terrain_size = 513.0; +uniform float u_tile_size = 513.0; +uniform sampler2D u_additive_heightmap; +uniform float u_additive_heightmap_factor = 0.0; uniform vec2 u_uv_offset; uniform vec2 u_uv_scale = vec2(1.0, 1.0); +uniform float u_island_weight = 0.0; +// 0: smooth transition, 1: sharp transition +uniform float u_island_sharpness = 0.0; +// 0: edge is min height (island), 1: edge is max height (canyon) +uniform float u_island_height_ratio = 0.0; +// 0: round, 1: square +uniform float u_island_shape = 0.0; + //////////////////////////////////////////////////////////////////////////////// // Perlin noise source: // https://github.com/curly-brace/Godot-3.0-Noise-Shaders @@ -103,22 +115,91 @@ float get_fractal_noise(vec2 uv) { return gs; } -float get_height(vec2 uv) { - float h = 0.5 + 0.5 * get_fractal_noise(uv); - h = pow(h, u_curve); - h = u_base_height + h * u_height_range; +// x is a ratio in 0..1 +float get_island_curve(float x) { + return smoothstep(min(0.999, u_island_sharpness), 1.0, x); +// float exponent = 1.0 + 10.0 * u_island_sharpness; +// return pow(abs(x), exponent); +} + +float smooth_union(float a, float b, float k) { + float h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0); + return mix(b, a, h) - k * h * (1.0 - h); +} + +float squareish_distance(vec2 a, vec2 b, float r, float s) { + vec2 v = b - a; + // TODO This is brute force but this is the first attempt that gave me a "rounded square" distance, + // where the "roundings" remained constant over distance (not the case with standard box SDF) + float da = -smooth_union(v.x+s, v.y+s, r)+s; + float db = -smooth_union(s-v.x, s-v.y, r)+s; + float dc = -smooth_union(s-v.x, v.y+s, r)+s; + float dd = -smooth_union(v.x+s, s-v.y, r)+s; + return max(max(da, db), max(dc, dd)); +} + +// This is too sharp +//float squareish_distance(vec2 a, vec2 b) { +// vec2 v = b - a; +// // Manhattan distance would produce a "diamond-shaped distance". +// // This gives "square-shaped" distance. +// return max(abs(v.x), abs(v.y)); +//} + +float get_island_distance(vec2 pos, vec2 center, float terrain_size) { + float rd = distance(pos, center); + float sd = squareish_distance(pos, center, terrain_size * 0.1, terrain_size); + return mix(rd, sd, u_island_shape); +} + +// pos is in terrain space +float get_height(vec2 pos) { + float h = 0.0; + + { + // Noise (0..1) + // Offset and scale for the noise itself + vec2 uv_noise = (pos / u_terrain_size + u_offset) * u_scale; + h = 0.5 + 0.5 * get_fractal_noise(uv_noise); + } + + // Curve + { + h = pow(h, u_curve); + } + + // Island + { + float terrain_size = u_terrain_size; + vec2 island_center = vec2(0.5 * terrain_size); + float island_height_ratio = 0.5 + 0.5 * u_island_height_ratio; + float island_distance = get_island_distance(pos, island_center, terrain_size); + float distance_ratio = clamp(island_distance / (0.5 * terrain_size), 0.0, 1.0); + float island_ratio = u_island_weight * get_island_curve(distance_ratio); + h = mix(h, island_height_ratio, island_ratio); + } + + // Height remapping + { + h = u_base_height + h * u_height_range; + } + + // Additive heightmap + { + h += u_additive_heightmap_factor * texture(u_additive_heightmap, pos / u_terrain_size).r; + } + return h; } void fragment() { - vec2 uv = SCREEN_UV; + // Handle screen padding: transform UV back into generation space. + // This is in tile space actually...? it spans 1 unit across the viewport, + // and starts from 0 when tile (0,0) is generated. + // Maybe we could change this into world units instead? + vec2 uv_tile = (SCREEN_UV + u_uv_offset) * u_uv_scale; - // Handle screen padding: transform UV back into generation space - uv = (uv + u_uv_offset) * u_uv_scale; - - // Offset and scale for the noise itself - uv = (uv + u_offset) * u_scale; - - float h = get_height(uv); + float h = get_height(uv_tile * u_tile_size); + COLOR = vec4(h, h, h, 1.0); } diff --git a/addons/zylann.hterrain/tools/importer/importer_dialog.tscn b/addons/zylann.hterrain/tools/importer/importer_dialog.tscn index 2cf339c..b5710ca 100644 --- a/addons/zylann.hterrain/tools/importer/importer_dialog.tscn +++ b/addons/zylann.hterrain/tools/importer/importer_dialog.tscn @@ -1,7 +1,8 @@ -[gd_scene load_steps=3 format=2] +[gd_scene load_steps=4 format=2] [ext_resource path="res://addons/zylann.hterrain/tools/importer/importer_dialog.gd" type="Script" id=1] [ext_resource path="res://addons/zylann.hterrain/tools/inspector/inspector.tscn" type="PackedScene" id=2] +[ext_resource path="res://addons/zylann.hterrain/tools/util/dialog_fitter.tscn" type="PackedScene" id=3] [node name="WindowDialog" type="WindowDialog"] visible = true @@ -13,6 +14,9 @@ rect_min_size = Vector2( 500, 380 ) window_title = "Import maps" resizable = true script = ExtResource( 1 ) +__meta__ = { +"_edit_use_anchors_": false +} [node name="VBoxContainer" type="VBoxContainer" parent="."] anchor_right = 1.0 @@ -93,6 +97,9 @@ margin_left = 312.0 margin_right = 366.0 margin_bottom = 20.0 text = "Cancel" + +[node name="DialogFitter" parent="." instance=ExtResource( 3 )] + [connection signal="property_changed" from="VBoxContainer/Inspector" to="." method="_on_Inspector_property_changed"] [connection signal="pressed" from="VBoxContainer/ButtonsArea/CheckButton" to="." method="_on_CheckButton_pressed"] [connection signal="pressed" from="VBoxContainer/ButtonsArea/ImportButton" to="." method="_on_ImportButton_pressed"] diff --git a/addons/zylann.hterrain/tools/inspector/inspector.gd b/addons/zylann.hterrain/tools/inspector/inspector.gd index 3a1a37c..e2b2dfd 100644 --- a/addons/zylann.hterrain/tools/inspector/inspector.gd +++ b/addons/zylann.hterrain/tools/inspector/inspector.gd @@ -422,7 +422,8 @@ func _dummy_setter(v): func _on_ask_load_texture(key): - _open_file_dialog(["*.png ; PNG files"], "_on_texture_selected", [key], FileDialog.ACCESS_RESOURCES) + _open_file_dialog(["*.png ; PNG files"], "_on_texture_selected", [key], + FileDialog.ACCESS_RESOURCES) func _open_file_dialog(filters, callback, binds, access): @@ -430,7 +431,8 @@ func _open_file_dialog(filters, callback, binds, access): _file_dialog.clear_filters() for filter in filters: _file_dialog.add_filter(filter) - _file_dialog.connect("popup_hide", self, "call_deferred", ["_on_file_dialog_close"], CONNECT_ONESHOT) + _file_dialog.connect("popup_hide", self, "call_deferred", ["_on_file_dialog_close"], + CONNECT_ONESHOT) _file_dialog.connect("file_selected", self, callback, binds) _file_dialog.popup_centered_ratio(0.7) diff --git a/addons/zylann.hterrain/tools/inspector/inspector.tscn b/addons/zylann.hterrain/tools/inspector/inspector.tscn index b5f2491..11de488 100644 --- a/addons/zylann.hterrain/tools/inspector/inspector.tscn +++ b/addons/zylann.hterrain/tools/inspector/inspector.tscn @@ -2,70 +2,23 @@ [ext_resource path="res://addons/zylann.hterrain/tools/inspector/inspector.gd" type="Script" id=1] -[node name="Inspector" type="Control" index="0"] - -anchor_left = 0.0 -anchor_top = 0.0 -anchor_right = 0.0 -anchor_bottom = 0.0 +[node name="Inspector" type="VBoxContainer"] margin_right = 348.0 margin_bottom = 383.0 -rect_pivot_offset = Vector2( 0, 0 ) -rect_clip_content = false -mouse_filter = 0 -mouse_default_cursor_shape = 0 -size_flags_horizontal = 1 -size_flags_vertical = 1 script = ExtResource( 1 ) -_sections_unfolded = [ "custom_constants" ] -[node name="GridContainer" type="GridContainer" parent="." index="0"] - -anchor_left = 0.0 -anchor_top = 0.0 -anchor_right = 1.0 -anchor_bottom = 1.0 -rect_pivot_offset = Vector2( 0, 0 ) -rect_clip_content = false -mouse_filter = 1 -mouse_default_cursor_shape = 0 -size_flags_horizontal = 1 -size_flags_vertical = 1 +[node name="GridContainer" type="GridContainer" parent="."] +margin_right = 348.0 custom_constants/vseparation = 4 custom_constants/hseparation = 8 columns = 2 -_sections_unfolded = [ "Anchor", "Margin", "custom_constants" ] -[node name="OpenFileDialog" type="FileDialog" parent="." index="1"] - -visible = false -anchor_left = 0.0 -anchor_top = 0.0 -anchor_right = 0.0 -anchor_bottom = 0.0 +[node name="OpenFileDialog" type="FileDialog" parent="."] margin_left = 261.0 margin_top = 150.0 margin_right = 710.0 margin_bottom = 426.0 rect_min_size = Vector2( 400, 300 ) -rect_pivot_offset = Vector2( 0, 0 ) -rect_clip_content = false -mouse_filter = 0 -mouse_default_cursor_shape = 0 -size_flags_horizontal = 1 -size_flags_vertical = 1 -popup_exclusive = false -window_title = "Ouvrir un fichier" +window_title = "Open a File" resizable = true -dialog_hide_on_ok = false -mode_overrides_title = true mode = 0 -access = 0 -filters = PoolStringArray( ) -show_hidden_files = false -current_dir = "res://" -current_file = "" -current_path = "res://" -_sections_unfolded = [ "Rect" ] - - diff --git a/addons/zylann.hterrain/tools/packed_textures/packed_texture_array_importer.gd b/addons/zylann.hterrain/tools/packed_textures/packed_texture_array_importer.gd index c4c80ff..bb51ca5 100644 --- a/addons/zylann.hterrain/tools/packed_textures/packed_texture_array_importer.gd +++ b/addons/zylann.hterrain/tools/packed_textures/packed_texture_array_importer.gd @@ -63,6 +63,10 @@ func get_import_options(preset_index: int) -> Array: { "name": "flags/mipmaps", "default_value": true + }, + { + "name": "flags/anisotropic", + "default_value": false } ] @@ -132,7 +136,8 @@ func _import(p_source_path: String, p_save_path: String, options: Dictionary, options["compress/mode"], options["flags/repeat"], options["flags/filter"], - options["flags/mipmaps"]) + options["flags/mipmaps"], + options["flags/anisotropic"]) if not result.success: return Result.new(false, diff --git a/addons/zylann.hterrain/tools/packed_textures/packed_texture_importer.gd b/addons/zylann.hterrain/tools/packed_textures/packed_texture_importer.gd index 9adeaa9..59ff372 100644 --- a/addons/zylann.hterrain/tools/packed_textures/packed_texture_importer.gd +++ b/addons/zylann.hterrain/tools/packed_textures/packed_texture_importer.gd @@ -63,6 +63,10 @@ func get_import_options(preset_index: int) -> Array: { "name": "flags/mipmaps", "default_value": true + }, + { + "name": "flags/anisotropic", + "default_value": false } ] @@ -115,6 +119,7 @@ func _import(p_source_path: String, p_save_path: String, options: Dictionary, .with_value(result.value) var image : Image = result.value + result = StreamTextureImporter.import( p_source_path, @@ -127,7 +132,8 @@ func _import(p_source_path: String, p_save_path: String, options: Dictionary, options["compress/mode"], options["flags/repeat"], options["flags/filter"], - options["flags/mipmaps"]) + options["flags/mipmaps"], + options["flags/anisotropic"]) if not result.success: return Result.new(false, diff --git a/addons/zylann.hterrain/tools/packed_textures/stream_texture_importer.gd b/addons/zylann.hterrain/tools/packed_textures/stream_texture_importer.gd index bc9e346..1751a20 100644 --- a/addons/zylann.hterrain/tools/packed_textures/stream_texture_importer.gd +++ b/addons/zylann.hterrain/tools/packed_textures/stream_texture_importer.gd @@ -43,14 +43,15 @@ static func import( p_compress_mode: int, p_repeat: int, p_filter: bool, - p_mipmaps: bool) -> Result: + p_mipmaps: bool, + p_anisotropic: bool) -> Result: var compress_mode := p_compress_mode var lossy := 0.7 var repeat := p_repeat var filter := p_filter var mipmaps := p_mipmaps - var anisotropic := false + var anisotropic := p_anisotropic var srgb := 1 if p_contains_albedo else 2 var fix_alpha_border := false var premult_alpha := false diff --git a/addons/zylann.hterrain/tools/packed_textures/texture_layered_importer.gd b/addons/zylann.hterrain/tools/packed_textures/texture_layered_importer.gd index 1eba88d..b2d4587 100644 --- a/addons/zylann.hterrain/tools/packed_textures/texture_layered_importer.gd +++ b/addons/zylann.hterrain/tools/packed_textures/texture_layered_importer.gd @@ -37,7 +37,8 @@ static func import( p_compress_mode: int, p_repeat: int, p_filter: bool, - p_mipmaps: bool) -> Result: + p_mipmaps: bool, + p_anisotropic: bool) -> Result: var compress_mode := p_compress_mode var no_bptc_if_rgb := false#p_options["compress/no_bptc_if_rgb"]; @@ -59,6 +60,8 @@ static func import( tex_flags |= Texture.FLAG_MIPMAPS if srgb == 1: tex_flags |= Texture.FLAG_CONVERT_TO_LINEAR + if p_anisotropic: + tex_flags |= Texture.FLAG_ANISOTROPIC_FILTER # Vector > slices; # diff --git a/addons/zylann.hterrain/tools/panel.gd b/addons/zylann.hterrain/tools/panel.gd index 2492654..d50ec2b 100644 --- a/addons/zylann.hterrain/tools/panel.gd +++ b/addons/zylann.hterrain/tools/panel.gd @@ -40,8 +40,8 @@ func set_camera_transform(cam_transform: Transform): _minimap.set_camera_transform(cam_transform) -func set_brush(brush): - _brush_editor.set_brush(brush) +func set_terrain_painter(terrain_painter): + _brush_editor.set_terrain_painter(terrain_painter) func _on_TextureEditor_texture_selected(index): diff --git a/addons/zylann.hterrain/tools/plugin.gd b/addons/zylann.hterrain/tools/plugin.gd index 4a2bde4..ba09880 100644 --- a/addons/zylann.hterrain/tools/plugin.gd +++ b/addons/zylann.hterrain/tools/plugin.gd @@ -10,7 +10,7 @@ const HTerrainTextureSet = preload("../hterrain_texture_set.gd") const PackedTextureImporter = preload("./packed_textures/packed_texture_importer.gd") const PackedTextureArrayImporter = preload("./packed_textures/packed_texture_array_importer.gd") const PreviewGenerator = preload("./preview_generator.gd") -const Brush = preload("./brush/terrain_painter.gd") +const TerrainPainter = preload("./brush/terrain_painter.gd") const BrushDecal = preload("./brush/decal.gd") const Util = preload("../util/util.gd") const EditorUtil = preload("./util/editor_util.gd") @@ -74,7 +74,7 @@ var _image_cache : ImageFileCache var _packed_texture_importer := PackedTextureImporter.new() var _packed_texture_array_importer := PackedTextureArrayImporter.new() -var _brush : Brush = null +var _terrain_painter : TerrainPainter = null var _brush_decal : BrushDecal = null var _mouse_pressed := false #var _pending_paint_action = null @@ -106,13 +106,13 @@ func _enter_tree(): _preview_generator = PreviewGenerator.new() get_editor_interface().get_resource_previewer().add_preview_generator(_preview_generator) - _brush = Brush.new() - _brush.set_brush_size(5) - _brush.connect("changed", self, "_on_brush_changed") - add_child(_brush) + _terrain_painter = TerrainPainter.new() + _terrain_painter.set_brush_size(5) + _terrain_painter.get_brush().connect("size_changed", self, "_on_brush_size_changed") + add_child(_terrain_painter) _brush_decal = BrushDecal.new() - _brush_decal.set_size(_brush.get_brush_size()) + _brush_decal.set_size(_terrain_painter.get_brush_size()) _image_cache = ImageFileCache.new("user://temp_hterrain_image_cache") @@ -124,7 +124,7 @@ func _enter_tree(): _panel.hide() add_control_to_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_BOTTOM, _panel) # Apparently _ready() still isn't called at this point... - _panel.call_deferred("set_brush", _brush) + _panel.call_deferred("set_terrain_painter", _terrain_painter) _panel.call_deferred("setup_dialogs", base_control) _panel.set_undo_redo(get_undo_redo()) _panel.set_image_cache(_image_cache) @@ -165,44 +165,43 @@ func _enter_tree(): _menu_button = menu var mode_icons := {} - mode_icons[Brush.MODE_RAISE] = get_icon("heightmap_raise") - mode_icons[Brush.MODE_LOWER] = get_icon("heightmap_lower") - mode_icons[Brush.MODE_SMOOTH] = get_icon("heightmap_smooth") - mode_icons[Brush.MODE_FLATTEN] = get_icon("heightmap_flatten") - # TODO Have different icons - mode_icons[Brush.MODE_SPLAT] = get_icon("heightmap_paint") - mode_icons[Brush.MODE_COLOR] = get_icon("heightmap_color") - mode_icons[Brush.MODE_DETAIL] = get_icon("grass") - mode_icons[Brush.MODE_MASK] = get_icon("heightmap_mask") - mode_icons[Brush.MODE_LEVEL] = get_icon("heightmap_level") - mode_icons[Brush.MODE_ERODE] = get_icon("heightmap_erode") + mode_icons[TerrainPainter.MODE_RAISE] = get_icon("heightmap_raise") + mode_icons[TerrainPainter.MODE_LOWER] = get_icon("heightmap_lower") + mode_icons[TerrainPainter.MODE_SMOOTH] = get_icon("heightmap_smooth") + mode_icons[TerrainPainter.MODE_FLATTEN] = get_icon("heightmap_flatten") + mode_icons[TerrainPainter.MODE_SPLAT] = get_icon("heightmap_paint") + mode_icons[TerrainPainter.MODE_COLOR] = get_icon("heightmap_color") + mode_icons[TerrainPainter.MODE_DETAIL] = get_icon("grass") + mode_icons[TerrainPainter.MODE_MASK] = get_icon("heightmap_mask") + mode_icons[TerrainPainter.MODE_LEVEL] = get_icon("heightmap_level") + mode_icons[TerrainPainter.MODE_ERODE] = get_icon("heightmap_erode") var mode_tooltips := {} - mode_tooltips[Brush.MODE_RAISE] = "Raise height" - mode_tooltips[Brush.MODE_LOWER] = "Lower height" - mode_tooltips[Brush.MODE_SMOOTH] = "Smooth height" - mode_tooltips[Brush.MODE_FLATTEN] = "Flatten (flatten to a specific height)" - mode_tooltips[Brush.MODE_SPLAT] = "Texture paint" - mode_tooltips[Brush.MODE_COLOR] = "Color paint" - mode_tooltips[Brush.MODE_DETAIL] = "Grass paint" - mode_tooltips[Brush.MODE_MASK] = "Cut holes" - mode_tooltips[Brush.MODE_LEVEL] = "Level (smoothly flattens to average)" - mode_tooltips[Brush.MODE_ERODE] = "Erode" + mode_tooltips[TerrainPainter.MODE_RAISE] = "Raise height" + mode_tooltips[TerrainPainter.MODE_LOWER] = "Lower height" + mode_tooltips[TerrainPainter.MODE_SMOOTH] = "Smooth height" + mode_tooltips[TerrainPainter.MODE_FLATTEN] = "Flatten (flatten to a specific height)" + mode_tooltips[TerrainPainter.MODE_SPLAT] = "Texture paint" + mode_tooltips[TerrainPainter.MODE_COLOR] = "Color paint" + mode_tooltips[TerrainPainter.MODE_DETAIL] = "Grass paint" + mode_tooltips[TerrainPainter.MODE_MASK] = "Cut holes" + mode_tooltips[TerrainPainter.MODE_LEVEL] = "Level (smoothly flattens to average)" + mode_tooltips[TerrainPainter.MODE_ERODE] = "Erode" _toolbar.add_child(VSeparator.new()) # I want modes to be in that order in the GUI var ordered_brush_modes := [ - Brush.MODE_RAISE, - Brush.MODE_LOWER, - Brush.MODE_SMOOTH, - Brush.MODE_LEVEL, - Brush.MODE_FLATTEN, - Brush.MODE_ERODE, - Brush.MODE_SPLAT, - Brush.MODE_COLOR, - Brush.MODE_DETAIL, - Brush.MODE_MASK + TerrainPainter.MODE_RAISE, + TerrainPainter.MODE_LOWER, + TerrainPainter.MODE_SMOOTH, + TerrainPainter.MODE_LEVEL, + TerrainPainter.MODE_FLATTEN, + TerrainPainter.MODE_ERODE, + TerrainPainter.MODE_SPLAT, + TerrainPainter.MODE_COLOR, + TerrainPainter.MODE_DETAIL, + TerrainPainter.MODE_MASK ] var mode_group := ButtonGroup.new() @@ -214,7 +213,7 @@ func _enter_tree(): button.set_toggle_mode(true) button.set_button_group(mode_group) - if mode == _brush.get_mode(): + if mode == _terrain_painter.get_mode(): button.set_pressed(true) button.connect("pressed", self, "_on_mode_selected", [mode]) @@ -362,7 +361,7 @@ func edit(object): _panel.set_terrain(_node) _generator_dialog.set_terrain(_node) _import_dialog.set_terrain(_node) - _brush.set_terrain(_node) + _terrain_painter.set_terrain(_node) _brush_decal.set_terrain(_node) _generate_mesh_dialog.set_terrain(_node) _resize_dialog.set_terrain(_node) @@ -396,12 +395,12 @@ func _update_brush_buttons_availability(): var has_details = (data.get_map_count(HTerrainData.CHANNEL_DETAIL) > 0) if has_details: - var button = _toolbar_brush_buttons[Brush.MODE_DETAIL] + var button = _toolbar_brush_buttons[TerrainPainter.MODE_DETAIL] button.disabled = false else: - var button = _toolbar_brush_buttons[Brush.MODE_DETAIL] + var button = _toolbar_brush_buttons[TerrainPainter.MODE_DETAIL] if button.pressed: - _select_brush_mode(Brush.MODE_RAISE) + _select_brush_mode(TerrainPainter.MODE_RAISE) button.disabled = true @@ -470,6 +469,10 @@ func forward_spatial_gui_input(p_camera: Camera, p_event: InputEvent) -> bool: # because they are used in navigation schemes if (not mb.control) and (not mb.alt) and mb.button_index == BUTTON_LEFT: if mb.pressed: + # TODO Allow to paint on click + # TODO `pressure` is not available in button press events + # So I have to assume zero to avoid discontinuities with move events + #_terrain_painter.paint_input(hit_pos_in_cells, 0.0) _mouse_pressed = true captured_event = true @@ -477,17 +480,19 @@ func forward_spatial_gui_input(p_camera: Camera, p_event: InputEvent) -> bool: if not _mouse_pressed: # Just finished painting _pending_paint_commit = true + _terrain_painter.get_brush().on_paint_end() - if _brush.get_mode() == Brush.MODE_FLATTEN and _brush.has_meta("pick_height") \ - and _brush.get_meta("pick_height"): - _brush.set_meta("pick_height", false) + if _terrain_painter.get_mode() == TerrainPainter.MODE_FLATTEN \ + and _terrain_painter.has_meta("pick_height") \ + and _terrain_painter.get_meta("pick_height"): + _terrain_painter.set_meta("pick_height", false) # Pick height var hit_pos_in_cells = _get_pointed_cell_position(mb.position, p_camera) if hit_pos_in_cells != null: var h = _node.get_data().get_height_at( int(hit_pos_in_cells.x), int(hit_pos_in_cells.y)) _logger.debug("Picking height {0}".format([h])) - _brush.set_flatten_height(h) + _terrain_painter.set_flatten_height(h) elif p_event is InputEventMouseMotion: var mm = p_event @@ -497,7 +502,7 @@ func forward_spatial_gui_input(p_camera: Camera, p_event: InputEvent) -> bool: if _mouse_pressed: if Input.is_mouse_button_pressed(BUTTON_LEFT): - _brush.paint_input(hit_pos_in_cells) + _terrain_painter.paint_input(hit_pos_in_cells, mm.pressure) captured_event = true # This is in case the data or textures change as the user edits the terrain, @@ -515,11 +520,12 @@ func _process(delta: float): if _pending_paint_commit: if has_data: - if _brush.has_modified_chunks() and not _brush.is_operation_pending(): + if not _terrain_painter.is_operation_pending(): _pending_paint_commit = false - _logger.debug("Paint completed") - var changes : Dictionary = _brush.commit() - _paint_completed(changes) + if _terrain_painter.has_modified_chunks(): + _logger.debug("Paint completed") + var changes : Dictionary = _terrain_painter.commit() + _paint_completed(changes) else: _pending_paint_commit = false @@ -536,6 +542,8 @@ func _paint_completed(changes: Dictionary): assert(heightmap_data != null) var chunk_positions : Array = changes.chunk_positions + # Should not create an UndoRedo action if nothing changed + assert(len(chunk_positions) > 0) var changed_maps : Array = changes.maps var action_name := "Modify HTerrainData " @@ -548,7 +556,7 @@ func _paint_completed(changes: Dictionary): var redo_maps := [] var undo_maps := [] - var chunk_size := _brush.get_undo_chunk_size() + var chunk_size := _terrain_painter.get_undo_chunk_size() for map in changed_maps: # Cache images to disk so RAM does not continuously go up (or at least much slower) @@ -710,20 +718,20 @@ func _on_lookdev_menu_id_pressed(id: int): func _on_mode_selected(mode: int): _logger.debug(str("On mode selected ", mode)) - _brush.set_mode(mode) + _terrain_painter.set_mode(mode) _panel.set_brush_editor_display_mode(mode) func _on_texture_selected(index: int): # Switch to texture paint mode when a texture is selected - _select_brush_mode(Brush.MODE_SPLAT) - _brush.set_texture_index(index) + _select_brush_mode(TerrainPainter.MODE_SPLAT) + _terrain_painter.set_texture_index(index) func _on_detail_selected(index: int): # Switch to detail paint mode when a detail item is selected - _select_brush_mode(Brush.MODE_DETAIL) - _brush.set_detail_index(index) + _select_brush_mode(TerrainPainter.MODE_DETAIL) + _terrain_painter.set_detail_index(index) func _select_brush_mode(mode: int): @@ -782,8 +790,8 @@ func _on_permanent_change_performed(message: String): ur.commit_action() -func _on_brush_changed(): - _brush_decal.set_size(_brush.get_brush_size()) +func _on_brush_size_changed(size): + _brush_decal.set_size(size) func _on_Panel_edit_texture_pressed(index: int): diff --git a/addons/zylann.hterrain/tools/resize_dialog/resize_dialog.tscn b/addons/zylann.hterrain/tools/resize_dialog/resize_dialog.tscn index 9419792..fe1c45c 100644 --- a/addons/zylann.hterrain/tools/resize_dialog/resize_dialog.tscn +++ b/addons/zylann.hterrain/tools/resize_dialog/resize_dialog.tscn @@ -1,624 +1,197 @@ -[gd_scene load_steps=6 format=2] +[gd_scene load_steps=7 format=2] [ext_resource path="res://addons/zylann.hterrain/tools/resize_dialog/resize_dialog.gd" type="Script" id=1] [ext_resource path="res://addons/zylann.hterrain/tools/icons/icon_heightmap_unmask.svg" type="Texture" id=2] [ext_resource path="res://addons/zylann.hterrain/tools/icons/icon_long_arrow_right.svg" type="Texture" id=3] [ext_resource path="res://addons/zylann.hterrain/tools/icons/icon_long_arrow_down.svg" type="Texture" id=4] [ext_resource path="res://addons/zylann.hterrain/tools/icons/icon_small_circle.svg" type="Texture" id=5] +[ext_resource path="res://addons/zylann.hterrain/tools/util/dialog_fitter.tscn" type="PackedScene" id=6] -[node name="ResizeDialog" type="WindowDialog" index="0"] - -anchor_left = 0.0 -anchor_top = 0.0 -anchor_right = 0.0 -anchor_bottom = 0.0 +[node name="ResizeDialog" type="WindowDialog"] margin_left = 130.0 margin_top = 126.0 margin_right = 430.0 margin_bottom = 326.0 rect_min_size = Vector2( 300, 200 ) -rect_pivot_offset = Vector2( 0, 0 ) -rect_clip_content = false -mouse_filter = 0 -mouse_default_cursor_shape = 0 -size_flags_horizontal = 1 -size_flags_vertical = 1 -popup_exclusive = false window_title = "Resize terrain" -resizable = false script = ExtResource( 1 ) -[node name="VBoxContainer" type="VBoxContainer" parent="." index="1"] - -anchor_left = 0.0 -anchor_top = 0.0 +[node name="VBoxContainer" type="VBoxContainer" parent="."] anchor_right = 1.0 anchor_bottom = 1.0 margin_left = 8.0 margin_top = 8.0 margin_right = -8.0 margin_bottom = -8.0 -rect_pivot_offset = Vector2( 0, 0 ) -rect_clip_content = false -mouse_filter = 1 -mouse_default_cursor_shape = 0 -size_flags_horizontal = 1 -size_flags_vertical = 1 custom_constants/separation = 24 -alignment = 0 -_sections_unfolded = [ "Margin", "custom_constants" ] -[node name="GridContainer" type="GridContainer" parent="VBoxContainer" index="0"] - -anchor_left = 0.0 -anchor_top = 0.0 -anchor_right = 0.0 -anchor_bottom = 0.0 +[node name="GridContainer" type="GridContainer" parent="VBoxContainer"] margin_right = 284.0 margin_bottom = 126.0 -rect_pivot_offset = Vector2( 0, 0 ) -rect_clip_content = false -mouse_filter = 1 -mouse_default_cursor_shape = 0 -size_flags_horizontal = 1 -size_flags_vertical = 1 columns = 2 -[node name="Label" type="Label" parent="VBoxContainer/GridContainer" index="0"] - -anchor_left = 0.0 -anchor_top = 0.0 -anchor_right = 0.0 -anchor_bottom = 0.0 +[node name="Label" type="Label" parent="VBoxContainer/GridContainer"] margin_top = 3.0 margin_right = 68.0 margin_bottom = 17.0 -rect_pivot_offset = Vector2( 0, 0 ) -rect_clip_content = false -mouse_filter = 2 -mouse_default_cursor_shape = 0 -size_flags_horizontal = 1 -size_flags_vertical = 4 text = "Resolution" -percent_visible = 1.0 -lines_skipped = 0 -max_lines_visible = -1 -[node name="ResolutionDropdown" type="OptionButton" parent="VBoxContainer/GridContainer" index="1"] - -anchor_left = 0.0 -anchor_top = 0.0 -anchor_right = 0.0 -anchor_bottom = 0.0 +[node name="ResolutionDropdown" type="OptionButton" parent="VBoxContainer/GridContainer"] margin_left = 72.0 margin_right = 284.0 margin_bottom = 20.0 -rect_pivot_offset = Vector2( 0, 0 ) -rect_clip_content = false -focus_mode = 2 -mouse_filter = 0 -mouse_default_cursor_shape = 0 size_flags_horizontal = 3 -size_flags_vertical = 1 toggle_mode = false -action_mode = 0 -enabled_focus_mode = 2 -shortcut = null -group = null -flat = false -align = 0 -items = [ ] -selected = -1 -_sections_unfolded = [ "Size Flags" ] -[node name="Label3" type="Label" parent="VBoxContainer/GridContainer" index="2"] - -anchor_left = 0.0 -anchor_top = 0.0 -anchor_right = 0.0 -anchor_bottom = 0.0 +[node name="Label3" type="Label" parent="VBoxContainer/GridContainer"] margin_top = 29.0 margin_right = 68.0 margin_bottom = 43.0 -rect_pivot_offset = Vector2( 0, 0 ) -rect_clip_content = false -mouse_filter = 2 -mouse_default_cursor_shape = 0 -size_flags_horizontal = 1 -size_flags_vertical = 4 text = "Stretch" -percent_visible = 1.0 -lines_skipped = 0 -max_lines_visible = -1 -[node name="StretchCheckBox" type="CheckBox" parent="VBoxContainer/GridContainer" index="3"] - -anchor_left = 0.0 -anchor_top = 0.0 -anchor_right = 0.0 -anchor_bottom = 0.0 +[node name="StretchCheckBox" type="CheckBox" parent="VBoxContainer/GridContainer"] margin_left = 72.0 margin_top = 24.0 margin_right = 284.0 margin_bottom = 48.0 -rect_pivot_offset = Vector2( 0, 0 ) -rect_clip_content = false -focus_mode = 2 -mouse_filter = 0 -mouse_default_cursor_shape = 0 -size_flags_horizontal = 1 -size_flags_vertical = 1 -toggle_mode = true -enabled_focus_mode = 2 -shortcut = null -group = null -flat = false -align = 0 -[node name="Label2" type="Label" parent="VBoxContainer/GridContainer" index="4"] - -anchor_left = 0.0 -anchor_top = 0.0 -anchor_right = 0.0 -anchor_bottom = 0.0 +[node name="Label2" type="Label" parent="VBoxContainer/GridContainer"] margin_top = 82.0 margin_right = 68.0 margin_bottom = 96.0 -rect_pivot_offset = Vector2( 0, 0 ) -rect_clip_content = false -mouse_filter = 2 -mouse_default_cursor_shape = 0 -size_flags_horizontal = 1 -size_flags_vertical = 4 text = "Direction" -percent_visible = 1.0 -lines_skipped = 0 -max_lines_visible = -1 -[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/GridContainer" index="5"] - -anchor_left = 0.0 -anchor_top = 0.0 -anchor_right = 0.0 -anchor_bottom = 0.0 +[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/GridContainer"] margin_left = 72.0 margin_top = 52.0 margin_right = 284.0 margin_bottom = 126.0 -rect_pivot_offset = Vector2( 0, 0 ) -rect_clip_content = false -mouse_filter = 1 -mouse_default_cursor_shape = 0 -size_flags_horizontal = 1 -size_flags_vertical = 1 -alignment = 0 -[node name="AnchorControl" type="GridContainer" parent="VBoxContainer/GridContainer/HBoxContainer" index="0"] - -anchor_left = 0.0 -anchor_top = 0.0 -anchor_right = 0.0 -anchor_bottom = 0.0 +[node name="AnchorControl" type="GridContainer" parent="VBoxContainer/GridContainer/HBoxContainer"] margin_right = 92.0 margin_bottom = 74.0 -rect_pivot_offset = Vector2( 0, 0 ) -rect_clip_content = false -mouse_filter = 1 -mouse_default_cursor_shape = 0 -size_flags_horizontal = 1 -size_flags_vertical = 1 columns = 3 -[node name="TopLeftButton" type="Button" parent="VBoxContainer/GridContainer/HBoxContainer/AnchorControl" index="0"] - -anchor_left = 0.0 -anchor_top = 0.0 -anchor_right = 0.0 -anchor_bottom = 0.0 +[node name="TopLeftButton" type="Button" parent="VBoxContainer/GridContainer/HBoxContainer/AnchorControl"] margin_right = 28.0 margin_bottom = 22.0 -rect_pivot_offset = Vector2( 0, 0 ) -rect_clip_content = false -focus_mode = 2 -mouse_filter = 0 -mouse_default_cursor_shape = 0 -size_flags_horizontal = 1 -size_flags_vertical = 1 -toggle_mode = false -enabled_focus_mode = 2 -shortcut = null -group = null icon = ExtResource( 2 ) -flat = false -align = 1 -[node name="TopButton" type="Button" parent="VBoxContainer/GridContainer/HBoxContainer/AnchorControl" index="1"] - -anchor_left = 0.0 -anchor_top = 0.0 -anchor_right = 0.0 -anchor_bottom = 0.0 +[node name="TopButton" type="Button" parent="VBoxContainer/GridContainer/HBoxContainer/AnchorControl"] margin_left = 32.0 margin_right = 60.0 margin_bottom = 22.0 -rect_pivot_offset = Vector2( 0, 0 ) -rect_clip_content = false -focus_mode = 2 -mouse_filter = 0 -mouse_default_cursor_shape = 0 -size_flags_horizontal = 1 -size_flags_vertical = 1 -toggle_mode = false -enabled_focus_mode = 2 -shortcut = null -group = null icon = ExtResource( 2 ) -flat = false -align = 1 -[node name="TopRightButton" type="Button" parent="VBoxContainer/GridContainer/HBoxContainer/AnchorControl" index="2"] - -anchor_left = 0.0 -anchor_top = 0.0 -anchor_right = 0.0 -anchor_bottom = 0.0 +[node name="TopRightButton" type="Button" parent="VBoxContainer/GridContainer/HBoxContainer/AnchorControl"] margin_left = 64.0 margin_right = 92.0 margin_bottom = 22.0 -rect_pivot_offset = Vector2( 0, 0 ) -rect_clip_content = false -focus_mode = 2 -mouse_filter = 0 -mouse_default_cursor_shape = 0 -size_flags_horizontal = 1 -size_flags_vertical = 1 -toggle_mode = false -enabled_focus_mode = 2 -shortcut = null -group = null icon = ExtResource( 2 ) -flat = false -align = 1 -[node name="LeftButton" type="Button" parent="VBoxContainer/GridContainer/HBoxContainer/AnchorControl" index="3"] - -anchor_left = 0.0 -anchor_top = 0.0 -anchor_right = 0.0 -anchor_bottom = 0.0 +[node name="LeftButton" type="Button" parent="VBoxContainer/GridContainer/HBoxContainer/AnchorControl"] margin_top = 26.0 margin_right = 28.0 margin_bottom = 48.0 -rect_pivot_offset = Vector2( 0, 0 ) -rect_clip_content = false -focus_mode = 2 -mouse_filter = 0 -mouse_default_cursor_shape = 0 -size_flags_horizontal = 1 -size_flags_vertical = 1 -toggle_mode = false -enabled_focus_mode = 2 -shortcut = null -group = null icon = ExtResource( 2 ) -flat = false -align = 1 -[node name="CenterButton" type="Button" parent="VBoxContainer/GridContainer/HBoxContainer/AnchorControl" index="4"] - -anchor_left = 0.0 -anchor_top = 0.0 -anchor_right = 0.0 -anchor_bottom = 0.0 +[node name="CenterButton" type="Button" parent="VBoxContainer/GridContainer/HBoxContainer/AnchorControl"] margin_left = 32.0 margin_top = 26.0 margin_right = 60.0 margin_bottom = 48.0 -rect_pivot_offset = Vector2( 0, 0 ) -rect_clip_content = false -focus_mode = 2 -mouse_filter = 0 -mouse_default_cursor_shape = 0 -size_flags_horizontal = 1 -size_flags_vertical = 1 -toggle_mode = false -enabled_focus_mode = 2 -shortcut = null -group = null icon = ExtResource( 2 ) -flat = false -align = 1 -[node name="RightButton" type="Button" parent="VBoxContainer/GridContainer/HBoxContainer/AnchorControl" index="5"] - -anchor_left = 0.0 -anchor_top = 0.0 -anchor_right = 0.0 -anchor_bottom = 0.0 +[node name="RightButton" type="Button" parent="VBoxContainer/GridContainer/HBoxContainer/AnchorControl"] margin_left = 64.0 margin_top = 26.0 margin_right = 92.0 margin_bottom = 48.0 -rect_pivot_offset = Vector2( 0, 0 ) -rect_clip_content = false -focus_mode = 2 -mouse_filter = 0 -mouse_default_cursor_shape = 0 -size_flags_horizontal = 1 -size_flags_vertical = 1 -toggle_mode = false -enabled_focus_mode = 2 -shortcut = null -group = null icon = ExtResource( 2 ) -flat = false -align = 1 -[node name="ButtomLeftButton" type="Button" parent="VBoxContainer/GridContainer/HBoxContainer/AnchorControl" index="6"] - -anchor_left = 0.0 -anchor_top = 0.0 -anchor_right = 0.0 -anchor_bottom = 0.0 +[node name="ButtomLeftButton" type="Button" parent="VBoxContainer/GridContainer/HBoxContainer/AnchorControl"] margin_top = 52.0 margin_right = 28.0 margin_bottom = 74.0 -rect_pivot_offset = Vector2( 0, 0 ) -rect_clip_content = false -focus_mode = 2 -mouse_filter = 0 -mouse_default_cursor_shape = 0 -size_flags_horizontal = 1 -size_flags_vertical = 1 -toggle_mode = false -enabled_focus_mode = 2 -shortcut = null -group = null icon = ExtResource( 2 ) -flat = false -align = 1 -[node name="ButtomButton" type="Button" parent="VBoxContainer/GridContainer/HBoxContainer/AnchorControl" index="7"] - -anchor_left = 0.0 -anchor_top = 0.0 -anchor_right = 0.0 -anchor_bottom = 0.0 +[node name="ButtomButton" type="Button" parent="VBoxContainer/GridContainer/HBoxContainer/AnchorControl"] margin_left = 32.0 margin_top = 52.0 margin_right = 60.0 margin_bottom = 74.0 -rect_pivot_offset = Vector2( 0, 0 ) -rect_clip_content = false -focus_mode = 2 -mouse_filter = 0 -mouse_default_cursor_shape = 0 -size_flags_horizontal = 1 -size_flags_vertical = 1 -toggle_mode = false -enabled_focus_mode = 2 -shortcut = null -group = null icon = ExtResource( 2 ) -flat = false -align = 1 -[node name="BottomRightButton" type="Button" parent="VBoxContainer/GridContainer/HBoxContainer/AnchorControl" index="8"] - -anchor_left = 0.0 -anchor_top = 0.0 -anchor_right = 0.0 -anchor_bottom = 0.0 +[node name="BottomRightButton" type="Button" parent="VBoxContainer/GridContainer/HBoxContainer/AnchorControl"] margin_left = 64.0 margin_top = 52.0 margin_right = 92.0 margin_bottom = 74.0 -rect_pivot_offset = Vector2( 0, 0 ) -rect_clip_content = false -focus_mode = 2 -mouse_filter = 0 -mouse_default_cursor_shape = 0 -size_flags_horizontal = 1 -size_flags_vertical = 1 -toggle_mode = false -enabled_focus_mode = 2 -shortcut = null -group = null icon = ExtResource( 2 ) -flat = false -align = 1 -[node name="Reference" type="Control" parent="VBoxContainer/GridContainer/HBoxContainer" index="1"] - -anchor_left = 0.0 -anchor_top = 0.0 -anchor_right = 0.0 -anchor_bottom = 0.0 +[node name="Reference" type="Control" parent="VBoxContainer/GridContainer/HBoxContainer"] margin_left = 96.0 margin_right = 196.0 margin_bottom = 74.0 rect_min_size = Vector2( 100, 0 ) -rect_pivot_offset = Vector2( 0, 0 ) -rect_clip_content = false -mouse_filter = 0 -mouse_default_cursor_shape = 0 -size_flags_horizontal = 1 -size_flags_vertical = 1 -_sections_unfolded = [ "Rect" ] - -[node name="XArrow" type="TextureRect" parent="VBoxContainer/GridContainer/HBoxContainer/Reference" index="0"] +[node name="XArrow" type="TextureRect" parent="VBoxContainer/GridContainer/HBoxContainer/Reference"] modulate = Color( 1, 0.292969, 0.292969, 1 ) -anchor_left = 0.0 -anchor_top = 0.0 anchor_right = 1.0 -anchor_bottom = 0.0 margin_left = 9.0 margin_bottom = 16.0 -rect_pivot_offset = Vector2( 0, 0 ) -rect_clip_content = false -mouse_filter = 1 -mouse_default_cursor_shape = 0 -size_flags_horizontal = 1 -size_flags_vertical = 1 texture = ExtResource( 3 ) -stretch_mode = 0 -_sections_unfolded = [ "Visibility" ] - -[node name="ZArrow" type="TextureRect" parent="VBoxContainer/GridContainer/HBoxContainer/Reference" index="1"] +[node name="ZArrow" type="TextureRect" parent="VBoxContainer/GridContainer/HBoxContainer/Reference"] modulate = Color( 0.292969, 0.602295, 1, 1 ) -anchor_left = 0.0 -anchor_top = 0.0 -anchor_right = 0.0 anchor_bottom = 1.0 margin_top = 10.0 margin_right = 16.0 -rect_pivot_offset = Vector2( 0, 0 ) -rect_clip_content = false -mouse_filter = 1 -mouse_default_cursor_shape = 0 -size_flags_horizontal = 1 -size_flags_vertical = 1 texture = ExtResource( 4 ) -stretch_mode = 0 -_sections_unfolded = [ "Visibility" ] -[node name="ZLabel" type="Label" parent="VBoxContainer/GridContainer/HBoxContainer/Reference" index="2"] - -anchor_left = 0.0 -anchor_top = 0.0 -anchor_right = 0.0 -anchor_bottom = 0.0 +[node name="ZLabel" type="Label" parent="VBoxContainer/GridContainer/HBoxContainer/Reference"] margin_left = 14.0 margin_top = 54.0 margin_right = 22.0 margin_bottom = 68.0 -rect_pivot_offset = Vector2( 0, 0 ) -rect_clip_content = false -mouse_filter = 2 -mouse_default_cursor_shape = 0 -size_flags_horizontal = 1 -size_flags_vertical = 4 text = "Z" -percent_visible = 1.0 -lines_skipped = 0 -max_lines_visible = -1 -[node name="XLabel" type="Label" parent="VBoxContainer/GridContainer/HBoxContainer/Reference" index="3"] - -anchor_left = 0.0 -anchor_top = 0.0 -anchor_right = 0.0 -anchor_bottom = 0.0 +[node name="XLabel" type="Label" parent="VBoxContainer/GridContainer/HBoxContainer/Reference"] margin_left = 52.0 margin_top = 14.0 margin_right = 60.0 margin_bottom = 28.0 -rect_pivot_offset = Vector2( 0, 0 ) -rect_clip_content = false -mouse_filter = 2 -mouse_default_cursor_shape = 0 -size_flags_horizontal = 1 -size_flags_vertical = 4 text = "X" -percent_visible = 1.0 -lines_skipped = 0 -max_lines_visible = -1 -[node name="Origin" type="TextureRect" parent="VBoxContainer/GridContainer/HBoxContainer/Reference" index="4"] - -anchor_left = 0.0 -anchor_top = 0.0 -anchor_right = 0.0 -anchor_bottom = 0.0 +[node name="Origin" type="TextureRect" parent="VBoxContainer/GridContainer/HBoxContainer/Reference"] margin_left = 3.0 margin_top = 4.0 margin_right = 11.0 margin_bottom = 12.0 -rect_pivot_offset = Vector2( 0, 0 ) -rect_clip_content = false -mouse_filter = 1 -mouse_default_cursor_shape = 0 -size_flags_horizontal = 1 -size_flags_vertical = 1 texture = ExtResource( 5 ) -stretch_mode = 0 -_sections_unfolded = [ "Anchor", "Margin" ] -[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer" index="1"] - -anchor_left = 0.0 -anchor_top = 0.0 -anchor_right = 0.0 -anchor_bottom = 0.0 +[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"] margin_top = 150.0 margin_right = 284.0 margin_bottom = 170.0 -rect_pivot_offset = Vector2( 0, 0 ) -rect_clip_content = false -mouse_filter = 1 -mouse_default_cursor_shape = 0 -size_flags_horizontal = 1 -size_flags_vertical = 1 custom_constants/separation = 16 alignment = 1 -_sections_unfolded = [ "Rect", "custom_constants" ] -[node name="ApplyButton" type="Button" parent="VBoxContainer/HBoxContainer" index="0"] - -anchor_left = 0.0 -anchor_top = 0.0 -anchor_right = 0.0 -anchor_bottom = 0.0 +[node name="ApplyButton" type="Button" parent="VBoxContainer/HBoxContainer"] margin_left = 51.0 margin_right = 163.0 margin_bottom = 20.0 -rect_pivot_offset = Vector2( 0, 0 ) -rect_clip_content = false -focus_mode = 2 -mouse_filter = 0 -mouse_default_cursor_shape = 0 -size_flags_horizontal = 1 -size_flags_vertical = 1 -toggle_mode = false -enabled_focus_mode = 2 -shortcut = null -group = null text = "Apply (no undo)" -flat = false -align = 1 -[node name="CancelButton" type="Button" parent="VBoxContainer/HBoxContainer" index="1"] - -anchor_left = 0.0 -anchor_top = 0.0 -anchor_right = 0.0 -anchor_bottom = 0.0 +[node name="CancelButton" type="Button" parent="VBoxContainer/HBoxContainer"] margin_left = 179.0 margin_right = 233.0 margin_bottom = 20.0 -rect_pivot_offset = Vector2( 0, 0 ) -rect_clip_content = false -focus_mode = 2 -mouse_filter = 0 -mouse_default_cursor_shape = 0 -size_flags_horizontal = 1 -size_flags_vertical = 1 -toggle_mode = false -enabled_focus_mode = 2 -shortcut = null -group = null text = "Cancel" -flat = false -align = 1 + +[node name="DialogFitter" parent="." instance=ExtResource( 6 )] [connection signal="item_selected" from="VBoxContainer/GridContainer/ResolutionDropdown" to="." method="_on_ResolutionDropdown_item_selected"] - [connection signal="toggled" from="VBoxContainer/GridContainer/StretchCheckBox" to="." method="_on_StretchCheckBox_toggled"] - [connection signal="pressed" from="VBoxContainer/HBoxContainer/ApplyButton" to="." method="_on_ApplyButton_pressed"] - [connection signal="pressed" from="VBoxContainer/HBoxContainer/CancelButton" to="." method="_on_CancelButton_pressed"] - - diff --git a/addons/zylann.hterrain/tools/texture_editor/set_editor/texture_set_editor.gd b/addons/zylann.hterrain/tools/texture_editor/set_editor/texture_set_editor.gd index 94e036f..2515a73 100644 --- a/addons/zylann.hterrain/tools/texture_editor/set_editor/texture_set_editor.gd +++ b/addons/zylann.hterrain/tools/texture_editor/set_editor/texture_set_editor.gd @@ -347,17 +347,19 @@ func _set_texture_array_action(slot_index: int, texture_array: TextureArray, typ # See https://github.com/godotengine/godot/issues/36895 if texture_array == null: _undo_redo.add_do_method(_texture_set, "set_texture_array_null", type) + # Can't select a slot after this because there won't be any after the array is removed else: _undo_redo.add_do_method(_texture_set, "set_texture_array", type, texture_array) - _undo_redo.add_do_method(self, "_select_slot", slot_index) + _undo_redo.add_do_method(self, "_select_slot", slot_index) # TODO This branch only exists because of a flaw in UndoRedo # See https://github.com/godotengine/godot/issues/36895 if prev_texture_array == null: _undo_redo.add_undo_method(_texture_set, "set_texture_array_null", type) + # Can't select a slot after this because there won't be any after the array is removed else: _undo_redo.add_undo_method(_texture_set, "set_texture_array", type, prev_texture_array) - _undo_redo.add_undo_method(self, "_select_slot", slot_index) + _undo_redo.add_undo_method(self, "_select_slot", slot_index) _undo_redo.commit_action() @@ -374,7 +376,11 @@ func _on_LoadTextureArrayDialog_file_selected(fpath: String): assert(_texture_set.get_mode() == HTerrainTextureSet.MODE_TEXTURE_ARRAYS) var texture_array = load(fpath) assert(texture_array != null) - var slot_index : int = _slots_list.get_selected_items()[0] + # It's possible no slot exists at the moment, + # because there could be no texture array already set. + # The number of slots in the new array might also be different. + # So in this case we'll default to selecting the first slot. + var slot_index := 0 _set_texture_array_action(slot_index, texture_array, _load_texture_type) diff --git a/addons/zylann.hterrain/tools/texture_editor/set_editor/texture_set_import_editor.gd b/addons/zylann.hterrain/tools/texture_editor/set_editor/texture_set_import_editor.gd index 9ed6704..b3cf277 100644 --- a/addons/zylann.hterrain/tools/texture_editor/set_editor/texture_set_import_editor.gd +++ b/addons/zylann.hterrain/tools/texture_editor/set_editor/texture_set_import_editor.gd @@ -250,7 +250,7 @@ func set_texture_set(texture_set: HTerrainTextureSet): while slot_index >= len(_slots_data): var slot = Slot.new() - _slots_data[slot_index] = slot + _slots_data.append(slot) var slot = _slots_data[slot_index] diff --git a/addons/zylann.hterrain/tools/util/dialog_fitter.gd b/addons/zylann.hterrain/tools/util/dialog_fitter.gd index 501848c..b08909e 100644 --- a/addons/zylann.hterrain/tools/util/dialog_fitter.gd +++ b/addons/zylann.hterrain/tools/util/dialog_fitter.gd @@ -31,4 +31,23 @@ func _fit_to_contents(): var margin : Vector2 = child.get_rect().position #print("Fitting ", dialog.get_path(), " from ", dialog.rect_size, # " to ", child_rect.size + margin * 2.0) - dialog.rect_size = child_rect.size + margin * 2.0 + dialog.rect_min_size = child_rect.size + margin * 2.0 + + +#func _process(delta): +# update() + +# DEBUG +#func _draw(): +# var self_global_pos = get_global_rect().position +# +# var dialog : Control = get_parent() +# var dialog_rect := dialog.get_global_rect() +# dialog_rect.position -= self_global_pos +# draw_rect(dialog_rect, Color(1,1,0), false) +# +# for child in dialog.get_children(): +# if child is Container: +# var child_rect : Rect2 = child.get_global_rect() +# child_rect.position -= self_global_pos +# draw_rect(child_rect, Color(1,1,0,0.1)) diff --git a/addons/zylann.hterrain/tools/util/spin_slider.gd b/addons/zylann.hterrain/tools/util/spin_slider.gd new file mode 100644 index 0000000..ea444f6 --- /dev/null +++ b/addons/zylann.hterrain/tools/util/spin_slider.gd @@ -0,0 +1,322 @@ +tool +extends Control + +const FG_MARGIN = 2 +const MAX_DECIMALS_VISUAL = 3 + +signal value_changed(value) + +export var _value := 0.0 setget set_value_no_notify +export var _min_value := 0.0 setget set_min_value +export var _max_value := 100.0 setget set_max_value +export var _prefix := "" setget set_prefix +export var _suffix := "" setget set_suffix +export var _rounded := false setget set_rounded +export var _centered := true setget set_centered +export var _allow_greater := false setget set_allow_greater +# There is still a limit when typing a larger value, but this one is to prevent software +# crashes or freezes. The regular min and max values are for slider UX. Exceeding it should be +# a corner case. +export var _greater_max_value := 10000.0 setget set_greater_max_value + +var _label : Label +var _label2 : Label +var _line_edit : LineEdit +var _ignore_line_edit := false +var _pressing := false +var _grabbing := false +var _press_pos := Vector2() + + +func _init(): + rect_min_size = Vector2(32, 28) + + _label = Label.new() + _label.align = Label.ALIGN_CENTER + _label.valign = Label.VALIGN_CENTER + _label.clip_text = true + #_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL + _label.anchor_top = 0 + _label.anchor_left = 0 + _label.anchor_right = 1 + _label.anchor_bottom = 1 + _label.mouse_filter = Control.MOUSE_FILTER_IGNORE + _label.add_color_override("font_color_shadow", Color(0,0,0,0.5)) + _label.add_constant_override("shadow_offset_x", 1) + _label.add_constant_override("shadow_offset_y", 1) + add_child(_label) + + _label2 = Label.new() + _label2.align = Label.ALIGN_LEFT + _label2.valign = Label.VALIGN_CENTER + _label2.clip_text = true + #_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL + _label2.anchor_top = 0 + _label2.anchor_left = 0 + _label2.anchor_right = 1 + _label2.anchor_bottom = 1 + _label2.margin_left = 8 + _label2.mouse_filter = Control.MOUSE_FILTER_IGNORE + _label2.add_color_override("font_color_shadow", Color(0,0,0,0.5)) + _label2.add_constant_override("shadow_offset_x", 1) + _label2.add_constant_override("shadow_offset_y", 1) + _label2.hide() + add_child(_label2) + + _line_edit = LineEdit.new() + _line_edit.align = LineEdit.ALIGN_CENTER + _line_edit.anchor_top = 0 + _line_edit.anchor_left = 0 + _line_edit.anchor_right = 1 + _line_edit.anchor_bottom = 1 + _line_edit.connect("gui_input", self, "_on_LineEdit_gui_input") + _line_edit.connect("focus_exited", self, "_on_LineEdit_focus_exited") + _line_edit.connect("text_entered", self, "_on_LineEdit_text_entered") + _line_edit.hide() + add_child(_line_edit) + + mouse_default_cursor_shape = Control.CURSOR_HSIZE + + +func _ready(): + pass # Replace with function body. + + +func set_centered(centered: bool): + _centered = centered + if _centered: + _label.align = Label.ALIGN_CENTER + _label.margin_right = 0 + _label2.hide() + else: + _label.align = Label.ALIGN_RIGHT + _label.margin_right = -8 + _label2.show() + update() + + +func is_centered() -> bool: + return _centered + + +func set_value_no_notify(v: float): + set_value(v, false, false) + + +func set_value(v: float, notify_change: bool, use_slider_maximum: bool = false): + if _allow_greater and not use_slider_maximum: + v = clamp(v, _min_value, _greater_max_value) + else: + v = clamp(v, _min_value, _max_value) + + if v != _value: + _value = v + + update() + + if notify_change: + emit_signal("value_changed", get_value()) + + +func get_value(): + if _rounded: + return int(round(_value)) + return _value + + +func set_min_value(minv: float): + _min_value = minv + #update() + + +func get_min_value() -> float: + return _min_value + + +func set_max_value(maxv: float): + _max_value = maxv + #update() + + +func get_max_value() -> float: + return _max_value + + +func set_greater_max_value(gmax: float): + _greater_max_value = gmax + + +func get_greater_max_value() -> float: + return _greater_max_value + + +func set_rounded(b: bool): + _rounded = b + update() + + +func is_rounded() -> bool: + return _rounded + + +func set_prefix(prefix: String): + _prefix = prefix + update() + + +func get_prefix() -> String: + return _prefix + + +func set_suffix(suffix: String): + _suffix = suffix + update() + + +func get_suffix() -> String: + return _suffix + + +func set_allow_greater(allow: bool): + _allow_greater = allow + + +func is_allowing_greater() -> bool: + return _allow_greater + + +func _set_from_pixel(px: float): + var r := (px - FG_MARGIN) / (rect_size.x - FG_MARGIN * 2.0) + var v := _ratio_to_value(r) + set_value(v, true, true) + + +func get_ratio() -> float: + return _value_to_ratio(get_value()) + + +func _ratio_to_value(r: float) -> float: + return r * (_max_value - _min_value) + _min_value + + +func _value_to_ratio(v: float) -> float: + if abs(_max_value - _min_value) < 0.001: + return 0.0 + return (v - _min_value) / (_max_value - _min_value) + + +func _on_LineEdit_gui_input(event): + if event is InputEventKey: + if event.pressed: + if event.scancode == KEY_ESCAPE: + _ignore_line_edit = true + _hide_line_edit() + grab_focus() + _ignore_line_edit = false + + +func _on_LineEdit_focus_exited(): + if _ignore_line_edit: + return + _enter_text() + + +func _on_LineEdit_text_entered(text: String): + _enter_text() + + +func _enter_text(): + var s = _line_edit.text.strip_edges() + if s.is_valid_float(): + var v = s.to_float() + if not _allow_greater: + v = min(v, _max_value) + set_value(v, true, false) + _hide_line_edit() + + +func _hide_line_edit(): + _line_edit.hide() + _label.show() + update() + + +func _show_line_edit(): + _line_edit.show() + _line_edit.text = str(get_value()) + _line_edit.select_all() + _line_edit.grab_focus() + _label.hide() + update() + + +func _gui_input(event): + if event is InputEventMouseButton: + if event.pressed: + if event.button_index == BUTTON_LEFT: + _press_pos = event.position + _pressing = true + else: + if event.button_index == BUTTON_LEFT: + _pressing = false + if _grabbing: + _grabbing = false + _set_from_pixel(event.position.x) + else: + _show_line_edit() + + elif event is InputEventMouseMotion: + if _pressing and _press_pos.distance_to(event.position) > 2.0: + _grabbing = true + if _grabbing: + _set_from_pixel(event.position.x) + + +func _draw(): + if _line_edit.visible: + return + + #var grabber_width := 3 + var background_v_margin := 0 + var foreground_margin := FG_MARGIN + #var grabber_color := Color(0.8, 0.8, 0.8) + var interval_color := Color(0.4,0.4,0.4) + var background_color := Color(0.1, 0.1, 0.1) + + var control_rect := Rect2(Vector2(), rect_size) + + var bg_rect := Rect2( + control_rect.position.x, + control_rect.position.y + background_v_margin, + control_rect.size.x, + control_rect.size.y - 2 * background_v_margin) + draw_rect(bg_rect, background_color) + + var fg_rect := control_rect.grow(-foreground_margin) + # Clamping the ratio because the value can be allowed to exceed the slider's boundaries + var ratio := clamp(get_ratio(), 0.0, 1.0) + fg_rect.size.x *= ratio + draw_rect(fg_rect, interval_color) + + var value_text := str(get_value()) + + var dot_pos := value_text.find(".") + if dot_pos != -1: + var decimal_count = len(value_text) - dot_pos + if decimal_count > MAX_DECIMALS_VISUAL: + value_text = value_text.substr(0, dot_pos + MAX_DECIMALS_VISUAL + 1) + + if _centered: + var text := value_text + if _prefix != "": + text = str(_prefix, " ", text) + if _suffix != "": + text = str(text, " ", _suffix) + _label.text = text + + else: + _label2.text = _prefix + var text = value_text + if _suffix != "": + text = str(text, " ", _suffix) + _label.text = text diff --git a/addons/zylann.hterrain/tools/util/spin_slider.tscn b/addons/zylann.hterrain/tools/util/spin_slider.tscn new file mode 100644 index 0000000..70a0da8 --- /dev/null +++ b/addons/zylann.hterrain/tools/util/spin_slider.tscn @@ -0,0 +1,13 @@ +[gd_scene load_steps=2 format=2] + +[ext_resource path="res://addons/zylann.hterrain/tools/util/spin_slider.gd" type="Script" id=1] + +[node name="SpinSlider" type="Control"] +anchor_right = 1.0 +anchor_bottom = 1.0 +rect_min_size = Vector2( 32, 28 ) +mouse_default_cursor_shape = 10 +script = ExtResource( 1 ) +__meta__ = { +"_edit_use_anchors_": false +} diff --git a/addons/zylann.hterrain/util/direct_multimesh_instance.gd b/addons/zylann.hterrain/util/direct_multimesh_instance.gd index ab3dd62..fe87a64 100644 --- a/addons/zylann.hterrain/util/direct_multimesh_instance.gd +++ b/addons/zylann.hterrain/util/direct_multimesh_instance.gd @@ -38,3 +38,6 @@ func set_material_override(material: Material): func set_aabb(aabb: AABB): VisualServer.instance_set_custom_aabb(_multimesh_instance, aabb) + +func set_layer_mask(mask: int): + VisualServer.instance_set_layer_mask(_multimesh_instance, mask)