Updating Zylann plugin with latest stable version

This commit is contained in:
yannk 2022-01-16 16:40:52 +01:00
parent 9d0cee5cb0
commit 0527866fc6
60 changed files with 2723 additions and 1333 deletions

View file

@ -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) ![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 ### 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). 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 ### 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) ![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. 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. 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) ![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: 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. - `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. - 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. 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). 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: 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 #### 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. 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: 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. - `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`: 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. - `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). 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. 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. - 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) - 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) - 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 - 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) - 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)

View file

@ -1,15 +1,11 @@
site_name: HTerrain plugin documentation site_name: HTerrain plugin documentation
theme: readthedocs 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: markdown_extensions:
# Makes permalinks appear on headings # Makes permalinks appear on headings
- toc: - toc:
permalink: True permalink: True
# Makes boxes for notes and warnings # Makes boxes for notes and warnings
- admonition - admonition
# Better highlighter which supports GDScript # Better highlighter which supports GDScript
- codehilite - codehilite

View file

@ -1,7 +1,7 @@
tool tool
extends Spatial extends Spatial
const QuadTreeLod = preload("./util/quad_tree_lod.gd") const NativeFactory = preload("./native/factory.gd")
const Mesher = preload("./hterrain_mesher.gd") const Mesher = preload("./hterrain_mesher.gd")
const Grid = preload("./util/grid.gd") const Grid = preload("./util/grid.gd")
const HTerrainData = preload("./hterrain_data.gd") const HTerrainData = preload("./hterrain_data.gd")
@ -156,6 +156,8 @@ var _shader_uses_texture_array := false
var _material := ShaderMaterial.new() var _material := ShaderMaterial.new()
var _material_params_need_update := false var _material_params_need_update := false
var _render_layer_mask := 1
# Actual number of textures supported by the shader currently selected # Actual number of textures supported by the shader currently selected
var _ground_texture_count_cache = 0 var _ground_texture_count_cache = 0
@ -168,7 +170,7 @@ var _texture_set_migration_textures = null
var _data: HTerrainData = null var _data: HTerrainData = null
var _mesher := Mesher.new() var _mesher := Mesher.new()
var _lodder := QuadTreeLod.new() var _lodder = NativeFactory.get_quad_tree_lod()
var _viewer_pos_world := Vector3() var _viewer_pos_world := Vector3()
# [lod][z][x] -> chunk # [lod][z][x] -> chunk
@ -277,7 +279,7 @@ func _get_property_list():
"hint": PROPERTY_HINT_LAYERS_3D_PHYSICS "hint": PROPERTY_HINT_LAYERS_3D_PHYSICS
}, },
{ {
"name": "Shader", "name": "Rendering",
"type": TYPE_NIL, "type": TYPE_NIL,
"usage": PROPERTY_USAGE_GROUP "usage": PROPERTY_USAGE_GROUP
}, },
@ -312,6 +314,12 @@ func _get_property_list():
# This triggers `ERROR: Cannot get class 'HTerrainTextureSet'` # This triggers `ERROR: Cannot get class 'HTerrainTextureSet'`
# See https://github.com/godotengine/godot/pull/41264 # See https://github.com/godotengine/godot/pull/41264
#"hint_string": "HTerrainTextureSet" #"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": elif key == "collision_mask":
return _collision_mask return _collision_mask
elif key == "render_layers":
return get_render_layer_mask()
func _set(key: String, value): func _set(key: String, value):
@ -424,6 +435,9 @@ func _set(key: String, value):
if _collider != null: if _collider != null:
_collider.set_collision_mask(value) _collider.set_collision_mask(value)
elif key == "render_layers":
return set_render_layer_mask(value)
func get_texture_set() -> HTerrainTextureSet: func get_texture_set() -> HTerrainTextureSet:
return _texture_set return _texture_set
@ -458,6 +472,15 @@ func set_shader_param(param_name: String, v):
_material.set_shader_param(param_name, 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): func _set_data_directory(dirpath: String):
if dirpath != _get_data_directory(): if dirpath != _get_data_directory():
if dirpath == "": if dirpath == "":
@ -473,7 +496,7 @@ func _set_data_directory(dirpath: String):
# Create new # Create new
var d := HTerrainData.new() var d := HTerrainData.new()
d.resource_path = fpath d.resource_path = fpath
set_data(d) set_data(d)
else: else:
_logger.warn("Setting twice the same terrain directory??") _logger.warn("Setting twice the same terrain directory??")
@ -1087,7 +1110,6 @@ func _process(delta: float):
if not Engine.is_editor_hint(): if not Engine.is_editor_hint():
# In editor, the camera is only accessible from an editor plugin # In editor, the camera is only accessible from an editor plugin
_update_viewer_position(null) _update_viewer_position(null)
var viewer_pos := _viewer_pos_world
if has_data(): if has_data():
if _data.is_locked(): if _data.is_locked():
@ -1096,9 +1118,10 @@ func _process(delta: float):
if _data.get_resolution() != 0: if _data.get_resolution() != 0:
var gt := get_internal_transform() 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() #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 #var time_elapsed = OS.get_ticks_msec() - time_before
#if Engine.get_frames_drawn() % 60 == 0: #if Engine.get_frames_drawn() % 60 == 0:
# _logger.debug(str("Lodder time: ", time_elapsed)) # _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, # Note: the detail system is not affected by map scale,
# so we have to send viewer position in world space # so we have to send viewer position in world space
for layer in _detail_layers: for layer in _detail_layers:
layer.process(delta, viewer_pos) layer.process(delta, _viewer_pos_world)
_updated_chunks = 0 _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()): for lod in range(_lodder.get_lod_count()):
# Get grid and chunk size # Get grid and chunk size
var grid = _chunks[lod] 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: # Convert rect into this lod's coordinates:
# Pick min and max (included), divide them, then add 1 to max so it's excluded again # 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: if chunk == null:
# This is the first time this chunk is required at this lod, generate it # 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_x := cpos_x * _chunk_size * lod_factor
var origin_in_cells_y := cpos_y * _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 = HTerrainChunk.new(self, origin_in_cells_x, origin_in_cells_y, material)
chunk.parent_transform_changed(get_internal_transform()) chunk.parent_transform_changed(get_internal_transform())
chunk.set_render_layer_mask(_render_layer_mask)
var grid = _chunks[lod] var grid = _chunks[lod]
var row = grid[cpos_y] var row = grid[cpos_y]
row[cpos_x] = chunk 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): 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_x := cpos_x * chunk_size
var origin_in_cells_y := cpos_y * chunk_size var origin_in_cells_y := cpos_y * chunk_size
# This is a hack for speed, # This is a hack for speed,
@ -1546,3 +1571,10 @@ class SetMaterialAction:
chunk.set_material(material) 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)

View file

@ -113,3 +113,8 @@ func set_aabb(aabb: AABB):
assert(_mesh_instance != RID()) assert(_mesh_instance != RID())
VisualServer.instance_set_custom_aabb(_mesh_instance, aabb) 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)

View file

@ -417,7 +417,7 @@ func get_all_heights() -> PoolRealArray:
# modified area. # modified area.
# #
# map_type: # map_type:
# which kind of map changed # which kind of map changed, see CHANNEL_* constants
# #
# index: # index:
# index of the map that changed # 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, # In this particular case, we can use Godot ResourceLoader directly,
# if the texture got imported. # 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 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 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: 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) var im = _try_load_0_8_0_heightmap(fpath, map_type, map.image, _logger)
if typeof(im) == TYPE_BOOL: if typeof(im) == TYPE_BOOL:
return false return false

View file

@ -47,10 +47,12 @@ export(Texture) var texture : Texture setget set_texture, get_texture
# How far detail meshes can be seen. # How far detail meshes can be seen.
# TODO Improve speed of _get_chunk_aabb() so we can increase the limit # TODO Improve speed of _get_chunk_aabb() so we can increase the limit
# See https://github.com/Zylann/godot_heightmap_plugin/issues/155 # 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. # 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. # Density modifier, to make more or less detail meshes appear overall.
export(float, 0, 10) var density := 4.0 setget set_density, get_density 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 ._. # 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 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 _material: ShaderMaterial = null
var _default_shader: Shader = null var _default_shader: Shader = null
@ -248,6 +254,17 @@ func get_instance_mesh() -> Mesh:
return instance_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: func _get_used_mesh() -> Mesh:
if instance_mesh == null: if instance_mesh == null:
return DefaultMesh return DefaultMesh
@ -305,8 +322,10 @@ func _on_terrain_transform_changed(gt: Transform):
if terrain == null: if terrain == null:
_logger.error("Detail layer is not child of a terrain!") _logger.error("Detail layer is not child of a terrain!")
return 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: for k in _chunks:
var mmi = _chunks[k] var mmi = _chunks[k]
var aabb = _get_chunk_aabb(terrain, Vector3(k.x * CHUNK_SIZE, 0, k.y * CHUNK_SIZE)) 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.x = 0
aabb.position.z = 0 aabb.position.z = 0
mmi.set_aabb(aabb) mmi.set_aabb(aabb)
mmi.set_transform(_get_chunk_transform(terrain_transform, k.x, k.y))
func process(delta: float, viewer_pos: Vector3): func process(delta: float, viewer_pos: Vector3):
@ -331,7 +351,7 @@ func process(delta: float, viewer_pos: Vector3):
var mmi = _chunks[k] var mmi = _chunks[k]
mmi.set_multimesh(_multimesh) 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_cx = local_viewer_pos.x / CHUNK_SIZE
var viewer_cz = local_viewer_pos.z / 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): for cx in range(cmin_x, cmax_x):
_add_debug_cube(terrain, _get_chunk_aabb(terrain, Vector3(cx, 0, cz) * CHUNK_SIZE)) _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 cz in range(cmin_z, cmax_z):
for cx in range(cmin_x, cmax_x): 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) var d = (aabb.position + 0.5 * aabb.size).distance_to(local_viewer_pos)
if d < view_distance: if d < view_distance:
_load_chunk(terrain, cx, cz, aabb) _load_chunk(terrain_transform, cx, cz, aabb)
var to_recycle = [] var to_recycle = []
@ -418,11 +440,14 @@ func _get_chunk_aabb(terrain, lpos: Vector3):
return aabb return aabb
func _load_chunk(terrain, cx: int, cz: int, aabb: AABB): func _get_chunk_transform(terrain_transform: Transform, cx: int, cz: int) -> Transform:
var lpos = Vector3(cx, 0, cz) * CHUNK_SIZE var lpos := Vector3(cx, 0, cz) * CHUNK_SIZE
# Terrain scale is not used on purpose. Rotation is not supported. # 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 # Nullify XZ translation because that's done by transform already
aabb.position.x = 0 aabb.position.x = 0
aabb.position.z = 0 aabb.position.z = 0
@ -433,12 +458,15 @@ func _load_chunk(terrain, cx: int, cz: int, aabb: AABB):
_multimesh_instance_pool.pop_back() _multimesh_instance_pool.pop_back()
else: else:
mmi = DirectMultiMeshInstance.new() mmi = DirectMultiMeshInstance.new()
mmi.set_world(terrain.get_world()) mmi.set_world(get_world())
mmi.set_multimesh(_multimesh) mmi.set_multimesh(_multimesh)
var trans := _get_chunk_transform(terrain_transform, cx, cz)
mmi.set_material_override(_material) mmi.set_material_override(_material)
mmi.set_transform(trans) mmi.set_transform(trans)
mmi.set_aabb(aabb) mmi.set_aabb(aabb)
mmi.set_layer_mask(render_layers)
mmi.set_visible(visible) mmi.set_visible(visible)
_chunks[Vector2(cx, cz)] = mmi _chunks[Vector2(cx, cz)] = mmi

View file

@ -16,7 +16,7 @@ target_path = "bin/"
TARGET_NAME = "hterrain_native" TARGET_NAME = "hterrain_native"
# Local dependency paths # Local dependency paths
godot_headers_path = "godot-cpp/godot_headers/" godot_headers_path = "godot-cpp/godot-headers/"
cpp_bindings_path = "godot-cpp/" cpp_bindings_path = "godot-cpp/"
cpp_bindings_library = "libgodot-cpp" cpp_bindings_library = "libgodot-cpp"

View file

