Updating Zylann plugin with latest stable version

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

View file

@ -11,6 +11,15 @@ It is entirely built on top of the `VisualServer` scripting API, which means it
![Screenshot of the editor with the plugin enabled and arrows showing where UIs are](images/overview.png)
### 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)

View file

@ -1,15 +1,11 @@
site_name: HTerrain plugin documentation
theme: readthedocs
# I had to specify this even though it's supposed to be the default
# See https://github.com/mkdocs/mkdocs/issues/2145#issuecomment-735342512
docs_dir: docs
markdown_extensions:
# Makes permalinks appear on headings
- toc:
permalink: True
# Makes boxes for notes and warnings
- admonition
# Better highlighter which supports GDScript
- codehilite
# Makes permalinks appear on headings
- toc:
permalink: True
# Makes boxes for notes and warnings
- admonition
# Better highlighter which supports GDScript
- codehilite

View file

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

View file

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

View file

@ -417,7 +417,7 @@ func get_all_heights() -> PoolRealArray:
# modified area.
#
# map_type:
# which kind of map changed
# which kind of map changed, see CHANNEL_* constants
#
# index:
# index of the map that changed
@ -1159,18 +1159,36 @@ func _load_map(dir: String, map_type: int, index: int) -> bool:
# In this particular case, we can use Godot ResourceLoader directly,
# if the texture got imported.
if Engine.editor_hint:
# But in the editor we want textures to be editable,
# so we have to automatically load the data also in RAM
if map.image == null:
map.image = Image.new()
map.image.load(fpath)
_ensure_map_format(map.image, map_type, index)
var tex = load(fpath)
var must_load_image_in_editor := true
if tex != null and tex is Image:
# The texture is imported as Image,
# perhaps the user wants it to be accessible from RAM in game.
_logger.debug("Map {0} is imported as Image. An ImageTexture will be generated." \
.format([get_map_debug_name(map_type, index)]))
map.image = tex
tex = ImageTexture.new()
var map_type_info = _map_types[map_type]
tex.create_from_image(map.image, map_type_info.texture_flags)
must_load_image_in_editor = false
map.texture = tex
if Engine.editor_hint:
if must_load_image_in_editor:
# But in the editor we want textures to be editable,
# so we have to automatically load the data also in RAM
if map.image == null:
map.image = Image.new()
map.image.load(fpath)
_ensure_map_format(map.image, map_type, index)
else:
# The heightmap is different.
# It has often uses beyond graphics, so we always keep a RAM copy by default.
var im = _try_load_0_8_0_heightmap(fpath, map_type, map.image, _logger)
if typeof(im) == TYPE_BOOL:
return false

View file

@ -47,10 +47,12 @@ export(Texture) var texture : Texture setget set_texture, get_texture
# How far detail meshes can be seen.
# TODO Improve speed of _get_chunk_aabb() so we can increase the limit
# See https://github.com/Zylann/godot_heightmap_plugin/issues/155
export(float, 1.0, 500.0) var view_distance := 100.0 setget set_view_distance, get_view_distance
export(float, 1.0, 500.0) \
var view_distance := 100.0 setget set_view_distance, get_view_distance
# Custom shader to replace the default one.
export(Shader) var custom_shader : Shader setget set_custom_shader, get_custom_shader
export(Shader) \
var custom_shader : Shader setget set_custom_shader, get_custom_shader
# Density modifier, to make more or less detail meshes appear overall.
export(float, 0, 10) var density := 4.0 setget set_density, get_density
@ -60,6 +62,10 @@ export(float, 0, 10) var density := 4.0 setget set_density, get_density
# I would have called it `mesh` but that's too broad and conflicts with local vars ._.
export(Mesh) var instance_mesh : Mesh setget set_instance_mesh, get_instance_mesh
# Exposes rendering layers, similar to `VisualInstance.layers`
export(int, LAYERS_3D_RENDER) \
var render_layers := 1 setget set_render_layer_mask, get_render_layer_mask
var _material: ShaderMaterial = null
var _default_shader: Shader = null
@ -248,6 +254,17 @@ func get_instance_mesh() -> Mesh:
return instance_mesh
func set_render_layer_mask(mask: int):
render_layers = mask
for k in _chunks:
var chunk = _chunks[k]
chunk.set_layer_mask(mask)
func get_render_layer_mask() -> int:
return render_layers
func _get_used_mesh() -> Mesh:
if instance_mesh == null:
return DefaultMesh
@ -305,8 +322,10 @@ func _on_terrain_transform_changed(gt: Transform):
if terrain == null:
_logger.error("Detail layer is not child of a terrain!")
return
var terrain_transform : Transform = terrain.get_internal_transform()
# Update AABBs, because scale might have changed
# Update AABBs and transforms, because scale might have changed
for k in _chunks:
var mmi = _chunks[k]
var aabb = _get_chunk_aabb(terrain, Vector3(k.x * CHUNK_SIZE, 0, k.y * CHUNK_SIZE))
@ -314,6 +333,7 @@ func _on_terrain_transform_changed(gt: Transform):
aabb.position.x = 0
aabb.position.z = 0
mmi.set_aabb(aabb)
mmi.set_transform(_get_chunk_transform(terrain_transform, k.x, k.y))
func process(delta: float, viewer_pos: Vector3):
@ -331,7 +351,7 @@ func process(delta: float, viewer_pos: Vector3):
var mmi = _chunks[k]
mmi.set_multimesh(_multimesh)
var local_viewer_pos = viewer_pos - terrain.translation
var local_viewer_pos = terrain.global_transform.affine_inverse() * viewer_pos
var viewer_cx = local_viewer_pos.x / CHUNK_SIZE
var viewer_cz = local_viewer_pos.z / CHUNK_SIZE
@ -367,6 +387,8 @@ func process(delta: float, viewer_pos: Vector3):
for cx in range(cmin_x, cmax_x):
_add_debug_cube(terrain, _get_chunk_aabb(terrain, Vector3(cx, 0, cz) * CHUNK_SIZE))
var terrain_transform : Transform = terrain.get_internal_transform()
for cz in range(cmin_z, cmax_z):
for cx in range(cmin_x, cmax_x):
@ -378,7 +400,7 @@ func process(delta: float, viewer_pos: Vector3):
var d = (aabb.position + 0.5 * aabb.size).distance_to(local_viewer_pos)
if d < view_distance:
_load_chunk(terrain, cx, cz, aabb)
_load_chunk(terrain_transform, cx, cz, aabb)
var to_recycle = []
@ -418,11 +440,14 @@ func _get_chunk_aabb(terrain, lpos: Vector3):
return aabb
func _load_chunk(terrain, cx: int, cz: int, aabb: AABB):
var lpos = Vector3(cx, 0, cz) * CHUNK_SIZE
func _get_chunk_transform(terrain_transform: Transform, cx: int, cz: int) -> Transform:
var lpos := Vector3(cx, 0, cz) * CHUNK_SIZE
# Terrain scale is not used on purpose. Rotation is not supported.
var trans = Transform(Basis(), terrain.get_internal_transform().origin + lpos)
var trans := Transform(Basis(), terrain_transform.origin + lpos)
return trans
func _load_chunk(terrain_transform: Transform, cx: int, cz: int, aabb: AABB):
# Nullify XZ translation because that's done by transform already
aabb.position.x = 0
aabb.position.z = 0
@ -433,12 +458,15 @@ func _load_chunk(terrain, cx: int, cz: int, aabb: AABB):
_multimesh_instance_pool.pop_back()
else:
mmi = DirectMultiMeshInstance.new()
mmi.set_world(terrain.get_world())
mmi.set_world(get_world())
mmi.set_multimesh(_multimesh)
var trans := _get_chunk_transform(terrain_transform, cx, cz)
mmi.set_material_override(_material)
mmi.set_transform(trans)
mmi.set_aabb(aabb)
mmi.set_layer_mask(render_layers)
mmi.set_visible(visible)
_chunks[Vector2(cx, cz)] = mmi

View file

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

View file

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

View file

@ -0,0 +1,8 @@
[gd_resource type="NativeScript" load_steps=2 format=2]
[ext_resource path="res://addons/zylann.hterrain/native/hterrain.gdnlib" type="GDNativeLibrary" id=1]
[resource]
resource_name = "quad_tree_lod"
class_name = "QuadTreeLod"
library = ExtResource( 1 )

View file

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

View file

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

View file

