aboutsummaryrefslogtreecommitdiff
path: root/game/addons/zylann.hterrain/tools/brush/terrain_painter.gd
diff options
context:
space:
mode:
Diffstat (limited to 'game/addons/zylann.hterrain/tools/brush/terrain_painter.gd')
-rw-r--r--game/addons/zylann.hterrain/tools/brush/terrain_painter.gd573
1 files changed, 573 insertions, 0 deletions
diff --git a/game/addons/zylann.hterrain/tools/brush/terrain_painter.gd b/game/addons/zylann.hterrain/tools/brush/terrain_painter.gd
new file mode 100644
index 0000000..81891b6
--- /dev/null
+++ b/game/addons/zylann.hterrain/tools/brush/terrain_painter.gd
@@ -0,0 +1,573 @@
+@tool
+extends Node
+
+const HT_Painter = preload("./painter.gd")
+const HTerrain = preload("../../hterrain.gd")
+const HTerrainData = preload("../../hterrain_data.gd")
+const HT_Logger = preload("../../util/logger.gd")
+const HT_Brush = preload("./brush.gd")
+
+const HT_RaiseShader = preload("./shaders/raise.gdshader")
+const HT_SmoothShader = preload("./shaders/smooth.gdshader")
+const HT_LevelShader = preload("./shaders/level.gdshader")
+const HT_FlattenShader = preload("./shaders/flatten.gdshader")
+const HT_ErodeShader = preload("./shaders/erode.gdshader")
+const HT_Splat4Shader = preload("./shaders/splat4.gdshader")
+const HT_Splat16Shader = preload("./shaders/splat16.gdshader")
+const HT_SplatIndexedShader = preload("./shaders/splat_indexed.gdshader")
+const HT_ColorShader = preload("./shaders/color.gdshader")
+const HT_AlphaShader = preload("./shaders/alpha.gdshader")
+
+const MODE_RAISE = 0
+const MODE_LOWER = 1
+const MODE_SMOOTH = 2
+const MODE_FLATTEN = 3
+const MODE_SPLAT = 4
+const MODE_COLOR = 5
+const MODE_MASK = 6
+const MODE_DETAIL = 7
+const MODE_LEVEL = 8
+const MODE_ERODE = 9
+const MODE_COUNT = 10
+
+class HT_ModifiedMap:
+ var map_type := 0
+ var map_index := 0
+ var painter_index := 0
+
+signal flatten_height_changed
+
+var _painters : Array[HT_Painter] = []
+
+var _brush := HT_Brush.new()
+
+var _color := Color(1, 0, 0, 1)
+var _mask_flag := false
+var _mode := MODE_RAISE
+var _flatten_height := 0.0
+var _detail_index := 0
+var _detail_density := 1.0
+var _texture_index := 0
+var _slope_limit_low_angle := 0.0
+var _slope_limit_high_angle := PI / 2.0
+
+var _modified_maps := []
+var _terrain : HTerrain
+var _logger = HT_Logger.get_for(self)
+
+
+func _init():
+ for i in 4:
+ var p := HT_Painter.new()
+ # The name is just for debugging
+ p.set_name(str("Painter", i))
+ #p.set_brush_size(_brush_size)
+ p.texture_region_changed.connect(_on_painter_texture_region_changed.bind(i))
+ add_child(p)
+ _painters.append(p)
+
+
+func get_brush() -> HT_Brush:
+ return _brush
+
+
+func get_brush_size() -> int:
+ return _brush.get_size()
+
+
+func set_brush_size(s: int):
+ _brush.set_size(s)
+# for p in _painters:
+# p.set_brush_size(_brush_size)
+
+
+func set_brush_texture(texture: Texture2D):
+ _brush.set_shapes([texture])
+# for p in _painters:
+# p.set_brush_texture(texture)
+
+
+func get_opacity() -> float:
+ return _brush.get_opacity()
+
+
+func set_opacity(opacity: float):
+ _brush.set_opacity(opacity)
+
+
+func set_flatten_height(h: float):
+ if h == _flatten_height:
+ return
+ _flatten_height = h
+ flatten_height_changed.emit()
+
+
+func get_flatten_height() -> float:
+ return _flatten_height
+
+
+func set_color(c: Color):
+ _color = c
+
+
+func get_color() -> Color:
+ return _color
+
+
+func set_mask_flag(m: bool):
+ _mask_flag = m
+
+
+func get_mask_flag() -> bool:
+ return _mask_flag
+
+
+func set_detail_density(d: float):
+ _detail_density = clampf(d, 0.0, 1.0)
+
+
+func get_detail_density() -> float:
+ return _detail_density
+
+
+func set_detail_index(di: int):
+ _detail_index = di
+
+
+func set_texture_index(i: int):
+ _texture_index = i
+
+
+func get_texture_index() -> int:
+ return _texture_index
+
+
+func get_slope_limit_low_angle() -> float:
+ return _slope_limit_low_angle
+
+
+func get_slope_limit_high_angle() -> float:
+ return _slope_limit_high_angle
+
+
+func set_slope_limit_angles(low: float, high: float):
+ _slope_limit_low_angle = low
+ _slope_limit_high_angle = high
+
+
+func is_operation_pending() -> bool:
+ for p in _painters:
+ if p.is_operation_pending():
+ return true
+ return false
+
+
+func has_modified_chunks() -> bool:
+ for p in _painters:
+ if p.has_modified_chunks():
+ return true
+ return false
+
+
+func get_undo_chunk_size() -> int:
+ return HT_Painter.UNDO_CHUNK_SIZE
+
+
+func commit() -> Dictionary:
+ assert(_terrain.get_data() != null)
+ var terrain_data = _terrain.get_data()
+ assert(not terrain_data.is_locked())
+
+ var changes := []
+ var chunk_positions : Array
+
+ assert(len(_modified_maps) > 0)
+
+ for mm in _modified_maps:
+ #print("Flushing painter ", mm.painter_index)
+ var painter : HT_Painter = _painters[mm.painter_index]
+ var info := painter.commit()
+
+ # Note, positions are always the same for each map
+ chunk_positions = info.chunk_positions
+
+ changes.append({
+ "map_type": mm.map_type,
+ "map_index": mm.map_index,
+ "chunk_initial_datas": info.chunk_initial_datas,
+ "chunk_final_datas": info.chunk_final_datas
+ })
+
+ var cs := get_undo_chunk_size()
+ for pos in info.chunk_positions:
+ var rect = Rect2(pos * cs, Vector2(cs, cs))
+ # This will update vertical bounds and notify normal map baker,
+ # 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 {
+ "chunk_positions": chunk_positions,
+ "maps": changes
+ }
+
+
+func set_mode(mode: int):
+ assert(mode >= 0 and mode < MODE_COUNT)
+ _mode = mode
+
+
+func get_mode() -> int:
+ return _mode
+
+
+func set_terrain(terrain: HTerrain):
+ if terrain == _terrain:
+ return
+ _terrain = terrain
+ # It's important to release resources here,
+ # otherwise Godot keeps modified terrain maps in memory and "reloads" them like that
+ # next time we reopen the scene, even if we didn't save it
+ for p in _painters:
+ p.set_image(null, null)
+ p.clear_brush_shader_params()
+
+
+# This may be called from an `_input` callback.
+# Returns `true` if any change was performed.
+func paint_input(position: Vector2, pressure: float) -> bool:
+ assert(_terrain.get_data() != null)
+ var data := _terrain.get_data()
+ assert(not data.is_locked())
+
+ if not _brush.configure_paint_input(_painters, position, pressure):
+ # Sometimes painting may not happen due to frequency options
+ return false
+
+ _modified_maps.clear()
+
+ match _mode:
+ MODE_RAISE:
+ _paint_height(data, position, 1.0)
+
+ MODE_LOWER:
+ _paint_height(data, position, -1.0)
+
+ MODE_SMOOTH:
+ _paint_smooth(data, position)
+
+ MODE_FLATTEN:
+ _paint_flatten(data, position)
+
+ MODE_LEVEL:
+ _paint_level(data, position)
+
+ MODE_ERODE:
+ _paint_erode(data, position)
+
+ MODE_SPLAT:
+ # TODO Properly support what happens when painting outside of supported index
+ # var supported_slots_count := terrain.get_cached_ground_texture_slot_count()
+ # if _texture_index >= supported_slots_count:
+ # _logger.debug("Painting out of range of supported texture slots: {0}/{1}" \
+ # .format([_texture_index, supported_slots_count]))
+ # return
+ if _terrain.is_using_indexed_splatmap():
+ _paint_splat_indexed(data, position)
+ else:
+ var splatmap_count := _terrain.get_used_splatmaps_count()
+ match splatmap_count:
+ 1:
+ _paint_splat4(data, position)
+ 4:
+ _paint_splat16(data, position)
+
+ MODE_COLOR:
+ _paint_color(data, position)
+
+ MODE_MASK:
+ _paint_mask(data, position)
+
+ 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):
+ var data := _terrain.get_data()
+ if data == null:
+ return
+ for mm in _modified_maps:
+ if mm.painter_index == painter_index:
+ # This will tell auto-baked maps to update (like normals).
+ data.notify_region_change(rect, mm.map_type, mm.map_index, false, false)
+ break
+
+
+func _paint_height(data: HTerrainData, position: Vector2, factor: float):
+ var image := data.get_image(HTerrainData.CHANNEL_HEIGHT)
+ var texture := data.get_texture(HTerrainData.CHANNEL_HEIGHT, 0, true)
+
+ var mm := HT_ModifiedMap.new()
+ mm.map_type = HTerrainData.CHANNEL_HEIGHT
+ mm.map_index = 0
+ mm.painter_index = 0
+ _modified_maps = [mm]
+
+ # When using sculpting tools, make it dependent on brush size
+ var raise_strength := 10.0 + float(_brush.get_size())
+ var delta := factor * (2.0 / 60.0) * raise_strength
+
+ var p : HT_Painter = _painters[0]
+
+ p.set_brush_shader(HT_RaiseShader)
+ p.set_brush_shader_param("u_factor", delta)
+ p.set_image(image, texture)
+ p.paint_input(position)
+
+
+func _paint_smooth(data: HTerrainData, position: Vector2):
+ var image := data.get_image(HTerrainData.CHANNEL_HEIGHT)
+ var texture := data.get_texture(HTerrainData.CHANNEL_HEIGHT, 0, true)
+
+ var mm := HT_ModifiedMap.new()
+ mm.map_type = HTerrainData.CHANNEL_HEIGHT
+ mm.map_index = 0
+ mm.painter_index = 0
+ _modified_maps = [mm]
+
+ var p : HT_Painter = _painters[0]
+
+ p.set_brush_shader(HT_SmoothShader)
+ p.set_brush_shader_param("u_factor", 1.0)
+ p.set_image(image, texture)
+ p.paint_input(position)
+
+
+func _paint_flatten(data: HTerrainData, position: Vector2):
+ var image := data.get_image(HTerrainData.CHANNEL_HEIGHT)
+ var texture := data.get_texture(HTerrainData.CHANNEL_HEIGHT, 0, true)
+
+ var mm := HT_ModifiedMap.new()
+ mm.map_type = HTerrainData.CHANNEL_HEIGHT
+ mm.map_index = 0
+ mm.painter_index = 0
+ _modified_maps = [mm]
+
+ var p : HT_Painter = _painters[0]
+
+ p.set_brush_shader(HT_FlattenShader)
+ p.set_brush_shader_param("u_flatten_value", _flatten_height)
+ p.set_image(image, texture)
+ p.paint_input(position)
+
+
+func _paint_level(data: HTerrainData, position: Vector2):
+ var image := data.get_image(HTerrainData.CHANNEL_HEIGHT)
+ var texture := data.get_texture(HTerrainData.CHANNEL_HEIGHT, 0, true)
+
+ var mm := HT_ModifiedMap.new()
+ mm.map_type = HTerrainData.CHANNEL_HEIGHT
+ mm.map_index = 0
+ mm.painter_index = 0
+ _modified_maps = [mm]
+
+ var p : HT_Painter = _painters[0]
+
+ p.set_brush_shader(HT_LevelShader)
+ p.set_brush_shader_param("u_factor", (10.0 / 60.0))
+ p.set_image(image, texture)
+ p.paint_input(position)
+
+
+func _paint_erode(data: HTerrainData, position: Vector2):
+ var image := data.get_image(HTerrainData.CHANNEL_HEIGHT)
+ var texture := data.get_texture(HTerrainData.CHANNEL_HEIGHT, 0, true)
+
+ var mm := HT_ModifiedMap.new()
+ mm.map_type = HTerrainData.CHANNEL_HEIGHT
+ mm.map_index = 0
+ mm.painter_index = 0
+ _modified_maps = [mm]
+
+ var p : HT_Painter = _painters[0]
+
+ p.set_brush_shader(HT_ErodeShader)
+ p.set_image(image, texture)
+ p.paint_input(position)
+
+
+func _paint_splat4(data: HTerrainData, position: Vector2):
+ var image := data.get_image(HTerrainData.CHANNEL_SPLAT)
+ var texture := data.get_texture(HTerrainData.CHANNEL_SPLAT, 0, true)
+ var heightmap_texture := data.get_texture(HTerrainData.CHANNEL_HEIGHT, 0)
+
+ var mm := HT_ModifiedMap.new()
+ mm.map_type = HTerrainData.CHANNEL_SPLAT
+ mm.map_index = 0
+ mm.painter_index = 0
+ _modified_maps = [mm]
+
+ var p : HT_Painter = _painters[0]
+ var splat := Color(0.0, 0.0, 0.0, 0.0)
+ splat[_texture_index] = 1.0;
+ p.set_brush_shader(HT_Splat4Shader)
+ p.set_brush_shader_param("u_splat", splat)
+ _set_slope_limit_shader_params(p, heightmap_texture)
+ p.set_image(image, texture)
+ p.paint_input(position)
+
+
+func _paint_splat_indexed(data: HTerrainData, position: Vector2):
+ var map_types := [
+ HTerrainData.CHANNEL_SPLAT_INDEX,
+ HTerrainData.CHANNEL_SPLAT_WEIGHT
+ ]
+ _modified_maps = []
+
+ var textures := []
+ for mode in 2:
+ textures.append(data.get_texture(map_types[mode], 0, true))
+
+ for mode in 2:
+ var image := data.get_image(map_types[mode])
+
+ var mm := HT_ModifiedMap.new()
+ mm.map_type = map_types[mode]
+ mm.map_index = 0
+ mm.painter_index = mode
+ _modified_maps.append(mm)
+
+ var p : HT_Painter = _painters[mode]
+
+ p.set_brush_shader(HT_SplatIndexedShader)
+ p.set_brush_shader_param("u_mode", mode)
+ 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)
+ p.set_image(image, textures[mode])
+ p.paint_input(position)
+
+
+func _paint_splat16(data: HTerrainData, position: Vector2):
+ # Make sure required maps are present
+ while data.get_map_count(HTerrainData.CHANNEL_SPLAT) < 4:
+ data._edit_add_map(HTerrainData.CHANNEL_SPLAT)
+
+ var splats := []
+ for i in 4:
+ splats.append(Color(0.0, 0.0, 0.0, 0.0))
+ splats[_texture_index / 4][_texture_index % 4] = 1.0
+
+ var textures := []
+ for i in 4:
+ textures.append(data.get_texture(HTerrainData.CHANNEL_SPLAT, i, true))
+
+ var heightmap_texture := data.get_texture(HTerrainData.CHANNEL_HEIGHT, 0)
+
+ for i in 4:
+ var image : Image = data.get_image(HTerrainData.CHANNEL_SPLAT, i)
+ var texture : Texture = textures[i]
+
+ var mm := HT_ModifiedMap.new()
+ mm.map_type = HTerrainData.CHANNEL_SPLAT
+ mm.map_index = i
+ mm.painter_index = i
+ _modified_maps.append(mm)
+
+ var p : HT_Painter = _painters[i]
+
+ var other_splatmaps := []
+ for tex in textures:
+ if tex != texture:
+ other_splatmaps.append(tex)
+
+ p.set_brush_shader(HT_Splat16Shader)
+ 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])
+ p.set_brush_shader_param("u_other_splatmap_3", other_splatmaps[2])
+ _set_slope_limit_shader_params(p, heightmap_texture)
+ p.set_image(image, texture)
+ p.paint_input(position)
+
+
+func _paint_color(data: HTerrainData, position: Vector2):
+ var image := data.get_image(HTerrainData.CHANNEL_COLOR)
+ var texture := data.get_texture(HTerrainData.CHANNEL_COLOR, 0, true)
+
+ var mm := HT_ModifiedMap.new()
+ mm.map_type = HTerrainData.CHANNEL_COLOR
+ mm.map_index = 0
+ mm.painter_index = 0
+ _modified_maps = [mm]
+
+ var p : HT_Painter = _painters[0]
+
+ # There was a problem with painting colors because of sRGB
+ # https://github.com/Zylann/godot_heightmap_plugin/issues/17#issuecomment-734001879
+
+ p.set_brush_shader(HT_ColorShader)
+ p.set_brush_shader_param("u_color", _color)
+ p.set_brush_shader_param("u_normal_min_y", 0.0)
+ p.set_brush_shader_param("u_normal_max_y", 1.0)
+ p.set_image(image, texture)
+ p.paint_input(position)
+
+
+func _paint_mask(data: HTerrainData, position: Vector2):
+ var image := data.get_image(HTerrainData.CHANNEL_COLOR)
+ var texture := data.get_texture(HTerrainData.CHANNEL_COLOR, 0, true)
+
+ var mm := HT_ModifiedMap.new()
+ mm.map_type = HTerrainData.CHANNEL_COLOR
+ mm.map_index = 0
+ mm.painter_index = 0
+ _modified_maps = [mm]
+
+ var p : HT_Painter = _painters[0]
+
+ p.set_brush_shader(HT_AlphaShader)
+ p.set_brush_shader_param("u_value", 1.0 if _mask_flag else 0.0)
+ p.set_image(image, texture)
+ p.paint_input(position)
+
+
+func _paint_detail(data: HTerrainData, position: Vector2):
+ var image := data.get_image(HTerrainData.CHANNEL_DETAIL, _detail_index)
+ var texture := data.get_texture(HTerrainData.CHANNEL_DETAIL, _detail_index, true)
+ var heightmap_texture = data.get_texture(HTerrainData.CHANNEL_HEIGHT, 0)
+
+ var mm := HT_ModifiedMap.new()
+ mm.map_type = HTerrainData.CHANNEL_DETAIL
+ mm.map_index = _detail_index
+ mm.painter_index = 0
+ _modified_maps = [mm]
+
+ var p : HT_Painter = _painters[0]
+ var c := Color(_detail_density, _detail_density, _detail_density, 1.0)
+
+ # TODO Don't use this shader (why?)
+ p.set_brush_shader(HT_ColorShader)
+ p.set_brush_shader_param("u_color", c)
+ _set_slope_limit_shader_params(p, heightmap_texture)
+ p.set_image(image, texture)
+ p.paint_input(position)
+
+
+func _set_slope_limit_shader_params(p: HT_Painter, heightmap_texture: Texture):
+ p.set_brush_shader_param("u_normal_min_y", cos(_slope_limit_high_angle))
+ p.set_brush_shader_param("u_normal_max_y", cos(_slope_limit_low_angle) + 0.001)
+ p.set_brush_shader_param("u_heightmap", heightmap_texture)