@ -2,8 +2,9 @@
const NATIVE_PATH = "res://addons/zylann.hterrain/native/" const NATIVE_PATH = "res://addons/zylann.hterrain/native/"
const ImageUtilsGeneric = preload("./image_utils_generic.gd") 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 = { const _supported_os = {
"Windows": true, "Windows": true,
"X11": true, "X11": true,
@ -17,7 +18,7 @@ static func is_native_available() -> bool:
return false return false
# API changes can cause binary incompatibility # API changes can cause binary incompatibility
var v = Engine.get_version_info() 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(): static func get_image_utils():
@ -27,3 +28,10 @@ static func get_image_utils():
return ImageUtilsNative.new() return ImageUtilsNative.new()
return ImageUtilsGeneric.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()

View file

@ -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 )

View file

@ -3,8 +3,8 @@ tool
class Quad: class Quad:
var children = null var children = null
var origin_x := 0 var origin_x : int = 0
var origin_y := 0 var origin_y : int = 0
var data = null var data = null
func _init(): func _init():
@ -22,9 +22,9 @@ class Quad:
var _tree := Quad.new() var _tree := Quad.new()
var _max_depth := 0 var _max_depth : int = 0
var _base_size := 16 var _base_size : int = 16
var _split_scale := 2.0 var _split_scale : float = 2.0
var _make_func : FuncRef = null var _make_func : FuncRef = null
var _recycle_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: static func compute_lod_count(base_size: int, full_size: int) -> int:
var po = 0 var po : int = 0
while full_size > base_size: while full_size > base_size:
full_size = full_size >> 1 full_size = full_size >> 1
po += 1 po += 1
@ -70,12 +70,7 @@ func set_split_scale(p_split_scale: float):
# Split scale must be greater than a threshold, # Split scale must be greater than a threshold,
# otherwise lods will decimate too fast and it will look messy # otherwise lods will decimate too fast and it will look messy
if p_split_scale < MIN: _split_scale = clamp(p_split_scale, MIN, MAX)
p_split_scale = MIN
if p_split_scale > MAX:
p_split_scale = MAX
_split_scale = float(p_split_scale)
func get_split_scale() -> float: func get_split_scale() -> float:
@ -91,16 +86,15 @@ func update(view_pos: Vector3):
_tree.data = _make_chunk(_max_depth, 0, 0) _tree.data = _make_chunk(_max_depth, 0, 0)
# TODO Should be renamed get_lod_factor func get_lod_factor(lod: int) -> int:
func get_lod_size(lod: int) -> int:
return 1 << lod return 1 << lod
func _update(quad: Quad, lod: int, view_pos: Vector3): func _update(quad: Quad, lod: int, view_pos: Vector3):
# This function should be called regularly over frames. # This function should be called regularly over frames.
var lod_factor := get_lod_size(lod) var lod_factor : int = get_lod_factor(lod)
var chunk_size := _base_size * lod_factor var chunk_size : int = _base_size * lod_factor
var world_center := \ var world_center := \
chunk_size * (Vector3(quad.origin_x, 0, quad.origin_y) + Vector3(0.5, 0, 0.5)) 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: if no_split_child and world_center.distance_to(view_pos) > split_distance:
# Join # Join
if quad.has_children(): for i in 4:
for i in 4: var child = quad.children[i]
var child = quad.children[i] _recycle_chunk(child.data, child.origin_x, child.origin_y, lod - 1)
_recycle_chunk(child.data, child.origin_x, child.origin_y, lod - 1) quad.clear_children()
quad.data = null quad.data = _make_chunk(lod, quad.origin_x, quad.origin_y)
quad.clear_children()
assert(quad.data == null)
quad.data = _make_chunk(lod, quad.origin_x, quad.origin_y)
func _join_all_recursively(quad: Quad, lod: int): func _join_all_recursively(quad: Quad, lod: int):
if quad.has_children(): if quad.has_children():
for i in range(4): for i in 4:
var child = quad.children[i] _join_all_recursively(quad.children[i], lod - 1)
_join_all_recursively(child, lod - 1)
quad.clear_children() quad.clear_children()
elif quad.data != null: elif quad.data != null:
_recycle_chunk(quad.data, quad.origin_x, quad.origin_y, lod) _recycle_chunk(quad.data, quad.origin_x, quad.origin_y, lod)
quad.data = null 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): func _debug_draw_tree_recursive(ci: CanvasItem, quad: Quad, lod_index: int, child_index: int):
if quad.has_children(): if quad.has_children():
for i in range(0, quad.children.size()): for i in 4:
var child = quad.children[i] _debug_draw_tree_recursive(ci, quad.children[i], lod_index - 1, i)
_debug_draw_tree_recursive(ci, child, lod_index - 1, i)
else: else:
var size := get_lod_size(lod_index) var size : int = get_lod_factor(lod_index)
var checker := 0 var checker : int = 0
if child_index == 1 or child_index == 2: if child_index == 1 or child_index == 2:
checker = 1 checker = 1
var chunk_indicator := 0 var chunk_indicator : int = 0
if quad.data != null: if quad.data != null:
chunk_indicator = 1 chunk_indicator = 1
var r := Rect2(Vector2(quad.origin_x, quad.origin_y) * size, Vector2(size, size)) var r := Rect2(Vector2(quad.origin_x, quad.origin_y) * size, Vector2(size, size))

View file

@ -1,4 +1,5 @@
#include "image_utils.h" #include "image_utils.h"
#include "quad_tree_lod.h"
extern "C" { extern "C" {
@ -23,6 +24,7 @@ void GDN_EXPORT godot_nativescript_init(void *handle) {
godot::Godot::nativescript_init(handle); godot::Godot::nativescript_init(handle);
godot::register_tool_class<godot::ImageUtils>(); godot::register_tool_class<godot::ImageUtils>();
godot::register_tool_class<godot::QuadTreeLod>();
} }
} // extern "C" } // extern "C"

View file