@ -0,0 +1,242 @@
#include "quad_tree_lod.h"
namespace godot {
void QuadTreeLod::set_callbacks(Ref<FuncRef> make_cb, Ref<FuncRef> recycle_cb, Ref<FuncRef> vbounds_cb) {
_make_func = make_cb;
_recycle_func = recycle_cb;
_vertical_bounds_func = vbounds_cb;
}
int QuadTreeLod::get_lod_count() {
// TODO make this a count, not max
return _max_depth + 1;
}
int QuadTreeLod::get_lod_factor(int lod) {
return 1 << lod;
}
int QuadTreeLod::compute_lod_count(int base_size, int full_size) {
int po = 0;
while (full_size > base_size) {
full_size = full_size >> 1;
po += 1;
}
return po;
}
// The higher, the longer LODs will spread and higher the quality.
// The lower, the shorter LODs will spread and lower the quality.
void QuadTreeLod::set_split_scale(real_t p_split_scale) {
real_t MIN = 2.0f;
real_t MAX = 5.0f;
// Split scale must be greater than a threshold,
// otherwise lods will decimate too fast and it will look messy
if (p_split_scale < MIN)
p_split_scale = MIN;
if (p_split_scale > MAX)
p_split_scale = MAX;
_split_scale = p_split_scale;
}
real_t QuadTreeLod::get_split_scale() {
return _split_scale;
}
void QuadTreeLod::clear() {
_join_all_recursively(ROOT, _max_depth);
_max_depth = 0;
_base_size = 0;
}
void QuadTreeLod::create_from_sizes(int base_size, int full_size) {
clear();
_base_size = base_size;
_max_depth = compute_lod_count(base_size, full_size);
// Total qty of nodes is (N^L - 1) / (N - 1). -1 for root, where N=num children, L=levels including the root
int node_count = ((static_cast<int>(pow(4, _max_depth+1)) - 1) / (4 - 1)) - 1;
_node_pool.resize(node_count); // e.g. ((4^6 -1) / 3 ) - 1 = 1364 excluding root
_free_indices.resize((node_count / 4)); // 1364 / 4 = 341
for (int i = 0; i < _free_indices.size(); i++) // i = 0 to 340, *4 = 0 to 1360
_free_indices[i] = 4 * i; // _node_pool[4*0 + i0] is first child, [4*340 + i3] is last
}
void QuadTreeLod::update(Vector3 view_pos) {
_update(ROOT, _max_depth, view_pos);
// This makes sure we keep seeing the lowest LOD,
// if the tree is cleared while we are far away
Quad *root = _get_root();
if (!root->has_children() && root->is_null())
root->set_data(_make_chunk(_max_depth, 0, 0));
}
void QuadTreeLod::debug_draw_tree(CanvasItem *ci) {
if (ci != nullptr)
_debug_draw_tree_recursive(ci, ROOT, _max_depth, 0);
}
// Intention is to only clear references to children
void QuadTreeLod::_clear_children(unsigned int index) {
Quad *quad = _get_node(index);
if (quad->has_children()) {
_recycle_children(quad->first_child);
quad->first_child = NO_CHILDREN;
}
}
// Returns the index of the first_child. Allocates from _free_indices.
unsigned int QuadTreeLod::_allocate_children() {
if (_free_indices.size() == 0) {
return NO_CHILDREN;
}
unsigned int i0 = _free_indices[_free_indices.size() - 1];
_free_indices.pop_back();
return i0;
}
// Pass the first_child index, not the parent index. Stores back in _free_indices.
void QuadTreeLod::_recycle_children(unsigned int i0) {
// Debug check, there is no use case in recycling a node which is not a first child
CRASH_COND(i0 % 4 != 0);
for (int i = 0; i < 4; ++i) {
_node_pool[i0 + i].init();
}
_free_indices.push_back(i0);
}
Variant QuadTreeLod::_make_chunk(int lod, int origin_x, int origin_y) {
if (_make_func.is_valid()) {
return _make_func->call_func(origin_x, origin_y, lod);
} else {
return Variant();
}
}
void QuadTreeLod::_recycle_chunk(unsigned int quad_index, int lod) {
Quad *quad = _get_node(quad_index);
if (_recycle_func.is_valid()) {
_recycle_func->call_func(quad->get_data(), quad->origin_x, quad->origin_y, lod);
}
}
void QuadTreeLod::_join_all_recursively(unsigned int quad_index, int lod) {
Quad *quad = _get_node(quad_index);
if (quad->has_children()) {
for (int i = 0; i < 4; i++) {
_join_all_recursively(quad->first_child + i, lod - 1);
}
_clear_children(quad_index);
} else if (quad->is_valid()) {
_recycle_chunk(quad_index, lod);
quad->clear_data();
}
}
void QuadTreeLod::_update(unsigned int quad_index, int lod, Vector3 view_pos) {
// This function should be called regularly over frames.
Quad *quad = _get_node(quad_index);
int lod_factor = get_lod_factor(lod);
int chunk_size = _base_size * lod_factor;
Vector3 world_center = static_cast<real_t>(chunk_size) * (Vector3(static_cast<real_t>(quad->origin_x), 0.f, static_cast<real_t>(quad->origin_y)) + Vector3(0.5f, 0.f, 0.5f));
if (_vertical_bounds_func.is_valid()) {
Variant result = _vertical_bounds_func->call_func(quad->origin_x, quad->origin_y, lod);
ERR_FAIL_COND(result.get_type() != Variant::VECTOR2);
Vector2 vbounds = static_cast<Vector2>(result);
world_center.y = (vbounds.x + vbounds.y) / 2.0f;
}
int split_distance = _base_size * lod_factor * static_cast<int>(_split_scale);
if (!quad->has_children()) {
if (lod > 0 && world_center.distance_to(view_pos) < split_distance) {
// Split
unsigned int new_idx = _allocate_children();
ERR_FAIL_COND(new_idx == NO_CHILDREN);
quad->first_child = new_idx;
for (int i = 0; i < 4; i++) {
Quad *child = _get_node(quad->first_child + i);
child->origin_x = quad->origin_x * 2 + (i & 1);
child->origin_y = quad->origin_y * 2 + ((i & 2) >> 1);
child->set_data(_make_chunk(lod - 1, child->origin_x, child->origin_y));
// If the quad needs to split more, we'll ask more recycling...
}
if (quad->is_valid()) {
_recycle_chunk(quad_index, lod);
quad->clear_data();
}
}
} else {
bool no_split_child = true;
for (int i = 0; i < 4; i++) {
_update(quad->first_child + i, lod - 1, view_pos);
if (_get_node(quad->first_child + i)->has_children())
no_split_child = false;
}
if (no_split_child && world_center.distance_to(view_pos) > split_distance) {
// Join
for (int i = 0; i < 4; i++) {
_recycle_chunk(quad->first_child + i, lod - 1);
}
_clear_children(quad_index);
quad->set_data(_make_chunk(lod, quad->origin_x, quad->origin_y));
}
}
} // _update
void QuadTreeLod::_debug_draw_tree_recursive(CanvasItem *ci, unsigned int quad_index, int lod_index, int child_index) {
Quad *quad = _get_node(quad_index);
if (quad->has_children()) {
int ch_index = quad->first_child;
for (int i = 0; i < 4; i++) {
_debug_draw_tree_recursive(ci, ch_index + i, lod_index - 1, i);
}
} else {
real_t size = static_cast<real_t>(get_lod_factor(lod_index));
int checker = 0;
if (child_index == 1 || child_index == 2)
checker = 1;
int chunk_indicator = 0;
if (quad->is_valid())
chunk_indicator = 1;
Rect2 rect2(Vector2(static_cast<real_t>(quad->origin_x), static_cast<real_t>(quad->origin_y)) * size,
Vector2(size, size));
Color color(1.0f - static_cast<real_t>(lod_index) * 0.2f, 0.2f * static_cast<real_t>(checker), static_cast<real_t>(chunk_indicator), 1.0f);
ci->draw_rect(rect2, color);
}
}
void QuadTreeLod::_register_methods() {
register_method("set_callbacks", &QuadTreeLod::set_callbacks);
register_method("get_lod_count", &QuadTreeLod::get_lod_count);
register_method("get_lod_factor", &QuadTreeLod::get_lod_factor);
register_method("compute_lod_count", &QuadTreeLod::compute_lod_count);
register_method("set_split_scale", &QuadTreeLod::set_split_scale);
register_method("get_split_scale", &QuadTreeLod::get_split_scale);
register_method("clear", &QuadTreeLod::clear);
register_method("create_from_sizes", &QuadTreeLod::create_from_sizes);
register_method("update", &QuadTreeLod::update);
register_method("debug_draw_tree", &QuadTreeLod::debug_draw_tree);
}
} // namespace godot

