Updating Zylann plugin with latest stable version
This commit is contained in:
parent
9d0cee5cb0
commit
0527866fc6
60 changed files with 2723 additions and 1333 deletions
|
@ -11,6 +11,15 @@ It is entirely built on top of the `VisualServer` scripting API, which means it
|
|||
![Screenshot of the editor with the plugin enabled and arrows showing where UIs are](images/overview.png)
|
||||
|
||||
|
||||
### Video tutorials
|
||||
|
||||
This written doc should be the most up to date and precise information, but video tutorials exist for a quick start.
|
||||
|
||||
- [Kasper's tutorial](https://www.youtube.com/watch?v=Af1f2JPvSIs) about version 1.5.2 (16 Jan 2021)
|
||||
- [GamesFromScratch presentation](https://www.youtube.com/watch?v=jYVO0-_sXZs), also featuring the [WaterWays](https://github.com/Arnklit/WaterGenGodot) plugin (23 dec 2020)
|
||||
- [qubodupDev's Tutorial](https://www.youtube.com/watch?v=k_ISq6JyVSs) about version 1.3.3 (27 aug 2020)
|
||||
- [Old tutorial](https://www.youtube.com/watch?v=eZuvfIHDeT4&) about version 0.8 (10 aug 2018! A lot is outdated in it but let's keep it here for the record)
|
||||
|
||||
### How to install
|
||||
|
||||
You will need to use Godot 3.1 or later. It is best to use latest stable 3.x version (Godot 4 is not supported yet).
|
||||
|
@ -90,7 +99,7 @@ Sculpting
|
|||
|
||||
### Brush types
|
||||
|
||||
The default terrain is flat, but you may want to create hills and mountains. Because it uses a heightmap, editing this terrain is equivalent to editing an image. Because of this, the main tool is a brush with a configurable size and shape. You can see which area will be affected inside a 3D red circle appearing under your mouse, and you can choose how strong painting is by changing the `strength` slider.
|
||||
The default terrain is flat, but you may want to create hills and mountains. Because it uses a heightmap, editing this terrain is equivalent to editing an image. Because of this, the main tool is a brush with a configurable size and shape. You can see which area will be affected inside a 3D red circle appearing under your mouse, and you can choose how strong painting is by changing the `Brush opacity` slider.
|
||||
|
||||
![Screenshot of the brush widget](images/brush_editor.png)
|
||||
|
||||
|
@ -118,7 +127,7 @@ As you sculpt, the plugin automatically recomputes normals of the terrain, and s
|
|||
You can enable or disable collisions by checking the `Collisions enabled` property in the inspector.
|
||||
|
||||
Heightmap-based terrains usually implement collisions directly using the heightmap, which saves a lot of computations compared to a classic mesh collider.
|
||||
This plugin depends on the **Bullet Physics** integration in Godot, which does have a height-field collider. **Godot Physics** does not support it, so you may want to make sure Bullet is enabled in your project settings:
|
||||
This plugin depends on the **Bullet Physics** integration in Godot, which does have a height-field collider. **Godot Physics** does not support it until version 3.4, so if you use an older version, you may want to make sure Bullet is enabled in your project settings:
|
||||
|
||||
![Screenshot of the option to choose physics engines in project settings](images/choose_bullet_physics.png)
|
||||
|
||||
|
@ -180,7 +189,8 @@ This magic is done with a single shader, i.e a single `ShaderMaterial` in Godot'
|
|||
There are mainly 3 families of shaders this plugin supports:
|
||||
|
||||
- `CLASSIC4`: simple shaders where each texture may be a separate resource. They are limited to 4 textures.
|
||||
- `ARRAY`: more modern shader using texture arrays, which comes with a few constraints, but allows to paint a lot more different textures.
|
||||
- `MULTISPLAT16`: more advanced shader using more splatmaps and texture arrays. It's expensive but supports up to 16 textures.
|
||||
- `ARRAY`: experimental shader also using texture arrays, which comes with constraints, but allows to paint a lot more different textures.
|
||||
- Other shaders don't need textures, like `LOW_POLY`, which only uses colors.
|
||||
|
||||
On the `HTerrain` node, there is a property called `shader_type`, which lets you choose among built-in shaders. The one you choose will define which workflow to follow: textures, or texture arrays.
|
||||
|
@ -205,6 +215,8 @@ For each texture, you may find the following types of images, common in PBR shad
|
|||
|
||||
You can find some of these textures for free at [cc0textures.com](http://cc0textures.com).
|
||||
|
||||
!!! note: Some shaders have a `Lite` and non-lite versions. One main difference between them is that `Lite` versions don't require normal maps, but the others require them. If you use a non-lite shader and forget to assign normal maps, shading will look wrong.
|
||||
|
||||
It is preferable to place those source images under a specific directory. Also, since the images will only serve as an input to generate the actual game resources, it is better to place a `.gdignore` file inside that directory. This way, Godot will not include those source files in the exported game:
|
||||
|
||||
```
|
||||
|
@ -267,7 +279,7 @@ If you use PBR textures, there might be a lot of files to assign. If you use a n
|
|||
|
||||
#### Normal maps
|
||||
|
||||
As indicated in the [Godot documentation](https://docs.godotengine.org/en/stable/tutorials/3d/spatial_material.html#normal-map), normal maps are expected to use OpenGL convention (X+, Y-, Z+). So it is possible that normalmaps you find online use a different convention.
|
||||
As indicated in the [Godot documentation](https://docs.godotengine.org/en/stable/tutorials/3d/spatial_material.html#normal-map), normal maps are expected to use OpenGL convention (X+, Y+, Z+). So it is possible that normalmaps you find online use a different convention.
|
||||
|
||||
To help with this, the import tool allows you to flip Y, in case the normalmap uses DirectX convention.
|
||||
|
||||
|
@ -349,7 +361,7 @@ The `CLASSIC4` shader is a simple splatmap technique, where R, G, B, A match the
|
|||
|
||||
It comes in two variants:
|
||||
|
||||
- `CLASSIC4`: full-featured shader, however it requires your textures to have normal maps.
|
||||
- `CLASSIC4`: full-featured shader, however it requires your textures to have normal maps. If you don't assign them, shading will look wrong.
|
||||
- `CLASSIC4_LITE`: simpler shader with less features. It only requires albedo textures.
|
||||
|
||||
|
||||
|
@ -362,7 +374,7 @@ It also comes in two variants:
|
|||
- `MULTISPLAT16`: full-featured shader, however it requires your texture arrays to have normal maps.
|
||||
- `MULTISPLAT16_LITE`: simpler shader with less features. It only requires albedo texture arrays.
|
||||
|
||||
It is the recommended choice if you need more than 4 textures, because it is much easier to use than the `ARRAY` shader and has produces less artifacts.
|
||||
It is the recommended choice if you need more than 4 textures, because it is much easier to use than the `ARRAY` shader and produces less artifacts.
|
||||
|
||||
One downside is performance: it is about twice slower than `CLASSIC4` (on an nVidia 1060, a fullscreen `CLASSIC4` is 0.8 ms, while `MULTISPLAT16` is 1.8 ms).
|
||||
Although, considering objects placed on the terrain should usually occlude ground pixels, the cost might be lower in a real game scenario.
|
||||
|
@ -1137,6 +1149,7 @@ This issue happened a few times and had various causes so if the checks mentionn
|
|||
- Check the contents of your terrain's data folder. It must contain a `.hterrain` file and a few textures.
|
||||
- If they are present, make sure Godot has imported those textures. If it didn't, unfocus the editor, and focus it back (you should see a short progress bar as it does it)
|
||||
- Check if you used Ctrl+Z (undo) after a non-undoable action, like described in [issue #101](https://github.com/Zylann/godot_heightmap_plugin/issues/101)
|
||||
- Make sure your `res://addons` folder is named `addons` *exactly lowercase*. It should not be named `Addons`. Plugins can fail if this convention is not respected.
|
||||
- If your problem relates to collisions in editor, update the collider using `Terrain -> Update Editor Collider`, because this one does not update automatically yet
|
||||
- Godot seems to randomly forget where the terrain saver is, but I need help to find out why because I could never reproduce it. See [issue #120](https://github.com/Zylann/godot_heightmap_plugin/issues/120)
|
||||
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
site_name: HTerrain plugin documentation
|
||||
theme: readthedocs
|
||||
|
||||
# I had to specify this even though it's supposed to be the default
|
||||
# See https://github.com/mkdocs/mkdocs/issues/2145#issuecomment-735342512
|
||||
docs_dir: docs
|
||||
|
||||
markdown_extensions:
|
||||
# Makes permalinks appear on headings
|
||||
- toc:
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
tool
|
||||
extends Spatial
|
||||
|
||||
const QuadTreeLod = preload("./util/quad_tree_lod.gd")
|
||||
const NativeFactory = preload("./native/factory.gd")
|
||||
const Mesher = preload("./hterrain_mesher.gd")
|
||||
const Grid = preload("./util/grid.gd")
|
||||
const HTerrainData = preload("./hterrain_data.gd")
|
||||
|
@ -156,6 +156,8 @@ var _shader_uses_texture_array := false
|
|||
var _material := ShaderMaterial.new()
|
||||
var _material_params_need_update := false
|
||||
|
||||
var _render_layer_mask := 1
|
||||
|
||||
# Actual number of textures supported by the shader currently selected
|
||||
var _ground_texture_count_cache = 0
|
||||
|
||||
|
@ -168,7 +170,7 @@ var _texture_set_migration_textures = null
|
|||
var _data: HTerrainData = null
|
||||
|
||||
var _mesher := Mesher.new()
|
||||
var _lodder := QuadTreeLod.new()
|
||||
var _lodder = NativeFactory.get_quad_tree_lod()
|
||||
var _viewer_pos_world := Vector3()
|
||||
|
||||
# [lod][z][x] -> chunk
|
||||
|
@ -277,7 +279,7 @@ func _get_property_list():
|
|||
"hint": PROPERTY_HINT_LAYERS_3D_PHYSICS
|
||||
},
|
||||
{
|
||||
"name": "Shader",
|
||||
"name": "Rendering",
|
||||
"type": TYPE_NIL,
|
||||
"usage": PROPERTY_USAGE_GROUP
|
||||
},
|
||||
|
@ -312,6 +314,12 @@ func _get_property_list():
|
|||
# This triggers `ERROR: Cannot get class 'HTerrainTextureSet'`
|
||||
# See https://github.com/godotengine/godot/pull/41264
|
||||
#"hint_string": "HTerrainTextureSet"
|
||||
},
|
||||
{
|
||||
"name": "render_layers",
|
||||
"type": TYPE_INT,
|
||||
"usage": PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_STORAGE,
|
||||
"hint": PROPERTY_HINT_LAYERS_3D_RENDER
|
||||
}
|
||||
]
|
||||
|
||||
|
@ -369,6 +377,9 @@ func _get(key: String):
|
|||
elif key == "collision_mask":
|
||||
return _collision_mask
|
||||
|
||||
elif key == "render_layers":
|
||||
return get_render_layer_mask()
|
||||
|
||||
|
||||
func _set(key: String, value):
|
||||
if key == "data_directory":
|
||||
|
@ -424,6 +435,9 @@ func _set(key: String, value):
|
|||
if _collider != null:
|
||||
_collider.set_collision_mask(value)
|
||||
|
||||
elif key == "render_layers":
|
||||
return set_render_layer_mask(value)
|
||||
|
||||
|
||||
func get_texture_set() -> HTerrainTextureSet:
|
||||
return _texture_set
|
||||
|
@ -458,6 +472,15 @@ func set_shader_param(param_name: String, v):
|
|||
_material.set_shader_param(param_name, v)
|
||||
|
||||
|
||||
func set_render_layer_mask(mask: int):
|
||||
_render_layer_mask = mask
|
||||
_for_all_chunks(SetRenderLayerMaskAction.new(mask))
|
||||
|
||||
|
||||
func get_render_layer_mask() -> int:
|
||||
return _render_layer_mask
|
||||
|
||||
|
||||
func _set_data_directory(dirpath: String):
|
||||
if dirpath != _get_data_directory():
|
||||
if dirpath == "":
|
||||
|
@ -1087,7 +1110,6 @@ func _process(delta: float):
|
|||
if not Engine.is_editor_hint():
|
||||
# In editor, the camera is only accessible from an editor plugin
|
||||
_update_viewer_position(null)
|
||||
var viewer_pos := _viewer_pos_world
|
||||
|
||||
if has_data():
|
||||
if _data.is_locked():
|
||||
|
@ -1096,9 +1118,10 @@ func _process(delta: float):
|
|||
|
||||
if _data.get_resolution() != 0:
|
||||
var gt := get_internal_transform()
|
||||
var local_viewer_pos := gt.affine_inverse() * viewer_pos
|
||||
# Viewer position such that 1 unit == 1 pixel in the heightmap
|
||||
var viewer_pos_heightmap_local := gt.affine_inverse() * _viewer_pos_world
|
||||
#var time_before = OS.get_ticks_msec()
|
||||
_lodder.update(local_viewer_pos)
|
||||
_lodder.update(viewer_pos_heightmap_local)
|
||||
#var time_elapsed = OS.get_ticks_msec() - time_before
|
||||
#if Engine.get_frames_drawn() % 60 == 0:
|
||||
# _logger.debug(str("Lodder time: ", time_elapsed))
|
||||
|
@ -1107,7 +1130,7 @@ func _process(delta: float):
|
|||
# Note: the detail system is not affected by map scale,
|
||||
# so we have to send viewer position in world space
|
||||
for layer in _detail_layers:
|
||||
layer.process(delta, viewer_pos)
|
||||
layer.process(delta, _viewer_pos_world)
|
||||
|
||||
_updated_chunks = 0
|
||||
|
||||
|
@ -1236,7 +1259,7 @@ func set_area_dirty(origin_in_cells_x: int, origin_in_cells_y: int, \
|
|||
for lod in range(_lodder.get_lod_count()):
|
||||
# Get grid and chunk size
|
||||
var grid = _chunks[lod]
|
||||
var s := _lodder.get_lod_size(lod)
|
||||
var s : int = _lodder.get_lod_factor(lod)
|
||||
|
||||
# Convert rect into this lod's coordinates:
|
||||
# Pick min and max (included), divide them, then add 1 to max so it's excluded again
|
||||
|
@ -1261,7 +1284,7 @@ func _cb_make_chunk(cpos_x: int, cpos_y: int, lod: int):
|
|||
if chunk == null:
|
||||
# This is the first time this chunk is required at this lod, generate it
|
||||
|
||||
var lod_factor := _lodder.get_lod_size(lod)
|
||||
var lod_factor : int = _lodder.get_lod_factor(lod)
|
||||
var origin_in_cells_x := cpos_x * _chunk_size * lod_factor
|
||||
var origin_in_cells_y := cpos_y * _chunk_size * lod_factor
|
||||
|
||||
|
@ -1276,6 +1299,8 @@ func _cb_make_chunk(cpos_x: int, cpos_y: int, lod: int):
|
|||
chunk = HTerrainChunk.new(self, origin_in_cells_x, origin_in_cells_y, material)
|
||||
chunk.parent_transform_changed(get_internal_transform())
|
||||
|
||||
chunk.set_render_layer_mask(_render_layer_mask)
|
||||
|
||||
var grid = _chunks[lod]
|
||||
var row = grid[cpos_y]
|
||||
row[cpos_x] = chunk
|
||||
|
@ -1294,7 +1319,7 @@ func _cb_recycle_chunk(chunk: HTerrainChunk, cx: int, cy: int, lod: int):
|
|||
|
||||
|
||||
func _cb_get_vertical_bounds(cpos_x: int, cpos_y: int, lod: int):
|
||||
var chunk_size := _chunk_size * _lodder.get_lod_size(lod)
|
||||
var chunk_size : int = _chunk_size * _lodder.get_lod_factor(lod)
|
||||
var origin_in_cells_x := cpos_x * chunk_size
|
||||
var origin_in_cells_y := cpos_y * chunk_size
|
||||
# This is a hack for speed,
|
||||
|
@ -1546,3 +1571,10 @@ class SetMaterialAction:
|
|||
chunk.set_material(material)
|
||||
|
||||
|
||||
class SetRenderLayerMaskAction:
|
||||
var mask: int = 0
|
||||
func _init(m: int):
|
||||
mask = m
|
||||
func exec(chunk):
|
||||
chunk.set_render_layer_mask(mask)
|
||||
|
||||
|
|
|
@ -113,3 +113,8 @@ func set_aabb(aabb: AABB):
|
|||
assert(_mesh_instance != RID())
|
||||
VisualServer.instance_set_custom_aabb(_mesh_instance, aabb)
|
||||
|
||||
|
||||
func set_render_layer_mask(mask: int):
|
||||
assert(_mesh_instance != RID())
|
||||
VisualServer.instance_set_layer_mask(_mesh_instance, mask)
|
||||
|
||||
|
|
|
@ -417,7 +417,7 @@ func get_all_heights() -> PoolRealArray:
|
|||
# modified area.
|
||||
#
|
||||
# map_type:
|
||||
# which kind of map changed
|
||||
# which kind of map changed, see CHANNEL_* constants
|
||||
#
|
||||
# index:
|
||||
# index of the map that changed
|
||||
|
@ -1159,7 +1159,25 @@ func _load_map(dir: String, map_type: int, index: int) -> bool:
|
|||
# In this particular case, we can use Godot ResourceLoader directly,
|
||||
# if the texture got imported.
|
||||
|
||||
var tex = load(fpath)
|
||||
|
||||
var must_load_image_in_editor := true
|
||||
|
||||
if tex != null and tex is Image:
|
||||
# The texture is imported as Image,
|
||||
# perhaps the user wants it to be accessible from RAM in game.
|
||||
_logger.debug("Map {0} is imported as Image. An ImageTexture will be generated." \
|
||||
.format([get_map_debug_name(map_type, index)]))
|
||||
map.image = tex
|
||||
tex = ImageTexture.new()
|
||||
var map_type_info = _map_types[map_type]
|
||||
tex.create_from_image(map.image, map_type_info.texture_flags)
|
||||
must_load_image_in_editor = false
|
||||
|
||||
map.texture = tex
|
||||
|
||||
if Engine.editor_hint:
|
||||
if must_load_image_in_editor:
|
||||
# But in the editor we want textures to be editable,
|
||||
# so we have to automatically load the data also in RAM
|
||||
if map.image == null:
|
||||
|
@ -1167,10 +1185,10 @@ func _load_map(dir: String, map_type: int, index: int) -> bool:
|
|||
map.image.load(fpath)
|
||||
_ensure_map_format(map.image, map_type, index)
|
||||
|
||||
var tex = load(fpath)
|
||||
map.texture = tex
|
||||
|
||||
else:
|
||||
# The heightmap is different.
|
||||
# It has often uses beyond graphics, so we always keep a RAM copy by default.
|
||||
|
||||
var im = _try_load_0_8_0_heightmap(fpath, map_type, map.image, _logger)
|
||||
if typeof(im) == TYPE_BOOL:
|
||||
return false
|
||||
|
|
|
@ -47,10 +47,12 @@ export(Texture) var texture : Texture setget set_texture, get_texture
|
|||
# How far detail meshes can be seen.
|
||||
# TODO Improve speed of _get_chunk_aabb() so we can increase the limit
|
||||
# See https://github.com/Zylann/godot_heightmap_plugin/issues/155
|
||||
export(float, 1.0, 500.0) var view_distance := 100.0 setget set_view_distance, get_view_distance
|
||||
export(float, 1.0, 500.0) \
|
||||
var view_distance := 100.0 setget set_view_distance, get_view_distance
|
||||
|
||||
# Custom shader to replace the default one.
|
||||
export(Shader) var custom_shader : Shader setget set_custom_shader, get_custom_shader
|
||||
export(Shader) \
|
||||
var custom_shader : Shader setget set_custom_shader, get_custom_shader
|
||||
|
||||
# Density modifier, to make more or less detail meshes appear overall.
|
||||
export(float, 0, 10) var density := 4.0 setget set_density, get_density
|
||||
|
@ -60,6 +62,10 @@ export(float, 0, 10) var density := 4.0 setget set_density, get_density
|
|||
# I would have called it `mesh` but that's too broad and conflicts with local vars ._.
|
||||
export(Mesh) var instance_mesh : Mesh setget set_instance_mesh, get_instance_mesh
|
||||
|
||||
# Exposes rendering layers, similar to `VisualInstance.layers`
|
||||
export(int, LAYERS_3D_RENDER) \
|
||||
var render_layers := 1 setget set_render_layer_mask, get_render_layer_mask
|
||||
|
||||
var _material: ShaderMaterial = null
|
||||
var _default_shader: Shader = null
|
||||
|
||||
|
@ -248,6 +254,17 @@ func get_instance_mesh() -> Mesh:
|
|||
return instance_mesh
|
||||
|
||||
|
||||
func set_render_layer_mask(mask: int):
|
||||
render_layers = mask
|
||||
for k in _chunks:
|
||||
var chunk = _chunks[k]
|
||||
chunk.set_layer_mask(mask)
|
||||
|
||||
|
||||
func get_render_layer_mask() -> int:
|
||||
return render_layers
|
||||
|
||||
|
||||
func _get_used_mesh() -> Mesh:
|
||||
if instance_mesh == null:
|
||||
return DefaultMesh
|
||||
|
@ -306,7 +323,9 @@ func _on_terrain_transform_changed(gt: Transform):
|
|||
_logger.error("Detail layer is not child of a terrain!")
|
||||
return
|
||||
|
||||
# Update AABBs, because scale might have changed
|
||||
var terrain_transform : Transform = terrain.get_internal_transform()
|
||||
|
||||
# Update AABBs and transforms, because scale might have changed
|
||||
for k in _chunks:
|
||||
var mmi = _chunks[k]
|
||||
var aabb = _get_chunk_aabb(terrain, Vector3(k.x * CHUNK_SIZE, 0, k.y * CHUNK_SIZE))
|
||||
|
@ -314,6 +333,7 @@ func _on_terrain_transform_changed(gt: Transform):
|
|||
aabb.position.x = 0
|
||||
aabb.position.z = 0
|
||||
mmi.set_aabb(aabb)
|
||||
mmi.set_transform(_get_chunk_transform(terrain_transform, k.x, k.y))
|
||||
|
||||
|
||||
func process(delta: float, viewer_pos: Vector3):
|
||||
|
@ -331,7 +351,7 @@ func process(delta: float, viewer_pos: Vector3):
|
|||
var mmi = _chunks[k]
|
||||
mmi.set_multimesh(_multimesh)
|
||||
|
||||
var local_viewer_pos = viewer_pos - terrain.translation
|
||||
var local_viewer_pos = terrain.global_transform.affine_inverse() * viewer_pos
|
||||
|
||||
var viewer_cx = local_viewer_pos.x / CHUNK_SIZE
|
||||
var viewer_cz = local_viewer_pos.z / CHUNK_SIZE
|
||||
|
@ -367,6 +387,8 @@ func process(delta: float, viewer_pos: Vector3):
|
|||
for cx in range(cmin_x, cmax_x):
|
||||
_add_debug_cube(terrain, _get_chunk_aabb(terrain, Vector3(cx, 0, cz) * CHUNK_SIZE))
|
||||
|
||||
var terrain_transform : Transform = terrain.get_internal_transform()
|
||||
|
||||
for cz in range(cmin_z, cmax_z):
|
||||
for cx in range(cmin_x, cmax_x):
|
||||
|
||||
|
@ -378,7 +400,7 @@ func process(delta: float, viewer_pos: Vector3):
|
|||
var d = (aabb.position + 0.5 * aabb.size).distance_to(local_viewer_pos)
|
||||
|
||||
if d < view_distance:
|
||||
_load_chunk(terrain, cx, cz, aabb)
|
||||
_load_chunk(terrain_transform, cx, cz, aabb)
|
||||
|
||||
var to_recycle = []
|
||||
|
||||
|
@ -418,11 +440,14 @@ func _get_chunk_aabb(terrain, lpos: Vector3):
|
|||
return aabb
|
||||
|
||||
|
||||
func _load_chunk(terrain, cx: int, cz: int, aabb: AABB):
|
||||
var lpos = Vector3(cx, 0, cz) * CHUNK_SIZE
|
||||
func _get_chunk_transform(terrain_transform: Transform, cx: int, cz: int) -> Transform:
|
||||
var lpos := Vector3(cx, 0, cz) * CHUNK_SIZE
|
||||
# Terrain scale is not used on purpose. Rotation is not supported.
|
||||
var trans = Transform(Basis(), terrain.get_internal_transform().origin + lpos)
|
||||
var trans := Transform(Basis(), terrain_transform.origin + lpos)
|
||||
return trans
|
||||
|
||||
|
||||
func _load_chunk(terrain_transform: Transform, cx: int, cz: int, aabb: AABB):
|
||||
# Nullify XZ translation because that's done by transform already
|
||||
aabb.position.x = 0
|
||||
aabb.position.z = 0
|
||||
|
@ -433,12 +458,15 @@ func _load_chunk(terrain, cx: int, cz: int, aabb: AABB):
|
|||
_multimesh_instance_pool.pop_back()
|
||||
else:
|
||||
mmi = DirectMultiMeshInstance.new()
|
||||
mmi.set_world(terrain.get_world())
|
||||
mmi.set_world(get_world())
|
||||
mmi.set_multimesh(_multimesh)
|
||||
|
||||
var trans := _get_chunk_transform(terrain_transform, cx, cz)
|
||||
|
||||
mmi.set_material_override(_material)
|
||||
mmi.set_transform(trans)
|
||||
mmi.set_aabb(aabb)
|
||||
mmi.set_layer_mask(render_layers)
|
||||
mmi.set_visible(visible)
|
||||
|
||||
_chunks[Vector2(cx, cz)] = mmi
|
||||
|
|
|
@ -16,7 +16,7 @@ target_path = "bin/"
|
|||
TARGET_NAME = "hterrain_native"
|
||||
|
||||
# Local dependency paths
|
||||
godot_headers_path = "godot-cpp/godot_headers/"
|
||||
godot_headers_path = "godot-cpp/godot-headers/"
|
||||
cpp_bindings_path = "godot-cpp/"
|
||||
cpp_bindings_library = "libgodot-cpp"
|
||||
|
||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -2,8 +2,9 @@
|
|||
const NATIVE_PATH = "res://addons/zylann.hterrain/native/"
|
||||
|
||||
const ImageUtilsGeneric = preload("./image_utils_generic.gd")
|
||||
const QuadTreeLodGeneric = preload("./quad_tree_lod_generic.gd")
|
||||
|
||||
# See https://docs.godotengine.org/en/3.2/classes/class_os.html#class-os-method-get-name
|
||||
# See https://docs.godotengine.org/en/stable/classes/class_os.html#class-os-method-get-name
|
||||
const _supported_os = {
|
||||
"Windows": true,
|
||||
"X11": true,
|
||||
|
@ -17,7 +18,7 @@ static func is_native_available() -> bool:
|
|||
return false
|
||||
# API changes can cause binary incompatibility
|
||||
var v = Engine.get_version_info()
|
||||
return v.major == 3 and v.minor == 2
|
||||
return v.major == 3 and v.minor >= 2 and v.minor <= 5
|
||||
|
||||
|
||||
static func get_image_utils():
|
||||
|
@ -27,3 +28,10 @@ static func get_image_utils():
|
|||
return ImageUtilsNative.new()
|
||||
return ImageUtilsGeneric.new()
|
||||
|
||||
|
||||
static func get_quad_tree_lod():
|
||||
if is_native_available():
|
||||
var QuadTreeLod = load(NATIVE_PATH + "quad_tree_lod.gdns")
|
||||
if QuadTreeLod != null:
|
||||
return QuadTreeLod.new()
|
||||
return QuadTreeLodGeneric.new()
|
||||
|
|
8
addons/zylann.hterrain/native/quad_tree_lod.gdns
Normal file
8
addons/zylann.hterrain/native/quad_tree_lod.gdns
Normal 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 )
|
|
@ -3,8 +3,8 @@ tool
|
|||
|
||||
class Quad:
|
||||
var children = null
|
||||
var origin_x := 0
|
||||
var origin_y := 0
|
||||
var origin_x : int = 0
|
||||
var origin_y : int = 0
|
||||
var data = null
|
||||
|
||||
func _init():
|
||||
|
@ -22,9 +22,9 @@ class Quad:
|
|||
|
||||
|
||||
var _tree := Quad.new()
|
||||
var _max_depth := 0
|
||||
var _base_size := 16
|
||||
var _split_scale := 2.0
|
||||
var _max_depth : int = 0
|
||||
var _base_size : int = 16
|
||||
var _split_scale : float = 2.0
|
||||
|
||||
var _make_func : FuncRef = null
|
||||
var _recycle_func : FuncRef = null
|
||||
|
@ -44,7 +44,7 @@ func clear():
|
|||
|
||||
|
||||
static func compute_lod_count(base_size: int, full_size: int) -> int:
|
||||
var po = 0
|
||||
var po : int = 0
|
||||
while full_size > base_size:
|
||||
full_size = full_size >> 1
|
||||
po += 1
|
||||
|
@ -70,12 +70,7 @@ func set_split_scale(p_split_scale: float):
|
|||
|
||||
# Split scale must be greater than a threshold,
|
||||
# otherwise lods will decimate too fast and it will look messy
|
||||
if p_split_scale < MIN:
|
||||
p_split_scale = MIN
|
||||
if p_split_scale > MAX:
|
||||
p_split_scale = MAX
|
||||
|
||||
_split_scale = float(p_split_scale)
|
||||
_split_scale = clamp(p_split_scale, MIN, MAX)
|
||||
|
||||
|
||||
func get_split_scale() -> float:
|
||||
|
@ -91,16 +86,15 @@ func update(view_pos: Vector3):
|
|||
_tree.data = _make_chunk(_max_depth, 0, 0)
|
||||
|
||||
|
||||
# TODO Should be renamed get_lod_factor
|
||||
func get_lod_size(lod: int) -> int:
|
||||
func get_lod_factor(lod: int) -> int:
|
||||
return 1 << lod
|
||||
|
||||
|
||||
func _update(quad: Quad, lod: int, view_pos: Vector3):
|
||||
# This function should be called regularly over frames.
|
||||
|
||||
var lod_factor := get_lod_size(lod)
|
||||
var chunk_size := _base_size * lod_factor
|
||||
var lod_factor : int = get_lod_factor(lod)
|
||||
var chunk_size : int = _base_size * lod_factor
|
||||
var world_center := \
|
||||
chunk_size * (Vector3(quad.origin_x, 0, quad.origin_y) + Vector3(0.5, 0, 0.5))
|
||||
|
||||
|
@ -137,22 +131,17 @@ func _update(quad: Quad, lod: int, view_pos: Vector3):
|
|||
|
||||
if no_split_child and world_center.distance_to(view_pos) > split_distance:
|
||||
# Join
|
||||
if quad.has_children():
|
||||
for i in 4:
|
||||
var child = quad.children[i]
|
||||
_recycle_chunk(child.data, child.origin_x, child.origin_y, lod - 1)
|
||||
quad.data = null
|
||||
quad.clear_children()
|
||||
|
||||
assert(quad.data == null)
|
||||
quad.data = _make_chunk(lod, quad.origin_x, quad.origin_y)
|
||||
|
||||
|
||||
func _join_all_recursively(quad: Quad, lod: int):
|
||||
if quad.has_children():
|
||||
for i in range(4):
|
||||
var child = quad.children[i]
|
||||
_join_all_recursively(child, lod - 1)
|
||||
for i in 4:
|
||||
_join_all_recursively(quad.children[i], lod - 1)
|
||||
|
||||
quad.clear_children()
|
||||
|
||||
|
@ -180,15 +169,14 @@ func debug_draw_tree(ci: CanvasItem):
|
|||
|
||||
func _debug_draw_tree_recursive(ci: CanvasItem, quad: Quad, lod_index: int, child_index: int):
|
||||
if quad.has_children():
|
||||
for i in range(0, quad.children.size()):
|
||||
var child = quad.children[i]
|
||||
_debug_draw_tree_recursive(ci, child, lod_index - 1, i)
|
||||
for i in 4:
|
||||
_debug_draw_tree_recursive(ci, quad.children[i], lod_index - 1, i)
|
||||
else:
|
||||
var size := get_lod_size(lod_index)
|
||||
var checker := 0
|
||||
var size : int = get_lod_factor(lod_index)
|
||||
var checker : int = 0
|
||||
if child_index == 1 or child_index == 2:
|
||||
checker = 1
|
||||
var chunk_indicator := 0
|
||||
var chunk_indicator : int = 0
|
||||
if quad.data != null:
|
||||
chunk_indicator = 1
|
||||
var r := Rect2(Vector2(quad.origin_x, quad.origin_y) * size, Vector2(size, size))
|
|
@ -1,4 +1,5 @@
|
|||
#include "image_utils.h"
|
||||
#include "quad_tree_lod.h"
|
||||
|
||||
extern "C" {
|
||||
|
||||
|
@ -23,6 +24,7 @@ void GDN_EXPORT godot_nativescript_init(void *handle) {
|
|||
godot::Godot::nativescript_init(handle);
|
||||
|
||||
godot::register_tool_class<godot::ImageUtils>();
|
||||
godot::register_tool_class<godot::QuadTreeLod>();
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
|
|
242
addons/zylann.hterrain/native/src/quad_tree_lod.cpp
Normal file
242
addons/zylann.hterrain/native/src/quad_tree_lod.cpp
Normal 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
|
121
addons/zylann.hterrain/native/src/quad_tree_lod.h
Normal file
121
addons/zylann.hterrain/native/src/quad_tree_lod.h
Normal 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
|
|
@ -3,5 +3,5 @@
|
|||
name="Heightmap Terrain"
|
||||
description="Heightmap-based terrain"
|
||||
author="Marc Gilleron"
|
||||
version="1.5.2"
|
||||
version="1.5.3 dev"
|
||||
script="tools/plugin.gd"
|
||||
|
|
|
@ -63,6 +63,18 @@ wacyym
|
|||
Sergey Lapin (slapin)
|
||||
Jonas (NoFr1ends)
|
||||
lenis0012
|
||||
Phyronnaz
|
||||
RonanZe
|
||||
furtherorbit
|
||||
jp.owo.Manda (segfault-god)
|
||||
hidemat
|
||||
Jakub Buriánek (Buri)
|
||||
Justin Swanhart (Greenlion)
|
||||
Sebastian Clausen (sclausen)
|
||||
MrGreaterThan
|
||||
baals
|
||||
Treer
|
||||
stackdump.eth
|
||||
"
|
||||
text = "Version: {version}
|
||||
Author: Marc Gilleron
|
||||
|
@ -74,6 +86,18 @@ wacyym
|
|||
Sergey Lapin (slapin)
|
||||
Jonas (NoFr1ends)
|
||||
lenis0012
|
||||
Phyronnaz
|
||||
RonanZe
|
||||
furtherorbit
|
||||
jp.owo.Manda (segfault-god)
|
||||
hidemat
|
||||
Jakub Buriánek (Buri)
|
||||
Justin Swanhart (Greenlion)
|
||||
Sebastian Clausen (sclausen)
|
||||
MrGreaterThan
|
||||
baals
|
||||
Treer
|
||||
stackdump.eth
|
||||
"
|
||||
script = ExtResource( 3 )
|
||||
|
||||
|
|
184
addons/zylann.hterrain/tools/brush/brush.gd
Normal file
184
addons/zylann.hterrain/tools/brush/brush.gd
Normal 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
|
||||
|
||||
|
|
@ -1,13 +1,15 @@
|
|||
tool
|
||||
extends Control
|
||||
|
||||
const Brush = preload("./terrain_painter.gd")
|
||||
const TerrainPainter = preload("./terrain_painter.gd")
|
||||
const Brush = preload("./brush.gd")
|
||||
const Errors = preload("../../util/errors.gd")
|
||||
#const NativeFactory = preload("../../native/factory.gd")
|
||||
const Logger = preload("../../util/logger.gd")
|
||||
|
||||
const SHAPES_DIR = "addons/zylann.hterrain/tools/brush/shapes"
|
||||
const DEFAULT_BRUSH = "round2.exr"
|
||||
const BrushSettingsDialogScene = preload("./settings_dialog/brush_settings_dialog.tscn")
|
||||
const BrushSettingsDialog = preload("./settings_dialog/brush_settings_dialog.gd")
|
||||
|
||||
|
||||
onready var _size_slider := $GridContainer/BrushSizeControl/Slider as Slider
|
||||
onready var _size_value_label := $GridContainer/BrushSizeControl/Label as Label
|
||||
|
@ -37,8 +39,9 @@ onready var _slope_limit_control = $GridContainer/SlopeLimit
|
|||
|
||||
onready var _shape_texture_rect = get_node("BrushShapeButton/TextureRect")
|
||||
|
||||
var _brush : Brush
|
||||
var _terrain_painter : TerrainPainter
|
||||
var _load_image_dialog = null
|
||||
var _brush_settings_dialog = null
|
||||
var _logger = Logger.get_for(self)
|
||||
|
||||
# TODO This is an ugly workaround for https://github.com/godotengine/godot/issues/19479
|
||||
|
@ -62,7 +65,7 @@ func _ready():
|
|||
_holes_checkbox.connect("toggled", self, "_on_holes_checkbox_toggled")
|
||||
_slope_limit_control.connect("changed", self, "_on_slope_limit_changed")
|
||||
|
||||
_size_slider.max_value = 200
|
||||
_size_slider.max_value = Brush.MAX_SIZE_FOR_SLIDERS
|
||||
#if NativeFactory.is_native_available():
|
||||
# _size_slider.max_value = 200
|
||||
#else:
|
||||
|
@ -70,21 +73,19 @@ func _ready():
|
|||
|
||||
|
||||
func setup_dialogs(base_control: Control):
|
||||
assert(_load_image_dialog == null)
|
||||
_load_image_dialog = EditorFileDialog.new()
|
||||
_load_image_dialog.mode = EditorFileDialog.MODE_OPEN_FILE
|
||||
_load_image_dialog.add_filter("*.exr ; EXR files")
|
||||
_load_image_dialog.resizable = true
|
||||
_load_image_dialog.access = EditorFileDialog.ACCESS_FILESYSTEM
|
||||
_load_image_dialog.current_dir = SHAPES_DIR
|
||||
_load_image_dialog.connect("file_selected", self, "_on_LoadImageDialog_file_selected")
|
||||
base_control.add_child(_load_image_dialog)
|
||||
assert(_brush_settings_dialog == null)
|
||||
_brush_settings_dialog = BrushSettingsDialogScene.instance()
|
||||
base_control.add_child(_brush_settings_dialog)
|
||||
|
||||
# That dialog has sub-dialogs
|
||||
_brush_settings_dialog.setup_dialogs(base_control)
|
||||
_brush_settings_dialog.set_brush(_terrain_painter.get_brush())
|
||||
|
||||
|
||||
func _exit_tree():
|
||||
if _load_image_dialog != null:
|
||||
_load_image_dialog.queue_free()
|
||||
_load_image_dialog = null
|
||||
if _brush_settings_dialog != null:
|
||||
_brush_settings_dialog.queue_free()
|
||||
_brush_settings_dialog = null
|
||||
|
||||
# Testing display modes
|
||||
#var mode = 0
|
||||
|
@ -96,48 +97,61 @@ func _exit_tree():
|
|||
# if mode >= Brush.MODE_COUNT:
|
||||
# mode = 0
|
||||
|
||||
func set_brush(brush: Brush):
|
||||
if _brush != null:
|
||||
_brush.disconnect("changed", self, "_on_brush_changed")
|
||||
func set_terrain_painter(terrain_painter: TerrainPainter):
|
||||
if _terrain_painter != null:
|
||||
_terrain_painter.disconnect("flatten_height_changed", self, "_on_flatten_height_changed")
|
||||
_terrain_painter.get_brush().disconnect("shapes_changed", self, "_on_brush_shapes_changed")
|
||||
|
||||
_brush = brush
|
||||
_terrain_painter = terrain_painter
|
||||
|
||||
if brush != null:
|
||||
if _terrain_painter != null:
|
||||
# TODO Had an issue in Godot 3.2.3 where mismatching type would silently cast to null...
|
||||
# It happens if the argument went through a Variant (for example if call_deferred is used)
|
||||
assert(_brush != null)
|
||||
assert(_terrain_painter != null)
|
||||
|
||||
if _brush != null:
|
||||
# Initial params
|
||||
_size_slider.value = brush.get_brush_size()
|
||||
_opacity_slider.ratio = brush.get_opacity()
|
||||
_flatten_height_box.value = brush.get_flatten_height()
|
||||
_color_picker.get_picker().color = brush.get_color()
|
||||
_density_slider.value = brush.get_detail_density()
|
||||
_holes_checkbox.pressed = not brush.get_mask_flag()
|
||||
if _terrain_painter != null:
|
||||
# Initial brush params
|
||||
_size_slider.value = _terrain_painter.get_brush().get_size()
|
||||
_opacity_slider.ratio = _terrain_painter.get_brush().get_opacity()
|
||||
# Initial specific params
|
||||
_flatten_height_box.value = _terrain_painter.get_flatten_height()
|
||||
_color_picker.get_picker().color = _terrain_painter.get_color()
|
||||
_density_slider.value = _terrain_painter.get_detail_density()
|
||||
_holes_checkbox.pressed = not _terrain_painter.get_mask_flag()
|
||||
|
||||
var low = rad2deg(brush.get_slope_limit_low_angle())
|
||||
var high = rad2deg(brush.get_slope_limit_high_angle())
|
||||
var low = rad2deg(_terrain_painter.get_slope_limit_low_angle())
|
||||
var high = rad2deg(_terrain_painter.get_slope_limit_high_angle())
|
||||
_slope_limit_control.set_values(low, high)
|
||||
|
||||
set_display_mode(brush.get_mode())
|
||||
_set_brush_shape_from_file(SHAPES_DIR.plus_file(DEFAULT_BRUSH))
|
||||
set_display_mode(_terrain_painter.get_mode())
|
||||
|
||||
_brush.connect("changed", self, "_on_brush_properties_changed")
|
||||
# Load default brush
|
||||
var brush := _terrain_painter.get_brush()
|
||||
var default_shape_fpath := Brush.SHAPES_DIR.plus_file(Brush.DEFAULT_BRUSH)
|
||||
var default_shape := Brush.load_shape_from_image_file(default_shape_fpath, _logger)
|
||||
brush.set_shapes([default_shape])
|
||||
_shape_texture_rect.texture = brush.get_shape(0)
|
||||
|
||||
_terrain_painter.connect("flatten_height_changed", self, "_on_flatten_height_changed")
|
||||
brush.connect("shapes_changed", self, "_on_brush_shapes_changed")
|
||||
|
||||
|
||||
func _on_brush_properties_changed():
|
||||
_flatten_height_box.value = _brush.get_flatten_height()
|
||||
func _on_flatten_height_changed():
|
||||
_flatten_height_box.value = _terrain_painter.get_flatten_height()
|
||||
_flatten_height_pick_button.pressed = false
|
||||
|
||||
|
||||
func _on_brush_shapes_changed():
|
||||
_shape_texture_rect.texture = _terrain_painter.get_brush().get_shape(0)
|
||||
|
||||
|
||||
func set_display_mode(mode: int):
|
||||
var show_flatten := mode == Brush.MODE_FLATTEN
|
||||
var show_color := mode == Brush.MODE_COLOR
|
||||
var show_density := mode == Brush.MODE_DETAIL
|
||||
var show_opacity := mode != Brush.MODE_MASK
|
||||
var show_holes := mode == Brush.MODE_MASK
|
||||
var show_slope_limit := mode == Brush.MODE_SPLAT
|
||||
var show_flatten := mode == TerrainPainter.MODE_FLATTEN
|
||||
var show_color := mode == TerrainPainter.MODE_COLOR
|
||||
var show_density := mode == TerrainPainter.MODE_DETAIL
|
||||
var show_opacity := mode != TerrainPainter.MODE_MASK
|
||||
var show_holes := mode == TerrainPainter.MODE_MASK
|
||||
var show_slope_limit := mode == TerrainPainter.MODE_SPLAT
|
||||
|
||||
_set_visibility_of(_opacity_label, show_opacity)
|
||||
_set_visibility_of(_opacity_control, show_opacity)
|
||||
|
@ -161,76 +175,47 @@ func set_display_mode(mode: int):
|
|||
|
||||
|
||||
func _on_size_slider_value_changed(v: float):
|
||||
if _brush != null:
|
||||
_brush.set_brush_size(int(v))
|
||||
if _terrain_painter != null:
|
||||
_terrain_painter.set_brush_size(int(v))
|
||||
_size_value_label.text = str(v)
|
||||
|
||||
|
||||
func _on_opacity_slider_value_changed(v: float):
|
||||
if _brush != null:
|
||||
_brush.set_opacity(_opacity_slider.ratio)
|
||||
if _terrain_painter != null:
|
||||
_terrain_painter.set_opacity(_opacity_slider.ratio)
|
||||
_opacity_value_label.text = str(v)
|
||||
|
||||
|
||||
func _on_flatten_height_box_value_changed(v: float):
|
||||
if _brush != null:
|
||||
_brush.set_flatten_height(v)
|
||||
if _terrain_painter != null:
|
||||
_terrain_painter.set_flatten_height(v)
|
||||
|
||||
|
||||
func _on_color_picker_color_changed(v: Color):
|
||||
if _brush != null:
|
||||
_brush.set_color(v)
|
||||
if _terrain_painter != null:
|
||||
_terrain_painter.set_color(v)
|
||||
|
||||
|
||||
func _on_density_slider_changed(v: float):
|
||||
if _brush != null:
|
||||
_brush.set_detail_density(v)
|
||||
if _terrain_painter != null:
|
||||
_terrain_painter.set_detail_density(v)
|
||||
|
||||
|
||||
func _on_holes_checkbox_toggled(v: bool):
|
||||
if _brush != null:
|
||||
if _terrain_painter != null:
|
||||
# When checked, we draw holes. When unchecked, we clear holes
|
||||
_brush.set_mask_flag(not v)
|
||||
_terrain_painter.set_mask_flag(not v)
|
||||
|
||||
|
||||
func _on_BrushShapeButton_pressed():
|
||||
_load_image_dialog.popup_centered_ratio(0.7)
|
||||
|
||||
|
||||
func _on_LoadImageDialog_file_selected(path: String):
|
||||
_set_brush_shape_from_file(path)
|
||||
|
||||
|
||||
func _set_brush_shape_from_file(path: String):
|
||||
var im := Image.new()
|
||||
var err := im.load(path)
|
||||
if err != OK:
|
||||
_logger.error("Could not load image at '{0}', error {1}" \
|
||||
.format([path, Errors.get_message(err)]))
|
||||
return
|
||||
|
||||
var tex := ImageTexture.new()
|
||||
tex.create_from_image(im, Texture.FLAG_FILTER)
|
||||
|
||||
if _brush != null:
|
||||
var im2 := im
|
||||
var v := Engine.get_version_info()
|
||||
if v.major == 3 and v.minor < 1:
|
||||
# Forcing image brushes would ruin resized ones,
|
||||
# due to https://github.com/godotengine/godot/issues/24244
|
||||
if path.find(SHAPES_DIR.plus_file(DEFAULT_BRUSH)) != -1:
|
||||
im2 = null
|
||||
|
||||
_brush.set_brush_texture(tex)
|
||||
|
||||
_shape_texture_rect.texture = tex
|
||||
_brush_settings_dialog.popup_centered()
|
||||
|
||||
|
||||
func _on_FlattenHeightPickButton_pressed():
|
||||
_brush.set_meta("pick_height", true)
|
||||
_terrain_painter.set_meta("pick_height", true)
|
||||
|
||||
|
||||
func _on_slope_limit_changed():
|
||||
var low = deg2rad(_slope_limit_control.get_low_value())
|
||||
var high = deg2rad(_slope_limit_control.get_high_value())
|
||||
_brush.set_slope_limit_angles(low, high)
|
||||
_terrain_painter.set_slope_limit_angles(low, high)
|
||||
|
|
|
@ -60,7 +60,7 @@ margin_bottom = 16.0
|
|||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 1
|
||||
min_value = 2.0
|
||||
max_value = 200.0
|
||||
max_value = 500.0
|
||||
value = 2.0
|
||||
exp_edit = true
|
||||
rounded = true
|
||||
|
@ -120,6 +120,7 @@ margin_bottom = 24.0
|
|||
size_flags_horizontal = 3
|
||||
min_value = -500.0
|
||||
max_value = 500.0
|
||||
step = 0.01
|
||||
|
||||
[node name="FlattenHeightPickButton" type="Button" parent="GridContainer/HB"]
|
||||
margin_left = 115.0
|
||||
|
@ -185,5 +186,6 @@ script = ExtResource( 3 )
|
|||
range = Vector2( 0, 90 )
|
||||
|
||||
[node name="Temp" type="Node" parent="."]
|
||||
|
||||
[connection signal="pressed" from="BrushShapeButton" to="." method="_on_BrushShapeButton_pressed"]
|
||||
[connection signal="pressed" from="GridContainer/HB/FlattenHeightPickButton" to="." method="_on_FlattenHeightPickButton_pressed"]
|
||||
|
|
|
@ -5,8 +5,8 @@ const DirectMeshInstance = preload("../../util/direct_mesh_instance.gd")
|
|||
const HTerrainData = preload("../../hterrain_data.gd")
|
||||
const Util = preload("../../util/util.gd")
|
||||
|
||||
var _mesh_instance = null
|
||||
var _mesh = null
|
||||
var _mesh_instance : DirectMeshInstance
|
||||
var _mesh : PlaneMesh
|
||||
var _material = ShaderMaterial.new()
|
||||
#var _debug_mesh = CubeMesh.new()
|
||||
#var _debug_mesh_instance = null
|
||||
|
@ -26,15 +26,13 @@ func _init():
|
|||
#_debug_mesh_instance.set_mesh(_debug_mesh)
|
||||
|
||||
|
||||
func set_size(size):
|
||||
func set_size(size: float):
|
||||
_mesh.size = Vector2(size, size)
|
||||
# Must line up to terrain vertex policy, so must apply an off-by-one.
|
||||
# If I don't do that, the brush will appear to wobble above the ground
|
||||
var ss = size - 1
|
||||
# Don't subdivide too much
|
||||
if ss > 50:
|
||||
ss /= 2
|
||||
if ss > 50:
|
||||
while ss > 50:
|
||||
ss /= 2
|
||||
_mesh.subdivide_width = ss
|
||||
_mesh.subdivide_depth = ss
|
||||
|
@ -72,7 +70,7 @@ func set_terrain(terrain):
|
|||
update_visibility()
|
||||
|
||||
|
||||
func set_position(p_local_pos):
|
||||
func set_position(p_local_pos: Vector3):
|
||||
assert(_terrain != null)
|
||||
assert(typeof(p_local_pos) == TYPE_VECTOR3)
|
||||
|
||||
|
|
6
addons/zylann.hterrain/tools/brush/no_blend.gdshader
Normal file
6
addons/zylann.hterrain/tools/brush/no_blend.gdshader
Normal file
|
@ -0,0 +1,6 @@
|
|||
shader_type canvas_item;
|
||||
render_mode blend_disabled;
|
||||
|
||||
void fragment() {
|
||||
COLOR = texture(TEXTURE, UV);
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
|
||||
# Core logic to paint a texture using shaders, with undo/redo support.
|
||||
# Operations are delayed so results are only available the next frame.
|
||||
# This doesn't implement UI, only the painting logic.
|
||||
# This doesn't implement UI or brush behavior, only rendering logic.
|
||||
#
|
||||
# Note: due to the absence of channel separation function in Image,
|
||||
# you may need to use multiple painters at once if your application exploits multiple channels.
|
||||
|
@ -13,9 +13,20 @@ extends Node
|
|||
|
||||
const Logger = preload("../../util/logger.gd")
|
||||
const Util = preload("../../util/util.gd")
|
||||
const NoBlendShader = preload("./no_blend.gdshader")
|
||||
|
||||
const UNDO_CHUNK_SIZE = 64
|
||||
const BRUSH_TEXTURE_SHADER_PARAM = "u_brush_texture"
|
||||
|
||||
# All painting shaders can use these common parameters
|
||||
const SHADER_PARAM_SRC_TEXTURE = "u_src_texture"
|
||||
const SHADER_PARAM_SRC_RECT = "u_src_rect"
|
||||
const SHADER_PARAM_OPACITY = "u_opacity"
|
||||
|
||||
const _API_SHADER_PARAMS = [
|
||||
SHADER_PARAM_SRC_TEXTURE,
|
||||
SHADER_PARAM_SRC_RECT,
|
||||
SHADER_PARAM_OPACITY
|
||||
]
|
||||
|
||||
# Emitted when a region of the painted texture actually changed.
|
||||
# Note 1: the image might not have changed yet at this point.
|
||||
|
@ -42,10 +53,20 @@ const _supported_formats = [
|
|||
Image.FORMAT_RGBAH
|
||||
]
|
||||
|
||||
# - Viewport (size of edited region + margin to allow quad rotation)
|
||||
# |- Background
|
||||
# | Fills pixels with unmodified source image.
|
||||
# |- Brush sprite
|
||||
# Size of actual brush, scaled/rotated, modifies source image.
|
||||
# Assigned texture is the brush texture, src image is a shader param
|
||||
|
||||
var _viewport : Viewport
|
||||
var _viewport_sprite : Sprite
|
||||
var _viewport_bg_sprite : Sprite
|
||||
var _viewport_brush_sprite : Sprite
|
||||
var _brush_size := 32
|
||||
var _brush_scale := 1.0
|
||||
var _brush_position := Vector2()
|
||||
var _brush_opacity := 1.0
|
||||
var _brush_texture : Texture
|
||||
var _last_brush_position := Vector2()
|
||||
var _brush_material := ShaderMaterial.new()
|
||||
|
@ -60,9 +81,7 @@ var _debug_display : TextureRect
|
|||
var _logger = Logger.get_for(self)
|
||||
|
||||
|
||||
func _ready():
|
||||
if Util.is_in_edited_scene(self):
|
||||
return
|
||||
func _init():
|
||||
_viewport = Viewport.new()
|
||||
_viewport.size = Vector2(_brush_size, _brush_size)
|
||||
_viewport.render_target_update_mode = Viewport.UPDATE_ONCE
|
||||
|
@ -74,10 +93,19 @@ func _ready():
|
|||
#_viewport.usage = Viewport.USAGE_2D
|
||||
#_viewport.keep_3d_linear
|
||||
|
||||
_viewport_sprite = Sprite.new()
|
||||
_viewport_sprite.centered = false
|
||||
_viewport_sprite.material = _brush_material
|
||||
_viewport.add_child(_viewport_sprite)
|
||||
# There is no "blend_disabled" option on standard CanvasItemMaterial...
|
||||
var no_blend_material := ShaderMaterial.new()
|
||||
no_blend_material.shader = NoBlendShader
|
||||
_viewport_bg_sprite = Sprite.new()
|
||||
_viewport_bg_sprite.centered = false
|
||||
_viewport_bg_sprite.material = no_blend_material
|
||||
_viewport.add_child(_viewport_bg_sprite)
|
||||
|
||||
_viewport_brush_sprite = Sprite.new()
|
||||
_viewport_brush_sprite.centered = true
|
||||
_viewport_brush_sprite.material = _brush_material
|
||||
_viewport_brush_sprite.position = _viewport.size / 2.0
|
||||
_viewport.add_child(_viewport_brush_sprite)
|
||||
|
||||
add_child(_viewport)
|
||||
|
||||
|
@ -91,12 +119,16 @@ func set_image(image: Image, texture: ImageTexture):
|
|||
assert((image == null and texture == null) or (image != null and texture != null))
|
||||
_image = image
|
||||
_texture = texture
|
||||
_viewport_sprite.texture = _texture
|
||||
_viewport_bg_sprite.texture = _texture
|
||||
_brush_material.set_shader_param(SHADER_PARAM_SRC_TEXTURE, _texture)
|
||||
if image != null:
|
||||
_viewport.hdr = image.get_format() in _hdr_formats
|
||||
#print("PAINTER VIEWPORT HDR: ", _viewport.hdr)
|
||||
|
||||
|
||||
# Sets the size of the brush in pixels.
|
||||
# This will cause the internal viewport to resize, which is expensive.
|
||||
# If you need to frequently change brush size during a paint stroke, prefer using scale instead.
|
||||
func set_brush_size(new_size: int):
|
||||
_brush_size = new_size
|
||||
|
||||
|
@ -105,8 +137,36 @@ func get_brush_size() -> int:
|
|||
return _brush_size
|
||||
|
||||
|
||||
func set_brush_rotation(rotation: float):
|
||||
_viewport_brush_sprite.rotation = rotation
|
||||
|
||||
|
||||
func get_brush_rotation() -> float:
|
||||
return _viewport_bg_sprite.rotation
|
||||
|
||||
|
||||
# The difference between size and scale, is that size is in pixels, while scale is a multiplier.
|
||||
# Scale is also a lot cheaper to change, so you may prefer changing it instead of size if that
|
||||
# happens often during a painting stroke.
|
||||
func set_brush_scale(s: float):
|
||||
_brush_scale = clamp(s, 0.0, 1.0)
|
||||
#_viewport_brush_sprite.scale = Vector2(s, s)
|
||||
|
||||
|
||||
func get_brush_scale() -> float:
|
||||
return _viewport_bg_sprite.scale.x
|
||||
|
||||
|
||||
func set_brush_opacity(opacity: float):
|
||||
_brush_opacity = clamp(opacity, 0.0, 1.0)
|
||||
|
||||
|
||||
func get_brush_opacity() -> float:
|
||||
return _brush_opacity
|
||||
|
||||
|
||||
func set_brush_texture(texture: Texture):
|
||||
_brush_material.set_shader_param(BRUSH_TEXTURE_SHADER_PARAM, texture)
|
||||
_viewport_brush_sprite.texture = texture
|
||||
|
||||
|
||||
func set_brush_shader(shader: Shader):
|
||||
|
@ -115,6 +175,7 @@ func set_brush_shader(shader: Shader):
|
|||
|
||||
|
||||
func set_brush_shader_param(p: String, v):
|
||||
assert(not _API_SHADER_PARAMS.has(p))
|
||||
_modified_shader_params[p] = true
|
||||
_brush_material.set_shader_param(p, v)
|
||||
|
||||
|
@ -125,28 +186,53 @@ func clear_brush_shader_params():
|
|||
_modified_shader_params.clear()
|
||||
|
||||
|
||||
# If we want to be able to rotate the brush quad every frame,
|
||||
# we must prepare a bigger viewport otherwise the quad will not fit inside
|
||||
static func _get_size_fit_for_rotation(src_size: Vector2) -> Vector2:
|
||||
var d = int(ceil(src_size.length()))
|
||||
return Vector2(d, d)
|
||||
|
||||
|
||||
# You must call this from an `_input` function or similar.
|
||||
func paint_input(center_pos: Vector2):
|
||||
var vp_size = Vector2(_brush_size, _brush_size)
|
||||
var vp_size = _get_size_fit_for_rotation(Vector2(_brush_size, _brush_size))
|
||||
if _viewport.size != vp_size:
|
||||
# Do this lazily so the brush slider won't lag while adjusting it
|
||||
# TODO An "sliding_ended" handling might produce better user experience
|
||||
_viewport.size = vp_size
|
||||
_viewport_brush_sprite.position = _viewport.size / 2.0
|
||||
|
||||
# Need to floor the position in case the brush has an odd size
|
||||
var brush_pos := (center_pos - Vector2(_brush_size, _brush_size) * 0.5).round()
|
||||
var brush_pos := (center_pos - _viewport.size * 0.5).round()
|
||||
_viewport.render_target_update_mode = Viewport.UPDATE_ONCE
|
||||
_viewport_sprite.position = -brush_pos
|
||||
_viewport.render_target_clear_mode = Viewport.CLEAR_MODE_ONLY_NEXT_FRAME
|
||||
_viewport_bg_sprite.position = -brush_pos
|
||||
_brush_position = brush_pos
|
||||
_cmd_paint = true
|
||||
|
||||
# We want this quad to have a specific size, regardless of the texture assigned to it
|
||||
_viewport_brush_sprite.scale = \
|
||||
_brush_scale * Vector2(_brush_size, _brush_size) / _viewport_brush_sprite.texture.get_size()
|
||||
|
||||
# Using a Color because Godot doesn't understand vec4
|
||||
var rect := Color()
|
||||
rect.r = brush_pos.x / _texture.get_width()
|
||||
rect.g = brush_pos.y / _texture.get_height()
|
||||
rect.b = _brush_size / _texture.get_width()
|
||||
rect.a = _brush_size / _texture.get_height()
|
||||
_brush_material.set_shader_param("u_texture_rect", rect)
|
||||
rect.b = _viewport.size.x / _texture.get_width()
|
||||
rect.a = _viewport.size.y / _texture.get_height()
|
||||
# In order to make sure that u_brush_rect is never bigger than the brush:
|
||||
# 1. we ceil() the result of lower-left corner
|
||||
# 2. we floor() the result of upper-right corner
|
||||
# and then rederive width and height from the result
|
||||
# var half_brush:Vector2 = Vector2(_brush_size, _brush_size) / 2
|
||||
# var brush_LL := (center_pos - half_brush).ceil()
|
||||
# var brush_UR := (center_pos + half_brush).floor()
|
||||
# rect.r = brush_LL.x / _texture.get_width()
|
||||
# rect.g = brush_LL.y / _texture.get_height()
|
||||
# rect.b = (brush_UR.x - brush_LL.x) / _texture.get_width()
|
||||
# rect.a = (brush_UR.y - brush_LL.y) / _texture.get_height()
|
||||
_brush_material.set_shader_param(SHADER_PARAM_SRC_RECT, rect)
|
||||
_brush_material.set_shader_param(SHADER_PARAM_OPACITY, _brush_opacity)
|
||||
|
||||
|
||||
# Don't commit until this is false
|
||||
|
@ -180,8 +266,8 @@ func _process(delta: float):
|
|||
|
||||
var src_x : int = max(-brush_pos.x, 0)
|
||||
var src_y : int = max(-brush_pos.y, 0)
|
||||
var src_w : int = min(max(_brush_size - src_x, 0), _texture.get_width() - dst_x)
|
||||
var src_h : int = min(max(_brush_size - src_y, 0), _texture.get_height() - dst_y)
|
||||
var src_w : int = min(max(_viewport.size.x - src_x, 0), _texture.get_width() - dst_x)
|
||||
var src_h : int = min(max(_viewport.size.y - src_y, 0), _texture.get_height() - dst_y)
|
||||
|
||||
if src_w != 0 and src_h != 0:
|
||||
_mark_modified_chunks(dst_x, dst_y, src_w, src_h)
|
||||
|
@ -207,6 +293,7 @@ func _mark_modified_chunks(bx: int, by: int, bw: int, bh: int):
|
|||
|
||||
for cy in range(cmin_y, cmax_y):
|
||||
for cx in range(cmin_x, cmax_x):
|
||||
#print("Marking chunk ", Vector2(cx, cy))
|
||||
_modified_chunks[Vector2(cx, cy)] = true
|
||||
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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"]
|
|
@ -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)
|
|
@ -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)
|
||||
|
|
@ -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
|
|
@ -1,13 +1,20 @@
|
|||
shader_type canvas_item;
|
||||
render_mode blend_disabled;
|
||||
|
||||
uniform sampler2D u_brush_texture;
|
||||
uniform sampler2D u_src_texture;
|
||||
uniform vec4 u_src_rect;
|
||||
uniform float u_opacity = 1.0;
|
||||
uniform float u_factor = 1.0;
|
||||
uniform float u_value = 1.0;
|
||||
|
||||
void fragment() {
|
||||
float brush_value = texture(u_brush_texture, SCREEN_UV).r;
|
||||
vec2 get_src_uv(vec2 screen_uv) {
|
||||
vec2 uv = u_src_rect.xy + screen_uv * u_src_rect.zw;
|
||||
return uv;
|
||||
}
|
||||
|
||||
vec4 src = texture(TEXTURE, 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));
|
||||
}
|
||||
|
|
|
@ -1,14 +1,21 @@
|
|||
shader_type canvas_item;
|
||||
render_mode blend_disabled;
|
||||
|
||||
uniform sampler2D u_brush_texture;
|
||||
uniform sampler2D u_src_texture;
|
||||
uniform vec4 u_src_rect;
|
||||
uniform float u_opacity = 1.0;
|
||||
uniform float u_factor = 1.0;
|
||||
uniform vec4 u_color = vec4(1.0);
|
||||
|
||||
void fragment() {
|
||||
float brush_value = texture(u_brush_texture, SCREEN_UV).r;
|
||||
vec2 get_src_uv(vec2 screen_uv) {
|
||||
vec2 uv = u_src_rect.xy + screen_uv * u_src_rect.zw;
|
||||
return uv;
|
||||
}
|
||||
|
||||
vec4 src = texture(TEXTURE, UV);
|
||||
void fragment() {
|
||||
float brush_value = u_opacity * texture(TEXTURE, UV).r;
|
||||
|
||||
vec4 src = texture(u_src_texture, get_src_uv(SCREEN_UV));
|
||||
|
||||
// Despite hints, albedo textures render darker.
|
||||
// Trying to undo sRGB does not work because of 8-bit precision loss
|
||||
|
|
|
@ -1,9 +1,16 @@
|
|||
shader_type canvas_item;
|
||||
|
||||
uniform sampler2D u_brush_texture;
|
||||
uniform sampler2D u_src_texture;
|
||||
uniform vec4 u_src_rect;
|
||||
uniform float u_opacity = 1.0;
|
||||
uniform float u_factor = 1.0;
|
||||
uniform vec4 u_color = vec4(1.0);
|
||||
|
||||
vec2 get_src_uv(vec2 screen_uv) {
|
||||
vec2 uv = u_src_rect.xy + screen_uv * u_src_rect.zw;
|
||||
return uv;
|
||||
}
|
||||
|
||||
// float get_noise(vec2 pos) {
|
||||
// return fract(sin(dot(pos.xy ,vec2(12.9898,78.233))) * 43758.5453);
|
||||
// }
|
||||
|
@ -43,8 +50,9 @@ float erode(sampler2D heightmap, vec2 uv, vec2 pixel_size, float weight) {
|
|||
}
|
||||
|
||||
void fragment() {
|
||||
float brush_value = texture(u_brush_texture, SCREEN_UV).r * u_factor;
|
||||
float ph = erode(TEXTURE, UV, TEXTURE_PIXEL_SIZE, brush_value);
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -1,14 +1,21 @@
|
|||
shader_type canvas_item;
|
||||
render_mode blend_disabled;
|
||||
|
||||
uniform sampler2D u_brush_texture;
|
||||
uniform sampler2D u_src_texture;
|
||||
uniform vec4 u_src_rect;
|
||||
uniform float u_opacity = 1.0;
|
||||
uniform float u_factor = 1.0;
|
||||
uniform float u_flatten_value;
|
||||
|
||||
void fragment() {
|
||||
float brush_value = texture(u_brush_texture, SCREEN_UV).r;
|
||||
vec2 get_src_uv(vec2 screen_uv) {
|
||||
vec2 uv = u_src_rect.xy + screen_uv * u_src_rect.zw;
|
||||
return uv;
|
||||
}
|
||||
|
||||
float src_h = texture(TEXTURE, UV).r;
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -1,33 +1,39 @@
|
|||
shader_type canvas_item;
|
||||
render_mode blend_disabled;
|
||||
|
||||
uniform sampler2D u_brush_texture;
|
||||
uniform sampler2D u_src_texture;
|
||||
uniform vec4 u_src_rect;
|
||||
uniform float u_opacity = 1.0;
|
||||
uniform float u_factor = 1.0;
|
||||
uniform vec4 u_texture_rect;
|
||||
|
||||
vec2 get_src_uv(vec2 screen_uv) {
|
||||
vec2 uv = u_src_rect.xy + screen_uv * u_src_rect.zw;
|
||||
return uv;
|
||||
}
|
||||
|
||||
// TODO Could actually level to whatever height the brush was at the beginning of the stroke?
|
||||
|
||||
void fragment() {
|
||||
float brush_value = texture(u_brush_texture, SCREEN_UV).r;
|
||||
float brush_value = u_opacity * texture(TEXTURE, UV).r;
|
||||
|
||||
// The heightmap does not have mipmaps,
|
||||
// so we need to use an approximation of average.
|
||||
// This is not a very good one though...
|
||||
float dst_h = 0.0;
|
||||
vec2 uv_min = vec2(u_texture_rect.xy);
|
||||
vec2 uv_max = vec2(u_texture_rect.xy + u_texture_rect.zw);
|
||||
vec2 uv_min = vec2(u_src_rect.xy);
|
||||
vec2 uv_max = vec2(u_src_rect.xy + u_src_rect.zw);
|
||||
for (int i = 0; i < 5; ++i) {
|
||||
for (int j = 0; j < 5; ++j) {
|
||||
float x = mix(uv_min.x, uv_max.x, float(i) / 4.0);
|
||||
float y = mix(uv_min.y, uv_max.y, float(j) / 4.0);
|
||||
float h = texture(TEXTURE, vec2(x, y)).r;
|
||||
float h = texture(u_src_texture, vec2(x, y)).r;
|
||||
dst_h += h;
|
||||
}
|
||||
}
|
||||
dst_h /= (5.0 * 5.0);
|
||||
|
||||
// TODO I have no idea if this will check out
|
||||
float src_h = texture(TEXTURE, UV).r;
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -1,13 +1,20 @@
|
|||
shader_type canvas_item;
|
||||
render_mode blend_disabled;
|
||||
|
||||
uniform sampler2D u_brush_texture;
|
||||
uniform sampler2D u_src_texture;
|
||||
uniform vec4 u_src_rect;
|
||||
uniform float u_opacity = 1.0;
|
||||
uniform float u_factor = 1.0;
|
||||
|
||||
void fragment() {
|
||||
float brush_value = texture(u_brush_texture, SCREEN_UV).r;
|
||||
vec2 get_src_uv(vec2 screen_uv) {
|
||||
vec2 uv = u_src_rect.xy + screen_uv * u_src_rect.zw;
|
||||
return uv;
|
||||
}
|
||||
|
||||
float src_h = texture(TEXTURE, UV).r;
|
||||
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 = src_h + u_factor * brush_value;
|
||||
COLOR = vec4(h, 0.0, 0.0, 1.0);
|
||||
}
|
||||
|
|
|
@ -1,18 +1,27 @@
|
|||
shader_type canvas_item;
|
||||
render_mode blend_disabled;
|
||||
|
||||
uniform sampler2D u_brush_texture;
|
||||
uniform sampler2D u_src_texture;
|
||||
uniform vec4 u_src_rect;
|
||||
uniform float u_opacity = 1.0;
|
||||
uniform float u_factor = 1.0;
|
||||
|
||||
void fragment() {
|
||||
float brush_value = texture(u_brush_texture, SCREEN_UV).r;
|
||||
vec2 get_src_uv(vec2 screen_uv) {
|
||||
vec2 uv = u_src_rect.xy + screen_uv * u_src_rect.zw;
|
||||
return uv;
|
||||
}
|
||||
|
||||
vec2 offset = TEXTURE_PIXEL_SIZE;
|
||||
float src_nx = texture(TEXTURE, UV - vec2(offset.x, 0.0)).r;
|
||||
float src_px = texture(TEXTURE, UV + vec2(offset.x, 0.0)).r;
|
||||
float src_ny = texture(TEXTURE, UV - vec2(0.0, offset.y)).r;
|
||||
float src_py = texture(TEXTURE, UV + vec2(0.0, offset.y)).r;
|
||||
float src_h = texture(TEXTURE, UV).r;
|
||||
void fragment() {
|
||||
float brush_value = u_opacity * texture(TEXTURE, UV).r;
|
||||
|
||||
vec2 src_pixel_size = 1.0 / vec2(textureSize(u_src_texture, 0));
|
||||
vec2 src_uv = get_src_uv(SCREEN_UV);
|
||||
vec2 offset = src_pixel_size;
|
||||
float src_nx = texture(u_src_texture, src_uv - vec2(offset.x, 0.0)).r;
|
||||
float src_px = texture(u_src_texture, src_uv + vec2(offset.x, 0.0)).r;
|
||||
float src_ny = texture(u_src_texture, src_uv - vec2(0.0, offset.y)).r;
|
||||
float src_py = texture(u_src_texture, src_uv + vec2(0.0, offset.y)).r;
|
||||
float src_h = texture(u_src_texture, src_uv).r;
|
||||
float dst_h = (src_h + src_nx + src_px + src_ny + src_py) * 0.2;
|
||||
float h = mix(src_h, dst_h, u_factor * brush_value);
|
||||
COLOR = vec4(h, 0.0, 0.0, 1.0);
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
shader_type canvas_item;
|
||||
render_mode blend_disabled;
|
||||
|
||||
uniform sampler2D u_brush_texture;
|
||||
uniform sampler2D u_src_texture;
|
||||
uniform vec4 u_src_rect;
|
||||
uniform float u_opacity = 1.0;
|
||||
uniform float u_factor = 1.0;
|
||||
uniform vec4 u_splat = vec4(1.0, 0.0, 0.0, 0.0);
|
||||
uniform sampler2D u_other_splatmap_1;
|
||||
|
@ -11,6 +13,11 @@ uniform sampler2D u_heightmap;
|
|||
uniform float u_normal_min_y = 0.0;
|
||||
uniform float u_normal_max_y = 1.0;
|
||||
|
||||
vec2 get_src_uv(vec2 screen_uv) {
|
||||
vec2 uv = u_src_rect.xy + screen_uv * u_src_rect.zw;
|
||||
return uv;
|
||||
}
|
||||
|
||||
float sum(vec4 v) {
|
||||
return v.x + v.y + v.z + v.w;
|
||||
}
|
||||
|
@ -44,16 +51,17 @@ float apply_slope_limit(float brush_value, vec3 normal, float normal_min_y, floa
|
|||
}
|
||||
|
||||
void fragment() {
|
||||
float brush_value = texture(u_brush_texture, SCREEN_UV).r * u_factor;
|
||||
float brush_value = u_opacity * texture(TEXTURE, UV).r * u_factor;
|
||||
|
||||
vec3 normal = get_normal(u_heightmap, UV);
|
||||
vec2 src_uv = get_src_uv(SCREEN_UV);
|
||||
vec3 normal = get_normal(u_heightmap, src_uv);
|
||||
brush_value = apply_slope_limit(brush_value, normal, u_normal_min_y, u_normal_max_y);
|
||||
|
||||
// It is assumed 3 other renders are done the same with the other 3
|
||||
vec4 src0 = texture(TEXTURE, UV);
|
||||
vec4 src1 = texture(u_other_splatmap_1, UV);
|
||||
vec4 src2 = texture(u_other_splatmap_2, UV);
|
||||
vec4 src3 = texture(u_other_splatmap_3, UV);
|
||||
vec4 src0 = texture(u_src_texture, src_uv);
|
||||
vec4 src1 = texture(u_other_splatmap_1, src_uv);
|
||||
vec4 src2 = texture(u_other_splatmap_2, src_uv);
|
||||
vec4 src3 = texture(u_other_splatmap_3, src_uv);
|
||||
float t = brush_value;
|
||||
vec4 s0 = mix(src0, u_splat, t);
|
||||
vec4 s1 = mix(src1, vec4(0.0), t);
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
shader_type canvas_item;
|
||||
render_mode blend_disabled;
|
||||
|
||||
uniform sampler2D u_brush_texture;
|
||||
uniform sampler2D u_src_texture;
|
||||
uniform vec4 u_src_rect;
|
||||
uniform float u_opacity = 1.0;
|
||||
uniform float u_factor = 1.0;
|
||||
uniform vec4 u_splat = vec4(1.0, 0.0, 0.0, 0.0);
|
||||
uniform sampler2D u_heightmap;
|
||||
|
@ -9,6 +11,11 @@ uniform float u_normal_min_y = 0.0;
|
|||
uniform float u_normal_max_y = 1.0;
|
||||
//uniform float u_normal_falloff = 0.0;
|
||||
|
||||
vec2 get_src_uv(vec2 screen_uv) {
|
||||
vec2 uv = u_src_rect.xy + screen_uv * u_src_rect.zw;
|
||||
return uv;
|
||||
}
|
||||
|
||||
vec3 get_normal(sampler2D heightmap, vec2 pos) {
|
||||
vec2 ps = vec2(1.0) / vec2(textureSize(heightmap, 0));
|
||||
float hnx = texture(heightmap, pos + vec2(-ps.x, 0.0)).r;
|
||||
|
@ -38,12 +45,13 @@ float apply_slope_limit(float brush_value, vec3 normal, float normal_min_y, floa
|
|||
}
|
||||
|
||||
void fragment() {
|
||||
float brush_value = texture(u_brush_texture, SCREEN_UV).r * u_factor;
|
||||
float brush_value = u_opacity * texture(TEXTURE, UV).r * u_factor;
|
||||
|
||||
vec3 normal = get_normal(u_heightmap, UV);
|
||||
vec2 src_uv = get_src_uv(SCREEN_UV);
|
||||
vec3 normal = get_normal(u_heightmap, src_uv);
|
||||
brush_value = apply_slope_limit(brush_value, normal, u_normal_min_y, u_normal_max_y);
|
||||
|
||||
vec4 src_splat = texture(TEXTURE, UV);
|
||||
vec4 src_splat = texture(u_src_texture, src_uv);
|
||||
vec4 s = mix(src_splat, u_splat, brush_value);
|
||||
s = s / (s.r + s.g + s.b + s.a);
|
||||
COLOR = s;
|
||||
|
|
|
@ -1,18 +1,26 @@
|
|||
shader_type canvas_item;
|
||||
render_mode blend_disabled;
|
||||
|
||||
uniform sampler2D u_brush_texture;
|
||||
uniform sampler2D u_src_texture;
|
||||
uniform vec4 u_src_rect;
|
||||
uniform float u_opacity = 1.0;
|
||||
uniform float u_factor = 1.0;
|
||||
uniform int u_texture_index;
|
||||
uniform int u_mode; // 0: index, 1: weight
|
||||
uniform int u_mode; // 0: output index, 1: output weight
|
||||
uniform sampler2D u_index_map;
|
||||
uniform sampler2D u_weight_map;
|
||||
|
||||
void fragment() {
|
||||
float brush_value = texture(u_brush_texture, SCREEN_UV).r * clamp(u_factor, 0.0, 1.0);
|
||||
vec2 get_src_uv(vec2 screen_uv) {
|
||||
vec2 uv = u_src_rect.xy + screen_uv * u_src_rect.zw;
|
||||
return uv;
|
||||
}
|
||||
|
||||
vec4 iv = texture(u_index_map, UV);
|
||||
vec4 wv = texture(u_weight_map, UV);
|
||||
void fragment() {
|
||||
float brush_value = u_opacity * texture(TEXTURE, UV).r * clamp(u_factor, 0.0, 1.0);
|
||||
|
||||
vec2 src_uv = get_src_uv(SCREEN_UV);
|
||||
vec4 iv = texture(u_index_map, src_uv);
|
||||
vec4 wv = texture(u_weight_map, src_uv);
|
||||
|
||||
float i[3] = {iv.r, iv.g, iv.b};
|
||||
float w[3] = {wv.r, wv.g, wv.b};
|
||||
|
|
|
@ -4,6 +4,7 @@ const Painter = preload("./painter.gd")
|
|||
const HTerrain = preload("../../hterrain.gd")
|
||||
const HTerrainData = preload("../../hterrain_data.gd")
|
||||
const Logger = preload("../../util/logger.gd")
|
||||
const Brush = preload("./brush.gd")
|
||||
|
||||
const RaiseShader = preload("./shaders/raise.shader")
|
||||
const SmoothShader = preload("./shaders/smooth.shader")
|
||||
|
@ -33,12 +34,12 @@ class ModifiedMap:
|
|||
var map_index := 0
|
||||
var painter_index := 0
|
||||
|
||||
signal changed
|
||||
signal flatten_height_changed
|
||||
|
||||
var _painters := []
|
||||
|
||||
var _brush_size := 32
|
||||
var _opacity := 1.0
|
||||
var _brush := Brush.new()
|
||||
|
||||
var _color := Color(1, 0, 0, 1)
|
||||
var _mask_flag := false
|
||||
var _mode := MODE_RAISE
|
||||
|
@ -59,43 +60,45 @@ func _init():
|
|||
var p = Painter.new()
|
||||
# The name is just for debugging
|
||||
p.set_name(str("Painter", i))
|
||||
p.set_brush_size(_brush_size)
|
||||
#p.set_brush_size(_brush_size)
|
||||
p.connect("texture_region_changed", self, "_on_painter_texture_region_changed", [i])
|
||||
add_child(p)
|
||||
_painters.append(p)
|
||||
|
||||
|
||||
func get_brush() -> Brush:
|
||||
return _brush
|
||||
|
||||
|
||||
func get_brush_size() -> int:
|
||||
return _brush_size
|
||||
return _brush.get_size()
|
||||
|
||||
|
||||
func set_brush_size(s: int):
|
||||
if _brush_size == s:
|
||||
return
|
||||
_brush_size = s
|
||||
for p in _painters:
|
||||
p.set_brush_size(_brush_size)
|
||||
emit_signal("changed")
|
||||
_brush.set_size(s)
|
||||
# for p in _painters:
|
||||
# p.set_brush_size(_brush_size)
|
||||
|
||||
|
||||
func set_brush_texture(texture: Texture):
|
||||
for p in _painters:
|
||||
p.set_brush_texture(texture)
|
||||
_brush.set_shapes([texture])
|
||||
# for p in _painters:
|
||||
# p.set_brush_texture(texture)
|
||||
|
||||
|
||||
func get_opacity() -> float:
|
||||
return _opacity
|
||||
return _brush.get_opacity()
|
||||
|
||||
|
||||
func set_opacity(opacity: float):
|
||||
_opacity = opacity
|
||||
_brush.set_opacity(opacity)
|
||||
|
||||
|
||||
func set_flatten_height(h: float):
|
||||
if h == _flatten_height:
|
||||
return
|
||||
_flatten_height = h
|
||||
emit_signal("changed")
|
||||
emit_signal("flatten_height_changed")
|
||||
|
||||
|
||||
func get_flatten_height() -> float:
|
||||
|
@ -177,7 +180,10 @@ func commit() -> Dictionary:
|
|||
var changes := []
|
||||
var chunk_positions : Array
|
||||
|
||||
assert(len(_modified_maps) > 0)
|
||||
|
||||
for mm in _modified_maps:
|
||||
#print("Flushing painter ", mm.painter_index)
|
||||
var painter : Painter = _painters[mm.painter_index]
|
||||
var info := painter.commit()
|
||||
|
||||
|
@ -198,6 +204,12 @@ func commit() -> Dictionary:
|
|||
# since the latter updates out of order for preview
|
||||
terrain_data.notify_region_change(rect, mm.map_type, mm.map_index, false, true)
|
||||
|
||||
# for i in len(_painters):
|
||||
# var p = _painters[i]
|
||||
# if p.has_modified_chunks():
|
||||
# print("Painter ", i, " has modified chunks")
|
||||
|
||||
# `commit()` is supposed to consume these chunks, there should be none left
|
||||
assert(not has_modified_chunks())
|
||||
|
||||
return {
|
||||
|
@ -227,12 +239,17 @@ func set_terrain(terrain: HTerrain):
|
|||
p.clear_brush_shader_params()
|
||||
|
||||
|
||||
# This may be called from an `_input` callback
|
||||
func paint_input(position: Vector2):
|
||||
# This may be called from an `_input` callback.
|
||||
# Returns `true` if any change was performed.
|
||||
func paint_input(position: Vector2, pressure: float) -> bool:
|
||||
assert(_terrain.get_data() != null)
|
||||
var data = _terrain.get_data()
|
||||
assert(not data.is_locked())
|
||||
|
||||
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:
|
||||
|
@ -284,6 +301,7 @@ func paint_input(position: Vector2):
|
|||
_logger.error("Unknown mode {0}".format([_mode]))
|
||||
|
||||
assert(len(_modified_maps) > 0)
|
||||
return true
|
||||
|
||||
|
||||
func _on_painter_texture_region_changed(rect: Rect2, painter_index: int):
|
||||
|
@ -308,8 +326,8 @@ func _paint_height(data: HTerrainData, position: Vector2, factor: float):
|
|||
_modified_maps = [mm]
|
||||
|
||||
# When using sculpting tools, make it dependent on brush size
|
||||
var raise_strength := 10.0 + float(_brush_size)
|
||||
var delta := factor * _opacity * (2.0 / 60.0) * raise_strength
|
||||
var raise_strength := 10.0 + float(_brush.get_size())
|
||||
var delta := factor * (2.0 / 60.0) * raise_strength
|
||||
|
||||
var p : Painter = _painters[0]
|
||||
|
||||
|
@ -332,7 +350,7 @@ func _paint_smooth(data: HTerrainData, position: Vector2):
|
|||
var p : Painter = _painters[0]
|
||||
|
||||
p.set_brush_shader(SmoothShader)
|
||||
p.set_brush_shader_param("u_factor", _opacity * (10.0 / 60.0))
|
||||
p.set_brush_shader_param("u_factor", (10.0 / 60.0))
|
||||
p.set_image(image, texture)
|
||||
p.paint_input(position)
|
||||
|
||||
|
@ -350,7 +368,7 @@ func _paint_flatten(data: HTerrainData, position: Vector2):
|
|||
var p : Painter = _painters[0]
|
||||
|
||||
p.set_brush_shader(FlattenShader)
|
||||
p.set_brush_shader_param("u_factor", _opacity)
|
||||
#p.set_brush_shader_param("u_factor", _opacity)
|
||||
p.set_brush_shader_param("u_flatten_value", _flatten_height)
|
||||
p.set_image(image, texture)
|
||||
p.paint_input(position)
|
||||
|
@ -369,7 +387,7 @@ func _paint_level(data: HTerrainData, position: Vector2):
|
|||
var p : Painter = _painters[0]
|
||||
|
||||
p.set_brush_shader(LevelShader)
|
||||
p.set_brush_shader_param("u_factor", _opacity * (10.0 / 60.0))
|
||||
p.set_brush_shader_param("u_factor", (10.0 / 60.0))
|
||||
p.set_image(image, texture)
|
||||
p.paint_input(position)
|
||||
|
||||
|
@ -387,7 +405,7 @@ func _paint_erode(data: HTerrainData, position: Vector2):
|
|||
var p : Painter = _painters[0]
|
||||
|
||||
p.set_brush_shader(ErodeShader)
|
||||
p.set_brush_shader_param("u_factor", _opacity)
|
||||
#p.set_brush_shader_param("u_factor", _opacity)
|
||||
p.set_image(image, texture)
|
||||
p.paint_input(position)
|
||||
|
||||
|
@ -407,7 +425,7 @@ func _paint_splat4(data: HTerrainData, position: Vector2):
|
|||
var splat = Color(0.0, 0.0, 0.0, 0.0)
|
||||
splat[_texture_index] = 1.0;
|
||||
p.set_brush_shader(Splat4Shader)
|
||||
p.set_brush_shader_param("u_factor", _opacity)
|
||||
#p.set_brush_shader_param("u_factor", _opacity)
|
||||
p.set_brush_shader_param("u_splat", splat)
|
||||
p.set_brush_shader_param("u_normal_min_y", cos(_slope_limit_high_angle))
|
||||
p.set_brush_shader_param("u_normal_max_y", cos(_slope_limit_low_angle) + 0.001)
|
||||
|
@ -440,7 +458,7 @@ func _paint_splat_indexed(data: HTerrainData, position: Vector2):
|
|||
|
||||
p.set_brush_shader(SplatIndexedShader)
|
||||
p.set_brush_shader_param("u_mode", mode)
|
||||
p.set_brush_shader_param("u_factor", _opacity)
|
||||
#p.set_brush_shader_param("u_factor", _opacity)
|
||||
p.set_brush_shader_param("u_index_map", textures[0])
|
||||
p.set_brush_shader_param("u_weight_map", textures[1])
|
||||
p.set_brush_shader_param("u_texture_index", _texture_index)
|
||||
|
@ -482,7 +500,7 @@ func _paint_splat16(data: HTerrainData, position: Vector2):
|
|||
other_splatmaps.append(tex)
|
||||
|
||||
p.set_brush_shader(Splat16Shader)
|
||||
p.set_brush_shader_param("u_factor", _opacity)
|
||||
#p.set_brush_shader_param("u_factor", _opacity)
|
||||
p.set_brush_shader_param("u_splat", splats[i])
|
||||
p.set_brush_shader_param("u_other_splatmap_1", other_splatmaps[0])
|
||||
p.set_brush_shader_param("u_other_splatmap_2", other_splatmaps[1])
|
||||
|
@ -510,7 +528,7 @@ func _paint_color(data: HTerrainData, position: Vector2):
|
|||
# https://github.com/Zylann/godot_heightmap_plugin/issues/17#issuecomment-734001879
|
||||
|
||||
p.set_brush_shader(ColorShader)
|
||||
p.set_brush_shader_param("u_factor", _opacity)
|
||||
#p.set_brush_shader_param("u_factor", _opacity)
|
||||
p.set_brush_shader_param("u_color", _color)
|
||||
p.set_image(image, texture)
|
||||
p.paint_input(position)
|
||||
|
@ -529,7 +547,7 @@ func _paint_mask(data: HTerrainData, position: Vector2):
|
|||
var p : Painter = _painters[0]
|
||||
|
||||
p.set_brush_shader(AlphaShader)
|
||||
p.set_brush_shader_param("u_factor", _opacity)
|
||||
#p.set_brush_shader_param("u_factor", _opacity)
|
||||
p.set_brush_shader_param("u_value", 1.0 if _mask_flag else 0.0)
|
||||
p.set_image(image, texture)
|
||||
p.paint_input(position)
|
||||
|
@ -550,7 +568,7 @@ func _paint_detail(data: HTerrainData, position: Vector2):
|
|||
|
||||
# TODO Don't use this shader
|
||||
p.set_brush_shader(ColorShader)
|
||||
p.set_brush_shader_param("u_factor", _opacity)
|
||||
#p.set_brush_shader_param("u_factor", _opacity)
|
||||
p.set_brush_shader_param("u_color", c)
|
||||
p.set_image(image, texture)
|
||||
p.paint_input(position)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
[gd_scene load_steps=2 format=2]
|
||||
[gd_scene load_steps=3 format=2]
|
||||
|
||||
[ext_resource path="res://addons/zylann.hterrain/tools/exporter/export_image_dialog.gd" type="Script" id=1]
|
||||
[ext_resource path="res://addons/zylann.hterrain/tools/util/dialog_fitter.tscn" type="PackedScene" id=2]
|
||||
|
||||
[node name="ExportImageDialog" type="WindowDialog"]
|
||||
margin_left = 77.0
|
||||
|
@ -156,6 +157,9 @@ margin_left = 257.0
|
|||
margin_right = 311.0
|
||||
margin_bottom = 20.0
|
||||
text = "Cancel"
|
||||
|
||||
[node name="DialogFitter" parent="." instance=ExtResource( 2 )]
|
||||
|
||||
[connection signal="text_changed" from="VB/Grid/OutputPath/HeightmapPathLineEdit" to="." method="_on_HeightmapPathLineEdit_text_changed"]
|
||||
[connection signal="pressed" from="VB/Grid/OutputPath/HeightmapPathBrowseButton" to="." method="_on_HeightmapPathBrowseButton_pressed"]
|
||||
[connection signal="item_selected" from="VB/Grid/FormatSelector" to="." method="_on_FormatSelector_item_selected"]
|
||||
|
|
|
@ -1,243 +1,84 @@
|
|||
[gd_scene load_steps=2 format=2]
|
||||
[gd_scene load_steps=3 format=2]
|
||||
|
||||
[ext_resource path="res://addons/zylann.hterrain/tools/generate_mesh_dialog.gd" type="Script" id=1]
|
||||
[ext_resource path="res://addons/zylann.hterrain/tools/util/dialog_fitter.tscn" type="PackedScene" id=2]
|
||||
|
||||
[node name="GenerateMeshDialog" type="WindowDialog" index="0"]
|
||||
|
||||
anchor_left = 0.0
|
||||
anchor_top = 0.0
|
||||
anchor_right = 0.0
|
||||
anchor_bottom = 0.0
|
||||
[node name="GenerateMeshDialog" type="WindowDialog"]
|
||||
margin_left = 57.0
|
||||
margin_top = 83.0
|
||||
margin_right = 505.0
|
||||
margin_bottom = 269.0
|
||||
rect_min_size = Vector2( 448, 186 )
|
||||
rect_pivot_offset = Vector2( 0, 0 )
|
||||
rect_clip_content = false
|
||||
mouse_filter = 0
|
||||
mouse_default_cursor_shape = 0
|
||||
size_flags_horizontal = 1
|
||||
size_flags_vertical = 1
|
||||
popup_exclusive = false
|
||||
window_title = "Generate full mesh"
|
||||
resizable = false
|
||||
script = ExtResource( 1 )
|
||||
_sections_unfolded = [ "Rect" ]
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="." index="1"]
|
||||
|
||||
anchor_left = 0.0
|
||||
anchor_top = 0.0
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="."]
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
margin_left = 8.0
|
||||
margin_top = 8.0
|
||||
margin_right = -8.0
|
||||
margin_bottom = -8.0
|
||||
rect_pivot_offset = Vector2( 0, 0 )
|
||||
rect_clip_content = false
|
||||
mouse_filter = 1
|
||||
mouse_default_cursor_shape = 0
|
||||
size_flags_horizontal = 1
|
||||
size_flags_vertical = 1
|
||||
alignment = 0
|
||||
_sections_unfolded = [ "Margin" ]
|
||||
|
||||
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer" index="0"]
|
||||
|
||||
anchor_left = 0.0
|
||||
anchor_top = 0.0
|
||||
anchor_right = 0.0
|
||||
anchor_bottom = 0.0
|
||||
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"]
|
||||
margin_right = 432.0
|
||||
margin_bottom = 24.0
|
||||
rect_pivot_offset = Vector2( 0, 0 )
|
||||
rect_clip_content = false
|
||||
mouse_filter = 1
|
||||
mouse_default_cursor_shape = 0
|
||||
size_flags_horizontal = 1
|
||||
size_flags_vertical = 1
|
||||
alignment = 0
|
||||
|
||||
[node name="Label" type="Label" parent="VBoxContainer/HBoxContainer" index="0"]
|
||||
|
||||
anchor_left = 0.0
|
||||
anchor_top = 0.0
|
||||
anchor_right = 0.0
|
||||
anchor_bottom = 0.0
|
||||
[node name="Label" type="Label" parent="VBoxContainer/HBoxContainer"]
|
||||
margin_top = 5.0
|
||||
margin_right = 28.0
|
||||
margin_bottom = 19.0
|
||||
rect_pivot_offset = Vector2( 0, 0 )
|
||||
rect_clip_content = false
|
||||
mouse_filter = 2
|
||||
mouse_default_cursor_shape = 0
|
||||
size_flags_horizontal = 1
|
||||
size_flags_vertical = 4
|
||||
text = "LOD"
|
||||
percent_visible = 1.0
|
||||
lines_skipped = 0
|
||||
max_lines_visible = -1
|
||||
|
||||
[node name="LODSpinBox" type="SpinBox" parent="VBoxContainer/HBoxContainer" index="1"]
|
||||
|
||||
anchor_left = 0.0
|
||||
anchor_top = 0.0
|
||||
anchor_right = 0.0
|
||||
anchor_bottom = 0.0
|
||||
[node name="LODSpinBox" type="SpinBox" parent="VBoxContainer/HBoxContainer"]
|
||||
margin_left = 32.0
|
||||
margin_right = 432.0
|
||||
margin_bottom = 24.0
|
||||
rect_pivot_offset = Vector2( 0, 0 )
|
||||
rect_clip_content = false
|
||||
mouse_filter = 0
|
||||
mouse_default_cursor_shape = 0
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 1
|
||||
min_value = 1.0
|
||||
max_value = 16.0
|
||||
step = 1.0
|
||||
page = 0.0
|
||||
value = 1.0
|
||||
exp_edit = false
|
||||
rounded = false
|
||||
editable = true
|
||||
prefix = ""
|
||||
suffix = ""
|
||||
_sections_unfolded = [ "Size Flags" ]
|
||||
|
||||
[node name="PreviewLabel" type="Label" parent="VBoxContainer" index="1"]
|
||||
|
||||
anchor_left = 0.0
|
||||
anchor_top = 0.0
|
||||
anchor_right = 0.0
|
||||
anchor_bottom = 0.0
|
||||
[node name="PreviewLabel" type="Label" parent="VBoxContainer"]
|
||||
margin_top = 28.0
|
||||
margin_right = 432.0
|
||||
margin_bottom = 42.0
|
||||
rect_pivot_offset = Vector2( 0, 0 )
|
||||
rect_clip_content = false
|
||||
mouse_filter = 2
|
||||
mouse_default_cursor_shape = 0
|
||||
size_flags_horizontal = 1
|
||||
size_flags_vertical = 4
|
||||
text = "9999 vertices, 9999 triangles"
|
||||
percent_visible = 1.0
|
||||
lines_skipped = 0
|
||||
max_lines_visible = -1
|
||||
|
||||
[node name="Spacer" type="Control" parent="VBoxContainer" index="2"]
|
||||
|
||||
anchor_left = 0.0
|
||||
anchor_top = 0.0
|
||||
anchor_right = 0.0
|
||||
anchor_bottom = 0.0
|
||||
[node name="Spacer" type="Control" parent="VBoxContainer"]
|
||||
margin_top = 46.0
|
||||
margin_right = 432.0
|
||||
margin_bottom = 54.0
|
||||
rect_min_size = Vector2( 0, 8 )
|
||||
rect_pivot_offset = Vector2( 0, 0 )
|
||||
rect_clip_content = false
|
||||
mouse_filter = 0
|
||||
mouse_default_cursor_shape = 0
|
||||
size_flags_horizontal = 1
|
||||
size_flags_vertical = 1
|
||||
_sections_unfolded = [ "Rect" ]
|
||||
|
||||
[node name="Label" type="Label" parent="VBoxContainer" index="3"]
|
||||
|
||||
anchor_left = 0.0
|
||||
anchor_top = 0.0
|
||||
anchor_right = 0.0
|
||||
anchor_bottom = 0.0
|
||||
[node name="Label" type="Label" parent="VBoxContainer"]
|
||||
margin_top = 58.0
|
||||
margin_right = 432.0
|
||||
margin_bottom = 123.0
|
||||
rect_pivot_offset = Vector2( 0, 0 )
|
||||
rect_clip_content = false
|
||||
mouse_filter = 2
|
||||
mouse_default_cursor_shape = 0
|
||||
size_flags_horizontal = 1
|
||||
size_flags_vertical = 4
|
||||
text = "Note: generating a full mesh from the terrain may result in a huge amount of vertices for a single object. It is preferred to do this for small terrains, or as a temporary workaround to generate a navmesh."
|
||||
autowrap = true
|
||||
percent_visible = 1.0
|
||||
lines_skipped = 0
|
||||
max_lines_visible = -1
|
||||
_sections_unfolded = [ "custom_colors" ]
|
||||
|
||||
[node name="Buttons" type="HBoxContainer" parent="VBoxContainer" index="4"]
|
||||
|
||||
anchor_left = 0.0
|
||||
anchor_top = 0.0
|
||||
anchor_right = 0.0
|
||||
anchor_bottom = 0.0
|
||||
[node name="Buttons" type="HBoxContainer" parent="VBoxContainer"]
|
||||
margin_top = 127.0
|
||||
margin_right = 432.0
|
||||
margin_bottom = 147.0
|
||||
rect_pivot_offset = Vector2( 0, 0 )
|
||||
rect_clip_content = false
|
||||
mouse_filter = 1
|
||||
mouse_default_cursor_shape = 0
|
||||
size_flags_horizontal = 1
|
||||
size_flags_vertical = 1
|
||||
custom_constants/separation = 32
|
||||
alignment = 1
|
||||
_sections_unfolded = [ "custom_constants" ]
|
||||
|
||||
[node name="Generate" type="Button" parent="VBoxContainer/Buttons" index="0"]
|
||||
|
||||
anchor_left = 0.0
|
||||
anchor_top = 0.0
|
||||
anchor_right = 0.0
|
||||
anchor_bottom = 0.0
|
||||
[node name="Generate" type="Button" parent="VBoxContainer/Buttons"]
|
||||
margin_left = 137.0
|
||||
margin_right = 208.0
|
||||
margin_bottom = 20.0
|
||||
rect_pivot_offset = Vector2( 0, 0 )
|
||||
rect_clip_content = false
|
||||
focus_mode = 2
|
||||
mouse_filter = 0
|
||||
mouse_default_cursor_shape = 0
|
||||
size_flags_horizontal = 1
|
||||
size_flags_vertical = 1
|
||||
toggle_mode = false
|
||||
enabled_focus_mode = 2
|
||||
shortcut = null
|
||||
group = null
|
||||
text = "Generate"
|
||||
flat = false
|
||||
align = 1
|
||||
|
||||
[node name="Cancel" type="Button" parent="VBoxContainer/Buttons" index="1"]
|
||||
|
||||
anchor_left = 0.0
|
||||
anchor_top = 0.0
|
||||
anchor_right = 0.0
|
||||
anchor_bottom = 0.0
|
||||
[node name="Cancel" type="Button" parent="VBoxContainer/Buttons"]
|
||||
margin_left = 240.0
|
||||
margin_right = 294.0
|
||||
margin_bottom = 20.0
|
||||
rect_pivot_offset = Vector2( 0, 0 )
|
||||
rect_clip_content = false
|
||||
focus_mode = 2
|
||||
mouse_filter = 0
|
||||
mouse_default_cursor_shape = 0
|
||||
size_flags_horizontal = 1
|
||||
size_flags_vertical = 1
|
||||
toggle_mode = false
|
||||
enabled_focus_mode = 2
|
||||
shortcut = null
|
||||
group = null
|
||||
text = "Cancel"
|
||||
flat = false
|
||||
align = 1
|
||||
|
||||
[node name="DialogFitter" parent="." instance=ExtResource( 2 )]
|
||||
|
||||
[connection signal="value_changed" from="VBoxContainer/HBoxContainer/LODSpinBox" to="." method="_on_LODSpinBox_value_changed"]
|
||||
|
||||
[connection signal="pressed" from="VBoxContainer/Buttons/Generate" to="." method="_on_Generate_pressed"]
|
||||
|
||||
[connection signal="pressed" from="VBoxContainer/Buttons/Cancel" to="." method="_on_Cancel_pressed"]
|
||||
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
tool
|
||||
extends WindowDialog
|
||||
|
||||
const HTerrain = preload("../../hterrain.gd")
|
||||
const HTerrainData = preload("../../hterrain_data.gd")
|
||||
const HTerrainMesher = preload("../../hterrain_mesher.gd")
|
||||
const Util = preload("../../util/util.gd")
|
||||
|
@ -19,7 +20,7 @@ onready var _preview = $VBoxContainer/Editor/Preview/TerrainPreview
|
|||
onready var _progress_bar = $VBoxContainer/Editor/Preview/ProgressBar
|
||||
|
||||
var _dummy_texture = load("res://addons/zylann.hterrain/tools/icons/empty.png")
|
||||
var _terrain = null
|
||||
var _terrain : HTerrain = null
|
||||
var _applying := false
|
||||
var _generator : TextureGenerator
|
||||
var _generated_textures := [null, null]
|
||||
|
@ -106,6 +107,30 @@ func _ready():
|
|||
"range": { "min": 0.0, "max": 1.0 },
|
||||
"default_value": 0.0
|
||||
},
|
||||
"island_weight": {
|
||||
"type": TYPE_REAL,
|
||||
"range": { "min": 0.0, "max": 1.0, "step": 0.01 },
|
||||
"default_value": 0.0
|
||||
},
|
||||
"island_sharpness": {
|
||||
"type": TYPE_REAL,
|
||||
"range": { "min": 0.0, "max": 1.0, "step": 0.01 },
|
||||
"default_value": 0.0
|
||||
},
|
||||
"island_height_ratio": {
|
||||
"type": TYPE_REAL,
|
||||
"range": { "min": -1.0, "max": 1.0, "step": 0.01 },
|
||||
"default_value": -1.0
|
||||
},
|
||||
"island_shape": {
|
||||
"type": TYPE_REAL,
|
||||
"range": { "min": 0.0, "max": 1.0, "step": 0.01 },
|
||||
"default_value": 0.0
|
||||
},
|
||||
"additive_heightmap": {
|
||||
"type": TYPE_BOOL,
|
||||
"default_value": false
|
||||
},
|
||||
"show_sea": {
|
||||
"type": TYPE_BOOL,
|
||||
"default_value": true
|
||||
|
@ -212,11 +237,19 @@ func _update_generator(preview: bool):
|
|||
|
||||
var preview_scale := 4.0 # As if 2049x2049
|
||||
var sectors := []
|
||||
var terrain_size = 513
|
||||
|
||||
var additive_heightmap : Texture = null
|
||||
|
||||
# Get preview scale and sectors to generate.
|
||||
# Allowing null terrain to make it testable.
|
||||
if _terrain != null and _terrain.get_data() != null:
|
||||
var terrain_size = _terrain.get_data().get_resolution()
|
||||
var terrain_data := _terrain.get_data()
|
||||
if _terrain != null and terrain_data != null:
|
||||
terrain_size = terrain_data.get_resolution()
|
||||
|
||||
if _inspector.get_value("additive_heightmap"):
|
||||
additive_heightmap = \
|
||||
terrain_data.get_texture(HTerrainData.CHANNEL_HEIGHT)
|
||||
|
||||
if preview:
|
||||
# When previewing the resolution does not span the entire terrain,
|
||||
|
@ -225,6 +258,15 @@ func _update_generator(preview: bool):
|
|||
sectors.append(Vector2(0, 0))
|
||||
|
||||
else:
|
||||
if additive_heightmap != null:
|
||||
# We have to duplicate the heightmap because we are going to write
|
||||
# into it during the generation process.
|
||||
# It would be fine when we don't read outside of a generated tile,
|
||||
# but we actually do that for erosion: neighboring pixels are read
|
||||
# again, and if they were modified by a previous tile it will
|
||||
# disrupt generation, so we need to use a copy of the original.
|
||||
additive_heightmap = additive_heightmap.duplicate()
|
||||
|
||||
# When we get to generate it fully, sectors are used,
|
||||
# so the size or shape of the terrain doesn't matter
|
||||
preview_scale = 1.0
|
||||
|
@ -262,12 +304,21 @@ func _update_generator(preview: bool):
|
|||
p.params = {
|
||||
"u_octaves": _inspector.get_value("octaves"),
|
||||
"u_seed": _inspector.get_value("seed"),
|
||||
"u_scale": scale * preview_scale,
|
||||
"u_offset": base_offset_ndc / preview_scale,
|
||||
"u_scale": scale,
|
||||
"u_offset": base_offset_ndc,
|
||||
"u_base_height": _inspector.get_value("base_height") / preview_scale,
|
||||
"u_height_range": _inspector.get_value("height_range") / preview_scale,
|
||||
"u_roughness": _inspector.get_value("roughness"),
|
||||
"u_curve": _inspector.get_value("curve")
|
||||
"u_curve": _inspector.get_value("curve"),
|
||||
"u_island_weight": _inspector.get_value("island_weight"),
|
||||
"u_island_sharpness": _inspector.get_value("island_sharpness"),
|
||||
"u_island_height_ratio": _inspector.get_value("island_height_ratio"),
|
||||
"u_island_shape": _inspector.get_value("island_shape"),
|
||||
"u_additive_heightmap": additive_heightmap,
|
||||
"u_additive_heightmap_factor": \
|
||||
(1.0 if additive_heightmap != null else 0.0) / preview_scale,
|
||||
"u_terrain_size": terrain_size / preview_scale,
|
||||
"u_tile_size": _viewport_resolution
|
||||
}
|
||||
_generator.add_pass(p)
|
||||
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
[gd_scene load_steps=4 format=2]
|
||||
[gd_scene load_steps=5 format=2]
|
||||
|
||||
[ext_resource path="res://addons/zylann.hterrain/tools/generator/generator_dialog.gd" type="Script" id=1]
|
||||
[ext_resource path="res://addons/zylann.hterrain/tools/inspector/inspector.tscn" type="PackedScene" id=2]
|
||||
[ext_resource path="res://addons/zylann.hterrain/tools/terrain_preview.tscn" type="PackedScene" id=3]
|
||||
[ext_resource path="res://addons/zylann.hterrain/tools/util/dialog_fitter.tscn" type="PackedScene" id=4]
|
||||
|
||||
[node name="GeneratorDialog" type="WindowDialog"]
|
||||
margin_left = 22.0
|
||||
margin_top = 32.0
|
||||
margin_right = 1122.0
|
||||
margin_bottom = 632.0
|
||||
margin_bottom = 666.0
|
||||
rect_min_size = Vector2( 1100, 600 )
|
||||
window_title = "Generate terrain"
|
||||
resizable = true
|
||||
|
@ -25,27 +26,28 @@ margin_top = 8.0
|
|||
margin_right = -8.0
|
||||
margin_bottom = -8.0
|
||||
custom_constants/separation = 16
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
|
||||
[node name="Editor" type="HBoxContainer" parent="VBoxContainer"]
|
||||
margin_right = 1084.0
|
||||
margin_bottom = 548.0
|
||||
margin_bottom = 584.0
|
||||
size_flags_vertical = 3
|
||||
|
||||
[node name="Settings" type="Control" parent="VBoxContainer/Editor"]
|
||||
[node name="Settings" type="VBoxContainer" parent="VBoxContainer/Editor"]
|
||||
margin_right = 420.0
|
||||
margin_bottom = 548.0
|
||||
margin_bottom = 584.0
|
||||
rect_min_size = Vector2( 420, 0 )
|
||||
|
||||
[node name="Inspector" parent="VBoxContainer/Editor/Settings" instance=ExtResource( 2 )]
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
margin_right = 0.0
|
||||
margin_bottom = 0.0
|
||||
margin_right = 420.0
|
||||
margin_bottom = 584.0
|
||||
|
||||
[node name="Preview" type="Control" parent="VBoxContainer/Editor"]
|
||||
margin_left = 424.0
|
||||
margin_right = 1084.0
|
||||
margin_bottom = 548.0
|
||||
margin_bottom = 584.0
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="TerrainPreview" parent="VBoxContainer/Editor/Preview" instance=ExtResource( 3 )]
|
||||
|
@ -66,9 +68,9 @@ margin_top = -16.0
|
|||
step = 1.0
|
||||
|
||||
[node name="Choices" type="HBoxContainer" parent="VBoxContainer"]
|
||||
margin_top = 564.0
|
||||
margin_top = 600.0
|
||||
margin_right = 1084.0
|
||||
margin_bottom = 584.0
|
||||
margin_bottom = 620.0
|
||||
custom_constants/separation = 32
|
||||
alignment = 1
|
||||
|
||||
|
@ -83,6 +85,9 @@ margin_left = 555.0
|
|||
margin_right = 609.0
|
||||
margin_bottom = 20.0
|
||||
text = "Cancel"
|
||||
|
||||
[node name="DialogFitter" parent="." instance=ExtResource( 4 )]
|
||||
|
||||
[connection signal="property_changed" from="VBoxContainer/Editor/Settings/Inspector" to="." method="_on_Inspector_property_changed"]
|
||||
[connection signal="dragged" from="VBoxContainer/Editor/Preview/TerrainPreview" to="." method="_on_TerrainPreview_dragged"]
|
||||
[connection signal="pressed" from="VBoxContainer/Choices/ApplyButton" to="." method="_on_ApplyButton_pressed"]
|
||||
|
|
|
@ -8,9 +8,21 @@ uniform int u_seed;
|
|||
uniform int u_octaves = 5;
|
||||
uniform float u_roughness = 0.5;
|
||||
uniform float u_curve = 1.0;
|
||||
uniform float u_terrain_size = 513.0;
|
||||
uniform float u_tile_size = 513.0;
|
||||
uniform sampler2D u_additive_heightmap;
|
||||
uniform float u_additive_heightmap_factor = 0.0;
|
||||
uniform vec2 u_uv_offset;
|
||||
uniform vec2 u_uv_scale = vec2(1.0, 1.0);
|
||||
|
||||
uniform float u_island_weight = 0.0;
|
||||
// 0: smooth transition, 1: sharp transition
|
||||
uniform float u_island_sharpness = 0.0;
|
||||
// 0: edge is min height (island), 1: edge is max height (canyon)
|
||||
uniform float u_island_height_ratio = 0.0;
|
||||
// 0: round, 1: square
|
||||
uniform float u_island_shape = 0.0;
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Perlin noise source:
|
||||
// https://github.com/curly-brace/Godot-3.0-Noise-Shaders
|
||||
|
@ -103,22 +115,91 @@ float get_fractal_noise(vec2 uv) {
|
|||
return gs;
|
||||
}
|
||||
|
||||
float get_height(vec2 uv) {
|
||||
float h = 0.5 + 0.5 * get_fractal_noise(uv);
|
||||
// x is a ratio in 0..1
|
||||
float get_island_curve(float x) {
|
||||
return smoothstep(min(0.999, u_island_sharpness), 1.0, x);
|
||||
// float exponent = 1.0 + 10.0 * u_island_sharpness;
|
||||
// return pow(abs(x), exponent);
|
||||
}
|
||||
|
||||
float smooth_union(float a, float b, float k) {
|
||||
float h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0);
|
||||
return mix(b, a, h) - k * h * (1.0 - h);
|
||||
}
|
||||
|
||||
float squareish_distance(vec2 a, vec2 b, float r, float s) {
|
||||
vec2 v = b - a;
|
||||
// TODO This is brute force but this is the first attempt that gave me a "rounded square" distance,
|
||||
// where the "roundings" remained constant over distance (not the case with standard box SDF)
|
||||
float da = -smooth_union(v.x+s, v.y+s, r)+s;
|
||||
float db = -smooth_union(s-v.x, s-v.y, r)+s;
|
||||
float dc = -smooth_union(s-v.x, v.y+s, r)+s;
|
||||
float dd = -smooth_union(v.x+s, s-v.y, r)+s;
|
||||
return max(max(da, db), max(dc, dd));
|
||||
}
|
||||
|
||||
// This is too sharp
|
||||
//float squareish_distance(vec2 a, vec2 b) {
|
||||
// vec2 v = b - a;
|
||||
// // Manhattan distance would produce a "diamond-shaped distance".
|
||||
// // This gives "square-shaped" distance.
|
||||
// return max(abs(v.x), abs(v.y));
|
||||
//}
|
||||
|
||||
float get_island_distance(vec2 pos, vec2 center, float terrain_size) {
|
||||
float rd = distance(pos, center);
|
||||
float sd = squareish_distance(pos, center, terrain_size * 0.1, terrain_size);
|
||||
return mix(rd, sd, u_island_shape);
|
||||
}
|
||||
|
||||
// pos is in terrain space
|
||||
float get_height(vec2 pos) {
|
||||
float h = 0.0;
|
||||
|
||||
{
|
||||
// Noise (0..1)
|
||||
// Offset and scale for the noise itself
|
||||
vec2 uv_noise = (pos / u_terrain_size + u_offset) * u_scale;
|
||||
h = 0.5 + 0.5 * get_fractal_noise(uv_noise);
|
||||
}
|
||||
|
||||
// Curve
|
||||
{
|
||||
h = pow(h, u_curve);
|
||||
}
|
||||
|
||||
// Island
|
||||
{
|
||||
float terrain_size = u_terrain_size;
|
||||
vec2 island_center = vec2(0.5 * terrain_size);
|
||||
float island_height_ratio = 0.5 + 0.5 * u_island_height_ratio;
|
||||
float island_distance = get_island_distance(pos, island_center, terrain_size);
|
||||
float distance_ratio = clamp(island_distance / (0.5 * terrain_size), 0.0, 1.0);
|
||||
float island_ratio = u_island_weight * get_island_curve(distance_ratio);
|
||||
h = mix(h, island_height_ratio, island_ratio);
|
||||
}
|
||||
|
||||
// Height remapping
|
||||
{
|
||||
h = u_base_height + h * u_height_range;
|
||||
}
|
||||
|
||||
// Additive heightmap
|
||||
{
|
||||
h += u_additive_heightmap_factor * texture(u_additive_heightmap, pos / u_terrain_size).r;
|
||||
}
|
||||
|
||||
return h;
|
||||
}
|
||||
|
||||
void fragment() {
|
||||
vec2 uv = SCREEN_UV;
|
||||
// Handle screen padding: transform UV back into generation space.
|
||||
// This is in tile space actually...? it spans 1 unit across the viewport,
|
||||
// and starts from 0 when tile (0,0) is generated.
|
||||
// Maybe we could change this into world units instead?
|
||||
vec2 uv_tile = (SCREEN_UV + u_uv_offset) * u_uv_scale;
|
||||
|
||||
// Handle screen padding: transform UV back into generation space
|
||||
uv = (uv + u_uv_offset) * u_uv_scale;
|
||||
float h = get_height(uv_tile * u_tile_size);
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
[gd_scene load_steps=3 format=2]
|
||||
[gd_scene load_steps=4 format=2]
|
||||
|
||||
[ext_resource path="res://addons/zylann.hterrain/tools/importer/importer_dialog.gd" type="Script" id=1]
|
||||
[ext_resource path="res://addons/zylann.hterrain/tools/inspector/inspector.tscn" type="PackedScene" id=2]
|
||||
[ext_resource path="res://addons/zylann.hterrain/tools/util/dialog_fitter.tscn" type="PackedScene" id=3]
|
||||
|
||||
[node name="WindowDialog" type="WindowDialog"]
|
||||
visible = true
|
||||
|
@ -13,6 +14,9 @@ rect_min_size = Vector2( 500, 380 )
|
|||
window_title = "Import maps"
|
||||
resizable = true
|
||||
script = ExtResource( 1 )
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="."]
|
||||
anchor_right = 1.0
|
||||
|
@ -93,6 +97,9 @@ margin_left = 312.0
|
|||
margin_right = 366.0
|
||||
margin_bottom = 20.0
|
||||
text = "Cancel"
|
||||
|
||||
[node name="DialogFitter" parent="." instance=ExtResource( 3 )]
|
||||
|
||||
[connection signal="property_changed" from="VBoxContainer/Inspector" to="." method="_on_Inspector_property_changed"]
|
||||
[connection signal="pressed" from="VBoxContainer/ButtonsArea/CheckButton" to="." method="_on_CheckButton_pressed"]
|
||||
[connection signal="pressed" from="VBoxContainer/ButtonsArea/ImportButton" to="." method="_on_ImportButton_pressed"]
|
||||
|
|
|
@ -422,7 +422,8 @@ func _dummy_setter(v):
|
|||
|
||||
|
||||
func _on_ask_load_texture(key):
|
||||
_open_file_dialog(["*.png ; PNG files"], "_on_texture_selected", [key], FileDialog.ACCESS_RESOURCES)
|
||||
_open_file_dialog(["*.png ; PNG files"], "_on_texture_selected", [key],
|
||||
FileDialog.ACCESS_RESOURCES)
|
||||
|
||||
|
||||
func _open_file_dialog(filters, callback, binds, access):
|
||||
|
@ -430,7 +431,8 @@ func _open_file_dialog(filters, callback, binds, access):
|
|||
_file_dialog.clear_filters()
|
||||
for filter in filters:
|
||||
_file_dialog.add_filter(filter)
|
||||
_file_dialog.connect("popup_hide", self, "call_deferred", ["_on_file_dialog_close"], CONNECT_ONESHOT)
|
||||
_file_dialog.connect("popup_hide", self, "call_deferred", ["_on_file_dialog_close"],
|
||||
CONNECT_ONESHOT)
|
||||
_file_dialog.connect("file_selected", self, callback, binds)
|
||||
_file_dialog.popup_centered_ratio(0.7)
|
||||
|
||||
|
|
|
@ -2,70 +2,23 @@
|
|||
|
||||
[ext_resource path="res://addons/zylann.hterrain/tools/inspector/inspector.gd" type="Script" id=1]
|
||||
|
||||
[node name="Inspector" type="Control" index="0"]
|
||||
|
||||
anchor_left = 0.0
|
||||
anchor_top = 0.0
|
||||
anchor_right = 0.0
|
||||
anchor_bottom = 0.0
|
||||
[node name="Inspector" type="VBoxContainer"]
|
||||
margin_right = 348.0
|
||||
margin_bottom = 383.0
|
||||
rect_pivot_offset = Vector2( 0, 0 )
|
||||
rect_clip_content = false
|
||||
mouse_filter = 0
|
||||
mouse_default_cursor_shape = 0
|
||||
size_flags_horizontal = 1
|
||||
size_flags_vertical = 1
|
||||
script = ExtResource( 1 )
|
||||
_sections_unfolded = [ "custom_constants" ]
|
||||
|
||||
[node name="GridContainer" type="GridContainer" parent="." index="0"]
|
||||
|
||||
anchor_left = 0.0
|
||||
anchor_top = 0.0
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
rect_pivot_offset = Vector2( 0, 0 )
|
||||
rect_clip_content = false
|
||||
mouse_filter = 1
|
||||
mouse_default_cursor_shape = 0
|
||||
size_flags_horizontal = 1
|
||||
size_flags_vertical = 1
|
||||
[node name="GridContainer" type="GridContainer" parent="."]
|
||||
margin_right = 348.0
|
||||
custom_constants/vseparation = 4
|
||||
custom_constants/hseparation = 8
|
||||
columns = 2
|
||||
_sections_unfolded = [ "Anchor", "Margin", "custom_constants" ]
|
||||
|
||||
[node name="OpenFileDialog" type="FileDialog" parent="." index="1"]
|
||||
|
||||
visible = false
|
||||
anchor_left = 0.0
|
||||
anchor_top = 0.0
|
||||
anchor_right = 0.0
|
||||
anchor_bottom = 0.0
|
||||
[node name="OpenFileDialog" type="FileDialog" parent="."]
|
||||
margin_left = 261.0
|
||||
margin_top = 150.0
|
||||
margin_right = 710.0
|
||||
margin_bottom = 426.0
|
||||
rect_min_size = Vector2( 400, 300 )
|
||||
rect_pivot_offset = Vector2( 0, 0 )
|
||||
rect_clip_content = false
|
||||
mouse_filter = 0
|
||||
mouse_default_cursor_shape = 0
|
||||
size_flags_horizontal = 1
|
||||
size_flags_vertical = 1
|
||||
popup_exclusive = false
|
||||
window_title = "Ouvrir un fichier"
|
||||
window_title = "Open a File"
|
||||
resizable = true
|
||||
dialog_hide_on_ok = false
|
||||
mode_overrides_title = true
|
||||
mode = 0
|
||||
access = 0
|
||||
filters = PoolStringArray( )
|
||||
show_hidden_files = false
|
||||
current_dir = "res://"
|
||||
current_file = ""
|
||||
current_path = "res://"
|
||||
_sections_unfolded = [ "Rect" ]
|
||||
|
||||
|
||||
|
|
|
@ -63,6 +63,10 @@ func get_import_options(preset_index: int) -> Array:
|
|||
{
|
||||
"name": "flags/mipmaps",
|
||||
"default_value": true
|
||||
},
|
||||
{
|
||||
"name": "flags/anisotropic",
|
||||
"default_value": false
|
||||
}
|
||||
]
|
||||
|
||||
|
@ -132,7 +136,8 @@ func _import(p_source_path: String, p_save_path: String, options: Dictionary,
|
|||
options["compress/mode"],
|
||||
options["flags/repeat"],
|
||||
options["flags/filter"],
|
||||
options["flags/mipmaps"])
|
||||
options["flags/mipmaps"],
|
||||
options["flags/anisotropic"])
|
||||
|
||||
if not result.success:
|
||||
return Result.new(false,
|
||||
|
|
|
@ -63,6 +63,10 @@ func get_import_options(preset_index: int) -> Array:
|
|||
{
|
||||
"name": "flags/mipmaps",
|
||||
"default_value": true
|
||||
},
|
||||
{
|
||||
"name": "flags/anisotropic",
|
||||
"default_value": false
|
||||
}
|
||||
]
|
||||
|
||||
|
@ -116,6 +120,7 @@ func _import(p_source_path: String, p_save_path: String, options: Dictionary,
|
|||
|
||||
var image : Image = result.value
|
||||
|
||||
|
||||
result = StreamTextureImporter.import(
|
||||
p_source_path,
|
||||
image,
|
||||
|
@ -127,7 +132,8 @@ func _import(p_source_path: String, p_save_path: String, options: Dictionary,
|
|||
options["compress/mode"],
|
||||
options["flags/repeat"],
|
||||
options["flags/filter"],
|
||||
options["flags/mipmaps"])
|
||||
options["flags/mipmaps"],
|
||||
options["flags/anisotropic"])
|
||||
|
||||
if not result.success:
|
||||
return Result.new(false,
|
||||
|
|
|
@ -43,14 +43,15 @@ static func import(
|
|||
p_compress_mode: int,
|
||||
p_repeat: int,
|
||||
p_filter: bool,
|
||||
p_mipmaps: bool) -> Result:
|
||||
p_mipmaps: bool,
|
||||
p_anisotropic: bool) -> Result:
|
||||
|
||||
var compress_mode := p_compress_mode
|
||||
var lossy := 0.7
|
||||
var repeat := p_repeat
|
||||
var filter := p_filter
|
||||
var mipmaps := p_mipmaps
|
||||
var anisotropic := false
|
||||
var anisotropic := p_anisotropic
|
||||
var srgb := 1 if p_contains_albedo else 2
|
||||
var fix_alpha_border := false
|
||||
var premult_alpha := false
|
||||
|
|
|
@ -37,7 +37,8 @@ static func import(
|
|||
p_compress_mode: int,
|
||||
p_repeat: int,
|
||||
p_filter: bool,
|
||||
p_mipmaps: bool) -> Result:
|
||||
p_mipmaps: bool,
|
||||
p_anisotropic: bool) -> Result:
|
||||
|
||||
var compress_mode := p_compress_mode
|
||||
var no_bptc_if_rgb := false#p_options["compress/no_bptc_if_rgb"];
|
||||
|
@ -59,6 +60,8 @@ static func import(
|
|||
tex_flags |= Texture.FLAG_MIPMAPS
|
||||
if srgb == 1:
|
||||
tex_flags |= Texture.FLAG_CONVERT_TO_LINEAR
|
||||
if p_anisotropic:
|
||||
tex_flags |= Texture.FLAG_ANISOTROPIC_FILTER
|
||||
|
||||
# Vector<Ref<Image> > slices;
|
||||
#
|
||||
|
|
|
@ -40,8 +40,8 @@ func set_camera_transform(cam_transform: Transform):
|
|||
_minimap.set_camera_transform(cam_transform)
|
||||
|
||||
|
||||
func set_brush(brush):
|
||||
_brush_editor.set_brush(brush)
|
||||
func set_terrain_painter(terrain_painter):
|
||||
_brush_editor.set_terrain_painter(terrain_painter)
|
||||
|
||||
|
||||
func _on_TextureEditor_texture_selected(index):
|
||||
|
|
|
@ -10,7 +10,7 @@ const HTerrainTextureSet = preload("../hterrain_texture_set.gd")
|
|||
const PackedTextureImporter = preload("./packed_textures/packed_texture_importer.gd")
|
||||
const PackedTextureArrayImporter = preload("./packed_textures/packed_texture_array_importer.gd")
|
||||
const PreviewGenerator = preload("./preview_generator.gd")
|
||||
const Brush = preload("./brush/terrain_painter.gd")
|
||||
const TerrainPainter = preload("./brush/terrain_painter.gd")
|
||||
const BrushDecal = preload("./brush/decal.gd")
|
||||
const Util = preload("../util/util.gd")
|
||||
const EditorUtil = preload("./util/editor_util.gd")
|
||||
|
@ -74,7 +74,7 @@ var _image_cache : ImageFileCache
|
|||
var _packed_texture_importer := PackedTextureImporter.new()
|
||||
var _packed_texture_array_importer := PackedTextureArrayImporter.new()
|
||||
|
||||
var _brush : Brush = null
|
||||
var _terrain_painter : TerrainPainter = null
|
||||
var _brush_decal : BrushDecal = null
|
||||
var _mouse_pressed := false
|
||||
#var _pending_paint_action = null
|
||||
|
@ -106,13 +106,13 @@ func _enter_tree():
|
|||
_preview_generator = PreviewGenerator.new()
|
||||
get_editor_interface().get_resource_previewer().add_preview_generator(_preview_generator)
|
||||
|
||||
_brush = Brush.new()
|
||||
_brush.set_brush_size(5)
|
||||
_brush.connect("changed", self, "_on_brush_changed")
|
||||
add_child(_brush)
|
||||
_terrain_painter = TerrainPainter.new()
|
||||
_terrain_painter.set_brush_size(5)
|
||||
_terrain_painter.get_brush().connect("size_changed", self, "_on_brush_size_changed")
|
||||
add_child(_terrain_painter)
|
||||
|
||||
_brush_decal = BrushDecal.new()
|
||||
_brush_decal.set_size(_brush.get_brush_size())
|
||||
_brush_decal.set_size(_terrain_painter.get_brush_size())
|
||||
|
||||
_image_cache = ImageFileCache.new("user://temp_hterrain_image_cache")
|
||||
|
||||
|
@ -124,7 +124,7 @@ func _enter_tree():
|
|||
_panel.hide()
|
||||
add_control_to_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_BOTTOM, _panel)
|
||||
# Apparently _ready() still isn't called at this point...
|
||||
_panel.call_deferred("set_brush", _brush)
|
||||
_panel.call_deferred("set_terrain_painter", _terrain_painter)
|
||||
_panel.call_deferred("setup_dialogs", base_control)
|
||||
_panel.set_undo_redo(get_undo_redo())
|
||||
_panel.set_image_cache(_image_cache)
|
||||
|
@ -165,44 +165,43 @@ func _enter_tree():
|
|||
_menu_button = menu
|
||||
|
||||
var mode_icons := {}
|
||||
mode_icons[Brush.MODE_RAISE] = get_icon("heightmap_raise")
|
||||
mode_icons[Brush.MODE_LOWER] = get_icon("heightmap_lower")
|
||||
mode_icons[Brush.MODE_SMOOTH] = get_icon("heightmap_smooth")
|
||||
mode_icons[Brush.MODE_FLATTEN] = get_icon("heightmap_flatten")
|
||||
# TODO Have different icons
|
||||
mode_icons[Brush.MODE_SPLAT] = get_icon("heightmap_paint")
|
||||
mode_icons[Brush.MODE_COLOR] = get_icon("heightmap_color")
|
||||
mode_icons[Brush.MODE_DETAIL] = get_icon("grass")
|
||||
mode_icons[Brush.MODE_MASK] = get_icon("heightmap_mask")
|
||||
mode_icons[Brush.MODE_LEVEL] = get_icon("heightmap_level")
|
||||
mode_icons[Brush.MODE_ERODE] = get_icon("heightmap_erode")
|
||||
mode_icons[TerrainPainter.MODE_RAISE] = get_icon("heightmap_raise")
|
||||
mode_icons[TerrainPainter.MODE_LOWER] = get_icon("heightmap_lower")
|
||||
mode_icons[TerrainPainter.MODE_SMOOTH] = get_icon("heightmap_smooth")
|
||||
mode_icons[TerrainPainter.MODE_FLATTEN] = get_icon("heightmap_flatten")
|
||||
mode_icons[TerrainPainter.MODE_SPLAT] = get_icon("heightmap_paint")
|
||||
mode_icons[TerrainPainter.MODE_COLOR] = get_icon("heightmap_color")
|
||||
mode_icons[TerrainPainter.MODE_DETAIL] = get_icon("grass")
|
||||
mode_icons[TerrainPainter.MODE_MASK] = get_icon("heightmap_mask")
|
||||
mode_icons[TerrainPainter.MODE_LEVEL] = get_icon("heightmap_level")
|
||||
mode_icons[TerrainPainter.MODE_ERODE] = get_icon("heightmap_erode")
|
||||
|
||||
var mode_tooltips := {}
|
||||
mode_tooltips[Brush.MODE_RAISE] = "Raise height"
|
||||
mode_tooltips[Brush.MODE_LOWER] = "Lower height"
|
||||
mode_tooltips[Brush.MODE_SMOOTH] = "Smooth height"
|
||||
mode_tooltips[Brush.MODE_FLATTEN] = "Flatten (flatten to a specific height)"
|
||||
mode_tooltips[Brush.MODE_SPLAT] = "Texture paint"
|
||||
mode_tooltips[Brush.MODE_COLOR] = "Color paint"
|
||||
mode_tooltips[Brush.MODE_DETAIL] = "Grass paint"
|
||||
mode_tooltips[Brush.MODE_MASK] = "Cut holes"
|
||||
mode_tooltips[Brush.MODE_LEVEL] = "Level (smoothly flattens to average)"
|
||||
mode_tooltips[Brush.MODE_ERODE] = "Erode"
|
||||
mode_tooltips[TerrainPainter.MODE_RAISE] = "Raise height"
|
||||
mode_tooltips[TerrainPainter.MODE_LOWER] = "Lower height"
|
||||
mode_tooltips[TerrainPainter.MODE_SMOOTH] = "Smooth height"
|
||||
mode_tooltips[TerrainPainter.MODE_FLATTEN] = "Flatten (flatten to a specific height)"
|
||||
mode_tooltips[TerrainPainter.MODE_SPLAT] = "Texture paint"
|
||||
mode_tooltips[TerrainPainter.MODE_COLOR] = "Color paint"
|
||||
mode_tooltips[TerrainPainter.MODE_DETAIL] = "Grass paint"
|
||||
mode_tooltips[TerrainPainter.MODE_MASK] = "Cut holes"
|
||||
mode_tooltips[TerrainPainter.MODE_LEVEL] = "Level (smoothly flattens to average)"
|
||||
mode_tooltips[TerrainPainter.MODE_ERODE] = "Erode"
|
||||
|
||||
_toolbar.add_child(VSeparator.new())
|
||||
|
||||
# I want modes to be in that order in the GUI
|
||||
var ordered_brush_modes := [
|
||||
Brush.MODE_RAISE,
|
||||
Brush.MODE_LOWER,
|
||||
Brush.MODE_SMOOTH,
|
||||
Brush.MODE_LEVEL,
|
||||
Brush.MODE_FLATTEN,
|
||||
Brush.MODE_ERODE,
|
||||
Brush.MODE_SPLAT,
|
||||
Brush.MODE_COLOR,
|
||||
Brush.MODE_DETAIL,
|
||||
Brush.MODE_MASK
|
||||
TerrainPainter.MODE_RAISE,
|
||||
TerrainPainter.MODE_LOWER,
|
||||
TerrainPainter.MODE_SMOOTH,
|
||||
TerrainPainter.MODE_LEVEL,
|
||||
TerrainPainter.MODE_FLATTEN,
|
||||
TerrainPainter.MODE_ERODE,
|
||||
TerrainPainter.MODE_SPLAT,
|
||||
TerrainPainter.MODE_COLOR,
|
||||
TerrainPainter.MODE_DETAIL,
|
||||
TerrainPainter.MODE_MASK
|
||||
]
|
||||
|
||||
var mode_group := ButtonGroup.new()
|
||||
|
@ -214,7 +213,7 @@ func _enter_tree():
|
|||
button.set_toggle_mode(true)
|
||||
button.set_button_group(mode_group)
|
||||
|
||||
if mode == _brush.get_mode():
|
||||
if mode == _terrain_painter.get_mode():
|
||||
button.set_pressed(true)
|
||||
|
||||
button.connect("pressed", self, "_on_mode_selected", [mode])
|
||||
|
@ -362,7 +361,7 @@ func edit(object):
|
|||
_panel.set_terrain(_node)
|
||||
_generator_dialog.set_terrain(_node)
|
||||
_import_dialog.set_terrain(_node)
|
||||
_brush.set_terrain(_node)
|
||||
_terrain_painter.set_terrain(_node)
|
||||
_brush_decal.set_terrain(_node)
|
||||
_generate_mesh_dialog.set_terrain(_node)
|
||||
_resize_dialog.set_terrain(_node)
|
||||
|
@ -396,12 +395,12 @@ func _update_brush_buttons_availability():
|
|||
var has_details = (data.get_map_count(HTerrainData.CHANNEL_DETAIL) > 0)
|
||||
|
||||
if has_details:
|
||||
var button = _toolbar_brush_buttons[Brush.MODE_DETAIL]
|
||||
var button = _toolbar_brush_buttons[TerrainPainter.MODE_DETAIL]
|
||||
button.disabled = false
|
||||
else:
|
||||
var button = _toolbar_brush_buttons[Brush.MODE_DETAIL]
|
||||
var button = _toolbar_brush_buttons[TerrainPainter.MODE_DETAIL]
|
||||
if button.pressed:
|
||||
_select_brush_mode(Brush.MODE_RAISE)
|
||||
_select_brush_mode(TerrainPainter.MODE_RAISE)
|
||||
button.disabled = true
|
||||
|
||||
|
||||
|
@ -470,6 +469,10 @@ func forward_spatial_gui_input(p_camera: Camera, p_event: InputEvent) -> bool:
|
|||
# because they are used in navigation schemes
|
||||
if (not mb.control) and (not mb.alt) and mb.button_index == BUTTON_LEFT:
|
||||
if mb.pressed:
|
||||
# TODO Allow to paint on click
|
||||
# TODO `pressure` is not available in button press events
|
||||
# So I have to assume zero to avoid discontinuities with move events
|
||||
#_terrain_painter.paint_input(hit_pos_in_cells, 0.0)
|
||||
_mouse_pressed = true
|
||||
|
||||
captured_event = true
|
||||
|
@ -477,17 +480,19 @@ func forward_spatial_gui_input(p_camera: Camera, p_event: InputEvent) -> bool:
|
|||
if not _mouse_pressed:
|
||||
# Just finished painting
|
||||
_pending_paint_commit = true
|
||||
_terrain_painter.get_brush().on_paint_end()
|
||||
|
||||
if _brush.get_mode() == Brush.MODE_FLATTEN and _brush.has_meta("pick_height") \
|
||||
and _brush.get_meta("pick_height"):
|
||||
_brush.set_meta("pick_height", false)
|
||||
if _terrain_painter.get_mode() == TerrainPainter.MODE_FLATTEN \
|
||||
and _terrain_painter.has_meta("pick_height") \
|
||||
and _terrain_painter.get_meta("pick_height"):
|
||||
_terrain_painter.set_meta("pick_height", false)
|
||||
# Pick height
|
||||
var hit_pos_in_cells = _get_pointed_cell_position(mb.position, p_camera)
|
||||
if hit_pos_in_cells != null:
|
||||
var h = _node.get_data().get_height_at(
|
||||
int(hit_pos_in_cells.x), int(hit_pos_in_cells.y))
|
||||
_logger.debug("Picking height {0}".format([h]))
|
||||
_brush.set_flatten_height(h)
|
||||
_terrain_painter.set_flatten_height(h)
|
||||
|
||||
elif p_event is InputEventMouseMotion:
|
||||
var mm = p_event
|
||||
|
@ -497,7 +502,7 @@ func forward_spatial_gui_input(p_camera: Camera, p_event: InputEvent) -> bool:
|
|||
|
||||
if _mouse_pressed:
|
||||
if Input.is_mouse_button_pressed(BUTTON_LEFT):
|
||||
_brush.paint_input(hit_pos_in_cells)
|
||||
_terrain_painter.paint_input(hit_pos_in_cells, mm.pressure)
|
||||
captured_event = true
|
||||
|
||||
# This is in case the data or textures change as the user edits the terrain,
|
||||
|
@ -515,10 +520,11 @@ func _process(delta: float):
|
|||
|
||||
if _pending_paint_commit:
|
||||
if has_data:
|
||||
if _brush.has_modified_chunks() and not _brush.is_operation_pending():
|
||||
if not _terrain_painter.is_operation_pending():
|
||||
_pending_paint_commit = false
|
||||
if _terrain_painter.has_modified_chunks():
|
||||
_logger.debug("Paint completed")
|
||||
var changes : Dictionary = _brush.commit()
|
||||
var changes : Dictionary = _terrain_painter.commit()
|
||||
_paint_completed(changes)
|
||||
else:
|
||||
_pending_paint_commit = false
|
||||
|
@ -536,6 +542,8 @@ func _paint_completed(changes: Dictionary):
|
|||
assert(heightmap_data != null)
|
||||
|
||||
var chunk_positions : Array = changes.chunk_positions
|
||||
# Should not create an UndoRedo action if nothing changed
|
||||
assert(len(chunk_positions) > 0)
|
||||
var changed_maps : Array = changes.maps
|
||||
|
||||
var action_name := "Modify HTerrainData "
|
||||
|
@ -548,7 +556,7 @@ func _paint_completed(changes: Dictionary):
|
|||
|
||||
var redo_maps := []
|
||||
var undo_maps := []
|
||||
var chunk_size := _brush.get_undo_chunk_size()
|
||||
var chunk_size := _terrain_painter.get_undo_chunk_size()
|
||||
|
||||
for map in changed_maps:
|
||||
# Cache images to disk so RAM does not continuously go up (or at least much slower)
|
||||
|
@ -710,20 +718,20 @@ func _on_lookdev_menu_id_pressed(id: int):
|
|||
|
||||
func _on_mode_selected(mode: int):
|
||||
_logger.debug(str("On mode selected ", mode))
|
||||
_brush.set_mode(mode)
|
||||
_terrain_painter.set_mode(mode)
|
||||
_panel.set_brush_editor_display_mode(mode)
|
||||
|
||||
|
||||
func _on_texture_selected(index: int):
|
||||
# Switch to texture paint mode when a texture is selected
|
||||
_select_brush_mode(Brush.MODE_SPLAT)
|
||||
_brush.set_texture_index(index)
|
||||
_select_brush_mode(TerrainPainter.MODE_SPLAT)
|
||||
_terrain_painter.set_texture_index(index)
|
||||
|
||||
|
||||
func _on_detail_selected(index: int):
|
||||
# Switch to detail paint mode when a detail item is selected
|
||||
_select_brush_mode(Brush.MODE_DETAIL)
|
||||
_brush.set_detail_index(index)
|
||||
_select_brush_mode(TerrainPainter.MODE_DETAIL)
|
||||
_terrain_painter.set_detail_index(index)
|
||||
|
||||
|
||||
func _select_brush_mode(mode: int):
|
||||
|
@ -782,8 +790,8 @@ func _on_permanent_change_performed(message: String):
|
|||
ur.commit_action()
|
||||
|
||||
|
||||
func _on_brush_changed():
|
||||
_brush_decal.set_size(_brush.get_brush_size())
|
||||
func _on_brush_size_changed(size):
|
||||
_brush_decal.set_size(size)
|
||||
|
||||
|
||||
func _on_Panel_edit_texture_pressed(index: int):
|
||||
|
|
|
@ -1,624 +1,197 @@
|
|||
[gd_scene load_steps=6 format=2]
|
||||
[gd_scene load_steps=7 format=2]
|
||||
|
||||
[ext_resource path="res://addons/zylann.hterrain/tools/resize_dialog/resize_dialog.gd" type="Script" id=1]
|
||||
[ext_resource path="res://addons/zylann.hterrain/tools/icons/icon_heightmap_unmask.svg" type="Texture" id=2]
|
||||
[ext_resource path="res://addons/zylann.hterrain/tools/icons/icon_long_arrow_right.svg" type="Texture" id=3]
|
||||
[ext_resource path="res://addons/zylann.hterrain/tools/icons/icon_long_arrow_down.svg" type="Texture" id=4]
|
||||
[ext_resource path="res://addons/zylann.hterrain/tools/icons/icon_small_circle.svg" type="Texture" id=5]
|
||||
[ext_resource path="res://addons/zylann.hterrain/tools/util/dialog_fitter.tscn" type="PackedScene" id=6]
|
||||
|
||||
[node name="ResizeDialog" type="WindowDialog" index="0"]
|
||||
|
||||
anchor_left = 0.0
|
||||
anchor_top = 0.0
|
||||
anchor_right = 0.0
|
||||
anchor_bottom = 0.0
|
||||
[node name="ResizeDialog" type="WindowDialog"]
|
||||
margin_left = 130.0
|
||||
margin_top = 126.0
|
||||
margin_right = 430.0
|
||||
margin_bottom = 326.0
|
||||
rect_min_size = Vector2( 300, 200 )
|
||||
rect_pivot_offset = Vector2( 0, 0 )
|
||||
rect_clip_content = false
|
||||
mouse_filter = 0
|
||||
mouse_default_cursor_shape = 0
|
||||
size_flags_horizontal = 1
|
||||
size_flags_vertical = 1
|
||||
popup_exclusive = false
|
||||
window_title = "Resize terrain"
|
||||
resizable = false
|
||||
script = ExtResource( 1 )
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="." index="1"]
|
||||
|
||||
anchor_left = 0.0
|
||||
anchor_top = 0.0
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="."]
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
margin_left = 8.0
|
||||
margin_top = 8.0
|
||||
margin_right = -8.0
|
||||
margin_bottom = -8.0
|
||||
rect_pivot_offset = Vector2( 0, 0 )
|
||||
rect_clip_content = false
|
||||
mouse_filter = 1
|
||||
mouse_default_cursor_shape = 0
|
||||
size_flags_horizontal = 1
|
||||
size_flags_vertical = 1
|
||||
custom_constants/separation = 24
|
||||
alignment = 0
|
||||
_sections_unfolded = [ "Margin", "custom_constants" ]
|
||||
|
||||
[node name="GridContainer" type="GridContainer" parent="VBoxContainer" index="0"]
|
||||
|
||||
anchor_left = 0.0
|
||||
anchor_top = 0.0
|
||||
anchor_right = 0.0
|
||||
anchor_bottom = 0.0
|
||||
[node name="GridContainer" type="GridContainer" parent="VBoxContainer"]
|
||||
margin_right = 284.0
|
||||
margin_bottom = 126.0
|
||||
rect_pivot_offset = Vector2( 0, 0 )
|
||||
rect_clip_content = false
|
||||
mouse_filter = 1
|
||||
mouse_default_cursor_shape = 0
|
||||
size_flags_horizontal = 1
|
||||
size_flags_vertical = 1
|
||||
columns = 2
|
||||
|
||||
[node name="Label" type="Label" parent="VBoxContainer/GridContainer" index="0"]
|
||||
|
||||
anchor_left = 0.0
|
||||
anchor_top = 0.0
|
||||
anchor_right = 0.0
|
||||
anchor_bottom = 0.0
|
||||
[node name="Label" type="Label" parent="VBoxContainer/GridContainer"]
|
||||
margin_top = 3.0
|
||||
margin_right = 68.0
|
||||
margin_bottom = 17.0
|
||||
rect_pivot_offset = Vector2( 0, 0 )
|
||||
rect_clip_content = false
|
||||
mouse_filter = 2
|
||||
mouse_default_cursor_shape = 0
|
||||
size_flags_horizontal = 1
|
||||
size_flags_vertical = 4
|
||||
text = "Resolution"
|
||||
percent_visible = 1.0
|
||||
lines_skipped = 0
|
||||
max_lines_visible = -1
|
||||
|
||||
[node name="ResolutionDropdown" type="OptionButton" parent="VBoxContainer/GridContainer" index="1"]
|
||||
|
||||
anchor_left = 0.0
|
||||
anchor_top = 0.0
|
||||
anchor_right = 0.0
|
||||
anchor_bottom = 0.0
|
||||
[node name="ResolutionDropdown" type="OptionButton" parent="VBoxContainer/GridContainer"]
|
||||
margin_left = 72.0
|
||||
margin_right = 284.0
|
||||
margin_bottom = 20.0
|
||||
rect_pivot_offset = Vector2( 0, 0 )
|
||||
rect_clip_content = false
|
||||
focus_mode = 2
|
||||
mouse_filter = 0
|
||||
mouse_default_cursor_shape = 0
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 1
|
||||
toggle_mode = false
|
||||
action_mode = 0
|
||||
enabled_focus_mode = 2
|
||||
shortcut = null
|
||||
group = null
|
||||
flat = false
|
||||
align = 0
|
||||
items = [ ]
|
||||
selected = -1
|
||||
_sections_unfolded = [ "Size Flags" ]
|
||||
|
||||
[node name="Label3" type="Label" parent="VBoxContainer/GridContainer" index="2"]
|
||||
|
||||
anchor_left = 0.0
|
||||
anchor_top = 0.0
|
||||
anchor_right = 0.0
|
||||
anchor_bottom = 0.0
|
||||
[node name="Label3" type="Label" parent="VBoxContainer/GridContainer"]
|
||||
margin_top = 29.0
|
||||
margin_right = 68.0
|
||||
margin_bottom = 43.0
|
||||
rect_pivot_offset = Vector2( 0, 0 )
|
||||
rect_clip_content = false
|
||||
mouse_filter = 2
|
||||
mouse_default_cursor_shape = 0
|
||||
size_flags_horizontal = 1
|
||||
size_flags_vertical = 4
|
||||
text = "Stretch"
|
||||
percent_visible = 1.0
|
||||
lines_skipped = 0
|
||||
max_lines_visible = -1
|
||||
|
||||
[node name="StretchCheckBox" type="CheckBox" parent="VBoxContainer/GridContainer" index="3"]
|
||||
|
||||
anchor_left = 0.0
|
||||
anchor_top = 0.0
|
||||
anchor_right = 0.0
|
||||
anchor_bottom = 0.0
|
||||
[node name="StretchCheckBox" type="CheckBox" parent="VBoxContainer/GridContainer"]
|
||||
margin_left = 72.0
|
||||
margin_top = 24.0
|
||||
margin_right = 284.0
|
||||
margin_bottom = 48.0
|
||||
rect_pivot_offset = Vector2( 0, 0 )
|
||||
rect_clip_content = false
|
||||
focus_mode = 2
|
||||
mouse_filter = 0
|
||||
mouse_default_cursor_shape = 0
|
||||
size_flags_horizontal = 1
|
||||
size_flags_vertical = 1
|
||||
toggle_mode = true
|
||||
enabled_focus_mode = 2
|
||||
shortcut = null
|
||||
group = null
|
||||
flat = false
|
||||
align = 0
|
||||
|
||||
[node name="Label2" type="Label" parent="VBoxContainer/GridContainer" index="4"]
|
||||
|
||||
anchor_left = 0.0
|
||||
anchor_top = 0.0
|
||||
anchor_right = 0.0
|
||||
anchor_bottom = 0.0
|
||||
[node name="Label2" type="Label" parent="VBoxContainer/GridContainer"]
|
||||
margin_top = 82.0
|
||||
margin_right = 68.0
|
||||
margin_bottom = 96.0
|
||||
rect_pivot_offset = Vector2( 0, 0 )
|
||||
rect_clip_content = false
|
||||
mouse_filter = 2
|
||||
mouse_default_cursor_shape = 0
|
||||
size_flags_horizontal = 1
|
||||
size_flags_vertical = 4
|
||||
text = "Direction"
|
||||
percent_visible = 1.0
|
||||
lines_skipped = 0
|
||||
max_lines_visible = -1
|
||||
|
||||
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/GridContainer" index="5"]
|
||||
|
||||
anchor_left = 0.0
|
||||
anchor_top = 0.0
|
||||
anchor_right = 0.0
|
||||
anchor_bottom = 0.0
|
||||
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/GridContainer"]
|
||||
margin_left = 72.0
|
||||
margin_top = 52.0
|
||||
margin_right = 284.0
|
||||
margin_bottom = 126.0
|
||||
rect_pivot_offset = Vector2( 0, 0 )
|
||||
rect_clip_content = false
|
||||
mouse_filter = 1
|
||||
mouse_default_cursor_shape = 0
|
||||
size_flags_horizontal = 1
|
||||
size_flags_vertical = 1
|
||||
alignment = 0
|
||||
|
||||
[node name="AnchorControl" type="GridContainer" parent="VBoxContainer/GridContainer/HBoxContainer" index="0"]
|
||||
|
||||
anchor_left = 0.0
|
||||
anchor_top = 0.0
|
||||
anchor_right = 0.0
|
||||
anchor_bottom = 0.0
|
||||
[node name="AnchorControl" type="GridContainer" parent="VBoxContainer/GridContainer/HBoxContainer"]
|
||||
margin_right = 92.0
|
||||
margin_bottom = 74.0
|
||||
rect_pivot_offset = Vector2( 0, 0 )
|
||||
rect_clip_content = false
|
||||
mouse_filter = 1
|
||||
mouse_default_cursor_shape = 0
|
||||
size_flags_horizontal = 1
|
||||
size_flags_vertical = 1
|
||||
columns = 3
|
||||
|
||||
[node name="TopLeftButton" type="Button" parent="VBoxContainer/GridContainer/HBoxContainer/AnchorControl" index="0"]
|
||||
|
||||
anchor_left = 0.0
|
||||
anchor_top = 0.0
|
||||
anchor_right = 0.0
|
||||
anchor_bottom = 0.0
|
||||
[node name="TopLeftButton" type="Button" parent="VBoxContainer/GridContainer/HBoxContainer/AnchorControl"]
|
||||
margin_right = 28.0
|
||||
margin_bottom = 22.0
|
||||
rect_pivot_offset = Vector2( 0, 0 )
|
||||
rect_clip_content = false
|
||||
focus_mode = 2
|
||||
mouse_filter = 0
|
||||
mouse_default_cursor_shape = 0
|
||||
size_flags_horizontal = 1
|
||||
size_flags_vertical = 1
|
||||
toggle_mode = false
|
||||
enabled_focus_mode = 2
|
||||
shortcut = null
|
||||
group = null
|
||||
icon = ExtResource( 2 )
|
||||
flat = false
|
||||
align = 1
|
||||
|
||||
[node name="TopButton" type="Button" parent="VBoxContainer/GridContainer/HBoxContainer/AnchorControl" index="1"]
|
||||
|
||||
anchor_left = 0.0
|
||||
anchor_top = 0.0
|
||||
anchor_right = 0.0
|
||||
anchor_bottom = 0.0
|
||||
[node name="TopButton" type="Button" parent="VBoxContainer/GridContainer/HBoxContainer/AnchorControl"]
|
||||
margin_left = 32.0
|
||||
margin_right = 60.0
|
||||
margin_bottom = 22.0
|
||||
rect_pivot_offset = Vector2( 0, 0 )
|
||||
rect_clip_content = false
|
||||
focus_mode = 2
|
||||
mouse_filter = 0
|
||||
mouse_default_cursor_shape = 0
|
||||
size_flags_horizontal = 1
|
||||
size_flags_vertical = 1
|
||||
toggle_mode = false
|
||||
enabled_focus_mode = 2
|
||||
shortcut = null
|
||||
group = null
|
||||
icon = ExtResource( 2 )
|
||||
flat = false
|
||||
align = 1
|
||||
|
||||
[node name="TopRightButton" type="Button" parent="VBoxContainer/GridContainer/HBoxContainer/AnchorControl" index="2"]
|
||||
|
||||
anchor_left = 0.0
|
||||
anchor_top = 0.0
|
||||
anchor_right = 0.0
|
||||
anchor_bottom = 0.0
|
||||
[node name="TopRightButton" type="Button" parent="VBoxContainer/GridContainer/HBoxContainer/AnchorControl"]
|
||||
margin_left = 64.0
|
||||
margin_right = 92.0
|
||||
margin_bottom = 22.0
|
||||
rect_pivot_offset = Vector2( 0, 0 )
|
||||
rect_clip_content = false
|
||||
focus_mode = 2
|
||||
mouse_filter = 0
|
||||
mouse_default_cursor_shape = 0
|
||||
size_flags_horizontal = 1
|
||||
size_flags_vertical = 1
|
||||
toggle_mode = false
|
||||
enabled_focus_mode = 2
|
||||
shortcut = null
|
||||
group = null
|
||||
icon = ExtResource( 2 )
|
||||
flat = false
|
||||
align = 1
|
||||
|
||||
[node name="LeftButton" type="Button" parent="VBoxContainer/GridContainer/HBoxContainer/AnchorControl" index="3"]
|
||||
|
||||
anchor_left = 0.0
|
||||
anchor_top = 0.0
|
||||
anchor_right = 0.0
|
||||
anchor_bottom = 0.0
|
||||
[node name="LeftButton" type="Button" parent="VBoxContainer/GridContainer/HBoxContainer/AnchorControl"]
|
||||
margin_top = 26.0
|
||||
margin_right = 28.0
|
||||
margin_bottom = 48.0
|
||||
rect_pivot_offset = Vector2( 0, 0 )
|
||||
rect_clip_content = false
|
||||
focus_mode = 2
|
||||
mouse_filter = 0
|
||||
mouse_default_cursor_shape = 0
|
||||
size_flags_horizontal = 1
|
||||
size_flags_vertical = 1
|
||||
toggle_mode = false
|
||||
enabled_focus_mode = 2
|
||||
shortcut = null
|
||||
group = null
|
||||
icon = ExtResource( 2 )
|
||||
flat = false
|
||||
align = 1
|
||||
|
||||
[node name="CenterButton" type="Button" parent="VBoxContainer/GridContainer/HBoxContainer/AnchorControl" index="4"]
|
||||
|
||||
anchor_left = 0.0
|
||||
anchor_top = 0.0
|
||||
anchor_right = 0.0
|
||||
anchor_bottom = 0.0
|
||||
[node name="CenterButton" type="Button" parent="VBoxContainer/GridContainer/HBoxContainer/AnchorControl"]
|
||||
margin_left = 32.0
|
||||
margin_top = 26.0
|
||||
margin_right = 60.0
|
||||
margin_bottom = 48.0
|
||||
rect_pivot_offset = Vector2( 0, 0 )
|
||||
rect_clip_content = false
|
||||
focus_mode = 2
|
||||
mouse_filter = 0
|
||||
mouse_default_cursor_shape = 0
|
||||
size_flags_horizontal = 1
|
||||
size_flags_vertical = 1
|
||||
toggle_mode = false
|
||||
enabled_focus_mode = 2
|
||||
shortcut = null
|
||||
group = null
|
||||
icon = ExtResource( 2 )
|
||||
flat = false
|
||||
align = 1
|
||||
|
||||
[node name="RightButton" type="Button" parent="VBoxContainer/GridContainer/HBoxContainer/AnchorControl" index="5"]
|
||||
|
||||
anchor_left = 0.0
|
||||
anchor_top = 0.0
|
||||
anchor_right = 0.0
|
||||
anchor_bottom = 0.0
|
||||
[node name="RightButton" type="Button" parent="VBoxContainer/GridContainer/HBoxContainer/AnchorControl"]
|
||||
margin_left = 64.0
|
||||
margin_top = 26.0
|
||||
margin_right = 92.0
|
||||
margin_bottom = 48.0
|
||||
rect_pivot_offset = Vector2( 0, 0 )
|
||||
rect_clip_content = false
|
||||
focus_mode = 2
|
||||
mouse_filter = 0
|
||||
mouse_default_cursor_shape = 0
|
||||
size_flags_horizontal = 1
|
||||
size_flags_vertical = 1
|
||||
toggle_mode = false
|
||||
enabled_focus_mode = 2
|
||||
shortcut = null
|
||||
group = null
|
||||
icon = ExtResource( 2 )
|
||||
flat = false
|
||||
align = 1
|
||||
|
||||
[node name="ButtomLeftButton" type="Button" parent="VBoxContainer/GridContainer/HBoxContainer/AnchorControl" index="6"]
|
||||
|
||||
anchor_left = 0.0
|
||||
anchor_top = 0.0
|
||||
anchor_right = 0.0
|
||||
anchor_bottom = 0.0
|
||||
[node name="ButtomLeftButton" type="Button" parent="VBoxContainer/GridContainer/HBoxContainer/AnchorControl"]
|
||||
margin_top = 52.0
|
||||
margin_right = 28.0
|
||||
margin_bottom = 74.0
|
||||
rect_pivot_offset = Vector2( 0, 0 )
|
||||
rect_clip_content = false
|
||||
focus_mode = 2
|
||||
mouse_filter = 0
|
||||
mouse_default_cursor_shape = 0
|
||||
size_flags_horizontal = 1
|
||||
size_flags_vertical = 1
|
||||
toggle_mode = false
|
||||
enabled_focus_mode = 2
|
||||
shortcut = null
|
||||
group = null
|
||||
icon = ExtResource( 2 )
|
||||
flat = false
|
||||
align = 1
|
||||
|
||||
[node name="ButtomButton" type="Button" parent="VBoxContainer/GridContainer/HBoxContainer/AnchorControl" index="7"]
|
||||
|
||||
anchor_left = 0.0
|
||||
anchor_top = 0.0
|
||||
anchor_right = 0.0
|
||||
anchor_bottom = 0.0
|
||||
[node name="ButtomButton" type="Button" parent="VBoxContainer/GridContainer/HBoxContainer/AnchorControl"]
|
||||
margin_left = 32.0
|
||||
margin_top = 52.0
|
||||
margin_right = 60.0
|
||||
margin_bottom = 74.0
|
||||
rect_pivot_offset = Vector2( 0, 0 )
|
||||
rect_clip_content = false
|
||||
focus_mode = 2
|
||||
mouse_filter = 0
|
||||
mouse_default_cursor_shape = 0
|
||||
size_flags_horizontal = 1
|
||||
size_flags_vertical = 1
|
||||
toggle_mode = false
|
||||
enabled_focus_mode = 2
|
||||
shortcut = null
|
||||
group = null
|
||||
icon = ExtResource( 2 )
|
||||
flat = false
|
||||
align = 1
|
||||
|
||||
[node name="BottomRightButton" type="Button" parent="VBoxContainer/GridContainer/HBoxContainer/AnchorControl" index="8"]
|
||||
|
||||
anchor_left = 0.0
|
||||
anchor_top = 0.0
|
||||
anchor_right = 0.0
|
||||
anchor_bottom = 0.0
|
||||
[node name="BottomRightButton" type="Button" parent="VBoxContainer/GridContainer/HBoxContainer/AnchorControl"]
|
||||
margin_left = 64.0
|
||||
margin_top = 52.0
|
||||
margin_right = 92.0
|
||||
margin_bottom = 74.0
|
||||
rect_pivot_offset = Vector2( 0, 0 )
|
||||
rect_clip_content = false
|
||||
focus_mode = 2
|
||||
mouse_filter = 0
|
||||
mouse_default_cursor_shape = 0
|
||||
size_flags_horizontal = 1
|
||||
size_flags_vertical = 1
|
||||
toggle_mode = false
|
||||
enabled_focus_mode = 2
|
||||
shortcut = null
|
||||
group = null
|
||||
icon = ExtResource( 2 )
|
||||
flat = false
|
||||
align = 1
|
||||
|
||||
[node name="Reference" type="Control" parent="VBoxContainer/GridContainer/HBoxContainer" index="1"]
|
||||
|
||||
anchor_left = 0.0
|
||||
anchor_top = 0.0
|
||||
anchor_right = 0.0
|
||||
anchor_bottom = 0.0
|
||||
[node name="Reference" type="Control" parent="VBoxContainer/GridContainer/HBoxContainer"]
|
||||
margin_left = 96.0
|
||||
margin_right = 196.0
|
||||
margin_bottom = 74.0
|
||||
rect_min_size = Vector2( 100, 0 )
|
||||
rect_pivot_offset = Vector2( 0, 0 )
|
||||
rect_clip_content = false
|
||||
mouse_filter = 0
|
||||
mouse_default_cursor_shape = 0
|
||||
size_flags_horizontal = 1
|
||||
size_flags_vertical = 1
|
||||
_sections_unfolded = [ "Rect" ]
|
||||
|
||||
[node name="XArrow" type="TextureRect" parent="VBoxContainer/GridContainer/HBoxContainer/Reference" index="0"]
|
||||
|
||||
[node name="XArrow" type="TextureRect" parent="VBoxContainer/GridContainer/HBoxContainer/Reference"]
|
||||
modulate = Color( 1, 0.292969, 0.292969, 1 )
|
||||
anchor_left = 0.0
|
||||
anchor_top = 0.0
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 0.0
|
||||
margin_left = 9.0
|
||||
margin_bottom = 16.0
|
||||
rect_pivot_offset = Vector2( 0, 0 )
|
||||
rect_clip_content = false
|
||||
mouse_filter = 1
|
||||
mouse_default_cursor_shape = 0
|
||||
size_flags_horizontal = 1
|
||||
size_flags_vertical = 1
|
||||
texture = ExtResource( 3 )
|
||||
stretch_mode = 0
|
||||
_sections_unfolded = [ "Visibility" ]
|
||||
|
||||
[node name="ZArrow" type="TextureRect" parent="VBoxContainer/GridContainer/HBoxContainer/Reference" index="1"]
|
||||
|
||||
[node name="ZArrow" type="TextureRect" parent="VBoxContainer/GridContainer/HBoxContainer/Reference"]
|
||||
modulate = Color( 0.292969, 0.602295, 1, 1 )
|
||||
anchor_left = 0.0
|
||||
anchor_top = 0.0
|
||||
anchor_right = 0.0
|
||||
anchor_bottom = 1.0
|
||||
margin_top = 10.0
|
||||
margin_right = 16.0
|
||||
rect_pivot_offset = Vector2( 0, 0 )
|
||||
rect_clip_content = false
|
||||
mouse_filter = 1
|
||||
mouse_default_cursor_shape = 0
|
||||
size_flags_horizontal = 1
|
||||
size_flags_vertical = 1
|
||||
texture = ExtResource( 4 )
|
||||
stretch_mode = 0
|
||||
_sections_unfolded = [ "Visibility" ]
|
||||
|
||||
[node name="ZLabel" type="Label" parent="VBoxContainer/GridContainer/HBoxContainer/Reference" index="2"]
|
||||
|
||||
anchor_left = 0.0
|
||||
anchor_top = 0.0
|
||||
anchor_right = 0.0
|
||||
anchor_bottom = 0.0
|
||||
[node name="ZLabel" type="Label" parent="VBoxContainer/GridContainer/HBoxContainer/Reference"]
|
||||
margin_left = 14.0
|
||||
margin_top = 54.0
|
||||
margin_right = 22.0
|
||||
margin_bottom = 68.0
|
||||
rect_pivot_offset = Vector2( 0, 0 )
|
||||
rect_clip_content = false
|
||||
mouse_filter = 2
|
||||
mouse_default_cursor_shape = 0
|
||||
size_flags_horizontal = 1
|
||||
size_flags_vertical = 4
|
||||
text = "Z"
|
||||
percent_visible = 1.0
|
||||
lines_skipped = 0
|
||||
max_lines_visible = -1
|
||||
|
||||
[node name="XLabel" type="Label" parent="VBoxContainer/GridContainer/HBoxContainer/Reference" index="3"]
|
||||
|
||||
anchor_left = 0.0
|
||||
anchor_top = 0.0
|
||||
anchor_right = 0.0
|
||||
anchor_bottom = 0.0
|
||||
[node name="XLabel" type="Label" parent="VBoxContainer/GridContainer/HBoxContainer/Reference"]
|
||||
margin_left = 52.0
|
||||
margin_top = 14.0
|
||||
margin_right = 60.0
|
||||
margin_bottom = 28.0
|
||||
rect_pivot_offset = Vector2( 0, 0 )
|
||||
rect_clip_content = false
|
||||
mouse_filter = 2
|
||||
mouse_default_cursor_shape = 0
|
||||
size_flags_horizontal = 1
|
||||
size_flags_vertical = 4
|
||||
text = "X"
|
||||
percent_visible = 1.0
|
||||
lines_skipped = 0
|
||||
max_lines_visible = -1
|
||||
|
||||
[node name="Origin" type="TextureRect" parent="VBoxContainer/GridContainer/HBoxContainer/Reference" index="4"]
|
||||
|
||||
anchor_left = 0.0
|
||||
anchor_top = 0.0
|
||||
anchor_right = 0.0
|
||||
anchor_bottom = 0.0
|
||||
[node name="Origin" type="TextureRect" parent="VBoxContainer/GridContainer/HBoxContainer/Reference"]
|
||||
margin_left = 3.0
|
||||
margin_top = 4.0
|
||||
margin_right = 11.0
|
||||
margin_bottom = 12.0
|
||||
rect_pivot_offset = Vector2( 0, 0 )
|
||||
rect_clip_content = false
|
||||
mouse_filter = 1
|
||||
mouse_default_cursor_shape = 0
|
||||
size_flags_horizontal = 1
|
||||
size_flags_vertical = 1
|
||||
texture = ExtResource( 5 )
|
||||
stretch_mode = 0
|
||||
_sections_unfolded = [ "Anchor", "Margin" ]
|
||||
|
||||
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer" index="1"]
|
||||
|
||||
anchor_left = 0.0
|
||||
anchor_top = 0.0
|
||||
anchor_right = 0.0
|
||||
anchor_bottom = 0.0
|
||||
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"]
|
||||
margin_top = 150.0
|
||||
margin_right = 284.0
|
||||
margin_bottom = 170.0
|
||||
rect_pivot_offset = Vector2( 0, 0 )
|
||||
rect_clip_content = false
|
||||
mouse_filter = 1
|
||||
mouse_default_cursor_shape = 0
|
||||
size_flags_horizontal = 1
|
||||
size_flags_vertical = 1
|
||||
custom_constants/separation = 16
|
||||
alignment = 1
|
||||
_sections_unfolded = [ "Rect", "custom_constants" ]
|
||||
|
||||
[node name="ApplyButton" type="Button" parent="VBoxContainer/HBoxContainer" index="0"]
|
||||
|
||||
anchor_left = 0.0
|
||||
anchor_top = 0.0
|
||||
anchor_right = 0.0
|
||||
anchor_bottom = 0.0
|
||||
[node name="ApplyButton" type="Button" parent="VBoxContainer/HBoxContainer"]
|
||||
margin_left = 51.0
|
||||
margin_right = 163.0
|
||||
margin_bottom = 20.0
|
||||
rect_pivot_offset = Vector2( 0, 0 )
|
||||
rect_clip_content = false
|
||||
focus_mode = 2
|
||||
mouse_filter = 0
|
||||
mouse_default_cursor_shape = 0
|
||||
size_flags_horizontal = 1
|
||||
size_flags_vertical = 1
|
||||
toggle_mode = false
|
||||
enabled_focus_mode = 2
|
||||
shortcut = null
|
||||
group = null
|
||||
text = "Apply (no undo)"
|
||||
flat = false
|
||||
align = 1
|
||||
|
||||
[node name="CancelButton" type="Button" parent="VBoxContainer/HBoxContainer" index="1"]
|
||||
|
||||
anchor_left = 0.0
|
||||
anchor_top = 0.0
|
||||
anchor_right = 0.0
|
||||
anchor_bottom = 0.0
|
||||
[node name="CancelButton" type="Button" parent="VBoxContainer/HBoxContainer"]
|
||||
margin_left = 179.0
|
||||
margin_right = 233.0
|
||||
margin_bottom = 20.0
|
||||
rect_pivot_offset = Vector2( 0, 0 )
|
||||
rect_clip_content = false
|
||||
focus_mode = 2
|
||||
mouse_filter = 0
|
||||
mouse_default_cursor_shape = 0
|
||||
size_flags_horizontal = 1
|
||||
size_flags_vertical = 1
|
||||
toggle_mode = false
|
||||
enabled_focus_mode = 2
|
||||
shortcut = null
|
||||
group = null
|
||||
text = "Cancel"
|
||||
flat = false
|
||||
align = 1
|
||||
|
||||
[node name="DialogFitter" parent="." instance=ExtResource( 6 )]
|
||||
|
||||
[connection signal="item_selected" from="VBoxContainer/GridContainer/ResolutionDropdown" to="." method="_on_ResolutionDropdown_item_selected"]
|
||||
|
||||
[connection signal="toggled" from="VBoxContainer/GridContainer/StretchCheckBox" to="." method="_on_StretchCheckBox_toggled"]
|
||||
|
||||
[connection signal="pressed" from="VBoxContainer/HBoxContainer/ApplyButton" to="." method="_on_ApplyButton_pressed"]
|
||||
|
||||
[connection signal="pressed" from="VBoxContainer/HBoxContainer/CancelButton" to="." method="_on_CancelButton_pressed"]
|
||||
|
||||
|
||||
|
|
|
@ -347,6 +347,7 @@ func _set_texture_array_action(slot_index: int, texture_array: TextureArray, typ
|
|||
# See https://github.com/godotengine/godot/issues/36895
|
||||
if texture_array == null:
|
||||
_undo_redo.add_do_method(_texture_set, "set_texture_array_null", type)
|
||||
# Can't select a slot after this because there won't be any after the array is removed
|
||||
else:
|
||||
_undo_redo.add_do_method(_texture_set, "set_texture_array", type, texture_array)
|
||||
_undo_redo.add_do_method(self, "_select_slot", slot_index)
|
||||
|
@ -355,6 +356,7 @@ func _set_texture_array_action(slot_index: int, texture_array: TextureArray, typ
|
|||
# See https://github.com/godotengine/godot/issues/36895
|
||||
if prev_texture_array == null:
|
||||
_undo_redo.add_undo_method(_texture_set, "set_texture_array_null", type)
|
||||
# Can't select a slot after this because there won't be any after the array is removed
|
||||
else:
|
||||
_undo_redo.add_undo_method(_texture_set, "set_texture_array", type, prev_texture_array)
|
||||
_undo_redo.add_undo_method(self, "_select_slot", slot_index)
|
||||
|
@ -374,7 +376,11 @@ func _on_LoadTextureArrayDialog_file_selected(fpath: String):
|
|||
assert(_texture_set.get_mode() == HTerrainTextureSet.MODE_TEXTURE_ARRAYS)
|
||||
var texture_array = load(fpath)
|
||||
assert(texture_array != null)
|
||||
var slot_index : int = _slots_list.get_selected_items()[0]
|
||||
# It's possible no slot exists at the moment,
|
||||
# because there could be no texture array already set.
|
||||
# The number of slots in the new array might also be different.
|
||||
# So in this case we'll default to selecting the first slot.
|
||||
var slot_index := 0
|
||||
_set_texture_array_action(slot_index, texture_array, _load_texture_type)
|
||||
|
||||
|
||||
|
|
|
@ -250,7 +250,7 @@ func set_texture_set(texture_set: HTerrainTextureSet):
|
|||
|
||||
while slot_index >= len(_slots_data):
|
||||
var slot = Slot.new()
|
||||
_slots_data[slot_index] = slot
|
||||
_slots_data.append(slot)
|
||||
|
||||
var slot = _slots_data[slot_index]
|
||||
|
||||
|
|
|
@ -31,4 +31,23 @@ func _fit_to_contents():
|
|||
var margin : Vector2 = child.get_rect().position
|
||||
#print("Fitting ", dialog.get_path(), " from ", dialog.rect_size,
|
||||
# " to ", child_rect.size + margin * 2.0)
|
||||
dialog.rect_size = child_rect.size + margin * 2.0
|
||||
dialog.rect_min_size = child_rect.size + margin * 2.0
|
||||
|
||||
|
||||
#func _process(delta):
|
||||
# update()
|
||||
|
||||
# DEBUG
|
||||
#func _draw():
|
||||
# var self_global_pos = get_global_rect().position
|
||||
#
|
||||
# var dialog : Control = get_parent()
|
||||
# var dialog_rect := dialog.get_global_rect()
|
||||
# dialog_rect.position -= self_global_pos
|
||||
# draw_rect(dialog_rect, Color(1,1,0), false)
|
||||
#
|
||||
# for child in dialog.get_children():
|
||||
# if child is Container:
|
||||
# var child_rect : Rect2 = child.get_global_rect()
|
||||
# child_rect.position -= self_global_pos
|
||||
# draw_rect(child_rect, Color(1,1,0,0.1))
|
||||
|
|
322
addons/zylann.hterrain/tools/util/spin_slider.gd
Normal file
322
addons/zylann.hterrain/tools/util/spin_slider.gd
Normal 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
|
13
addons/zylann.hterrain/tools/util/spin_slider.tscn
Normal file
13
addons/zylann.hterrain/tools/util/spin_slider.tscn
Normal 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
|
||||
}
|
|
@ -38,3 +38,6 @@ func set_material_override(material: Material):
|
|||
func set_aabb(aabb: AABB):
|
||||
VisualServer.instance_set_custom_aabb(_multimesh_instance, aabb)
|
||||
|
||||
|
||||
func set_layer_mask(mask: int):
|
||||
VisualServer.instance_set_layer_mask(_multimesh_instance, mask)
|
||||
|
|
Loading…
Reference in a new issue