@ -0,0 +1,242 @@
#include "quad_tree_lod.h"
namespace godot {
void QuadTreeLod::set_callbacks(Ref<FuncRef> make_cb, Ref<FuncRef> recycle_cb, Ref<FuncRef> 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<int>(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<real_t>(chunk_size) * (Vector3(static_cast<real_t>(quad->origin_x), 0.f, static_cast<real_t>(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<Vector2>(result);
world_center.y = (vbounds.x + vbounds.y) / 2.0f;
}
int split_distance = _base_size * lod_factor * static_cast<int>(_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<real_t>(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<real_t>(quad->origin_x), static_cast<real_t>(quad->origin_y)) * size,
Vector2(size, size));
Color color(1.0f - static_cast<real_t>(lod_index) * 0.2f, 0.2f * static_cast<real_t>(checker), static_cast<real_t>(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

View file

@ -0,0 +1,121 @@
#ifndef QUAD_TREE_LOD_H
#define QUAD_TREE_LOD_H
#include <CanvasItem.hpp>
#include <FuncRef.hpp>
#include <Godot.hpp>
#include <vector>
namespace godot {
class QuadTreeLod : public Reference {
GODOT_CLASS(QuadTreeLod, Reference)
public:
static void _register_methods();
QuadTreeLod() {}
~QuadTreeLod() {}
void _init() {}
void set_callbacks(Ref<FuncRef> make_cb, Ref<FuncRef> recycle_cb, Ref<FuncRef> 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<Quad> _node_pool;
std::vector<unsigned int> _free_indices;
int _max_depth = 0;
int _base_size = 16;
real_t _split_scale = 2.0f;
Ref<FuncRef> _make_func;
Ref<FuncRef> _recycle_func;
Ref<FuncRef> _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

View file

@ -3,5 +3,5 @@
name="Heightmap Terrain" name="Heightmap Terrain"
description="Heightmap-based terrain" description="Heightmap-based terrain"
author="Marc Gilleron" author="Marc Gilleron"
version="1.5.2" version="1.5.3 dev"
script="tools/plugin.gd" script="tools/plugin.gd"

View file

@ -63,6 +63,18 @@ wacyym
Sergey Lapin (slapin) Sergey Lapin (slapin)
Jonas (NoFr1ends) Jonas (NoFr1ends)
lenis0012 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} text = "Version: {version}
Author: Marc Gilleron Author: Marc Gilleron
@ -74,6 +86,18 @@ wacyym
Sergey Lapin (slapin) Sergey Lapin (slapin)
Jonas (NoFr1ends) Jonas (NoFr1ends)
lenis0012 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 ) script = ExtResource( 3 )

View file

@ -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

View file

@ -1,13 +1,15 @@
tool tool
extends Control 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 Errors = preload("../../util/errors.gd")
#const NativeFactory = preload("../../native/factory.gd") #const NativeFactory = preload("../../native/factory.gd")
const Logger = preload("../../util/logger.gd") const Logger = preload("../../util/logger.gd")
const SHAPES_DIR = "addons/zylann.hterrain/tools/brush/shapes" const BrushSettingsDialogScene = preload("./settings_dialog/brush_settings_dialog.tscn")
const DEFAULT_BRUSH = "round2.exr" const BrushSettingsDialog = preload("./settings_dialog/brush_settings_dialog.gd")
onready var _size_slider := $GridContainer/BrushSizeControl/Slider as Slider onready var _size_slider := $GridContainer/BrushSizeControl/Slider as Slider
onready var _size_value_label := $GridContainer/BrushSizeControl/Label as Label 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") onready var _shape_texture_rect = get_node("BrushShapeButton/TextureRect")
var _brush : Brush var _terrain_painter : TerrainPainter
var _load_image_dialog = null var _load_image_dialog = null
var _brush_settings_dialog = null
var _logger = Logger.get_for(self) var _logger = Logger.get_for(self)
# TODO This is an ugly workaround for https://github.com/godotengine/godot/issues/19479 # 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") _holes_checkbox.connect("toggled", self, "_on_holes_checkbox_toggled")
_slope_limit_control.connect("changed", self, "_on_slope_limit_changed") _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(): #if NativeFactory.is_native_available():
# _size_slider.max_value = 200 # _size_slider.max_value = 200
#else: #else:
@ -70,21 +73,19 @@ func _ready():
func setup_dialogs(base_control: Control): func setup_dialogs(base_control: Control):
assert(_load_image_dialog == null) assert(_brush_settings_dialog == null)
_load_image_dialog = EditorFileDialog.new() _brush_settings_dialog = BrushSettingsDialogScene.instance()
_load_image_dialog.mode = EditorFileDialog.MODE_OPEN_FILE base_control.add_child(_brush_settings_dialog)
_load_image_dialog.add_filter("*.exr ; EXR files")
_load_image_dialog.resizable = true # That dialog has sub-dialogs
_load_image_dialog.access = EditorFileDialog.ACCESS_FILESYSTEM _brush_settings_dialog.setup_dialogs(base_control)
_load_image_dialog.current_dir = SHAPES_DIR _brush_settings_dialog.set_brush(_terrain_painter.get_brush())
_load_image_dialog.connect("file_selected", self, "_on_LoadImageDialog_file_selected")
base_control.add_child(_load_image_dialog)
func _exit_tree(): func _exit_tree():
if _load_image_dialog != null: if _brush_settings_dialog != null:
_load_image_dialog.queue_free() _brush_settings_dialog.queue_free()
_load_image_dialog = null _brush_settings_dialog = null
# Testing display modes # Testing display modes
#var mode = 0 #var mode = 0
@ -96,48 +97,61 @@ func _exit_tree():
# if mode >= Brush.MODE_COUNT: # if mode >= Brush.MODE_COUNT:
# mode = 0 # mode = 0
func set_brush(brush: Brush): func set_terrain_painter(terrain_painter: TerrainPainter):
if _brush != null: if _terrain_painter != null:
_brush.disconnect("changed", self, "_on_brush_changed") _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... # 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) # 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: if _terrain_painter != null:
# Initial params # Initial brush params
_size_slider.value = brush.get_brush_size() _size_slider.value = _terrain_painter.get_brush().get_size()
_opacity_slider.ratio = brush.get_opacity() _opacity_slider.ratio = _terrain_painter.get_brush().get_opacity()
_flatten_height_box.value = brush.get_flatten_height() # Initial specific params
_color_picker.get_picker().color = brush.get_color() _flatten_height_box.value = _terrain_painter.get_flatten_height()
_density_slider.value = brush.get_detail_density() _color_picker.get_picker().color = _terrain_painter.get_color()
_holes_checkbox.pressed = not brush.get_mask_flag() _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 low = rad2deg(_terrain_painter.get_slope_limit_low_angle())
var high = rad2deg(brush.get_slope_limit_high_angle()) var high = rad2deg(_terrain_painter.get_slope_limit_high_angle())
_slope_limit_control.set_values(low, high) _slope_limit_control.set_values(low, high)
set_display_mode(brush.get_mode()) set_display_mode(_terrain_painter.get_mode())
_set_brush_shape_from_file(SHAPES_DIR.plus_file(DEFAULT_BRUSH))
_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(): func _on_flatten_height_changed():
_flatten_height_box.value = _brush.get_flatten_height() _flatten_height_box.value = _terrain_painter.get_flatten_height()
_flatten_height_pick_button.pressed = false _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): func set_display_mode(mode: int):
var show_flatten := mode == Brush.MODE_FLATTEN var show_flatten := mode == TerrainPainter.MODE_FLATTEN
var show_color := mode == Brush.MODE_COLOR var show_color := mode == TerrainPainter.MODE_COLOR
var show_density := mode == Brush.MODE_DETAIL var show_density := mode == TerrainPainter.MODE_DETAIL
var show_opacity := mode != Brush.MODE_MASK var show_opacity := mode != TerrainPainter.MODE_MASK
var show_holes := mode == Brush.MODE_MASK var show_holes := mode == TerrainPainter.MODE_MASK
var show_slope_limit := mode == Brush.MODE_SPLAT var show_slope_limit := mode == TerrainPainter.MODE_SPLAT
_set_visibility_of(_opacity_label, show_opacity) _set_visibility_of(_opacity_label, show_opacity)
_set_visibility_of(_opacity_control, 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): func _on_size_slider_value_changed(v: float):
if _brush != null: if _terrain_painter != null:
_brush.set_brush_size(int(v)) _terrain_painter.set_brush_size(int(v))
_size_value_label.text = str(v) _size_value_label.text = str(v)
func _on_opacity_slider_value_changed(v: float): func _on_opacity_slider_value_changed(v: float):
if _brush != null: if _terrain_painter != null:
_brush.set_opacity(_opacity_slider.ratio) _terrain_painter.set_opacity(_opacity_slider.ratio)
_opacity_value_label.text = str(v) _opacity_value_label.text = str(v)
func _on_flatten_height_box_value_changed(v: float): func _on_flatten_height_box_value_changed(v: float):
if _brush != null: if _terrain_painter != null:
_brush.set_flatten_height(v) _terrain_painter.set_flatten_height(v)
func _on_color_picker_color_changed(v: Color): func _on_color_picker_color_changed(v: Color):
if _brush != null: if _terrain_painter != null:
_brush.set_color(v) _terrain_painter.set_color(v)
func _on_density_slider_changed(v: float): func _on_density_slider_changed(v: float):
if _brush != null: if _terrain_painter != null:
_brush.set_detail_density(v) _terrain_painter.set_detail_density(v)
func _on_holes_checkbox_toggled(v: bool): func _on_holes_checkbox_toggled(v: bool):
if _brush != null: if _terrain_painter != null:
# When checked, we draw holes. When unchecked, we clear holes # 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(): func _on_BrushShapeButton_pressed():
_load_image_dialog.popup_centered_ratio(0.7) _brush_settings_dialog.popup_centered()
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
func _on_FlattenHeightPickButton_pressed(): func _on_FlattenHeightPickButton_pressed():
_brush.set_meta("pick_height", true) _terrain_painter.set_meta("pick_height", true)
func _on_slope_limit_changed(): func _on_slope_limit_changed():
var low = deg2rad(_slope_limit_control.get_low_value()) var low = deg2rad(_slope_limit_control.get_low_value())
var high = deg2rad(_slope_limit_control.get_high_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)

View file

@ -60,7 +60,7 @@ margin_bottom = 16.0
size_flags_horizontal = 3 size_flags_horizontal = 3
size_flags_vertical = 1 size_flags_vertical = 1
min_value = 2.0 min_value = 2.0
max_value = 200.0 max_value = 500.0
value = 2.0 value = 2.0
exp_edit = true exp_edit = true
rounded = true rounded = true
@ -120,6 +120,7 @@ margin_bottom = 24.0
size_flags_horizontal = 3 size_flags_horizontal = 3
min_value = -500.0 min_value = -500.0
max_value = 500.0 max_value = 500.0
step = 0.01
[node name="FlattenHeightPickButton" type="Button" parent="GridContainer/HB"] [node name="FlattenHeightPickButton" type="Button" parent="GridContainer/HB"]
margin_left = 115.0 margin_left = 115.0
@ -185,5 +186,6 @@ script = ExtResource( 3 )
range = Vector2( 0, 90 ) range = Vector2( 0, 90 )
[node name="Temp" type="Node" parent="."] [node name="Temp" type="Node" parent="."]
[connection signal="pressed" from="BrushShapeButton" to="." method="_on_BrushShapeButton_pressed"] [connection signal="pressed" from="BrushShapeButton" to="." method="_on_BrushShapeButton_pressed"]
[connection signal="pressed" from="GridContainer/HB/FlattenHeightPickButton" to="." method="_on_FlattenHeightPickButton_pressed"] [connection signal="pressed" from="GridContainer/HB/FlattenHeightPickButton" to="." method="_on_FlattenHeightPickButton_pressed"]

View file

@ -5,8 +5,8 @@ const DirectMeshInstance = preload("../../util/direct_mesh_instance.gd")
const HTerrainData = preload("../../hterrain_data.gd") const HTerrainData = preload("../../hterrain_data.gd")
const Util = preload("../../util/util.gd") const Util = preload("../../util/util.gd")
var _mesh_instance = null var _mesh_instance : DirectMeshInstance
var _mesh = null var _mesh : PlaneMesh
var _material = ShaderMaterial.new() var _material = ShaderMaterial.new()
#var _debug_mesh = CubeMesh.new() #var _debug_mesh = CubeMesh.new()
#var _debug_mesh_instance = null #var _debug_mesh_instance = null
@ -18,7 +18,7 @@ func _init():
_material.shader = load("res://addons/zylann.hterrain/tools/brush/decal.shader") _material.shader = load("res://addons/zylann.hterrain/tools/brush/decal.shader")
_mesh_instance = DirectMeshInstance.new() _mesh_instance = DirectMeshInstance.new()
_mesh_instance.set_material(_material) _mesh_instance.set_material(_material)
_mesh = PlaneMesh.new() _mesh = PlaneMesh.new()
_mesh_instance.set_mesh(_mesh) _mesh_instance.set_mesh(_mesh)
@ -26,15 +26,13 @@ func _init():
#_debug_mesh_instance.set_mesh(_debug_mesh) #_debug_mesh_instance.set_mesh(_debug_mesh)
func set_size(size): func set_size(size: float):
_mesh.size = Vector2(size, size) _mesh.size = Vector2(size, size)
# Must line up to terrain vertex policy, so must apply an off-by-one. # 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 # If I don't do that, the brush will appear to wobble above the ground
var ss = size - 1 var ss = size - 1
# Don't subdivide too much # Don't subdivide too much
if ss > 50: while ss > 50:
ss /= 2
if ss > 50:
ss /= 2 ss /= 2
_mesh.subdivide_width = ss _mesh.subdivide_width = ss
_mesh.subdivide_depth = ss _mesh.subdivide_depth = ss
@ -72,7 +70,7 @@ func set_terrain(terrain):
update_visibility() update_visibility()
func set_position(p_local_pos): func set_position(p_local_pos: Vector3):
assert(_terrain != null) assert(_terrain != null)
assert(typeof(p_local_pos) == TYPE_VECTOR3) assert(typeof(p_local_pos) == TYPE_VECTOR3)

View file

@ -0,0 +1,6 @@
shader_type canvas_item;
render_mode blend_disabled;
void fragment() {
COLOR = texture(TEXTURE, UV);
}

View file

@ -1,7 +1,7 @@
# Core logic to paint a texture using shaders, with undo/redo support. # Core logic to paint a texture using shaders, with undo/redo support.
# Operations are delayed so results are only available the next frame. # 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, # 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. # 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 Logger = preload("../../util/logger.gd")
const Util = preload("../../util/util.gd") const Util = preload("../../util/util.gd")
const NoBlendShader = preload("./no_blend.gdshader")
const UNDO_CHUNK_SIZE = 64 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. # Emitted when a region of the painted texture actually changed.
# Note 1: the image might not have changed yet at this point. # Note 1: the image might not have changed yet at this point.
@ -42,10 +53,20 @@ const _supported_formats = [
Image.FORMAT_RGBAH 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 : Viewport
var _viewport_sprite : Sprite var _viewport_bg_sprite : Sprite
var _viewport_brush_sprite : Sprite
var _brush_size := 32 var _brush_size := 32
var _brush_scale := 1.0
var _brush_position := Vector2() var _brush_position := Vector2()
var _brush_opacity := 1.0
var _brush_texture : Texture var _brush_texture : Texture
var _last_brush_position := Vector2() var _last_brush_position := Vector2()
var _brush_material := ShaderMaterial.new() var _brush_material := ShaderMaterial.new()
@ -60,9 +81,7 @@ var _debug_display : TextureRect
var _logger = Logger.get_for(self) var _logger = Logger.get_for(self)
func _ready(): func _init():
if Util.is_in_edited_scene(self):
return
_viewport = Viewport.new() _viewport = Viewport.new()
_viewport.size = Vector2(_brush_size, _brush_size) _viewport.size = Vector2(_brush_size, _brush_size)
_viewport.render_target_update_mode = Viewport.UPDATE_ONCE _viewport.render_target_update_mode = Viewport.UPDATE_ONCE
@ -74,10 +93,19 @@ func _ready():
#_viewport.usage = Viewport.USAGE_2D #_viewport.usage = Viewport.USAGE_2D
#_viewport.keep_3d_linear #_viewport.keep_3d_linear
_viewport_sprite = Sprite.new() # There is no "blend_disabled" option on standard CanvasItemMaterial...
_viewport_sprite.centered = false var no_blend_material := ShaderMaterial.new()
_viewport_sprite.material = _brush_material no_blend_material.shader = NoBlendShader
_viewport.add_child(_viewport_sprite) _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) 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)) assert((image == null and texture == null) or (image != null and texture != null))
_image = image _image = image
_texture = texture _texture = texture
_viewport_sprite.texture = _texture _viewport_bg_sprite.texture = _texture
_brush_material.set_shader_param(SHADER_PARAM_SRC_TEXTURE, _texture)
if image != null: if image != null:
_viewport.hdr = image.get_format() in _hdr_formats _viewport.hdr = image.get_format() in _hdr_formats
#print("PAINTER VIEWPORT HDR: ", _viewport.hdr) #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): func set_brush_size(new_size: int):
_brush_size = new_size _brush_size = new_size
@ -105,8 +137,36 @@ func get_brush_size() -> int:
return _brush_size 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): 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): func set_brush_shader(shader: Shader):
@ -115,6 +175,7 @@ func set_brush_shader(shader: Shader):
func set_brush_shader_param(p: String, v): func set_brush_shader_param(p: String, v):
assert(not _API_SHADER_PARAMS.has(p))
_modified_shader_params[p] = true _modified_shader_params[p] = true
_brush_material.set_shader_param(p, v) _brush_material.set_shader_param(p, v)
@ -125,28 +186,53 @@ func clear_brush_shader_params():
_modified_shader_params.clear() _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. # You must call this from an `_input` function or similar.
func paint_input(center_pos: Vector2): 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: if _viewport.size != vp_size:
# Do this lazily so the brush slider won't lag while adjusting it # Do this lazily so the brush slider won't lag while adjusting it
# TODO An "sliding_ended" handling might produce better user experience # TODO An "sliding_ended" handling might produce better user experience
_viewport.size = vp_size _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 # 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.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 _brush_position = brush_pos
_cmd_paint = true _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 # Using a Color because Godot doesn't understand vec4
var rect := Color() var rect := Color()
rect.r = brush_pos.x / _texture.get_width() rect.r = brush_pos.x / _texture.get_width()
rect.g = brush_pos.y / _texture.get_height() rect.g = brush_pos.y / _texture.get_height()
rect.b = _brush_size / _texture.get_width() rect.b = _viewport.size.x / _texture.get_width()
rect.a = _brush_size / _texture.get_height() rect.a = _viewport.size.y / _texture.get_height()
_brush_material.set_shader_param("u_texture_rect", rect) # 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 # 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_x : int = max(-brush_pos.x, 0)
var src_y : int = max(-brush_pos.y, 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_w : int = min(max(_viewport.size.x - 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_h : int = min(max(_viewport.size.y - src_y, 0), _texture.get_height() - dst_y)
if src_w != 0 and src_h != 0: if src_w != 0 and src_h != 0:
_mark_modified_chunks(dst_x, dst_y, src_w, src_h) _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 cy in range(cmin_y, cmax_y):
for cx in range(cmin_x, cmax_x): for cx in range(cmin_x, cmax_x):
#print("Marking chunk ", Vector2(cx, cy))
_modified_chunks[Vector2(cx, cy)] = true _modified_chunks[Vector2(cx, cy)] = true

View file

@ -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

View file

@ -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"]

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -1,13 +1,20 @@
shader_type canvas_item; shader_type canvas_item;
render_mode blend_disabled; render_mode blend_disabled;
uniform sampler2D u_brush_texture; uniform sampler2D u_src_texture;
uniform float u_factor = 1.0; uniform vec4 u_src_rect;
uniform float u_value = 1.0; uniform float u_opacity = 1.0;
uniform float u_factor = 1.0;
void fragment() { uniform float u_value = 1.0;
float brush_value = texture(u_brush_texture, SCREEN_UV).r;
vec2 get_src_uv(vec2 screen_uv) {
vec4 src = texture(TEXTURE, UV); vec2 uv = u_src_rect.xy + screen_uv * u_src_rect.zw;
COLOR = vec4(src.rgb, mix(src.a, u_value, u_factor * brush_value)); 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));
}

View file

@ -1,21 +1,28 @@
shader_type canvas_item; shader_type canvas_item;
render_mode blend_disabled; render_mode blend_disabled;
uniform sampler2D u_brush_texture; uniform sampler2D u_src_texture;
uniform float u_factor = 1.0; uniform vec4 u_src_rect;
uniform vec4 u_color = vec4(1.0); uniform float u_opacity = 1.0;
uniform float u_factor = 1.0;
void fragment() { uniform vec4 u_color = vec4(1.0);
float brush_value = texture(u_brush_texture, SCREEN_UV).r;
vec2 get_src_uv(vec2 screen_uv) {
vec4 src = texture(TEXTURE, UV); vec2 uv = u_src_rect.xy + screen_uv * u_src_rect.zw;
return 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. void fragment() {
// So it's not possible to use viewports to paint albedo... float brush_value = u_opacity * texture(TEXTURE, UV).r;
//src.rgb = pow(src.rgb, vec3(0.4545));
vec4 src = texture(u_src_texture, get_src_uv(SCREEN_UV));
vec4 col = vec4(mix(src.rgb, u_color.rgb, brush_value * u_factor), src.a);
COLOR = col; // 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;
}

View file

@ -1,50 +1,58 @@
shader_type canvas_item; shader_type canvas_item;
uniform sampler2D u_brush_texture; uniform sampler2D u_src_texture;
uniform float u_factor = 1.0; uniform vec4 u_src_rect;
uniform vec4 u_color = vec4(1.0); uniform float u_opacity = 1.0;
uniform float u_factor = 1.0;
// float get_noise(vec2 pos) { uniform vec4 u_color = vec4(1.0);
// return fract(sin(dot(pos.xy ,vec2(12.9898,78.233))) * 43758.5453);
// } vec2 get_src_uv(vec2 screen_uv) {
vec2 uv = u_src_rect.xy + screen_uv * u_src_rect.zw;
float erode(sampler2D heightmap, vec2 uv, vec2 pixel_size, float weight) { return uv;
float r = 3.0; }
// Divide so the shader stays neighbor dependent 1 pixel across. // float get_noise(vec2 pos) {
// For this to work, filtering must be enabled. // return fract(sin(dot(pos.xy ,vec2(12.9898,78.233))) * 43758.5453);
vec2 eps = pixel_size / (0.99 * r); // }
float h = texture(heightmap, uv).r; float erode(sampler2D heightmap, vec2 uv, vec2 pixel_size, float weight) {
float eh = h; float r = 3.0;
//float dh = h;
// Divide so the shader stays neighbor dependent 1 pixel across.
// Morphology with circular structuring element // For this to work, filtering must be enabled.
for (float y = -r; y <= r; ++y) { vec2 eps = pixel_size / (0.99 * r);
for (float x = -r; x <= r; ++x) {
float h = texture(heightmap, uv).r;
vec2 p = vec2(x, y); float eh = h;
float nh = texture(heightmap, uv + p * eps).r; //float dh = h;
float s = max(length(p) - r, 0); // Morphology with circular structuring element
eh = min(eh, nh + s); for (float y = -r; y <= r; ++y) {
for (float x = -r; x <= r; ++x) {
//s = min(r - length(p), 0);
//dh = max(dh, nh + s); vec2 p = vec2(x, y);
} float nh = texture(heightmap, uv + p * eps).r;
}
float s = max(length(p) - r, 0);
eh = mix(h, eh, weight); eh = min(eh, nh + s);
//dh = mix(h, dh, u_weight);
//s = min(r - length(p), 0);
float ph = eh;//mix(eh, dh, u_dilation); //dh = max(dh, nh + s);
}
return ph; }
}
eh = mix(h, eh, weight);
void fragment() { //dh = mix(h, dh, u_weight);
float brush_value = texture(u_brush_texture, SCREEN_UV).r * u_factor;
float ph = erode(TEXTURE, UV, TEXTURE_PIXEL_SIZE, brush_value); float ph = eh;//mix(eh, dh, u_dilation);
//ph += brush_value * 0.35;
COLOR = vec4(ph, ph, ph, 1.0); 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);
}

View file

@ -1,14 +1,21 @@
shader_type canvas_item; shader_type canvas_item;
render_mode blend_disabled; render_mode blend_disabled;
uniform sampler2D u_brush_texture; uniform sampler2D u_src_texture;
uniform float u_factor = 1.0; uniform vec4 u_src_rect;
uniform float u_flatten_value; uniform float u_opacity = 1.0;
uniform float u_factor = 1.0;
void fragment() { uniform float u_flatten_value;
float brush_value = texture(u_brush_texture, SCREEN_UV).r;
vec2 get_src_uv(vec2 screen_uv) {
float src_h = texture(TEXTURE, UV).r; vec2 uv = u_src_rect.xy + screen_uv * u_src_rect.zw;
float h = mix(src_h, u_flatten_value, u_factor * brush_value); return uv;
COLOR = vec4(h, 0.0, 0.0, 1.0); }
}
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);
}

View file

@ -1,33 +1,39 @@
shader_type canvas_item; shader_type canvas_item;
render_mode blend_disabled; render_mode blend_disabled;
uniform sampler2D u_brush_texture; uniform sampler2D u_src_texture;
uniform float u_factor = 1.0; uniform vec4 u_src_rect;
uniform vec4 u_texture_rect; uniform float u_opacity = 1.0;
uniform float u_factor = 1.0;
// TODO Could actually level to whatever height the brush was at the beginning of the stroke?
vec2 get_src_uv(vec2 screen_uv) {
void fragment() { vec2 uv = u_src_rect.xy + screen_uv * u_src_rect.zw;
float brush_value = texture(u_brush_texture, SCREEN_UV).r; return uv;
}
// The heightmap does not have mipmaps,
// so we need to use an approximation of average. // TODO Could actually level to whatever height the brush was at the beginning of the stroke?
// This is not a very good one though...
float dst_h = 0.0; void fragment() {
vec2 uv_min = vec2(u_texture_rect.xy); float brush_value = u_opacity * texture(TEXTURE, UV).r;
vec2 uv_max = vec2(u_texture_rect.xy + u_texture_rect.zw);
for (int i = 0; i < 5; ++i) { // The heightmap does not have mipmaps,
for (int j = 0; j < 5; ++j) { // so we need to use an approximation of average.
float x = mix(uv_min.x, uv_max.x, float(i) / 4.0); // This is not a very good one though...
float y = mix(uv_min.y, uv_max.y, float(j) / 4.0); float dst_h = 0.0;
float h = texture(TEXTURE, vec2(x, y)).r; vec2 uv_min = vec2(u_src_rect.xy);
dst_h += h; 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) {
dst_h /= (5.0 * 5.0); 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);
// TODO I have no idea if this will check out float h = texture(u_src_texture, vec2(x, y)).r;
float src_h = texture(TEXTURE, UV).r; dst_h += h;
float h = mix(src_h, dst_h, u_factor * brush_value); }
COLOR = vec4(h, 0.0, 0.0, 1.0); }
} 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);
}

View file

@ -1,13 +1,20 @@
shader_type canvas_item; shader_type canvas_item;
render_mode blend_disabled; 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 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() { 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; float h = src_h + u_factor * brush_value;
COLOR = vec4(h, 0.0, 0.0, 1.0); COLOR = vec4(h, 0.0, 0.0, 1.0);
} }

View file

@ -1,19 +1,28 @@
shader_type canvas_item; shader_type canvas_item;
render_mode blend_disabled; render_mode blend_disabled;
uniform sampler2D u_brush_texture; uniform sampler2D u_src_texture;
uniform float u_factor = 1.0; uniform vec4 u_src_rect;
uniform float u_opacity = 1.0;
void fragment() { uniform float u_factor = 1.0;
float brush_value = texture(u_brush_texture, SCREEN_UV).r;
vec2 get_src_uv(vec2 screen_uv) {
vec2 offset = TEXTURE_PIXEL_SIZE; vec2 uv = u_src_rect.xy + screen_uv * u_src_rect.zw;
float src_nx = texture(TEXTURE, UV - vec2(offset.x, 0.0)).r; return uv;
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; void fragment() {
float src_h = texture(TEXTURE, UV).r; float brush_value = u_opacity * 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); vec2 src_pixel_size = 1.0 / vec2(textureSize(u_src_texture, 0));
COLOR = vec4(h, 0.0, 0.0, 1.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);
}

View file

@ -1,68 +1,76 @@
shader_type canvas_item; shader_type canvas_item;
render_mode blend_disabled; render_mode blend_disabled;
uniform sampler2D u_brush_texture; uniform sampler2D u_src_texture;
uniform float u_factor = 1.0; uniform vec4 u_src_rect;
uniform vec4 u_splat = vec4(1.0, 0.0, 0.0, 0.0); uniform float u_opacity = 1.0;
uniform sampler2D u_other_splatmap_1; uniform float u_factor = 1.0;
uniform sampler2D u_other_splatmap_2; uniform vec4 u_splat = vec4(1.0, 0.0, 0.0, 0.0);
uniform sampler2D u_other_splatmap_3; uniform sampler2D u_other_splatmap_1;
uniform sampler2D u_heightmap; uniform sampler2D u_other_splatmap_2;
uniform float u_normal_min_y = 0.0; uniform sampler2D u_other_splatmap_3;
uniform float u_normal_max_y = 1.0; uniform sampler2D u_heightmap;
uniform float u_normal_min_y = 0.0;
float sum(vec4 v) { uniform float u_normal_max_y = 1.0;
return v.x + v.y + v.z + v.w;
} vec2 get_src_uv(vec2 screen_uv) {
vec2 uv = u_src_rect.xy + screen_uv * u_src_rect.zw;
vec3 get_normal(sampler2D heightmap, vec2 pos) { return uv;
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 sum(vec4 v) {
float hny = texture(heightmap, pos + vec2(0.0, -ps.y)).r; return v.x + v.y + v.z + v.w;
float hpy = texture(heightmap, pos + vec2(0.0, ps.y)).r; }
return normalize(vec3(hnx - hpx, 2.0, hpy - hny));
} vec3 get_normal(sampler2D heightmap, vec2 pos) {
vec2 ps = vec2(1.0) / vec2(textureSize(heightmap, 0));
// Limits painting based on the slope, with a bit of falloff float hnx = texture(heightmap, pos + vec2(-ps.x, 0.0)).r;
float apply_slope_limit(float brush_value, vec3 normal, float normal_min_y, float normal_max_y) { float hpx = texture(heightmap, pos + vec2(ps.x, 0.0)).r;
float normal_falloff = 0.2; float hny = texture(heightmap, pos + vec2(0.0, -ps.y)).r;
float hpy = texture(heightmap, pos + vec2(0.0, ps.y)).r;
// If an edge is at min/max, make sure it won't be affected by falloff return normalize(vec3(hnx - hpx, 2.0, hpy - hny));
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;
// Limits painting based on the slope, with a bit of falloff
brush_value *= 1.0 - smoothstep( float apply_slope_limit(float brush_value, vec3 normal, float normal_min_y, float normal_max_y) {
normal_max_y - normal_falloff, float normal_falloff = 0.2;
normal_max_y + normal_falloff, normal.y);
// If an edge is at min/max, make sure it won't be affected by falloff
brush_value *= smoothstep( normal_min_y = normal_min_y <= 0.0 ? -2.0 : normal_min_y;
normal_min_y - normal_falloff, normal_max_y = normal_max_y >= 1.0 ? 2.0 : normal_max_y;
normal_min_y + normal_falloff, normal.y);
brush_value *= 1.0 - smoothstep(
return brush_value; normal_max_y - normal_falloff,
} normal_max_y + normal_falloff, normal.y);
void fragment() { brush_value *= smoothstep(
float brush_value = texture(u_brush_texture, SCREEN_UV).r * u_factor; normal_min_y - normal_falloff,
normal_min_y + normal_falloff, normal.y);
vec3 normal = get_normal(u_heightmap, UV);
brush_value = apply_slope_limit(brush_value, normal, u_normal_min_y, u_normal_max_y); return brush_value;
}
// It is assumed 3 other renders are done the same with the other 3
vec4 src0 = texture(TEXTURE, UV); void fragment() {
vec4 src1 = texture(u_other_splatmap_1, UV); float brush_value = u_opacity * texture(TEXTURE, UV).r * u_factor;
vec4 src2 = texture(u_other_splatmap_2, UV);
vec4 src3 = texture(u_other_splatmap_3, UV); vec2 src_uv = get_src_uv(SCREEN_UV);
float t = brush_value; vec3 normal = get_normal(u_heightmap, src_uv);
vec4 s0 = mix(src0, u_splat, t); brush_value = apply_slope_limit(brush_value, normal, u_normal_min_y, u_normal_max_y);
vec4 s1 = mix(src1, vec4(0.0), t);
vec4 s2 = mix(src2, vec4(0.0), t); // It is assumed 3 other renders are done the same with the other 3
vec4 s3 = mix(src3, vec4(0.0), t); vec4 src0 = texture(u_src_texture, src_uv);
float sum = sum(s0) + sum(s1) + sum(s2) + sum(s3); vec4 src1 = texture(u_other_splatmap_1, src_uv);
s0 /= sum; vec4 src2 = texture(u_other_splatmap_2, src_uv);
s1 /= sum; vec4 src3 = texture(u_other_splatmap_3, src_uv);
s2 /= sum; float t = brush_value;
s3 /= sum; vec4 s0 = mix(src0, u_splat, t);
COLOR = s0; 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;
}

View file

@ -1,7 +1,9 @@
shader_type canvas_item; shader_type canvas_item;
render_mode blend_disabled; 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 float u_factor = 1.0;
uniform vec4 u_splat = vec4(1.0, 0.0, 0.0, 0.0); uniform vec4 u_splat = vec4(1.0, 0.0, 0.0, 0.0);
uniform sampler2D u_heightmap; 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_max_y = 1.0;
//uniform float u_normal_falloff = 0.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) { vec3 get_normal(sampler2D heightmap, vec2 pos) {
vec2 ps = vec2(1.0) / vec2(textureSize(heightmap, 0)); vec2 ps = vec2(1.0) / vec2(textureSize(heightmap, 0));
float hnx = texture(heightmap, pos + vec2(-ps.x, 0.0)).r; 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() { 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); 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); vec4 s = mix(src_splat, u_splat, brush_value);
s = s / (s.r + s.g + s.b + s.a); s = s / (s.r + s.g + s.b + s.a);
COLOR = s; COLOR = s;

View file

@ -1,82 +1,90 @@
shader_type canvas_item; shader_type canvas_item;
render_mode blend_disabled; render_mode blend_disabled;
uniform sampler2D u_brush_texture; uniform sampler2D u_src_texture;
uniform float u_factor = 1.0; uniform vec4 u_src_rect;
uniform int u_texture_index; uniform float u_opacity = 1.0;
uniform int u_mode; // 0: index, 1: weight uniform float u_factor = 1.0;
uniform sampler2D u_index_map; uniform int u_texture_index;
uniform sampler2D u_weight_map; uniform int u_mode; // 0: output index, 1: output weight
uniform sampler2D u_index_map;
void fragment() { uniform sampler2D u_weight_map;
float brush_value = texture(u_brush_texture, SCREEN_UV).r * clamp(u_factor, 0.0, 1.0);
vec2 get_src_uv(vec2 screen_uv) {
vec4 iv = texture(u_index_map, UV); vec2 uv = u_src_rect.xy + screen_uv * u_src_rect.zw;
vec4 wv = texture(u_weight_map, UV); return uv;
}
float i[3] = {iv.r, iv.g, iv.b};
float w[3] = {wv.r, wv.g, wv.b}; void fragment() {
float brush_value = u_opacity * texture(TEXTURE, UV).r * clamp(u_factor, 0.0, 1.0);
if (brush_value > 0.0) {
float texture_index_f = float(u_texture_index) / 255.0; vec2 src_uv = get_src_uv(SCREEN_UV);
int ci = u_texture_index % 3; vec4 iv = texture(u_index_map, src_uv);
vec4 wv = texture(u_weight_map, src_uv);
float cm[3] = {-1.0, -1.0, -1.0};
cm[ci] = 1.0; float i[3] = {iv.r, iv.g, iv.b};
float w[3] = {wv.r, wv.g, wv.b};
// Decompress third weight to make computations easier
w[2] = 1.0 - w[0] - w[1]; if (brush_value > 0.0) {
float texture_index_f = float(u_texture_index) / 255.0;
if (abs(i[ci] - texture_index_f) > 0.001) { int ci = u_texture_index % 3;
// Pixel does not have our texture index,
// transfer its weight to other components first float cm[3] = {-1.0, -1.0, -1.0};
if (w[ci] > brush_value) { cm[ci] = 1.0;
w[0] -= cm[0] * brush_value;
w[1] -= cm[1] * brush_value; // Decompress third weight to make computations easier
w[2] -= cm[2] * brush_value; w[2] = 1.0 - w[0] - w[1];
} else if (w[ci] >= 0.f) { if (abs(i[ci] - texture_index_f) > 0.001) {
w[ci] = 0.f; // Pixel does not have our texture index,
i[ci] = texture_index_f; // transfer its weight to other components first
} if (w[ci] > brush_value) {
w[0] -= cm[0] * brush_value;
} else { w[1] -= cm[1] * brush_value;
// Pixel has our texture index, increase its weight w[2] -= cm[2] * brush_value;
if (w[ci] + brush_value < 1.f) {
w[0] += cm[0] * brush_value; } else if (w[ci] >= 0.f) {
w[1] += cm[1] * brush_value; w[ci] = 0.f;
w[2] += cm[2] * brush_value; i[ci] = texture_index_f;
}
} else {
// Pixel weight is full, we can set all components to the same index. } else {
// Need to nullify other weights because they would otherwise never reach // Pixel has our texture index, increase its weight
// zero due to normalization if (w[ci] + brush_value < 1.f) {
w[0] = 0.0; w[0] += cm[0] * brush_value;
w[1] = 0.0; w[1] += cm[1] * brush_value;
w[2] = 0.0; w[2] += cm[2] * brush_value;
w[ci] = 1.0; } else {
// Pixel weight is full, we can set all components to the same index.
i[0] = texture_index_f; // Need to nullify other weights because they would otherwise never reach
i[1] = texture_index_f; // zero due to normalization
i[2] = texture_index_f; w[0] = 0.0;
} w[1] = 0.0;
} w[2] = 0.0;
w[0] = clamp(w[0], 0.0, 1.0); w[ci] = 1.0;
w[1] = clamp(w[1], 0.0, 1.0);
w[2] = clamp(w[2], 0.0, 1.0); i[0] = texture_index_f;
i[1] = texture_index_f;
// Renormalize i[2] = texture_index_f;
float sum = w[0] + w[1] + w[2]; }
w[0] /= sum; }
w[1] /= sum;
w[2] /= sum; 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);
if(u_mode == 0) {
COLOR = vec4(i[0], i[1], i[2], 1.0); // Renormalize
} else { float sum = w[0] + w[1] + w[2];
COLOR = vec4(w[0], w[1], w[2], 1.0); 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);
}
}

View file

@ -4,6 +4,7 @@ const Painter = preload("./painter.gd")
const HTerrain = preload("../../hterrain.gd") const HTerrain = preload("../../hterrain.gd")
const HTerrainData = preload("../../hterrain_data.gd") const HTerrainData = preload("../../hterrain_data.gd")
const Logger = preload("../../util/logger.gd") const Logger = preload("../../util/logger.gd")
const Brush = preload("./brush.gd")
const RaiseShader = preload("./shaders/raise.shader") const RaiseShader = preload("./shaders/raise.shader")
const SmoothShader = preload("./shaders/smooth.shader") const SmoothShader = preload("./shaders/smooth.shader")
@ -33,12 +34,12 @@ class ModifiedMap:
var map_index := 0 var map_index := 0
var painter_index := 0 var painter_index := 0
signal changed signal flatten_height_changed
var _painters := [] var _painters := []
var _brush_size := 32 var _brush := Brush.new()
var _opacity := 1.0
var _color := Color(1, 0, 0, 1) var _color := Color(1, 0, 0, 1)
var _mask_flag := false var _mask_flag := false
var _mode := MODE_RAISE var _mode := MODE_RAISE
@ -59,43 +60,45 @@ func _init():
var p = Painter.new() var p = Painter.new()
# The name is just for debugging # The name is just for debugging
p.set_name(str("Painter", i)) 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]) p.connect("texture_region_changed", self, "_on_painter_texture_region_changed", [i])
add_child(p) add_child(p)
_painters.append(p) _painters.append(p)
func get_brush() -> Brush:
return _brush
func get_brush_size() -> int: func get_brush_size() -> int:
return _brush_size return _brush.get_size()
func set_brush_size(s: int): func set_brush_size(s: int):
if _brush_size == s: _brush.set_size(s)
return # for p in _painters:
_brush_size = s # p.set_brush_size(_brush_size)
for p in _painters:
p.set_brush_size(_brush_size)
emit_signal("changed")
func set_brush_texture(texture: Texture): func set_brush_texture(texture: Texture):
for p in _painters: _brush.set_shapes([texture])
p.set_brush_texture(texture) # for p in _painters:
# p.set_brush_texture(texture)
func get_opacity() -> float: func get_opacity() -> float:
return _opacity return _brush.get_opacity()
func set_opacity(opacity: float): func set_opacity(opacity: float):
_opacity = opacity _brush.set_opacity(opacity)
func set_flatten_height(h: float): func set_flatten_height(h: float):
if h == _flatten_height: if h == _flatten_height:
return return
_flatten_height = h _flatten_height = h
emit_signal("changed") emit_signal("flatten_height_changed")
func get_flatten_height() -> float: func get_flatten_height() -> float:
@ -177,7 +180,10 @@ func commit() -> Dictionary:
var changes := [] var changes := []
var chunk_positions : Array var chunk_positions : Array
assert(len(_modified_maps) > 0)
for mm in _modified_maps: for mm in _modified_maps:
#print("Flushing painter ", mm.painter_index)
var painter : Painter = _painters[mm.painter_index] var painter : Painter = _painters[mm.painter_index]
var info := painter.commit() var info := painter.commit()
@ -198,6 +204,12 @@ func commit() -> Dictionary:
# since the latter updates out of order for preview # since the latter updates out of order for preview
terrain_data.notify_region_change(rect, mm.map_type, mm.map_index, false, true) 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()) assert(not has_modified_chunks())
return { return {
@ -227,14 +239,19 @@ func set_terrain(terrain: HTerrain):
p.clear_brush_shader_params() p.clear_brush_shader_params()
# This may be called from an `_input` callback # This may be called from an `_input` callback.
func paint_input(position: Vector2): # Returns `true` if any change was performed.
func paint_input(position: Vector2, pressure: float) -> bool:
assert(_terrain.get_data() != null) assert(_terrain.get_data() != null)
var data = _terrain.get_data() var data = _terrain.get_data()
assert(not data.is_locked()) 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: match _mode:
MODE_RAISE: MODE_RAISE:
_paint_height(data, position, 1.0) _paint_height(data, position, 1.0)
@ -279,11 +296,12 @@ func paint_input(position: Vector2):
MODE_DETAIL: MODE_DETAIL:
_paint_detail(data, position) _paint_detail(data, position)
_: _:
_logger.error("Unknown mode {0}".format([_mode])) _logger.error("Unknown mode {0}".format([_mode]))
assert(len(_modified_maps) > 0) assert(len(_modified_maps) > 0)
return true
func _on_painter_texture_region_changed(rect: Rect2, painter_index: int): 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] _modified_maps = [mm]
# When using sculpting tools, make it dependent on brush size # When using sculpting tools, make it dependent on brush size
var raise_strength := 10.0 + float(_brush_size) var raise_strength := 10.0 + float(_brush.get_size())
var delta := factor * _opacity * (2.0 / 60.0) * raise_strength var delta := factor * (2.0 / 60.0) * raise_strength
var p : Painter = _painters[0] var p : Painter = _painters[0]
@ -332,7 +350,7 @@ func _paint_smooth(data: HTerrainData, position: Vector2):
var p : Painter = _painters[0] var p : Painter = _painters[0]
p.set_brush_shader(SmoothShader) 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.set_image(image, texture)
p.paint_input(position) p.paint_input(position)
@ -350,7 +368,7 @@ func _paint_flatten(data: HTerrainData, position: Vector2):
var p : Painter = _painters[0] var p : Painter = _painters[0]
p.set_brush_shader(FlattenShader) 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_brush_shader_param("u_flatten_value", _flatten_height)
p.set_image(image, texture) p.set_image(image, texture)
p.paint_input(position) p.paint_input(position)
@ -369,7 +387,7 @@ func _paint_level(data: HTerrainData, position: Vector2):
var p : Painter = _painters[0] var p : Painter = _painters[0]
p.set_brush_shader(LevelShader) 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.set_image(image, texture)
p.paint_input(position) p.paint_input(position)
@ -387,7 +405,7 @@ func _paint_erode(data: HTerrainData, position: Vector2):
var p : Painter = _painters[0] var p : Painter = _painters[0]
p.set_brush_shader(ErodeShader) 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.set_image(image, texture)
p.paint_input(position) 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) var splat = Color(0.0, 0.0, 0.0, 0.0)
splat[_texture_index] = 1.0; splat[_texture_index] = 1.0;
p.set_brush_shader(Splat4Shader) 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_splat", splat)
p.set_brush_shader_param("u_normal_min_y", cos(_slope_limit_high_angle)) 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) 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(SplatIndexedShader)
p.set_brush_shader_param("u_mode", mode) 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_index_map", textures[0])
p.set_brush_shader_param("u_weight_map", textures[1]) p.set_brush_shader_param("u_weight_map", textures[1])
p.set_brush_shader_param("u_texture_index", _texture_index) 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) other_splatmaps.append(tex)
p.set_brush_shader(Splat16Shader) 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_splat", splats[i])
p.set_brush_shader_param("u_other_splatmap_1", other_splatmaps[0]) p.set_brush_shader_param("u_other_splatmap_1", other_splatmaps[0])
p.set_brush_shader_param("u_other_splatmap_2", other_splatmaps[1]) 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 # https://github.com/Zylann/godot_heightmap_plugin/issues/17#issuecomment-734001879
p.set_brush_shader(ColorShader) 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_brush_shader_param("u_color", _color)
p.set_image(image, texture) p.set_image(image, texture)
p.paint_input(position) p.paint_input(position)
@ -529,7 +547,7 @@ func _paint_mask(data: HTerrainData, position: Vector2):
var p : Painter = _painters[0] var p : Painter = _painters[0]
p.set_brush_shader(AlphaShader) 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_brush_shader_param("u_value", 1.0 if _mask_flag else 0.0)
p.set_image(image, texture) p.set_image(image, texture)
p.paint_input(position) p.paint_input(position)
@ -550,7 +568,7 @@ func _paint_detail(data: HTerrainData, position: Vector2):
# TODO Don't use this shader # TODO Don't use this shader
p.set_brush_shader(ColorShader) 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_brush_shader_param("u_color", c)
p.set_image(image, texture) p.set_image(image, texture)
p.paint_input(position) p.paint_input(position)