View file

@ -0,0 +1,121 @@
#ifndef QUAD_TREE_LOD_H
#define QUAD_TREE_LOD_H
#include <CanvasItem.hpp>
#include <FuncRef.hpp>
#include <Godot.hpp>
#include <vector>
namespace godot {
class QuadTreeLod : public Reference {
GODOT_CLASS(QuadTreeLod, Reference)
public:
static void _register_methods();
QuadTreeLod() {}
~QuadTreeLod() {}
void _init() {}
void set_callbacks(Ref<FuncRef> make_cb, Ref<FuncRef> recycle_cb, Ref<FuncRef> vbounds_cb);
int get_lod_count();
int get_lod_factor(int lod);
int compute_lod_count(int base_size, int full_size);
void set_split_scale(real_t p_split_scale);
real_t get_split_scale();
void clear();
void create_from_sizes(int base_size, int full_size);
void update(Vector3 view_pos);
void debug_draw_tree(CanvasItem *ci);
private:
static const unsigned int NO_CHILDREN = -1;
static const unsigned int ROOT = -1;
class Quad {
public:
unsigned int first_child = NO_CHILDREN;
int origin_x = 0;
int origin_y = 0;
Quad() {
init();
}
~Quad() {
}
inline void init() {
first_child = NO_CHILDREN;
origin_x = 0;
origin_y = 0;
clear_data();
}
inline void clear_data() {
_data = Variant();
}
inline bool has_children() {
return first_child != NO_CHILDREN;
}
inline bool is_null() {
return _data.get_type() == Variant::NIL;
}
inline bool is_valid() {
return _data.get_type() != Variant::NIL;
}
inline Variant get_data() {
return _data;
}
inline void set_data(Variant p_data) {
_data = p_data;
}
private:
Variant _data; // Type is HTerrainChunk.gd : Object
};
Quad _root;
std::vector<Quad> _node_pool;
std::vector<unsigned int> _free_indices;
int _max_depth = 0;
int _base_size = 16;
real_t _split_scale = 2.0f;
Ref<FuncRef> _make_func;
Ref<FuncRef> _recycle_func;
Ref<FuncRef> _vertical_bounds_func;
inline Quad *_get_root() {
return &_root;
}
inline Quad *_get_node(unsigned int index) {
if (index == ROOT) {
return &_root;
} else {
return &_node_pool[index];
}
}
void _clear_children(unsigned int index);
unsigned int _allocate_children();
void _recycle_children(unsigned int i0);
Variant _make_chunk(int lod, int origin_x, int origin_y);
void _recycle_chunk(unsigned int quad_index, int lod);
void _join_all_recursively(unsigned int quad_index, int lod);
void _update(unsigned int quad_index, int lod, Vector3 view_pos);
void _debug_draw_tree_recursive(CanvasItem *ci, unsigned int quad_index, int lod_index, int child_index);
}; // class QuadTreeLod
} // namespace godot
#endif // QUAD_TREE_LOD_H

View file

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

View file

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

View file

@ -0,0 +1,184 @@
tool
# Brush properties (shape, transform, timing and opacity).
# Other attributes like color, height or texture index are tool-specific,
# while brush properties apply to all of them.
# This is separate from Painter because it could apply to multiple Painters at once.
const Errors = preload("../../util/errors.gd")
const SHAPES_DIR = "addons/zylann.hterrain/tools/brush/shapes"
const DEFAULT_BRUSH = "round2.exr"
# Reasonable size for sliders to be usable
const MAX_SIZE_FOR_SLIDERS = 500
# Absolute size limit. Terrains can't be larger than that, and it will be very slow to paint
const MAX_SIZE = 4000
signal size_changed(new_size)
signal shapes_changed
var _size := 32
var _opacity := 1.0
var _random_rotation := false
var _pressure_enabled := false
var _pressure_over_scale := 0.5
var _pressure_over_opacity := 0.5
# TODO Rename stamp_*?
var _frequency_distance := 0.0
var _frequency_time_ms := 0
# Array of greyscale Textures
var _shapes := []
var _shape_index := 0
var _prev_position := Vector2(-999, -999)
var _prev_time_ms := 0
func set_size(size: int):
if size < 1:
size = 1
if size != _size:
_size = size
emit_signal("size_changed", _size)
func get_size() -> int:
return _size
func set_opacity(opacity: float):
_opacity = clamp(opacity, 0.0, 1.0)
func get_opacity() -> float:
return _opacity
func set_random_rotation_enabled(enabled: bool):
_random_rotation = enabled
func is_random_rotation_enabled() -> bool:
return _random_rotation
func set_pressure_enabled(enabled: bool):
_pressure_enabled = enabled
func is_pressure_enabled() -> bool:
return _pressure_enabled
func set_pressure_over_scale(amount: float):
_pressure_over_scale = clamp(amount, 0.0, 1.0)
func get_pressure_over_scale() -> float:
return _pressure_over_scale
func set_pressure_over_opacity(amount: float):
_pressure_over_opacity = clamp(amount, 0.0, 1.0)
func get_pressure_over_opacity() -> float:
return _pressure_over_opacity
func set_frequency_distance(d: float):
_frequency_distance = max(d, 0.0)
func get_frequency_distance() -> float:
return _frequency_distance
func set_frequency_time_ms(t: int):
if t < 0:
t = 0
_frequency_time_ms = t
func get_frequency_time_ms() -> int:
return _frequency_time_ms
func set_shapes(shapes: Array):
assert(len(shapes) >= 1)
for s in shapes:
assert(s != null)
assert(s is Texture)
_shapes = shapes.duplicate(false)
if _shape_index >= len(_shapes):
_shape_index = len(_shapes) - 1
emit_signal("shapes_changed")
func get_shapes() -> Array:
return _shapes.duplicate(false)
func get_shape(i: int) -> Texture:
return _shapes[i]
static func load_shape_from_image_file(fpath: String, logger) -> Texture:
var im := Image.new()
var err := im.load(fpath)
if err != OK:
logger.error("Could not load image at '{0}', error {1}" \
.format([fpath, Errors.get_message(err)]))
return null
var tex := ImageTexture.new()
tex.create_from_image(im, Texture.FLAG_FILTER)
return tex
# Call this while handling mouse or pen input.
# If it returns false, painting should not run.
func configure_paint_input(painters: Array, position: Vector2, pressure: float) -> bool:
assert(len(_shapes) != 0)
# DEBUG
#pressure = 0.5 + 0.5 * sin(OS.get_ticks_msec() / 200.0)
if position.distance_to(_prev_position) < _frequency_distance:
return false
var now = OS.get_ticks_msec()
if (now - _prev_time_ms) < _frequency_time_ms:
return false
_prev_position = position
_prev_time_ms = now
for painter in painters:
if _random_rotation:
painter.set_brush_rotation(rand_range(-PI, PI))
else:
painter.set_brush_rotation(0.0)
painter.set_brush_texture(_shapes[_shape_index])
painter.set_brush_size(_size)
if _pressure_enabled:
painter.set_brush_scale(lerp(1.0, pressure, _pressure_over_scale))
painter.set_brush_opacity(_opacity * lerp(1.0, pressure, _pressure_over_opacity))
else:
painter.set_brush_scale(1.0)
painter.set_brush_opacity(_opacity)
#painter.paint_input(position)
_shape_index += 1
if _shape_index >= len(_shapes):
_shape_index = 0
return true
# Call this when the user releases the pen or mouse button
func on_paint_end():
_prev_position = Vector2(-999, -999)
_prev_time_ms = 0

View file

@ -1,13 +1,15 @@
tool
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)

View file

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

View file

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

View file

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

View file

@ -1,7 +1,7 @@
# Core logic to paint a texture using shaders, with undo/redo support.
# 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

View file