View file

@ -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/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"] [node name="ExportImageDialog" type="WindowDialog"]
margin_left = 77.0 margin_left = 77.0
@ -156,6 +157,9 @@ margin_left = 257.0
margin_right = 311.0 margin_right = 311.0
margin_bottom = 20.0 margin_bottom = 20.0
text = "Cancel" 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="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="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"] [connection signal="item_selected" from="VB/Grid/FormatSelector" to="." method="_on_FormatSelector_item_selected"]

View file

@ -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/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"] [node name="GenerateMeshDialog" type="WindowDialog"]
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 0.0
anchor_bottom = 0.0
margin_left = 57.0 margin_left = 57.0
margin_top = 83.0 margin_top = 83.0
margin_right = 505.0 margin_right = 505.0
margin_bottom = 269.0 margin_bottom = 269.0
rect_min_size = Vector2( 448, 186 ) 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" window_title = "Generate full mesh"
resizable = false
script = ExtResource( 1 ) script = ExtResource( 1 )
_sections_unfolded = [ "Rect" ]
[node name="VBoxContainer" type="VBoxContainer" parent="." index="1"] [node name="VBoxContainer" type="VBoxContainer" parent="."]
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 1.0 anchor_right = 1.0
anchor_bottom = 1.0 anchor_bottom = 1.0
margin_left = 8.0 margin_left = 8.0
margin_top = 8.0 margin_top = 8.0
margin_right = -8.0 margin_right = -8.0
margin_bottom = -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"] [node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"]
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 0.0
anchor_bottom = 0.0
margin_right = 432.0 margin_right = 432.0
margin_bottom = 24.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"] [node name="Label" type="Label" parent="VBoxContainer/HBoxContainer"]
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 0.0
anchor_bottom = 0.0
margin_top = 5.0 margin_top = 5.0
margin_right = 28.0 margin_right = 28.0
margin_bottom = 19.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" text = "LOD"
percent_visible = 1.0
lines_skipped = 0
max_lines_visible = -1
[node name="LODSpinBox" type="SpinBox" parent="VBoxContainer/HBoxContainer" index="1"] [node name="LODSpinBox" type="SpinBox" parent="VBoxContainer/HBoxContainer"]
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 0.0
anchor_bottom = 0.0
margin_left = 32.0 margin_left = 32.0
margin_right = 432.0 margin_right = 432.0
margin_bottom = 24.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_horizontal = 3
size_flags_vertical = 1
min_value = 1.0 min_value = 1.0
max_value = 16.0 max_value = 16.0
step = 1.0
page = 0.0
value = 1.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"] [node name="PreviewLabel" type="Label" parent="VBoxContainer"]
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 0.0
anchor_bottom = 0.0
margin_top = 28.0 margin_top = 28.0
margin_right = 432.0 margin_right = 432.0
margin_bottom = 42.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" 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"] [node name="Spacer" type="Control" parent="VBoxContainer"]
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 0.0
anchor_bottom = 0.0
margin_top = 46.0 margin_top = 46.0
margin_right = 432.0 margin_right = 432.0
margin_bottom = 54.0 margin_bottom = 54.0
rect_min_size = Vector2( 0, 8 ) 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"] [node name="Label" type="Label" parent="VBoxContainer"]
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 0.0
anchor_bottom = 0.0
margin_top = 58.0 margin_top = 58.0
margin_right = 432.0 margin_right = 432.0
margin_bottom = 123.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." 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 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"] [node name="Buttons" type="HBoxContainer" parent="VBoxContainer"]
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 0.0
anchor_bottom = 0.0
margin_top = 127.0 margin_top = 127.0
margin_right = 432.0 margin_right = 432.0
margin_bottom = 147.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 custom_constants/separation = 32
alignment = 1 alignment = 1
_sections_unfolded = [ "custom_constants" ]
[node name="Generate" type="Button" parent="VBoxContainer/Buttons" index="0"] [node name="Generate" type="Button" parent="VBoxContainer/Buttons"]
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 0.0
anchor_bottom = 0.0
margin_left = 137.0 margin_left = 137.0
margin_right = 208.0 margin_right = 208.0
margin_bottom = 20.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" text = "Generate"
flat = false
align = 1
[node name="Cancel" type="Button" parent="VBoxContainer/Buttons" index="1"] [node name="Cancel" type="Button" parent="VBoxContainer/Buttons"]
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 0.0
anchor_bottom = 0.0
margin_left = 240.0 margin_left = 240.0
margin_right = 294.0 margin_right = 294.0
margin_bottom = 20.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" 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="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/Generate" to="." method="_on_Generate_pressed"]
[connection signal="pressed" from="VBoxContainer/Buttons/Cancel" to="." method="_on_Cancel_pressed"] [connection signal="pressed" from="VBoxContainer/Buttons/Cancel" to="." method="_on_Cancel_pressed"]

View file

@ -1,6 +1,7 @@
tool tool
extends WindowDialog extends WindowDialog
const HTerrain = preload("../../hterrain.gd")
const HTerrainData = preload("../../hterrain_data.gd") const HTerrainData = preload("../../hterrain_data.gd")
const HTerrainMesher = preload("../../hterrain_mesher.gd") const HTerrainMesher = preload("../../hterrain_mesher.gd")
const Util = preload("../../util/util.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 onready var _progress_bar = $VBoxContainer/Editor/Preview/ProgressBar
var _dummy_texture = load("res://addons/zylann.hterrain/tools/icons/empty.png") var _dummy_texture = load("res://addons/zylann.hterrain/tools/icons/empty.png")
var _terrain = null var _terrain : HTerrain = null
var _applying := false var _applying := false
var _generator : TextureGenerator var _generator : TextureGenerator
var _generated_textures := [null, null] var _generated_textures := [null, null]
@ -106,6 +107,30 @@ func _ready():
"range": { "min": 0.0, "max": 1.0 }, "range": { "min": 0.0, "max": 1.0 },
"default_value": 0.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": { "show_sea": {
"type": TYPE_BOOL, "type": TYPE_BOOL,
"default_value": true "default_value": true
@ -212,11 +237,19 @@ func _update_generator(preview: bool):
var preview_scale := 4.0 # As if 2049x2049 var preview_scale := 4.0 # As if 2049x2049
var sectors := [] var sectors := []
var terrain_size = 513
var additive_heightmap : Texture = null
# Get preview scale and sectors to generate. # Get preview scale and sectors to generate.
# Allowing null terrain to make it testable. # Allowing null terrain to make it testable.
if _terrain != null and _terrain.get_data() != null: var terrain_data := _terrain.get_data()
var terrain_size = _terrain.get_data().get_resolution() 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: if preview:
# When previewing the resolution does not span the entire terrain, # When previewing the resolution does not span the entire terrain,
@ -225,6 +258,15 @@ func _update_generator(preview: bool):
sectors.append(Vector2(0, 0)) sectors.append(Vector2(0, 0))
else: 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, # When we get to generate it fully, sectors are used,
# so the size or shape of the terrain doesn't matter # so the size or shape of the terrain doesn't matter
preview_scale = 1.0 preview_scale = 1.0
@ -262,12 +304,21 @@ func _update_generator(preview: bool):
p.params = { p.params = {
"u_octaves": _inspector.get_value("octaves"), "u_octaves": _inspector.get_value("octaves"),
"u_seed": _inspector.get_value("seed"), "u_seed": _inspector.get_value("seed"),
"u_scale": scale * preview_scale, "u_scale": scale,
"u_offset": base_offset_ndc / preview_scale, "u_offset": base_offset_ndc,
"u_base_height": _inspector.get_value("base_height") / preview_scale, "u_base_height": _inspector.get_value("base_height") / preview_scale,
"u_height_range": _inspector.get_value("height_range") / preview_scale, "u_height_range": _inspector.get_value("height_range") / preview_scale,
"u_roughness": _inspector.get_value("roughness"), "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) _generator.add_pass(p)

View file

@ -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/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/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/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"] [node name="GeneratorDialog" type="WindowDialog"]
margin_left = 22.0 margin_left = 22.0
margin_top = 32.0 margin_top = 32.0
margin_right = 1122.0 margin_right = 1122.0
margin_bottom = 632.0 margin_bottom = 666.0
rect_min_size = Vector2( 1100, 600 ) rect_min_size = Vector2( 1100, 600 )
window_title = "Generate terrain" window_title = "Generate terrain"
resizable = true resizable = true
@ -25,27 +26,28 @@ margin_top = 8.0
margin_right = -8.0 margin_right = -8.0
margin_bottom = -8.0 margin_bottom = -8.0
custom_constants/separation = 16 custom_constants/separation = 16
__meta__ = {
"_edit_use_anchors_": false
}
[node name="Editor" type="HBoxContainer" parent="VBoxContainer"] [node name="Editor" type="HBoxContainer" parent="VBoxContainer"]
margin_right = 1084.0 margin_right = 1084.0
margin_bottom = 548.0 margin_bottom = 584.0
size_flags_vertical = 3 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_right = 420.0
margin_bottom = 548.0 margin_bottom = 584.0
rect_min_size = Vector2( 420, 0 ) rect_min_size = Vector2( 420, 0 )
[node name="Inspector" parent="VBoxContainer/Editor/Settings" instance=ExtResource( 2 )] [node name="Inspector" parent="VBoxContainer/Editor/Settings" instance=ExtResource( 2 )]
anchor_right = 1.0 margin_right = 420.0
anchor_bottom = 1.0 margin_bottom = 584.0
margin_right = 0.0
margin_bottom = 0.0
[node name="Preview" type="Control" parent="VBoxContainer/Editor"] [node name="Preview" type="Control" parent="VBoxContainer/Editor"]
margin_left = 424.0 margin_left = 424.0
margin_right = 1084.0 margin_right = 1084.0
margin_bottom = 548.0 margin_bottom = 584.0
size_flags_horizontal = 3 size_flags_horizontal = 3
[node name="TerrainPreview" parent="VBoxContainer/Editor/Preview" instance=ExtResource( 3 )] [node name="TerrainPreview" parent="VBoxContainer/Editor/Preview" instance=ExtResource( 3 )]
@ -66,9 +68,9 @@ margin_top = -16.0
step = 1.0 step = 1.0
[node name="Choices" type="HBoxContainer" parent="VBoxContainer"] [node name="Choices" type="HBoxContainer" parent="VBoxContainer"]
margin_top = 564.0 margin_top = 600.0
margin_right = 1084.0 margin_right = 1084.0
margin_bottom = 584.0 margin_bottom = 620.0
custom_constants/separation = 32 custom_constants/separation = 32
alignment = 1 alignment = 1
@ -83,6 +85,9 @@ margin_left = 555.0
margin_right = 609.0 margin_right = 609.0
margin_bottom = 20.0 margin_bottom = 20.0
text = "Cancel" 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="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="dragged" from="VBoxContainer/Editor/Preview/TerrainPreview" to="." method="_on_TerrainPreview_dragged"]
[connection signal="pressed" from="VBoxContainer/Choices/ApplyButton" to="." method="_on_ApplyButton_pressed"] [connection signal="pressed" from="VBoxContainer/Choices/ApplyButton" to="." method="_on_ApplyButton_pressed"]

View file

@ -8,9 +8,21 @@ uniform int u_seed;
uniform int u_octaves = 5; uniform int u_octaves = 5;
uniform float u_roughness = 0.5; uniform float u_roughness = 0.5;
uniform float u_curve = 1.0; 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_offset;
uniform vec2 u_uv_scale = vec2(1.0, 1.0); 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: // Perlin noise source:
// https://github.com/curly-brace/Godot-3.0-Noise-Shaders // https://github.com/curly-brace/Godot-3.0-Noise-Shaders
@ -103,22 +115,91 @@ float get_fractal_noise(vec2 uv) {
return gs; return gs;
} }
float get_height(vec2 uv) { // x is a ratio in 0..1
float h = 0.5 + 0.5 * get_fractal_noise(uv); float get_island_curve(float x) {
h = pow(h, u_curve); return smoothstep(min(0.999, u_island_sharpness), 1.0, x);
h = u_base_height + h * u_height_range; // 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; return h;
} }
void fragment() { 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 float h = get_height(uv_tile * u_tile_size);
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);
COLOR = vec4(h, h, h, 1.0); COLOR = vec4(h, h, h, 1.0);
} }