@ -0,0 +1,259 @@
tool
extends AcceptDialog
const Util = preload("../../../util/util.gd")
const Brush = preload("../brush.gd")
const Logger = preload("../../../util/logger.gd")
const EditorUtil = preload("../../util/editor_util.gd")
onready var _scratchpad = $VB/HB/VB3/PreviewScratchpad
onready var _shape_list = $VB/HB/VB/ShapeList
onready var _remove_shape_button = $VB/HB/VB/HBoxContainer/RemoveShape
onready var _change_shape_button = $VB/HB/VB/ChangeShape
onready var _size_slider = $VB/HB/VB2/Settings/Size
onready var _opacity_slider = $VB/HB/VB2/Settings/Opacity
onready var _pressure_enabled_checkbox = $VB/HB/VB2/Settings/PressureEnabled
onready var _pressure_over_size_slider = $VB/HB/VB2/Settings/PressureOverSize
onready var _pressure_over_opacity_slider = $VB/HB/VB2/Settings/PressureOverOpacity
onready var _frequency_distance_slider = $VB/HB/VB2/Settings/FrequencyDistance
onready var _frequency_time_slider = $VB/HB/VB2/Settings/FrequencyTime
onready var _random_rotation_checkbox = $VB/HB/VB2/Settings/RandomRotation
var _brush : Brush
# This is a `EditorFileDialog`,
# but cannot type it because I want to be able to test it by running the scene.
# And when I run it, Godot does not allow to use `EditorFileDialog`.
var _load_image_dialog
# -1 means add, otherwise replace
var _load_image_index := -1
var _logger = Logger.get_for(self)
func _ready():
if Util.is_in_edited_scene(self):
return
_size_slider.set_max_value(Brush.MAX_SIZE_FOR_SLIDERS)
_size_slider.set_greater_max_value(Brush.MAX_SIZE)
# TESTING
if not Engine.editor_hint:
setup_dialogs(self)
call_deferred("popup")
func set_brush(brush : Brush):
assert(brush != null)
_brush = brush
_update_controls_from_brush()
func setup_dialogs(base_control: Control):
assert(_load_image_dialog == null)
_load_image_dialog = EditorUtil.create_open_file_dialog()
_load_image_dialog.mode = EditorFileDialog.MODE_OPEN_FILE
_load_image_dialog.add_filter("*.exr ; EXR files")
_load_image_dialog.resizable = true
_load_image_dialog.access = EditorFileDialog.ACCESS_FILESYSTEM
_load_image_dialog.current_dir = Brush.SHAPES_DIR
_load_image_dialog.connect("file_selected", self, "_on_LoadImageDialog_file_selected")
_load_image_dialog.connect("files_selected", self, "_on_LoadImageDialog_files_selected")
base_control.add_child(_load_image_dialog)
func _exit_tree():
if _load_image_dialog != null:
_load_image_dialog.queue_free()
_load_image_dialog = null
func _get_shapes_from_gui() -> Array:
var shapes = []
for i in _shape_list.get_item_count():
var icon = _shape_list.get_item_icon(i)
assert(icon != null)
shapes.append(icon)
return shapes
func _update_shapes_gui(shapes: Array):
_shape_list.clear()
for shape in shapes:
assert(shape != null)
assert(shape is Texture)
_shape_list.add_icon_item(shape)
_update_shape_list_buttons()
func _on_AddShape_pressed():
_load_image_index = -1
_load_image_dialog.mode = EditorFileDialog.MODE_OPEN_FILES
_load_image_dialog.popup_centered_ratio(0.7)
func _on_RemoveShape_pressed():
var selected_indices = _shape_list.get_selected_items()
if len(selected_indices) == 0:
return
var index : int = selected_indices[0]
_shape_list.remove_item(index)
var shapes = _get_shapes_from_gui()
for brush in _get_brushes():
brush.set_shapes(shapes)
_update_shape_list_buttons()
func _on_ShapeList_item_activated(index):
_request_modify_shape(index)
func _on_ChangeShape_pressed():
var selected = _shape_list.get_selected_items()
if len(selected) == 0:
return
_request_modify_shape(selected[0])
func _request_modify_shape(index: int):
_load_image_index = index
_load_image_dialog.mode = EditorFileDialog.MODE_OPEN_FILE
_load_image_dialog.popup_centered_ratio(0.7)
func _on_LoadImageDialog_files_selected(fpaths: PoolStringArray):
var shapes := _get_shapes_from_gui()
for fpath in fpaths:
var tex := Brush.load_shape_from_image_file(fpath, _logger)
if tex == null:
# Failed
continue
shapes.append(tex)
for brush in _get_brushes():
brush.set_shapes(shapes)
_update_shapes_gui(shapes)
func _on_LoadImageDialog_file_selected(fpath: String):
var tex := Brush.load_shape_from_image_file(fpath, _logger)
if tex == null:
# Failed
return
var shapes := _get_shapes_from_gui()
if _load_image_index == -1 or _load_image_index >= len(shapes):
# Add
shapes.append(tex)
else:
# Replace
assert(_load_image_index >= 0)
shapes[_load_image_index] = tex
for brush in _get_brushes():
brush.set_shapes(shapes)
_update_shapes_gui(shapes)
func _notification(what: int):
if what == NOTIFICATION_VISIBILITY_CHANGED:
if visible:
_update_controls_from_brush()
func _update_controls_from_brush():
var brush := _brush
if brush == null:
# To allow testing
brush = _scratchpad.get_painter().get_brush()
_update_shapes_gui(brush.get_shapes())
_size_slider.set_value(brush.get_size(), false)
_opacity_slider.set_value(brush.get_opacity() * 100.0, false)
_pressure_enabled_checkbox.pressed = brush.is_pressure_enabled()
_pressure_over_size_slider.set_value(brush.get_pressure_over_scale() * 100.0, false)
_pressure_over_opacity_slider.set_value(brush.get_pressure_over_opacity() * 100.0, false)
_frequency_distance_slider.set_value(brush.get_frequency_distance(), false)
_frequency_time_slider.set_value(1000.0 / max(0.1, float(brush.get_frequency_time_ms())), false)
_random_rotation_checkbox.pressed = brush.is_random_rotation_enabled()
func _on_ClearScratchpad_pressed():
_scratchpad.reset_image()
func _on_Size_value_changed(value: float):
for brush in _get_brushes():
brush.set_size(value)
func _on_Opacity_value_changed(value):
for brush in _get_brushes():
brush.set_opacity(value / 100.0)
func _on_PressureEnabled_toggled(button_pressed):
for brush in _get_brushes():
brush.set_pressure_enabled(button_pressed)
func _on_PressureOverSize_value_changed(value):
for brush in _get_brushes():
brush.set_pressure_over_scale(value / 100.0)
func _on_PressureOverOpacity_value_changed(value):
for brush in _get_brushes():
brush.set_pressure_over_opacity(value / 100.0)
func _on_FrequencyDistance_value_changed(value):
for brush in _get_brushes():
brush.set_frequency_distance(value)
func _on_FrequencyTime_value_changed(fps):
fps = max(1.0, fps)
var ms = 1000.0 / fps
if is_equal_approx(fps, 60.0):
ms = 0
for brush in _get_brushes():
brush.set_frequency_time_ms(ms)
func _on_RandomRotation_toggled(button_pressed: bool):
for brush in _get_brushes():
brush.set_random_rotation_enabled(button_pressed)
func _get_brushes() -> Array:
if _brush != null:
# We edit both the preview brush and the terrain brush
# TODO Could we simply share the brush?
return [_brush, _scratchpad.get_painter().get_brush()]
# When testing the dialog in isolation, the edited brush might be null
return [_scratchpad.get_painter().get_brush()]
func _on_ShapeList_item_selected(index):
_update_shape_list_buttons()
func _on_ShapeList_nothing_selected():
_update_shape_list_buttons()
func _update_shape_list_buttons():
var selected_count = len(_shape_list.get_selected_items())
# There must be at least one shape
_remove_shape_button.disabled = _shape_list.get_item_count() == 1 or selected_count == 0
_change_shape_button.disabled = selected_count == 0

View file