View file

@ -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/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/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"] [node name="WindowDialog" type="WindowDialog"]
visible = true visible = true
@ -13,6 +14,9 @@ rect_min_size = Vector2( 500, 380 )
window_title = "Import maps" window_title = "Import maps"
resizable = true resizable = true
script = ExtResource( 1 ) script = ExtResource( 1 )
__meta__ = {
"_edit_use_anchors_": false
}
[node name="VBoxContainer" type="VBoxContainer" parent="."] [node name="VBoxContainer" type="VBoxContainer" parent="."]
anchor_right = 1.0 anchor_right = 1.0
@ -93,6 +97,9 @@ margin_left = 312.0
margin_right = 366.0 margin_right = 366.0
margin_bottom = 20.0 margin_bottom = 20.0
text = "Cancel" text = "Cancel"
[node name="DialogFitter" parent="." instance=ExtResource( 3 )]
[connection signal="property_changed" from="VBoxContainer/Inspector" to="." method="_on_Inspector_property_changed"] [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/CheckButton" to="." method="_on_CheckButton_pressed"]
[connection signal="pressed" from="VBoxContainer/ButtonsArea/ImportButton" to="." method="_on_ImportButton_pressed"] [connection signal="pressed" from="VBoxContainer/ButtonsArea/ImportButton" to="." method="_on_ImportButton_pressed"]

View file

@ -422,7 +422,8 @@ func _dummy_setter(v):
func _on_ask_load_texture(key): 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): func _open_file_dialog(filters, callback, binds, access):
@ -430,7 +431,8 @@ func _open_file_dialog(filters, callback, binds, access):
_file_dialog.clear_filters() _file_dialog.clear_filters()
for filter in filters: for filter in filters:
_file_dialog.add_filter(filter) _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.connect("file_selected", self, callback, binds)
_file_dialog.popup_centered_ratio(0.7) _file_dialog.popup_centered_ratio(0.7)

View file

@ -2,70 +2,23 @@
[ext_resource path="res://addons/zylann.hterrain/tools/inspector/inspector.gd" type="Script" id=1] [ext_resource path="res://addons/zylann.hterrain/tools/inspector/inspector.gd" type="Script" id=1]
[node name="Inspector" type="Control" index="0"] [node name="Inspector" type="VBoxContainer"]
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 0.0
anchor_bottom = 0.0
margin_right = 348.0 margin_right = 348.0
margin_bottom = 383.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 ) script = ExtResource( 1 )
_sections_unfolded = [ "custom_constants" ]
[node name="GridContainer" type="GridContainer" parent="." index="0"] [node name="GridContainer" type="GridContainer" parent="."]
margin_right = 348.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
custom_constants/vseparation = 4 custom_constants/vseparation = 4
custom_constants/hseparation = 8 custom_constants/hseparation = 8
columns = 2 columns = 2
_sections_unfolded = [ "Anchor", "Margin", "custom_constants" ]
[node name="OpenFileDialog" type="FileDialog" parent="." index="1"] [node name="OpenFileDialog" type="FileDialog" parent="."]
visible = false
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 0.0
anchor_bottom = 0.0
margin_left = 261.0 margin_left = 261.0
margin_top = 150.0 margin_top = 150.0
margin_right = 710.0 margin_right = 710.0
margin_bottom = 426.0 margin_bottom = 426.0
rect_min_size = Vector2( 400, 300 ) rect_min_size = Vector2( 400, 300 )
rect_pivot_offset = Vector2( 0, 0 ) window_title = "Open a File"
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"
resizable = true resizable = true
dialog_hide_on_ok = false
mode_overrides_title = true
mode = 0 mode = 0
access = 0
filters = PoolStringArray( )
show_hidden_files = false
current_dir = "res://"
current_file = ""
current_path = "res://"
_sections_unfolded = [ "Rect" ]

View file