@ -0,0 +1,273 @@
[gd_scene load_steps=5 format=2]
[ext_resource path="res://addons/zylann.hterrain/tools/brush/shapes/round2.exr" type="Texture" id=1]
[ext_resource path="res://addons/zylann.hterrain/tools/util/spin_slider.tscn" type="PackedScene" id=2]
[ext_resource path="res://addons/zylann.hterrain/tools/brush/settings_dialog/brush_settings_dialog.gd" type="Script" id=3]
[ext_resource path="res://addons/zylann.hterrain/tools/brush/settings_dialog/preview_scratchpad.tscn" type="PackedScene" id=4]
[node name="BrushSettingsDialog" type="AcceptDialog"]
visible = true
margin_left = 46.0
margin_top = 65.0
margin_right = 746.0
margin_bottom = 465.0
rect_min_size = Vector2( 700, 400 )
window_title = "Brush settings"
resizable = true
script = ExtResource( 3 )
__meta__ = {
"_edit_use_anchors_": false
}
[node name="VB" type="VBoxContainer" parent="."]
anchor_right = 1.0
anchor_bottom = 1.0
margin_left = 8.0
margin_top = 8.0
margin_right = -8.0
margin_bottom = -36.0
__meta__ = {
"_edit_use_anchors_": false
}
[node name="HB" type="HBoxContainer" parent="VB"]
margin_right = 684.0
margin_bottom = 356.0
size_flags_vertical = 3
custom_constants/separation = 8
[node name="VB" type="VBoxContainer" parent="VB/HB"]
margin_right = 120.0
margin_bottom = 356.0
size_flags_vertical = 3
[node name="Label" type="Label" parent="VB/HB/VB"]
margin_right = 120.0
margin_bottom = 14.0
text = "Shapes"
[node name="ShapeList" type="ItemList" parent="VB/HB/VB"]
margin_top = 18.0
margin_right = 120.0
margin_bottom = 308.0
rect_min_size = Vector2( 120, 0 )
size_flags_vertical = 3
items = [ "", ExtResource( 1 ), false ]
fixed_icon_size = Vector2( 100, 100 )
[node name="ChangeShape" type="Button" parent="VB/HB/VB"]
margin_top = 312.0
margin_right = 120.0
margin_bottom = 332.0
disabled = true
text = "Change..."
[node name="HBoxContainer" type="HBoxContainer" parent="VB/HB/VB"]
margin_top = 336.0
margin_right = 120.0
margin_bottom = 356.0
[node name="AddShape" type="Button" parent="VB/HB/VB/HBoxContainer"]
margin_right = 49.0
margin_bottom = 20.0
text = "Add..."
[node name="RemoveShape" type="Button" parent="VB/HB/VB/HBoxContainer"]
margin_left = 53.0
margin_right = 117.0
margin_bottom = 20.0
disabled = true
text = "Remove"
[node name="VB2" type="VBoxContainer" parent="VB/HB"]
margin_left = 128.0
margin_right = 476.0
margin_bottom = 356.0
size_flags_horizontal = 3
[node name="Label" type="Label" parent="VB/HB/VB2"]
margin_right = 348.0
margin_bottom = 14.0
[node name="Settings" type="VBoxContainer" parent="VB/HB/VB2"]
margin_top = 18.0
margin_right = 348.0
margin_bottom = 262.0
size_flags_horizontal = 3
[node name="Size" parent="VB/HB/VB2/Settings" instance=ExtResource( 2 )]
anchor_right = 0.0
anchor_bottom = 0.0
margin_right = 348.0
margin_bottom = 28.0
size_flags_horizontal = 3
_value = 32.0
_min_value = 2.0
_max_value = 500.0
_prefix = "Size:"
_suffix = "px"
_rounded = true
_centered = false
_allow_greater = true
_greater_max_value = 4000.0
[node name="Opacity" parent="VB/HB/VB2/Settings" instance=ExtResource( 2 )]
anchor_right = 0.0
anchor_bottom = 0.0
margin_top = 32.0
margin_right = 348.0
margin_bottom = 60.0
size_flags_horizontal = 3
_value = 100.0
_prefix = "Opacity"
_suffix = "%"
_rounded = true
_centered = false
[node name="PressureEnabled" type="CheckBox" parent="VB/HB/VB2/Settings"]
margin_top = 64.0
margin_right = 348.0
margin_bottom = 88.0
text = "Enable pressure (pen tablets)"
[node name="PressureOverSize" parent="VB/HB/VB2/Settings" instance=ExtResource( 2 )]
anchor_right = 0.0
anchor_bottom = 0.0
margin_top = 92.0
margin_right = 348.0
margin_bottom = 120.0
_value = 50.0
_prefix = "Pressure affects size:"
_suffix = "%"
_centered = false
[node name="PressureOverOpacity" parent="VB/HB/VB2/Settings" instance=ExtResource( 2 )]
anchor_right = 0.0
anchor_bottom = 0.0
margin_top = 124.0
margin_right = 348.0
margin_bottom = 152.0
_value = 50.0
_prefix = "Pressure affects opacity:"
_suffix = "%"
_centered = false
[node name="FrequencyTime" parent="VB/HB/VB2/Settings" instance=ExtResource( 2 )]
anchor_right = 0.0
anchor_bottom = 0.0
margin_top = 156.0
margin_right = 348.0
margin_bottom = 184.0
_value = 60.0
_min_value = 1.0
_max_value = 60.0
_prefix = "Frequency time:"
_suffix = "fps"
_centered = false
[node name="FrequencyDistance" parent="VB/HB/VB2/Settings" instance=ExtResource( 2 )]
anchor_right = 0.0
anchor_bottom = 0.0
margin_top = 188.0
margin_right = 348.0
margin_bottom = 216.0
_prefix = "Frequency distance:"
_suffix = "px"
_centered = false
_greater_max_value = 4000.0
[node name="RandomRotation" type="CheckBox" parent="VB/HB/VB2/Settings"]
margin_top = 220.0
margin_right = 348.0
margin_bottom = 244.0
text = "Random rotation"
[node name="HSeparator" type="HSeparator" parent="VB/HB/VB2/Settings"]
visible = false
margin_top = 124.0
margin_right = 292.0
margin_bottom = 128.0
[node name="SizeLimitHB" type="HBoxContainer" parent="VB/HB/VB2/Settings"]
visible = false
margin_top = 124.0
margin_right = 142.0
margin_bottom = 152.0
[node name="Label" type="Label" parent="VB/HB/VB2/Settings/SizeLimitHB"]
margin_top = 7.0
margin_right = 64.0
margin_bottom = 21.0
hint_tooltip = "This allows to change the upper limit of the brush size slider. Bear in mind high values can slow down the editor."
mouse_filter = 0
text = "Size limit:"
[node name="SizeLimit" type="SpinBox" parent="VB/HB/VB2/Settings/SizeLimitHB"]
margin_left = 68.0
margin_right = 142.0
margin_bottom = 28.0
size_flags_horizontal = 3
min_value = 1.0
max_value = 1000.0
value = 200.0
[node name="HSeparator2" type="HSeparator" parent="VB/HB/VB2/Settings"]
visible = false
margin_top = 188.0
margin_right = 292.0
margin_bottom = 192.0
[node name="HB" type="HBoxContainer" parent="VB/HB/VB2/Settings"]
visible = false
margin_top = 248.0
margin_right = 292.0
margin_bottom = 268.0
[node name="Button" type="Button" parent="VB/HB/VB2/Settings/HB"]
margin_right = 99.0
margin_bottom = 20.0
text = "Load preset..."
[node name="Button2" type="Button" parent="VB/HB/VB2/Settings/HB"]
margin_left = 103.0
margin_right = 201.0
margin_bottom = 20.0
text = "Save preset..."
[node name="VB3" type="VBoxContainer" parent="VB/HB"]
margin_left = 484.0
margin_right = 684.0
margin_bottom = 356.0
[node name="Label" type="Label" parent="VB/HB/VB3"]
margin_right = 200.0
margin_bottom = 14.0
text = "Scratchpad"
[node name="PreviewScratchpad" parent="VB/HB/VB3" instance=ExtResource( 4 )]
margin_top = 18.0
margin_right = 200.0
margin_bottom = 318.0
rect_min_size = Vector2( 200, 300 )
[node name="ClearScratchpad" type="Button" parent="VB/HB/VB3"]
margin_top = 322.0
margin_right = 200.0
margin_bottom = 342.0
text = "Clear"
[connection signal="item_activated" from="VB/HB/VB/ShapeList" to="." method="_on_ShapeList_item_activated"]
[connection signal="item_selected" from="VB/HB/VB/ShapeList" to="." method="_on_ShapeList_item_selected"]
[connection signal="nothing_selected" from="VB/HB/VB/ShapeList" to="." method="_on_ShapeList_nothing_selected"]
[connection signal="pressed" from="VB/HB/VB/ChangeShape" to="." method="_on_ChangeShape_pressed"]
[connection signal="pressed" from="VB/HB/VB/HBoxContainer/AddShape" to="." method="_on_AddShape_pressed"]
[connection signal="pressed" from="VB/HB/VB/HBoxContainer/RemoveShape" to="." method="_on_RemoveShape_pressed"]
[connection signal="value_changed" from="VB/HB/VB2/Settings/Size" to="." method="_on_Size_value_changed"]
[connection signal="value_changed" from="VB/HB/VB2/Settings/Opacity" to="." method="_on_Opacity_value_changed"]
[connection signal="toggled" from="VB/HB/VB2/Settings/PressureEnabled" to="." method="_on_PressureEnabled_toggled"]
[connection signal="value_changed" from="VB/HB/VB2/Settings/PressureOverSize" to="." method="_on_PressureOverSize_value_changed"]
[connection signal="value_changed" from="VB/HB/VB2/Settings/PressureOverOpacity" to="." method="_on_PressureOverOpacity_value_changed"]
[connection signal="value_changed" from="VB/HB/VB2/Settings/FrequencyTime" to="." method="_on_FrequencyTime_value_changed"]
[connection signal="value_changed" from="VB/HB/VB2/Settings/FrequencyDistance" to="." method="_on_FrequencyDistance_value_changed"]
[connection signal="toggled" from="VB/HB/VB2/Settings/RandomRotation" to="." method="_on_RandomRotation_toggled"]
[connection signal="pressed" from="VB/HB/VB3/ClearScratchpad" to="." method="_on_ClearScratchpad_pressed"]