@ -63,6 +63,10 @@ func get_import_options(preset_index: int) -> Array:
{ {
"name": "flags/mipmaps", "name": "flags/mipmaps",
"default_value": true "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["compress/mode"],
options["flags/repeat"], options["flags/repeat"],
options["flags/filter"], options["flags/filter"],
options["flags/mipmaps"]) options["flags/mipmaps"],
options["flags/anisotropic"])
if not result.success: if not result.success:
return Result.new(false, return Result.new(false,

View file

@ -63,6 +63,10 @@ func get_import_options(preset_index: int) -> Array:
{ {
"name": "flags/mipmaps", "name": "flags/mipmaps",
"default_value": true "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) .with_value(result.value)
var image : Image = result.value var image : Image = result.value
result = StreamTextureImporter.import( result = StreamTextureImporter.import(
p_source_path, p_source_path,
@ -127,7 +132,8 @@ func _import(p_source_path: String, p_save_path: String, options: Dictionary,
options["compress/mode"], options["compress/mode"],
options["flags/repeat"], options["flags/repeat"],
options["flags/filter"], options["flags/filter"],
options["flags/mipmaps"]) options["flags/mipmaps"],
options["flags/anisotropic"])
if not result.success: if not result.success:
return Result.new(false, return Result.new(false,

View file

@ -43,14 +43,15 @@ static func import(
p_compress_mode: int, p_compress_mode: int,
p_repeat: int, p_repeat: int,
p_filter: bool, p_filter: bool,
p_mipmaps: bool) -> Result: p_mipmaps: bool,
p_anisotropic: bool) -> Result:
var compress_mode := p_compress_mode var compress_mode := p_compress_mode
var lossy := 0.7 var lossy := 0.7
var repeat := p_repeat var repeat := p_repeat
var filter := p_filter var filter := p_filter
var mipmaps := p_mipmaps var mipmaps := p_mipmaps
var anisotropic := false var anisotropic := p_anisotropic
var srgb := 1 if p_contains_albedo else 2 var srgb := 1 if p_contains_albedo else 2
var fix_alpha_border := false var fix_alpha_border := false
var premult_alpha := false var premult_alpha := false

View file

@ -37,7 +37,8 @@ static func import(
p_compress_mode: int, p_compress_mode: int,
p_repeat: int, p_repeat: int,
p_filter: bool, p_filter: bool,
p_mipmaps: bool) -> Result: p_mipmaps: bool,
p_anisotropic: bool) -> Result:
var compress_mode := p_compress_mode var compress_mode := p_compress_mode
var no_bptc_if_rgb := false#p_options["compress/no_bptc_if_rgb"]; 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 tex_flags |= Texture.FLAG_MIPMAPS
if srgb == 1: if srgb == 1:
tex_flags |= Texture.FLAG_CONVERT_TO_LINEAR tex_flags |= Texture.FLAG_CONVERT_TO_LINEAR
if p_anisotropic:
tex_flags |= Texture.FLAG_ANISOTROPIC_FILTER
# Vector<Ref<Image> > slices; # Vector<Ref<Image> > slices;
# #

View file

@ -40,8 +40,8 @@ func set_camera_transform(cam_transform: Transform):
_minimap.set_camera_transform(cam_transform) _minimap.set_camera_transform(cam_transform)
func set_brush(brush): func set_terrain_painter(terrain_painter):
_brush_editor.set_brush(brush) _brush_editor.set_terrain_painter(terrain_painter)
func _on_TextureEditor_texture_selected(index): func _on_TextureEditor_texture_selected(index):

View file

@ -10,7 +10,7 @@ const HTerrainTextureSet = preload("../hterrain_texture_set.gd")
const PackedTextureImporter = preload("./packed_textures/packed_texture_importer.gd") const PackedTextureImporter = preload("./packed_textures/packed_texture_importer.gd")
const PackedTextureArrayImporter = preload("./packed_textures/packed_texture_array_importer.gd") const PackedTextureArrayImporter = preload("./packed_textures/packed_texture_array_importer.gd")
const PreviewGenerator = preload("./preview_generator.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 BrushDecal = preload("./brush/decal.gd")
const Util = preload("../util/util.gd") const Util = preload("../util/util.gd")
const EditorUtil = preload("./util/editor_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_importer := PackedTextureImporter.new()
var _packed_texture_array_importer := PackedTextureArrayImporter.new() var _packed_texture_array_importer := PackedTextureArrayImporter.new()
var _brush : Brush = null var _terrain_painter : TerrainPainter = null
var _brush_decal : BrushDecal = null var _brush_decal : BrushDecal = null
var _mouse_pressed := false var _mouse_pressed := false
#var _pending_paint_action = null #var _pending_paint_action = null
@ -106,13 +106,13 @@ func _enter_tree():
_preview_generator = PreviewGenerator.new() _preview_generator = PreviewGenerator.new()
get_editor_interface().get_resource_previewer().add_preview_generator(_preview_generator) get_editor_interface().get_resource_previewer().add_preview_generator(_preview_generator)
_brush = Brush.new() _terrain_painter = TerrainPainter.new()
_brush.set_brush_size(5) _terrain_painter.set_brush_size(5)
_brush.connect("changed", self, "_on_brush_changed") _terrain_painter.get_brush().connect("size_changed", self, "_on_brush_size_changed")
add_child(_brush) add_child(_terrain_painter)
_brush_decal = BrushDecal.new() _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") _image_cache = ImageFileCache.new("user://temp_hterrain_image_cache")
@ -124,7 +124,7 @@ func _enter_tree():
_panel.hide() _panel.hide()
add_control_to_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_BOTTOM, _panel) add_control_to_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_BOTTOM, _panel)
# Apparently _ready() still isn't called at this point... # 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.call_deferred("setup_dialogs", base_control)
_panel.set_undo_redo(get_undo_redo()) _panel.set_undo_redo(get_undo_redo())
_panel.set_image_cache(_image_cache) _panel.set_image_cache(_image_cache)
@ -165,44 +165,43 @@ func _enter_tree():
_menu_button = menu _menu_button = menu
var mode_icons := {} var mode_icons := {}
mode_icons[Brush.MODE_RAISE] = get_icon("heightmap_raise") mode_icons[TerrainPainter.MODE_RAISE] = get_icon("heightmap_raise")
mode_icons[Brush.MODE_LOWER] = get_icon("heightmap_lower") mode_icons[TerrainPainter.MODE_LOWER] = get_icon("heightmap_lower")
mode_icons[Brush.MODE_SMOOTH] = get_icon("heightmap_smooth") mode_icons[TerrainPainter.MODE_SMOOTH] = get_icon("heightmap_smooth")
mode_icons[Brush.MODE_FLATTEN] = get_icon("heightmap_flatten") mode_icons[TerrainPainter.MODE_FLATTEN] = get_icon("heightmap_flatten")
# TODO Have different icons mode_icons[TerrainPainter.MODE_SPLAT] = get_icon("heightmap_paint")
mode_icons[Brush.MODE_SPLAT] = get_icon("heightmap_paint") mode_icons[TerrainPainter.MODE_COLOR] = get_icon("heightmap_color")
mode_icons[Brush.MODE_COLOR] = get_icon("heightmap_color") mode_icons[TerrainPainter.MODE_DETAIL] = get_icon("grass")
mode_icons[Brush.MODE_DETAIL] = get_icon("grass") mode_icons[TerrainPainter.MODE_MASK] = get_icon("heightmap_mask")
mode_icons[Brush.MODE_MASK] = get_icon("heightmap_mask") mode_icons[TerrainPainter.MODE_LEVEL] = get_icon("heightmap_level")
mode_icons[Brush.MODE_LEVEL] = get_icon("heightmap_level") mode_icons[TerrainPainter.MODE_ERODE] = get_icon("heightmap_erode")
mode_icons[Brush.MODE_ERODE] = get_icon("heightmap_erode")
var mode_tooltips := {} var mode_tooltips := {}
mode_tooltips[Brush.MODE_RAISE] = "Raise height" mode_tooltips[TerrainPainter.MODE_RAISE] = "Raise height"
mode_tooltips[Brush.MODE_LOWER] = "Lower height" mode_tooltips[TerrainPainter.MODE_LOWER] = "Lower height"
mode_tooltips[Brush.MODE_SMOOTH] = "Smooth height" mode_tooltips[TerrainPainter.MODE_SMOOTH] = "Smooth height"
mode_tooltips[Brush.MODE_FLATTEN] = "Flatten (flatten to a specific height)" mode_tooltips[TerrainPainter.MODE_FLATTEN] = "Flatten (flatten to a specific height)"
mode_tooltips[Brush.MODE_SPLAT] = "Texture paint" mode_tooltips[TerrainPainter.MODE_SPLAT] = "Texture paint"
mode_tooltips[Brush.MODE_COLOR] = "Color paint" mode_tooltips[TerrainPainter.MODE_COLOR] = "Color paint"
mode_tooltips[Brush.MODE_DETAIL] = "Grass paint" mode_tooltips[TerrainPainter.MODE_DETAIL] = "Grass paint"
mode_tooltips[Brush.MODE_MASK] = "Cut holes" mode_tooltips[TerrainPainter.MODE_MASK] = "Cut holes"
mode_tooltips[Brush.MODE_LEVEL] = "Level (smoothly flattens to average)" mode_tooltips[TerrainPainter.MODE_LEVEL] = "Level (smoothly flattens to average)"
mode_tooltips[Brush.MODE_ERODE] = "Erode" mode_tooltips[TerrainPainter.MODE_ERODE] = "Erode"
_toolbar.add_child(VSeparator.new()) _toolbar.add_child(VSeparator.new())
# I want modes to be in that order in the GUI # I want modes to be in that order in the GUI
var ordered_brush_modes := [ var ordered_brush_modes := [
Brush.MODE_RAISE, TerrainPainter.MODE_RAISE,
Brush.MODE_LOWER, TerrainPainter.MODE_LOWER,
Brush.MODE_SMOOTH, TerrainPainter.MODE_SMOOTH,
Brush.MODE_LEVEL, TerrainPainter.MODE_LEVEL,
Brush.MODE_FLATTEN, TerrainPainter.MODE_FLATTEN,
Brush.MODE_ERODE, TerrainPainter.MODE_ERODE,
Brush.MODE_SPLAT, TerrainPainter.MODE_SPLAT,
Brush.MODE_COLOR, TerrainPainter.MODE_COLOR,
Brush.MODE_DETAIL, TerrainPainter.MODE_DETAIL,
Brush.MODE_MASK TerrainPainter.MODE_MASK
] ]
var mode_group := ButtonGroup.new() var mode_group := ButtonGroup.new()
@ -214,7 +213,7 @@ func _enter_tree():
button.set_toggle_mode(true) button.set_toggle_mode(true)
button.set_button_group(mode_group) button.set_button_group(mode_group)
if mode == _brush.get_mode(): if mode == _terrain_painter.get_mode():
button.set_pressed(true) button.set_pressed(true)
button.connect("pressed", self, "_on_mode_selected", [mode]) button.connect("pressed", self, "_on_mode_selected", [mode])
@ -362,7 +361,7 @@ func edit(object):
_panel.set_terrain(_node) _panel.set_terrain(_node)
_generator_dialog.set_terrain(_node) _generator_dialog.set_terrain(_node)
_import_dialog.set_terrain(_node) _import_dialog.set_terrain(_node)
_brush.set_terrain(_node) _terrain_painter.set_terrain(_node)
_brush_decal.set_terrain(_node) _brush_decal.set_terrain(_node)
_generate_mesh_dialog.set_terrain(_node) _generate_mesh_dialog.set_terrain(_node)
_resize_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) var has_details = (data.get_map_count(HTerrainData.CHANNEL_DETAIL) > 0)
if has_details: if has_details:
var button = _toolbar_brush_buttons[Brush.MODE_DETAIL] var button = _toolbar_brush_buttons[TerrainPainter.MODE_DETAIL]
button.disabled = false button.disabled = false
else: else:
var button = _toolbar_brush_buttons[Brush.MODE_DETAIL] var button = _toolbar_brush_buttons[TerrainPainter.MODE_DETAIL]
if button.pressed: if button.pressed:
_select_brush_mode(Brush.MODE_RAISE) _select_brush_mode(TerrainPainter.MODE_RAISE)
button.disabled = true 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 # because they are used in navigation schemes
if (not mb.control) and (not mb.alt) and mb.button_index == BUTTON_LEFT: if (not mb.control) and (not mb.alt) and mb.button_index == BUTTON_LEFT:
if mb.pressed: 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 _mouse_pressed = true
captured_event = true captured_event = true
@ -477,17 +480,19 @@ func forward_spatial_gui_input(p_camera: Camera, p_event: InputEvent) -> bool:
if not _mouse_pressed: if not _mouse_pressed:
# Just finished painting # Just finished painting
_pending_paint_commit = true _pending_paint_commit = true
_terrain_painter.get_brush().on_paint_end()
if _brush.get_mode() == Brush.MODE_FLATTEN and _brush.has_meta("pick_height") \ if _terrain_painter.get_mode() == TerrainPainter.MODE_FLATTEN \
and _brush.get_meta("pick_height"): and _terrain_painter.has_meta("pick_height") \
_brush.set_meta("pick_height", false) and _terrain_painter.get_meta("pick_height"):
_terrain_painter.set_meta("pick_height", false)
# Pick height # Pick height
var hit_pos_in_cells = _get_pointed_cell_position(mb.position, p_camera) var hit_pos_in_cells = _get_pointed_cell_position(mb.position, p_camera)
if hit_pos_in_cells != null: if hit_pos_in_cells != null:
var h = _node.get_data().get_height_at( var h = _node.get_data().get_height_at(
int(hit_pos_in_cells.x), int(hit_pos_in_cells.y)) int(hit_pos_in_cells.x), int(hit_pos_in_cells.y))
_logger.debug("Picking height {0}".format([h])) _logger.debug("Picking height {0}".format([h]))
_brush.set_flatten_height(h) _terrain_painter.set_flatten_height(h)
elif p_event is InputEventMouseMotion: elif p_event is InputEventMouseMotion:
var mm = p_event var mm = p_event
@ -497,7 +502,7 @@ func forward_spatial_gui_input(p_camera: Camera, p_event: InputEvent) -> bool:
if _mouse_pressed: if _mouse_pressed:
if Input.is_mouse_button_pressed(BUTTON_LEFT): 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 captured_event = true
# This is in case the data or textures change as the user edits the terrain, # 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 _pending_paint_commit:
if has_data: 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 _pending_paint_commit = false
_logger.debug("Paint completed") if _terrain_painter.has_modified_chunks():
var changes : Dictionary = _brush.commit() _logger.debug("Paint completed")
_paint_completed(changes) var changes : Dictionary = _terrain_painter.commit()
_paint_completed(changes)
else: else:
_pending_paint_commit = false _pending_paint_commit = false
@ -536,6 +542,8 @@ func _paint_completed(changes: Dictionary):
assert(heightmap_data != null) assert(heightmap_data != null)
var chunk_positions : Array = changes.chunk_positions 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 changed_maps : Array = changes.maps
var action_name := "Modify HTerrainData " var action_name := "Modify HTerrainData "
@ -548,7 +556,7 @@ func _paint_completed(changes: Dictionary):
var redo_maps := [] var redo_maps := []
var undo_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: for map in changed_maps:
# Cache images to disk so RAM does not continuously go up (or at least much slower) # 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): func _on_mode_selected(mode: int):
_logger.debug(str("On mode selected ", mode)) _logger.debug(str("On mode selected ", mode))
_brush.set_mode(mode) _terrain_painter.set_mode(mode)
_panel.set_brush_editor_display_mode(mode) _panel.set_brush_editor_display_mode(mode)
func _on_texture_selected(index: int): func _on_texture_selected(index: int):
# Switch to texture paint mode when a texture is selected # Switch to texture paint mode when a texture is selected
_select_brush_mode(Brush.MODE_SPLAT) _select_brush_mode(TerrainPainter.MODE_SPLAT)
_brush.set_texture_index(index) _terrain_painter.set_texture_index(index)
func _on_detail_selected(index: int): func _on_detail_selected(index: int):
# Switch to detail paint mode when a detail item is selected # Switch to detail paint mode when a detail item is selected
_select_brush_mode(Brush.MODE_DETAIL) _select_brush_mode(TerrainPainter.MODE_DETAIL)
_brush.set_detail_index(index) _terrain_painter.set_detail_index(index)
func _select_brush_mode(mode: int): func _select_brush_mode(mode: int):
@ -782,8 +790,8 @@ func _on_permanent_change_performed(message: String):
ur.commit_action() ur.commit_action()
func _on_brush_changed(): func _on_brush_size_changed(size):
_brush_decal.set_size(_brush.get_brush_size()) _brush_decal.set_size(size)
func _on_Panel_edit_texture_pressed(index: int): func _on_Panel_edit_texture_pressed(index: int):

View file

@ -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/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_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_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_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/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"] [node name="ResizeDialog" type="WindowDialog"]
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 0.0
anchor_bottom = 0.0
margin_left = 130.0 margin_left = 130.0
margin_top = 126.0 margin_top = 126.0
margin_right = 430.0 margin_right = 430.0
margin_bottom = 326.0 margin_bottom = 326.0
rect_min_size = Vector2( 300, 200 ) 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" window_title = "Resize terrain"
resizable = false
script = ExtResource( 1 ) script = ExtResource( 1 )
[node name="VBoxContainer" type="VBoxContainer" parent="." index="1"] [node name="VBoxContainer" type="VBoxContainer" parent="."]
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 1.0 anchor_right = 1.0
anchor_bottom = 1.0 anchor_bottom = 1.0
margin_left = 8.0 margin_left = 8.0
margin_top = 8.0 margin_top = 8.0
margin_right = -8.0 margin_right = -8.0
margin_bottom = -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 custom_constants/separation = 24
alignment = 0
_sections_unfolded = [ "Margin", "custom_constants" ]
[node name="GridContainer" type="GridContainer" parent="VBoxContainer" index="0"] [node name="GridContainer" type="GridContainer" parent="VBoxContainer"]
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 0.0
anchor_bottom = 0.0
margin_right = 284.0 margin_right = 284.0
margin_bottom = 126.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 columns = 2
[node name="Label" type="Label" parent="VBoxContainer/GridContainer" index="0"] [node name="Label" type="Label" parent="VBoxContainer/GridContainer"]
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 0.0
anchor_bottom = 0.0
margin_top = 3.0 margin_top = 3.0
margin_right = 68.0 margin_right = 68.0
margin_bottom = 17.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" text = "Resolution"
percent_visible = 1.0
lines_skipped = 0
max_lines_visible = -1
[node name="ResolutionDropdown" type="OptionButton" parent="VBoxContainer/GridContainer" index="1"] [node name="ResolutionDropdown" type="OptionButton" parent="VBoxContainer/GridContainer"]
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 0.0
anchor_bottom = 0.0
margin_left = 72.0 margin_left = 72.0
margin_right = 284.0 margin_right = 284.0
margin_bottom = 20.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_horizontal = 3
size_flags_vertical = 1
toggle_mode = false 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"] [node name="Label3" type="Label" parent="VBoxContainer/GridContainer"]
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 0.0
anchor_bottom = 0.0
margin_top = 29.0 margin_top = 29.0
margin_right = 68.0 margin_right = 68.0
margin_bottom = 43.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" text = "Stretch"
percent_visible = 1.0
lines_skipped = 0
max_lines_visible = -1
[node name="StretchCheckBox" type="CheckBox" parent="VBoxContainer/GridContainer" index="3"] [node name="StretchCheckBox" type="CheckBox" parent="VBoxContainer/GridContainer"]
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 0.0
anchor_bottom = 0.0
margin_left = 72.0 margin_left = 72.0
margin_top = 24.0 margin_top = 24.0
margin_right = 284.0 margin_right = 284.0
margin_bottom = 48.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"] [node name="Label2" type="Label" parent="VBoxContainer/GridContainer"]
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 0.0
anchor_bottom = 0.0
margin_top = 82.0 margin_top = 82.0
margin_right = 68.0 margin_right = 68.0
margin_bottom = 96.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" text = "Direction"
percent_visible = 1.0
lines_skipped = 0
max_lines_visible = -1
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/GridContainer" index="5"] [node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/GridContainer"]
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 0.0
anchor_bottom = 0.0
margin_left = 72.0 margin_left = 72.0
margin_top = 52.0 margin_top = 52.0
margin_right = 284.0 margin_right = 284.0
margin_bottom = 126.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"] [node name="AnchorControl" type="GridContainer" parent="VBoxContainer/GridContainer/HBoxContainer"]
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 0.0
anchor_bottom = 0.0
margin_right = 92.0 margin_right = 92.0
margin_bottom = 74.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 columns = 3
[node name="TopLeftButton" type="Button" parent="VBoxContainer/GridContainer/HBoxContainer/AnchorControl" index="0"] [node name="TopLeftButton" type="Button" parent="VBoxContainer/GridContainer/HBoxContainer/AnchorControl"]
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 0.0
anchor_bottom = 0.0
margin_right = 28.0 margin_right = 28.0
margin_bottom = 22.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 ) icon = ExtResource( 2 )
flat = false
align = 1
[node name="TopButton" type="Button" parent="VBoxContainer/GridContainer/HBoxContainer/AnchorControl" index="1"] [node name="TopButton" type="Button" parent="VBoxContainer/GridContainer/HBoxContainer/AnchorControl"]
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 0.0
anchor_bottom = 0.0
margin_left = 32.0 margin_left = 32.0
margin_right = 60.0 margin_right = 60.0
margin_bottom = 22.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 ) icon = ExtResource( 2 )
flat = false
align = 1
[node name="TopRightButton" type="Button" parent="VBoxContainer/GridContainer/HBoxContainer/AnchorControl" index="2"] [node name="TopRightButton" type="Button" parent="VBoxContainer/GridContainer/HBoxContainer/AnchorControl"]
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 0.0
anchor_bottom = 0.0
margin_left = 64.0 margin_left = 64.0
margin_right = 92.0 margin_right = 92.0
margin_bottom = 22.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 ) icon = ExtResource( 2 )
flat = false
align = 1
[node name="LeftButton" type="Button" parent="VBoxContainer/GridContainer/HBoxContainer/AnchorControl" index="3"] [node name="LeftButton" type="Button" parent="VBoxContainer/GridContainer/HBoxContainer/AnchorControl"]
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 0.0
anchor_bottom = 0.0
margin_top = 26.0 margin_top = 26.0
margin_right = 28.0 margin_right = 28.0
margin_bottom = 48.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 ) icon = ExtResource( 2 )
flat = false
align = 1
[node name="CenterButton" type="Button" parent="VBoxContainer/GridContainer/HBoxContainer/AnchorControl" index="4"] [node name="CenterButton" type="Button" parent="VBoxContainer/GridContainer/HBoxContainer/AnchorControl"]
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 0.0
anchor_bottom = 0.0
margin_left = 32.0 margin_left = 32.0
margin_top = 26.0 margin_top = 26.0
margin_right = 60.0 margin_right = 60.0
margin_bottom = 48.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 ) icon = ExtResource( 2 )
flat = false
align = 1
[node name="RightButton" type="Button" parent="VBoxContainer/GridContainer/HBoxContainer/AnchorControl" index="5"] [node name="RightButton" type="Button" parent="VBoxContainer/GridContainer/HBoxContainer/AnchorControl"]
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 0.0
anchor_bottom = 0.0
margin_left = 64.0 margin_left = 64.0
margin_top = 26.0 margin_top = 26.0
margin_right = 92.0 margin_right = 92.0
margin_bottom = 48.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 ) icon = ExtResource( 2 )
flat = false
align = 1
[node name="ButtomLeftButton" type="Button" parent="VBoxContainer/GridContainer/HBoxContainer/AnchorControl" index="6"] [node name="ButtomLeftButton" type="Button" parent="VBoxContainer/GridContainer/HBoxContainer/AnchorControl"]
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 0.0
anchor_bottom = 0.0
margin_top = 52.0 margin_top = 52.0
margin_right = 28.0 margin_right = 28.0
margin_bottom = 74.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 ) icon = ExtResource( 2 )
flat = false
align = 1
[node name="ButtomButton" type="Button" parent="VBoxContainer/GridContainer/HBoxContainer/AnchorControl" index="7"] [node name="ButtomButton" type="Button" parent="VBoxContainer/GridContainer/HBoxContainer/AnchorControl"]
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 0.0
anchor_bottom = 0.0
margin_left = 32.0 margin_left = 32.0
margin_top = 52.0 margin_top = 52.0
margin_right = 60.0 margin_right = 60.0
margin_bottom = 74.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 ) icon = ExtResource( 2 )
flat = false
align = 1
[node name="BottomRightButton" type="Button" parent="VBoxContainer/GridContainer/HBoxContainer/AnchorControl" index="8"] [node name="BottomRightButton" type="Button" parent="VBoxContainer/GridContainer/HBoxContainer/AnchorControl"]
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 0.0
anchor_bottom = 0.0
margin_left = 64.0 margin_left = 64.0
margin_top = 52.0 margin_top = 52.0
margin_right = 92.0 margin_right = 92.0
margin_bottom = 74.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 ) icon = ExtResource( 2 )
flat = false
align = 1
[node name="Reference" type="Control" parent="VBoxContainer/GridContainer/HBoxContainer" index="1"] [node name="Reference" type="Control" parent="VBoxContainer/GridContainer/HBoxContainer"]
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 0.0
anchor_bottom = 0.0
margin_left = 96.0 margin_left = 96.0
margin_right = 196.0 margin_right = 196.0
margin_bottom = 74.0 margin_bottom = 74.0
rect_min_size = Vector2( 100, 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 ) modulate = Color( 1, 0.292969, 0.292969, 1 )
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 1.0 anchor_right = 1.0
anchor_bottom = 0.0
margin_left = 9.0 margin_left = 9.0
margin_bottom = 16.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 ) 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 ) modulate = Color( 0.292969, 0.602295, 1, 1 )
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 0.0
anchor_bottom = 1.0 anchor_bottom = 1.0
margin_top = 10.0 margin_top = 10.0
margin_right = 16.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 ) texture = ExtResource( 4 )
stretch_mode = 0
_sections_unfolded = [ "Visibility" ]
[node name="ZLabel" type="Label" parent="VBoxContainer/GridContainer/HBoxContainer/Reference" index="2"] [node name="ZLabel" type="Label" parent="VBoxContainer/GridContainer/HBoxContainer/Reference"]
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 0.0
anchor_bottom = 0.0
margin_left = 14.0 margin_left = 14.0
margin_top = 54.0 margin_top = 54.0
margin_right = 22.0 margin_right = 22.0
margin_bottom = 68.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" 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"] [node name="XLabel" type="Label" parent="VBoxContainer/GridContainer/HBoxContainer/Reference"]
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 0.0
anchor_bottom = 0.0
margin_left = 52.0 margin_left = 52.0
margin_top = 14.0 margin_top = 14.0
margin_right = 60.0 margin_right = 60.0
margin_bottom = 28.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" 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"] [node name="Origin" type="TextureRect" parent="VBoxContainer/GridContainer/HBoxContainer/Reference"]
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 0.0
anchor_bottom = 0.0
margin_left = 3.0 margin_left = 3.0
margin_top = 4.0 margin_top = 4.0
margin_right = 11.0 margin_right = 11.0
margin_bottom = 12.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 ) texture = ExtResource( 5 )
stretch_mode = 0
_sections_unfolded = [ "Anchor", "Margin" ]
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer" index="1"] [node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"]
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 0.0
anchor_bottom = 0.0
margin_top = 150.0 margin_top = 150.0
margin_right = 284.0 margin_right = 284.0
margin_bottom = 170.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 custom_constants/separation = 16
alignment = 1 alignment = 1
_sections_unfolded = [ "Rect", "custom_constants" ]
[node name="ApplyButton" type="Button" parent="VBoxContainer/HBoxContainer" index="0"] [node name="ApplyButton" type="Button" parent="VBoxContainer/HBoxContainer"]
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 0.0
anchor_bottom = 0.0
margin_left = 51.0 margin_left = 51.0
margin_right = 163.0 margin_right = 163.0
margin_bottom = 20.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)" text = "Apply (no undo)"
flat = false
align = 1
[node name="CancelButton" type="Button" parent="VBoxContainer/HBoxContainer" index="1"] [node name="CancelButton" type="Button" parent="VBoxContainer/HBoxContainer"]
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 0.0
anchor_bottom = 0.0
margin_left = 179.0 margin_left = 179.0
margin_right = 233.0 margin_right = 233.0
margin_bottom = 20.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" 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="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="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/ApplyButton" to="." method="_on_ApplyButton_pressed"]
[connection signal="pressed" from="VBoxContainer/HBoxContainer/CancelButton" to="." method="_on_CancelButton_pressed"] [connection signal="pressed" from="VBoxContainer/HBoxContainer/CancelButton" to="." method="_on_CancelButton_pressed"]