View file

@ -0,0 +1,42 @@
tool
extends Node
const Painter = preload("./../painter.gd")
const Brush = preload("./../brush.gd")
const ColorShader = preload("../shaders/color.shader")
var _painter : Painter
var _brush : Brush
func _init():
var p = Painter.new()
# The name is just for debugging
p.set_name("Painter")
add_child(p)
_painter = p
_brush = Brush.new()
func set_image_texture(image: Image, texture: ImageTexture):
_painter.set_image(image, texture)
func get_brush() -> Brush:
return _brush
# This may be called from an `_input` callback
func paint_input(position: Vector2, pressure: float):
var p : Painter = _painter
if not _brush.configure_paint_input([p], position, pressure):
return
p.set_brush_shader(ColorShader)
#p.set_brush_shader_param("u_factor", _opacity)
p.set_brush_shader_param("u_color", Color(0,0,0,1))
#p.set_image(_image, _texture)
p.paint_input(position)

View file

@ -0,0 +1,51 @@
tool
extends Control
const PreviewPainter = preload("./preview_painter.gd")
const DefaultBrushTexture = preload("../shapes/round2.exr")
onready var _texture_rect : TextureRect = $TextureRect
onready var _painter : PreviewPainter = $Painter
func _ready():
reset_image()
# Default so it doesnt crash when painting and can be tested
_painter.get_brush().set_shapes([DefaultBrushTexture])
func reset_image():
var image = Image.new()
image.create(_texture_rect.rect_size.x, _texture_rect.rect_size.y, false, Image.FORMAT_RGB8)
image.fill(Color(1,1,1))
var texture = ImageTexture.new()
texture.create_from_image(image)
_texture_rect.texture = texture
_painter.set_image_texture(image, texture)
func get_painter() -> PreviewPainter:
return _painter
func _gui_input(event):
if event is InputEventMouseMotion:
if Input.is_mouse_button_pressed(BUTTON_LEFT):
_painter.paint_input(event.position, event.pressure)
update()
elif event is InputEventMouseButton:
if event.button_index == BUTTON_LEFT:
if event.pressed:
# TODO `pressure` is not available on button events
# So I have to assume zero... which means clicks do not paint anything?
_painter.paint_input(event.position, 0.0)
else:
_painter.get_brush().on_paint_end()
func _draw():
var mpos = get_local_mouse_position()
var brush = _painter.get_brush()
draw_arc(mpos, 0.5 * brush.get_size(), -PI, PI, 32, Color(1, 0.2, 0.2), 2.0, true)

View file

@ -0,0 +1,22 @@
[gd_scene load_steps=3 format=2]
[ext_resource path="res://addons/zylann.hterrain/tools/brush/settings_dialog/preview_scratchpad.gd" type="Script" id=1]
[ext_resource path="res://addons/zylann.hterrain/tools/brush/settings_dialog/preview_painter.gd" type="Script" id=2]
[node name="PreviewScratchpad" type="Control"]
margin_right = 380.0
margin_bottom = 383.0
rect_clip_content = true
script = ExtResource( 1 )
__meta__ = {
"_edit_use_anchors_": false
}
[node name="Painter" type="Node" parent="."]
script = ExtResource( 2 )
[node name="TextureRect" type="TextureRect" parent="."]
show_behind_parent = true
anchor_right = 1.0
anchor_bottom = 1.0
stretch_mode = 5

View file

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

View file

@ -1,21 +1,28 @@
shader_type canvas_item;
render_mode blend_disabled;
uniform sampler2D u_brush_texture;
uniform float u_factor = 1.0;
uniform vec4 u_color = vec4(1.0);
void fragment() {
float brush_value = texture(u_brush_texture, SCREEN_UV).r;
vec4 src = texture(TEXTURE, UV);
// Despite hints, albedo textures render darker.
// Trying to undo sRGB does not work because of 8-bit precision loss
// that would occur either in texture, or on the source image.
// So it's not possible to use viewports to paint albedo...
//src.rgb = pow(src.rgb, vec3(0.4545));
vec4 col = vec4(mix(src.rgb, u_color.rgb, brush_value * u_factor), src.a);
COLOR = col;
}
shader_type canvas_item;
render_mode blend_disabled;
uniform sampler2D u_src_texture;
uniform vec4 u_src_rect;
uniform float u_opacity = 1.0;
uniform float u_factor = 1.0;
uniform vec4 u_color = vec4(1.0);
vec2 get_src_uv(vec2 screen_uv) {
vec2 uv = u_src_rect.xy + screen_uv * u_src_rect.zw;
return uv;
}
void fragment() {
float brush_value = u_opacity * texture(TEXTURE, UV).r;
vec4 src = texture(u_src_texture, get_src_uv(SCREEN_UV));
// Despite hints, albedo textures render darker.
// Trying to undo sRGB does not work because of 8-bit precision loss
// that would occur either in texture, or on the source image.
// So it's not possible to use viewports to paint albedo...
//src.rgb = pow(src.rgb, vec3(0.4545));
vec4 col = vec4(mix(src.rgb, u_color.rgb, brush_value * u_factor), src.a);
COLOR = col;
}

View file

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

View file

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

View file

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

View file

@ -1,13 +1,20 @@
shader_type canvas_item;
render_mode blend_disabled;
uniform sampler2D u_brush_texture;
uniform sampler2D u_src_texture;
uniform vec4 u_src_rect;
uniform float u_opacity = 1.0;
uniform float u_factor = 1.0;
vec2 get_src_uv(vec2 screen_uv) {
vec2 uv = u_src_rect.xy + screen_uv * u_src_rect.zw;
return uv;
}
void fragment() {
float brush_value = texture(u_brush_texture, SCREEN_UV).r;
float brush_value = u_opacity * texture(TEXTURE, UV).r;
float src_h = texture(TEXTURE, UV).r;
float src_h = texture(u_src_texture, get_src_uv(SCREEN_UV)).r;
float h = src_h + u_factor * brush_value;
COLOR = vec4(h, 0.0, 0.0, 1.0);
}

View file

@ -1,19 +1,28 @@
shader_type canvas_item;
render_mode blend_disabled;
uniform sampler2D u_brush_texture;
uniform float u_factor = 1.0;
void fragment() {
float brush_value = texture(u_brush_texture, SCREEN_UV).r;
vec2 offset = TEXTURE_PIXEL_SIZE;
float src_nx = texture(TEXTURE, UV - vec2(offset.x, 0.0)).r;
float src_px = texture(TEXTURE, UV + vec2(offset.x, 0.0)).r;
float src_ny = texture(TEXTURE, UV - vec2(0.0, offset.y)).r;
float src_py = texture(TEXTURE, UV + vec2(0.0, offset.y)).r;
float src_h = texture(TEXTURE, UV).r;
float dst_h = (src_h + src_nx + src_px + src_ny + src_py) * 0.2;
float h = mix(src_h, dst_h, u_factor * brush_value);
COLOR = vec4(h, 0.0, 0.0, 1.0);
}
shader_type canvas_item;
render_mode blend_disabled;
uniform sampler2D u_src_texture;
uniform vec4 u_src_rect;
uniform float u_opacity = 1.0;
uniform float u_factor = 1.0;
vec2 get_src_uv(vec2 screen_uv) {
vec2 uv = u_src_rect.xy + screen_uv * u_src_rect.zw;
return uv;
}
void fragment() {
float brush_value = u_opacity * texture(TEXTURE, UV).r;
vec2 src_pixel_size = 1.0 / vec2(textureSize(u_src_texture, 0));
vec2 src_uv = get_src_uv(SCREEN_UV);
vec2 offset = src_pixel_size;
float src_nx = texture(u_src_texture, src_uv - vec2(offset.x, 0.0)).r;
float src_px = texture(u_src_texture, src_uv + vec2(offset.x, 0.0)).r;
float src_ny = texture(u_src_texture, src_uv - vec2(0.0, offset.y)).r;
float src_py = texture(u_src_texture, src_uv + vec2(0.0, offset.y)).r;
float src_h = texture(u_src_texture, src_uv).r;
float dst_h = (src_h + src_nx + src_px + src_ny + src_py) * 0.2;
float h = mix(src_h, dst_h, u_factor * brush_value);
COLOR = vec4(h, 0.0, 0.0, 1.0);
}

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,7 @@
[gd_scene load_steps=2 format=2]
[gd_scene load_steps=3 format=2]
[ext_resource path="res://addons/zylann.hterrain/tools/exporter/export_image_dialog.gd" type="Script" id=1]
[ext_resource path="res://addons/zylann.hterrain/tools/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"]

View file

@ -1,243 +1,84 @@
[gd_scene load_steps=2 format=2]
[gd_scene load_steps=3 format=2]
[ext_resource path="res://addons/zylann.hterrain/tools/generate_mesh_dialog.gd" type="Script" id=1]
[ext_resource path="res://addons/zylann.hterrain/tools/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"]

View file

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

View file

@ -1,14 +1,15 @@
[gd_scene load_steps=4 format=2]
[gd_scene load_steps=5 format=2]
[ext_resource path="res://addons/zylann.hterrain/tools/generator/generator_dialog.gd" type="Script" id=1]
[ext_resource path="res://addons/zylann.hterrain/tools/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"]

View file

@ -8,9 +8,21 @@ uniform int u_seed;
uniform int u_octaves = 5;
uniform float u_roughness = 0.5;
uniform float u_curve = 1.0;
uniform float u_terrain_size = 513.0;
uniform float u_tile_size = 513.0;
uniform sampler2D u_additive_heightmap;
uniform float u_additive_heightmap_factor = 0.0;
uniform vec2 u_uv_offset;
uniform vec2 u_uv_scale = vec2(1.0, 1.0);
uniform float u_island_weight = 0.0;
// 0: smooth transition, 1: sharp transition
uniform float u_island_sharpness = 0.0;
// 0: edge is min height (island), 1: edge is max height (canyon)
uniform float u_island_height_ratio = 0.0;
// 0: round, 1: square
uniform float u_island_shape = 0.0;
////////////////////////////////////////////////////////////////////////////////
// Perlin noise source:
// https://github.com/curly-brace/Godot-3.0-Noise-Shaders
@ -103,22 +115,91 @@ float get_fractal_noise(vec2 uv) {
return gs;
}
float get_height(vec2 uv) {
float h = 0.5 + 0.5 * get_fractal_noise(uv);
h = pow(h, u_curve);
h = u_base_height + h * u_height_range;
// x is a ratio in 0..1
float get_island_curve(float x) {
return smoothstep(min(0.999, u_island_sharpness), 1.0, x);
// float exponent = 1.0 + 10.0 * u_island_sharpness;
// return pow(abs(x), exponent);
}
float smooth_union(float a, float b, float k) {
float h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0);
return mix(b, a, h) - k * h * (1.0 - h);
}
float squareish_distance(vec2 a, vec2 b, float r, float s) {
vec2 v = b - a;
// TODO This is brute force but this is the first attempt that gave me a "rounded square" distance,
// where the "roundings" remained constant over distance (not the case with standard box SDF)
float da = -smooth_union(v.x+s, v.y+s, r)+s;
float db = -smooth_union(s-v.x, s-v.y, r)+s;
float dc = -smooth_union(s-v.x, v.y+s, r)+s;
float dd = -smooth_union(v.x+s, s-v.y, r)+s;
return max(max(da, db), max(dc, dd));
}
// This is too sharp
//float squareish_distance(vec2 a, vec2 b) {
// vec2 v = b - a;
// // Manhattan distance would produce a "diamond-shaped distance".
// // This gives "square-shaped" distance.
// return max(abs(v.x), abs(v.y));
//}
float get_island_distance(vec2 pos, vec2 center, float terrain_size) {
float rd = distance(pos, center);
float sd = squareish_distance(pos, center, terrain_size * 0.1, terrain_size);
return mix(rd, sd, u_island_shape);
}
// pos is in terrain space
float get_height(vec2 pos) {
float h = 0.0;
{
// Noise (0..1)
// Offset and scale for the noise itself
vec2 uv_noise = (pos / u_terrain_size + u_offset) * u_scale;
h = 0.5 + 0.5 * get_fractal_noise(uv_noise);
}
// Curve
{
h = pow(h, u_curve);
}
// Island
{
float terrain_size = u_terrain_size;
vec2 island_center = vec2(0.5 * terrain_size);
float island_height_ratio = 0.5 + 0.5 * u_island_height_ratio;
float island_distance = get_island_distance(pos, island_center, terrain_size);
float distance_ratio = clamp(island_distance / (0.5 * terrain_size), 0.0, 1.0);
float island_ratio = u_island_weight * get_island_curve(distance_ratio);
h = mix(h, island_height_ratio, island_ratio);
}
// Height remapping
{
h = u_base_height + h * u_height_range;
}
// Additive heightmap
{
h += u_additive_heightmap_factor * texture(u_additive_heightmap, pos / u_terrain_size).r;
}
return h;
}
void fragment() {
vec2 uv = SCREEN_UV;
// Handle screen padding: transform UV back into generation space.
// This is in tile space actually...? it spans 1 unit across the viewport,
// and starts from 0 when tile (0,0) is generated.
// Maybe we could change this into world units instead?
vec2 uv_tile = (SCREEN_UV + u_uv_offset) * u_uv_scale;
// Handle screen padding: transform UV back into generation space
uv = (uv + u_uv_offset) * u_uv_scale;
// Offset and scale for the noise itself
uv = (uv + u_offset) * u_scale;
float h = get_height(uv);
float h = get_height(uv_tile * u_tile_size);
COLOR = vec4(h, h, h, 1.0);
}

View file

@ -1,7 +1,8 @@
[gd_scene load_steps=3 format=2]
[gd_scene load_steps=4 format=2]
[ext_resource path="res://addons/zylann.hterrain/tools/importer/importer_dialog.gd" type="Script" id=1]
[ext_resource path="res://addons/zylann.hterrain/tools/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"]

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,624 +1,197 @@
[gd_scene load_steps=6 format=2]
[gd_scene load_steps=7 format=2]
[ext_resource path="res://addons/zylann.hterrain/tools/resize_dialog/resize_dialog.gd" type="Script" id=1]
[ext_resource path="res://addons/zylann.hterrain/tools/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"]

View file