View file

@ -347,17 +347,19 @@ func _set_texture_array_action(slot_index: int, texture_array: TextureArray, typ
# See https://github.com/godotengine/godot/issues/36895 # See https://github.com/godotengine/godot/issues/36895
if texture_array == null: if texture_array == null:
_undo_redo.add_do_method(_texture_set, "set_texture_array_null", type) _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: else:
_undo_redo.add_do_method(_texture_set, "set_texture_array", type, texture_array) _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 # TODO This branch only exists because of a flaw in UndoRedo
# See https://github.com/godotengine/godot/issues/36895 # See https://github.com/godotengine/godot/issues/36895
if prev_texture_array == null: if prev_texture_array == null:
_undo_redo.add_undo_method(_texture_set, "set_texture_array_null", type) _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: else:
_undo_redo.add_undo_method(_texture_set, "set_texture_array", type, prev_texture_array) _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() _undo_redo.commit_action()
@ -374,7 +376,11 @@ func _on_LoadTextureArrayDialog_file_selected(fpath: String):
assert(_texture_set.get_mode() == HTerrainTextureSet.MODE_TEXTURE_ARRAYS) assert(_texture_set.get_mode() == HTerrainTextureSet.MODE_TEXTURE_ARRAYS)
var texture_array = load(fpath) var texture_array = load(fpath)
assert(texture_array != null) 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) _set_texture_array_action(slot_index, texture_array, _load_texture_type)

View file

@ -250,7 +250,7 @@ func set_texture_set(texture_set: HTerrainTextureSet):
while slot_index >= len(_slots_data): while slot_index >= len(_slots_data):
var slot = Slot.new() var slot = Slot.new()
_slots_data[slot_index] = slot _slots_data.append(slot)
var slot = _slots_data[slot_index] var slot = _slots_data[slot_index]

View file

@ -31,4 +31,23 @@ func _fit_to_contents():
var margin : Vector2 = child.get_rect().position var margin : Vector2 = child.get_rect().position
#print("Fitting ", dialog.get_path(), " from ", dialog.rect_size, #print("Fitting ", dialog.get_path(), " from ", dialog.rect_size,
# " to ", child_rect.size + margin * 2.0) # " 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))

View file

@ -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

View file

@ -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
}

View file

@ -38,3 +38,6 @@ func set_material_override(material: Material):
func set_aabb(aabb: AABB): func set_aabb(aabb: AABB):
VisualServer.instance_set_custom_aabb(_multimesh_instance, aabb) VisualServer.instance_set_custom_aabb(_multimesh_instance, aabb)
func set_layer_mask(mask: int):
VisualServer.instance_set_layer_mask(_multimesh_instance, mask)