@ -347,17 +347,19 @@ func _set_texture_array_action(slot_index: int, texture_array: TextureArray, typ
# See https://github.com/godotengine/godot/issues/36895
if texture_array == null:
_undo_redo.add_do_method(_texture_set, "set_texture_array_null", type)
# Can't select a slot after this because there won't be any after the array is removed
else:
_undo_redo.add_do_method(_texture_set, "set_texture_array", type, texture_array)
_undo_redo.add_do_method(self, "_select_slot", slot_index)
_undo_redo.add_do_method(self, "_select_slot", slot_index)
# TODO This branch only exists because of a flaw in UndoRedo
# See https://github.com/godotengine/godot/issues/36895
if prev_texture_array == null:
_undo_redo.add_undo_method(_texture_set, "set_texture_array_null", type)
# Can't select a slot after this because there won't be any after the array is removed
else:
_undo_redo.add_undo_method(_texture_set, "set_texture_array", type, prev_texture_array)
_undo_redo.add_undo_method(self, "_select_slot", slot_index)
_undo_redo.add_undo_method(self, "_select_slot", slot_index)
_undo_redo.commit_action()
@ -374,7 +376,11 @@ func _on_LoadTextureArrayDialog_file_selected(fpath: String):
assert(_texture_set.get_mode() == HTerrainTextureSet.MODE_TEXTURE_ARRAYS)
var texture_array = load(fpath)
assert(texture_array != null)
var slot_index : int = _slots_list.get_selected_items()[0]
# It's possible no slot exists at the moment,
# because there could be no texture array already set.
# The number of slots in the new array might also be different.
# So in this case we'll default to selecting the first slot.
var slot_index := 0
_set_texture_array_action(slot_index, texture_array, _load_texture_type)

View file

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

View file

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

View file

@ -0,0 +1,322 @@
tool
extends Control
const FG_MARGIN = 2
const MAX_DECIMALS_VISUAL = 3
signal value_changed(value)
export var _value := 0.0 setget set_value_no_notify
export var _min_value := 0.0 setget set_min_value
export var _max_value := 100.0 setget set_max_value
export var _prefix := "" setget set_prefix
export var _suffix := "" setget set_suffix
export var _rounded := false setget set_rounded
export var _centered := true setget set_centered
export var _allow_greater := false setget set_allow_greater
# There is still a limit when typing a larger value, but this one is to prevent software
# crashes or freezes. The regular min and max values are for slider UX. Exceeding it should be
# a corner case.
export var _greater_max_value := 10000.0 setget set_greater_max_value
var _label : Label
var _label2 : Label
var _line_edit : LineEdit
var _ignore_line_edit := false
var _pressing := false
var _grabbing := false
var _press_pos := Vector2()
func _init():
rect_min_size = Vector2(32, 28)
_label = Label.new()
_label.align = Label.ALIGN_CENTER
_label.valign = Label.VALIGN_CENTER
_label.clip_text = true
#_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_label.anchor_top = 0
_label.anchor_left = 0
_label.anchor_right = 1
_label.anchor_bottom = 1
_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
_label.add_color_override("font_color_shadow", Color(0,0,0,0.5))
_label.add_constant_override("shadow_offset_x", 1)
_label.add_constant_override("shadow_offset_y", 1)
add_child(_label)
_label2 = Label.new()
_label2.align = Label.ALIGN_LEFT
_label2.valign = Label.VALIGN_CENTER
_label2.clip_text = true
#_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_label2.anchor_top = 0
_label2.anchor_left = 0
_label2.anchor_right = 1
_label2.anchor_bottom = 1
_label2.margin_left = 8
_label2.mouse_filter = Control.MOUSE_FILTER_IGNORE
_label2.add_color_override("font_color_shadow", Color(0,0,0,0.5))
_label2.add_constant_override("shadow_offset_x", 1)
_label2.add_constant_override("shadow_offset_y", 1)
_label2.hide()
add_child(_label2)
_line_edit = LineEdit.new()
_line_edit.align = LineEdit.ALIGN_CENTER
_line_edit.anchor_top = 0
_line_edit.anchor_left = 0
_line_edit.anchor_right = 1
_line_edit.anchor_bottom = 1
_line_edit.connect("gui_input", self, "_on_LineEdit_gui_input")
_line_edit.connect("focus_exited", self, "_on_LineEdit_focus_exited")
_line_edit.connect("text_entered", self, "_on_LineEdit_text_entered")
_line_edit.hide()
add_child(_line_edit)
mouse_default_cursor_shape = Control.CURSOR_HSIZE
func _ready():
pass # Replace with function body.
func set_centered(centered: bool):
_centered = centered
if _centered:
_label.align = Label.ALIGN_CENTER
_label.margin_right = 0
_label2.hide()
else:
_label.align = Label.ALIGN_RIGHT
_label.margin_right = -8
_label2.show()
update()
func is_centered() -> bool:
return _centered
func set_value_no_notify(v: float):
set_value(v, false, false)
func set_value(v: float, notify_change: bool, use_slider_maximum: bool = false):
if _allow_greater and not use_slider_maximum:
v = clamp(v, _min_value, _greater_max_value)
else:
v = clamp(v, _min_value, _max_value)
if v != _value:
_value = v
update()
if notify_change:
emit_signal("value_changed", get_value())
func get_value():
if _rounded:
return int(round(_value))
return _value
func set_min_value(minv: float):
_min_value = minv
#update()
func get_min_value() -> float:
return _min_value
func set_max_value(maxv: float):
_max_value = maxv
#update()
func get_max_value() -> float:
return _max_value
func set_greater_max_value(gmax: float):
_greater_max_value = gmax
func get_greater_max_value() -> float:
return _greater_max_value
func set_rounded(b: bool):
_rounded = b
update()
func is_rounded() -> bool:
return _rounded
func set_prefix(prefix: String):
_prefix = prefix
update()
func get_prefix() -> String:
return _prefix
func set_suffix(suffix: String):
_suffix = suffix
update()
func get_suffix() -> String:
return _suffix
func set_allow_greater(allow: bool):
_allow_greater = allow
func is_allowing_greater() -> bool:
return _allow_greater
func _set_from_pixel(px: float):
var r := (px - FG_MARGIN) / (rect_size.x - FG_MARGIN * 2.0)
var v := _ratio_to_value(r)
set_value(v, true, true)
func get_ratio() -> float:
return _value_to_ratio(get_value())
func _ratio_to_value(r: float) -> float:
return r * (_max_value - _min_value) + _min_value
func _value_to_ratio(v: float) -> float:
if abs(_max_value - _min_value) < 0.001:
return 0.0
return (v - _min_value) / (_max_value - _min_value)
func _on_LineEdit_gui_input(event):
if event is InputEventKey:
if event.pressed:
if event.scancode == KEY_ESCAPE:
_ignore_line_edit = true
_hide_line_edit()
grab_focus()
_ignore_line_edit = false
func _on_LineEdit_focus_exited():
if _ignore_line_edit:
return
_enter_text()
func _on_LineEdit_text_entered(text: String):
_enter_text()
func _enter_text():
var s = _line_edit.text.strip_edges()
if s.is_valid_float():
var v = s.to_float()
if not _allow_greater:
v = min(v, _max_value)
set_value(v, true, false)
_hide_line_edit()
func _hide_line_edit():
_line_edit.hide()
_label.show()
update()
func _show_line_edit():
_line_edit.show()
_line_edit.text = str(get_value())
_line_edit.select_all()
_line_edit.grab_focus()
_label.hide()
update()
func _gui_input(event):
if event is InputEventMouseButton:
if event.pressed:
if event.button_index == BUTTON_LEFT:
_press_pos = event.position
_pressing = true
else:
if event.button_index == BUTTON_LEFT:
_pressing = false
if _grabbing:
_grabbing = false
_set_from_pixel(event.position.x)
else:
_show_line_edit()
elif event is InputEventMouseMotion:
if _pressing and _press_pos.distance_to(event.position) > 2.0:
_grabbing = true
if _grabbing:
_set_from_pixel(event.position.x)
func _draw():
if _line_edit.visible:
return
#var grabber_width := 3
var background_v_margin := 0
var foreground_margin := FG_MARGIN
#var grabber_color := Color(0.8, 0.8, 0.8)
var interval_color := Color(0.4,0.4,0.4)
var background_color := Color(0.1, 0.1, 0.1)
var control_rect := Rect2(Vector2(), rect_size)
var bg_rect := Rect2(
control_rect.position.x,
control_rect.position.y + background_v_margin,
control_rect.size.x,
control_rect.size.y - 2 * background_v_margin)
draw_rect(bg_rect, background_color)
var fg_rect := control_rect.grow(-foreground_margin)
# Clamping the ratio because the value can be allowed to exceed the slider's boundaries
var ratio := clamp(get_ratio(), 0.0, 1.0)
fg_rect.size.x *= ratio
draw_rect(fg_rect, interval_color)
var value_text := str(get_value())
var dot_pos := value_text.find(".")
if dot_pos != -1:
var decimal_count = len(value_text) - dot_pos
if decimal_count > MAX_DECIMALS_VISUAL:
value_text = value_text.substr(0, dot_pos + MAX_DECIMALS_VISUAL + 1)
if _centered:
var text := value_text
if _prefix != "":
text = str(_prefix, " ", text)
if _suffix != "":
text = str(text, " ", _suffix)
_label.text = text
else:
_label2.text = _prefix
var text = value_text
if _suffix != "":
text = str(text, " ", _suffix)
_label.text = text

View file

@ -0,0 +1,13 @@
[gd_scene load_steps=2 format=2]
[ext_resource path="res://addons/zylann.hterrain/tools/util/spin_slider.gd" type="Script" id=1]
[node name="SpinSlider" type="Control"]
anchor_right = 1.0
anchor_bottom = 1.0
rect_min_size = Vector2( 32, 28 )
mouse_default_cursor_shape = 10
script = ExtResource( 1 )
__meta__ = {
"_edit_use_anchors_": false
}

View file

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