first commit

This commit is contained in:
Ugric
2026-03-02 02:17:04 +00:00
commit 5d56860f3a
813 changed files with 41799 additions and 0 deletions

View File

@@ -0,0 +1,11 @@
HeightMap terrain for Godot Engine
------------------------------------
Copyright (c) 2016-2023 Marc Gilleron
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 313 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 722 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 405 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 724 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 426 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 816 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,11 @@
site_name: HTerrain plugin documentation
theme: readthedocs
markdown_extensions:
# Makes permalinks appear on headings
- toc:
permalink: True
# Makes boxes for notes and warnings
- admonition
# Better highlighter which supports GDScript
- codehilite

View File

@@ -0,0 +1 @@
mkdocs>=1.1.2

1665
addons/zylann.hterrain/hterrain.gd Executable file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1 @@
uid://dp5xpux4ixqlp

View File

@@ -0,0 +1,125 @@
@tool
var cell_origin_x := 0
var cell_origin_y := 0
var _visible : bool
# This is true when the chunk is meant to be displayed.
# A chunk can be active and hidden (due to the terrain being hidden).
var _active : bool
var _pending_update : bool
var _mesh_instance : RID
# Need to keep a reference so that the mesh RID doesn't get freed
# TODO Use RID directly, no need to keep all those meshes in memory
var _mesh : Mesh = null
# TODO p_parent is HTerrain, can't add type hint due to cyclic reference
func _init(p_parent: Node3D, p_cell_x: int, p_cell_y: int, p_material: Material):
assert(p_parent is Node3D)
assert(typeof(p_cell_x) == TYPE_INT)
assert(typeof(p_cell_y) == TYPE_INT)
assert(p_material is Material)
cell_origin_x = p_cell_x
cell_origin_y = p_cell_y
var rs := RenderingServer
_mesh_instance = rs.instance_create()
if p_material != null:
rs.instance_geometry_set_material_override(_mesh_instance, p_material.get_rid())
var world := p_parent.get_world_3d()
if world != null:
rs.instance_set_scenario(_mesh_instance, world.get_scenario())
_visible = true
# TODO Is this needed?
rs.instance_set_visible(_mesh_instance, _visible)
_active = true
_pending_update = false
func _notification(p_what: int):
if p_what == NOTIFICATION_PREDELETE:
if _mesh_instance != RID():
RenderingServer.free_rid(_mesh_instance)
_mesh_instance = RID()
func is_active() -> bool:
return _active
func set_active(a: bool):
_active = a
func is_pending_update() -> bool:
return _pending_update
func set_pending_update(p: bool):
_pending_update = p
func enter_world(world: World3D):
assert(_mesh_instance != RID())
RenderingServer.instance_set_scenario(_mesh_instance, world.get_scenario())
func exit_world():
assert(_mesh_instance != RID())
RenderingServer.instance_set_scenario(_mesh_instance, RID())
func parent_transform_changed(parent_transform: Transform3D):
assert(_mesh_instance != RID())
var local_transform := Transform3D(Basis(), Vector3(cell_origin_x, 0, cell_origin_y))
var world_transform := parent_transform * local_transform
RenderingServer.instance_set_transform(_mesh_instance, world_transform)
func set_mesh(mesh: Mesh):
assert(_mesh_instance != RID())
if mesh == _mesh:
return
RenderingServer.instance_set_base(_mesh_instance, mesh.get_rid() if mesh != null else RID())
_mesh = mesh
func set_material(material: Material):
assert(_mesh_instance != RID())
RenderingServer.instance_geometry_set_material_override( \
_mesh_instance, material.get_rid() if material != null else RID())
func set_visible(visible: bool):
assert(_mesh_instance != RID())
RenderingServer.instance_set_visible(_mesh_instance, visible)
_visible = visible
func is_visible() -> bool:
return _visible
func set_aabb(aabb: AABB):
assert(_mesh_instance != RID())
RenderingServer.instance_set_custom_aabb(_mesh_instance, aabb)
func set_render_layer_mask(mask: int):
assert(_mesh_instance != RID())
RenderingServer.instance_set_layer_mask(_mesh_instance, mask)
func set_cast_shadow_setting(setting: int):
assert(_mesh_instance != RID())
RenderingServer.instance_geometry_set_cast_shadows_setting(_mesh_instance, setting)

View File

@@ -0,0 +1 @@
uid://due1nxh1rsvxx

View File

@@ -0,0 +1,67 @@
@tool
extends "hterrain_chunk.gd"
# I wrote this because Godot has no debug option to show AABBs.
# https://github.com/godotengine/godot/issues/20722
const HT_DirectMeshInstance = preload("./util/direct_mesh_instance.gd")
const HT_Util = preload("./util/util.gd")
var _debug_cube : HT_DirectMeshInstance = null
var _aabb := AABB()
var _parent_transform := Transform3D()
func _init(p_parent: Node3D, p_cell_x: int, p_cell_y: int, p_material: Material):
super(p_parent, p_cell_x, p_cell_y, p_material)
var wirecube : Mesh
if not p_parent.has_meta("debug_wirecube_mesh"):
wirecube = HT_Util.create_wirecube_mesh()
var mat := StandardMaterial3D.new()
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
wirecube.surface_set_material(0, mat)
# Cache the debug cube in the parent node to avoid re-creating each time
p_parent.set_meta("debug_wirecube_mesh", wirecube)
else:
wirecube = p_parent.get_meta("debug_wirecube_mesh")
_debug_cube = HT_DirectMeshInstance.new()
_debug_cube.set_mesh(wirecube)
_debug_cube.set_world(p_parent.get_world_3d())
func enter_world(world: World3D):
super(world)
_debug_cube.enter_world(world)
func exit_world():
super()
_debug_cube.exit_world()
func parent_transform_changed(parent_transform: Transform3D):
super(parent_transform)
_parent_transform = parent_transform
_debug_cube.set_transform(_compute_aabb())
func set_visible(visible: bool):
super(visible)
_debug_cube.set_visible(visible)
func set_aabb(aabb: AABB):
super(aabb)
#aabb.position.y += 0.2*randf()
_aabb = aabb
_debug_cube.set_transform(_compute_aabb())
func _compute_aabb():
var pos = Vector3(cell_origin_x, 0, cell_origin_y)
return _parent_transform * Transform3D(Basis().scaled(_aabb.size), pos + _aabb.position)

View File

@@ -0,0 +1 @@
uid://cpfxvylotlb6v

View File

@@ -0,0 +1,118 @@
@tool
const HT_Logger = preload("./util/logger.gd")
const HTerrainData = preload("./hterrain_data.gd")
var _shape_rid := RID()
var _body_rid := RID()
var _terrain_transform := Transform3D()
var _terrain_data : HTerrainData = null
var _logger = HT_Logger.get_for(self)
func _init(attached_node: Node, initial_layer: int, initial_mask: int):
_logger.debug("HTerrainCollider: creating body")
assert(attached_node != null)
_shape_rid = PhysicsServer3D.heightmap_shape_create()
_body_rid = PhysicsServer3D.body_create()
PhysicsServer3D.body_set_mode(_body_rid, PhysicsServer3D.BODY_MODE_STATIC)
PhysicsServer3D.body_set_collision_layer(_body_rid, initial_layer)
PhysicsServer3D.body_set_collision_mask(_body_rid, initial_mask)
# TODO This is an attempt to workaround https://github.com/godotengine/godot/issues/24390
PhysicsServer3D.body_set_ray_pickable(_body_rid, false)
# Assigng dummy data
# TODO This is a workaround to https://github.com/godotengine/godot/issues/25304
PhysicsServer3D.shape_set_data(_shape_rid, {
"width": 2,
"depth": 2,
"heights": PackedFloat32Array([0, 0, 0, 0]),
"min_height": -1,
"max_height": 1
})
PhysicsServer3D.body_add_shape(_body_rid, _shape_rid)
# This makes collision hits report the provided object as `collider`
PhysicsServer3D.body_attach_object_instance_id(_body_rid, attached_node.get_instance_id())
func set_collision_layer(layer: int):
PhysicsServer3D.body_set_collision_layer(_body_rid, layer)
func set_collision_mask(mask: int):
PhysicsServer3D.body_set_collision_mask(_body_rid, mask)
func _notification(what: int):
if what == NOTIFICATION_PREDELETE:
_logger.debug("Destroy HTerrainCollider")
PhysicsServer3D.free_rid(_body_rid)
# The shape needs to be freed after the body, otherwise the engine crashes
PhysicsServer3D.free_rid(_shape_rid)
func set_transform(transform: Transform3D):
assert(_body_rid != RID())
_terrain_transform = transform
_update_transform()
func set_world(world: World3D):
assert(_body_rid != RID())
PhysicsServer3D.body_set_space(_body_rid, world.get_space() if world != null else RID())
func create_from_terrain_data(terrain_data: HTerrainData):
assert(terrain_data != null)
assert(not terrain_data.is_locked())
_logger.debug("HTerrainCollider: setting up heightmap")
_terrain_data = terrain_data
var aabb := terrain_data.get_aabb()
var width := terrain_data.get_resolution()
var depth := terrain_data.get_resolution()
var height := aabb.size.y
var shape_data = {
"width": terrain_data.get_resolution(),
"depth": terrain_data.get_resolution(),
"heights": terrain_data.get_all_heights(),
"min_height": aabb.position.y,
"max_height": aabb.end.y
}
PhysicsServer3D.shape_set_data(_shape_rid, shape_data)
_update_transform(aabb)
func _update_transform(aabb=null):
if _terrain_data == null:
_logger.debug("HTerrainCollider: terrain data not set yet")
return
# if aabb == null:
# aabb = _terrain_data.get_aabb()
var width := _terrain_data.get_resolution()
var depth := _terrain_data.get_resolution()
#var height = aabb.size.y
#_terrain_transform
var trans := Transform3D(Basis(), 0.5 * Vector3(width - 1, 0, depth - 1))
# And then apply the terrain transform
trans = _terrain_transform * trans
PhysicsServer3D.body_set_state(_body_rid, PhysicsServer3D.BODY_STATE_TRANSFORM, trans)
# Cannot use shape transform when scaling is involved,
# because Godot is undoing that scale for some reason.
# See https://github.com/Zylann/godot_heightmap_plugin/issues/70
#PhysicsServer.body_set_shape_transform(_body_rid, 0, trans)

View File

@@ -0,0 +1 @@
uid://c7hwcslu4m0r5

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1 @@
uid://64b7twoj0w31

View File

@@ -0,0 +1,742 @@
@tool
extends Node3D
# Child node of the terrain, used to render numerous small objects on the ground
# such as grass or rocks. They do so by using a texture covering the terrain
# (a "detail map"), which is found in the terrain data itself.
# A terrain can have multiple detail maps, and you can choose which one will be
# used with `layer_index`.
# Details use instanced rendering within their own chunk grid, scattered around
# the player. Importantly, the position and rotation of this node don't matter,
# and they also do NOT scale with map scale. Indeed, scaling the heightmap
# doesn't mean we want to scale grass blades (which is not a use case I know of).
const HTerrainData = preload("./hterrain_data.gd")
const HT_DirectMultiMeshInstance = preload("./util/direct_multimesh_instance.gd")
const HT_DirectMeshInstance = preload("./util/direct_mesh_instance.gd")
const HT_Util = preload("./util/util.gd")
const HT_Logger = preload("./util/logger.gd")
# TODO Can't preload because it causes the plugin to fail loading if assets aren't imported
const DEFAULT_MESH_PATH = "res://addons/zylann.hterrain/models/grass_quad.obj"
# Cannot use `const` because `HTerrain` depends on the current script
var HTerrain = load("res://addons/zylann.hterrain/hterrain.gd")
const CHUNK_SIZE = 32
const DEFAULT_SHADER_PATH = "res://addons/zylann.hterrain/shaders/detail.gdshader"
const DEBUG = false
# These parameters are considered built-in,
# they are managed internally so they are not directly exposed
const _API_SHADER_PARAMS = {
"u_terrain_heightmap": true,
"u_terrain_detailmap": true,
"u_terrain_normalmap": true,
"u_terrain_globalmap": true,
"u_terrain_inverse_transform": true,
"u_terrain_normal_basis": true,
"u_albedo_alpha": true,
"u_view_distance": true,
"u_ambient_wind": true
}
# TODO Should be renamed `map_index`
# Which detail map this layer will use
@export var layer_index := 0:
get:
return layer_index
set(v):
if layer_index == v:
return
layer_index = v
if is_inside_tree():
_update_material()
HT_Util.update_configuration_warning(self, false)
# Texture to render on the detail meshes.
@export var texture : Texture:
get:
return texture
set(tex):
texture = tex
_material.set_shader_parameter("u_albedo_alpha", tex)
# 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_range(1.0, 500.0) var view_distance := 100.0:
get:
return view_distance
set(v):
if view_distance == v:
return
view_distance = maxf(v, 1.0)
if is_inside_tree():
_update_material()
# Custom shader to replace the default one.
@export var custom_shader : Shader:
get:
return custom_shader
set(shader):
if custom_shader == shader:
return
custom_shader = shader
if custom_shader == null:
_material.shader = load(DEFAULT_SHADER_PATH)
else:
_material.shader = custom_shader
if Engine.is_editor_hint():
# Ability to fork default shader
if shader.code == "":
shader.code = _default_shader.code
# Density modifier, to make more or less detail meshes appear overall.
@export_range(0, 10) var density := 4.0:
get:
return density
set(v):
v = clampf(v, 0, 10)
if v == density:
return
density = v
_multimesh_need_regen = true
# Mesh used for every detail instance (for example, every grass patch).
# If not assigned, an internal quad mesh will be used.
# I would have called it `mesh` but that's too broad and conflicts with local vars ._.
@export var instance_mesh : Mesh:
get:
return instance_mesh
set(p_mesh):
if p_mesh == instance_mesh:
return
instance_mesh = p_mesh
_multimesh.mesh = _get_used_mesh()
# Exposes rendering layers, similar to `VisualInstance.layers`
# (IMO this annotation is not specific enough, something might be off...)
@export_flags_3d_render var render_layers := 1:
get:
return render_layers
set(mask):
render_layers = mask
for k in _chunks:
var chunk = _chunks[k]
chunk.set_layer_mask(mask)
# Exposes shadow casting setting.
# Possible values are the same as the enum `GeometryInstance.SHADOW_CASTING_SETTING_*`.
# TODO Casting to `int` should not be necessary! Had to do it otherwise GDScript complains...
@export_enum("Off", "On", "DoubleSided", "ShadowsOnly") \
var cast_shadow := int(GeometryInstance3D.SHADOW_CASTING_SETTING_ON):
get:
return cast_shadow
set(option):
if option == cast_shadow:
return
cast_shadow = option
for k in _chunks:
var mmi : HT_DirectMultiMeshInstance = _chunks[k]
mmi.set_cast_shadow(option)
var _material: ShaderMaterial = null
var _default_shader: Shader = null
# Vector2 => DirectMultiMeshInstance
var _chunks := {}
var _multimesh: MultiMesh
var _multimesh_need_regen = true
var _multimesh_instance_pool := []
var _ambient_wind_time := 0.0
#var _auto_pick_index_on_enter_tree := Engine.is_editor_hint()
var _debug_wirecube_mesh: Mesh = null
var _debug_cubes := []
var _logger := HT_Logger.get_for(self)
func _init():
_default_shader = load(DEFAULT_SHADER_PATH)
_material = ShaderMaterial.new()
_material.shader = _default_shader
_multimesh = MultiMesh.new()
_multimesh.transform_format = MultiMesh.TRANSFORM_3D
# TODO Godot 3 had the option to specify color format, but Godot 4 no longer does...
# I only need 8-bit, but Godot 4 uses 32-bit components colors...
#_multimesh.color_format = MultiMesh.COLOR_8BIT
_multimesh.use_colors = true
func _enter_tree():
var terrain = _get_terrain()
if terrain != null:
terrain.transform_changed.connect(_on_terrain_transform_changed)
#if _auto_pick_index_on_enter_tree:
# _auto_pick_index_on_enter_tree = false
# _auto_pick_index()
terrain._internal_add_detail_layer(self)
_update_material()
func _exit_tree():
var terrain = _get_terrain()
if terrain != null:
terrain.transform_changed.disconnect(_on_terrain_transform_changed)
terrain._internal_remove_detail_layer(self)
_update_material()
for k in _chunks.keys():
_recycle_chunk(k)
_chunks.clear()
#func _auto_pick_index():
# # Automatically pick an unused layer
#
# var terrain = _get_terrain()
# if terrain == null:
# return
#
# var terrain_data = terrain.get_data()
# if terrain_data == null or terrain_data.is_locked():
# return
#
# var auto_index := layer_index
# var others = terrain.get_detail_layers()
#
# if len(others) > 0:
# var used_layers := []
# for other in others:
# used_layers.append(other.layer_index)
# used_layers.sort()
#
# auto_index = used_layers[-1]
# for i in range(1, len(used_layers)):
# if used_layers[i - 1] - used_layers[i] > 1:
# # Found a hole, take it instead
# auto_index = used_layers[i] - 1
# break
#
# print("Auto picked ", auto_index, " ")
# layer_index = auto_index
func _get_property_list() -> Array:
# Dynamic properties coming from the shader
var props := []
if _material != null:
var shader_params = RenderingServer.get_shader_parameter_list(_material.shader.get_rid())
for p in shader_params:
if _API_SHADER_PARAMS.has(p.name):
continue
var cp := {}
for k in p:
cp[k] = p[k]
cp.name = str("shader_params/", p.name)
props.append(cp)
return props
func _get(key: StringName):
var key_str := String(key)
if key_str.begins_with("shader_params/"):
var param_name = key_str.substr(len("shader_params/"))
return get_shader_param(param_name)
func _set(key: StringName, v):
var key_str := String(key)
if key_str.begins_with("shader_params/"):
var param_name = key_str.substr(len("shader_params/"))
set_shader_param(param_name, v)
func get_shader_param(param_name: String):
return _material.get_shader_parameter(param_name)
func set_shader_param(param_name: String, v):
_material.set_shader_parameter(param_name, v)
func _get_terrain():
if is_inside_tree():
return get_parent()
return null
# Compat
func set_texture(tex: Texture):
texture = tex
# Compat
func get_texture() -> Texture:
return texture
# Compat
func set_layer_index(v: int):
layer_index = v
# Compat
func get_layer_index() -> int:
return layer_index
# Compat
func set_view_distance(v: float):
return view_distance
# Compat
func get_view_distance() -> float:
return view_distance
# Compat
func set_custom_shader(shader: Shader):
custom_shader = shader
# Compat
func get_custom_shader() -> Shader:
return custom_shader
# Compat
func set_instance_mesh(p_mesh: Mesh):
instance_mesh = p_mesh
# Compat
func get_instance_mesh() -> Mesh:
return instance_mesh
# Compat
func set_render_layer_mask(mask: int):
render_layers = mask
# Compat
func get_render_layer_mask() -> int:
return render_layers
func _get_used_mesh() -> Mesh:
if instance_mesh == null:
var mesh = load(DEFAULT_MESH_PATH) as Mesh
if mesh == null:
_logger.error(str("Failed to load default mesh: ", DEFAULT_MESH_PATH))
return mesh
return instance_mesh
# Compat
func set_density(v: float):
density = v
# Compat
func get_density() -> float:
return density
# Updates texture references and values that come from the terrain itself.
# This is typically used when maps are being swapped around in terrain data,
# so we can restore texture references that may break.
func update_material():
_update_material()
# Formerly update_ambient_wind, reset
func _notification(what: int):
match what:
NOTIFICATION_ENTER_WORLD:
_set_world(get_world_3d())
NOTIFICATION_EXIT_WORLD:
_set_world(null)
NOTIFICATION_VISIBILITY_CHANGED:
_set_visible(visible)
NOTIFICATION_PREDELETE:
# Force DirectMeshInstances to be destroyed before the material.
# Otherwise it causes RenderingServer errors...
_chunks.clear()
_multimesh_instance_pool.clear()
func _set_visible(v: bool):
for k in _chunks:
var chunk = _chunks[k]
chunk.set_visible(v)
func _set_world(w: World3D):
for k in _chunks:
var chunk = _chunks[k]
chunk.set_world(w)
func _on_terrain_transform_changed(gt: Transform3D):
_update_material()
var terrain = _get_terrain()
if terrain == null:
_logger.error("Detail layer is not child of a terrain!")
return
var terrain_transform : Transform3D = terrain.get_internal_transform()
# Update AABBs and transforms, because scale might have changed
for k in _chunks:
var mmi = _chunks[k]
var aabb = _get_chunk_aabb(terrain, Vector3(k.x * CHUNK_SIZE, 0, k.y * CHUNK_SIZE))
# Nullify XZ translation because that's done by transform already
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):
var terrain = _get_terrain()
if terrain == null:
_logger.error("DetailLayer processing while terrain is null!")
return
if _multimesh_need_regen:
_regen_multimesh()
_multimesh_need_regen = false
# Crash workaround for Godot 3.1
# See https://github.com/godotengine/godot/issues/32500
for k in _chunks:
var mmi = _chunks[k]
mmi.set_multimesh(_multimesh)
# Detail layers are unaffected by ground map_scale
var terrain_transform_without_map_scale : Transform3D = \
terrain.get_internal_transform_unscaled()
var local_viewer_pos := terrain_transform_without_map_scale.affine_inverse() * viewer_pos
var viewer_cx = local_viewer_pos.x / CHUNK_SIZE
var viewer_cz = local_viewer_pos.z / CHUNK_SIZE
var cr = int(view_distance) / CHUNK_SIZE + 1
var cmin_x = viewer_cx - cr
var cmin_z = viewer_cz - cr
var cmax_x = viewer_cx + cr
var cmax_z = viewer_cz + cr
var map_res = terrain.get_data().get_resolution()
var map_scale = terrain.map_scale
var terrain_size_x = map_res * map_scale.x
var terrain_size_z = map_res * map_scale.z
var terrain_chunks_x = terrain_size_x / CHUNK_SIZE
var terrain_chunks_z = terrain_size_z / CHUNK_SIZE
cmin_x = clampi(cmin_x, 0, terrain_chunks_x)
cmin_z = clampi(cmin_z, 0, terrain_chunks_z)
if DEBUG and visible:
_debug_cubes.clear()
for cz in range(cmin_z, cmax_z):
for cx in range(cmin_x, cmax_x):
_add_debug_cube(terrain, _get_chunk_aabb(terrain, Vector3(cx, 0, cz) * CHUNK_SIZE))
for cz in range(cmin_z, cmax_z):
for cx in range(cmin_x, cmax_x):
var cpos2d = Vector2(cx, cz)
if _chunks.has(cpos2d):
continue
var aabb = _get_chunk_aabb(terrain, Vector3(cx, 0, cz) * CHUNK_SIZE)
var d = (aabb.position + 0.5 * aabb.size).distance_to(local_viewer_pos)
if d < view_distance:
_load_chunk(terrain_transform_without_map_scale, cx, cz, aabb)
var to_recycle = []
for k in _chunks:
var chunk = _chunks[k]
var aabb = _get_chunk_aabb(terrain, Vector3(k.x, 0, k.y) * CHUNK_SIZE)
var d = (aabb.position + 0.5 * aabb.size).distance_to(local_viewer_pos)
if d > view_distance:
to_recycle.append(k)
for k in to_recycle:
_recycle_chunk(k)
# Update time manually, so we can accelerate the animation when strength is increased,
# without causing phase jumps (which would be the case if we just scaled TIME)
var ambient_wind_frequency = 1.0 + 3.0 * terrain.ambient_wind
_ambient_wind_time += delta * ambient_wind_frequency
var awp = _get_ambient_wind_params()
_material.set_shader_parameter("u_ambient_wind", awp)
# Gets local-space AABB of a detail chunk.
# This only apply map_scale in Y, because details are not affected by X and Z map scale.
func _get_chunk_aabb(terrain, lpos: Vector3):
var terrain_scale = terrain.map_scale
var terrain_data = terrain.get_data()
var origin_cells_x := int(lpos.x / terrain_scale.x)
var origin_cells_z := int(lpos.z / terrain_scale.z)
var size_cells_x := int(CHUNK_SIZE / terrain_scale.x)
var size_cells_z := int(CHUNK_SIZE / terrain_scale.z)
var aabb = terrain_data.get_region_aabb(
origin_cells_x, origin_cells_z, size_cells_x, size_cells_z)
aabb.position = Vector3(lpos.x, lpos.y + aabb.position.y * terrain_scale.y, lpos.z)
aabb.size = Vector3(CHUNK_SIZE, aabb.size.y * terrain_scale.y, CHUNK_SIZE)
return aabb
func _get_chunk_transform(terrain_transform: Transform3D, cx: int, cz: int) -> Transform3D:
var lpos := Vector3(cx, 0, cz) * CHUNK_SIZE
# `terrain_transform` should be the terrain's internal transform, without `map_scale`.
var trans := Transform3D(
terrain_transform.basis,
terrain_transform.origin + terrain_transform.basis * lpos)
return trans
func _load_chunk(terrain_transform_without_map_scale: Transform3D, cx: int, cz: int, aabb: AABB):
aabb.position.x = 0
aabb.position.z = 0
var mmi = null
if len(_multimesh_instance_pool) != 0:
mmi = _multimesh_instance_pool[-1]
_multimesh_instance_pool.pop_back()
else:
mmi = HT_DirectMultiMeshInstance.new()
mmi.set_world(get_world_3d())
mmi.set_multimesh(_multimesh)
var trans := _get_chunk_transform(terrain_transform_without_map_scale, cx, cz)
mmi.set_material_override(_material)
mmi.set_transform(trans)
mmi.set_aabb(aabb)
mmi.set_layer_mask(render_layers)
mmi.set_cast_shadow(cast_shadow)
mmi.set_visible(visible)
_chunks[Vector2(cx, cz)] = mmi
func _recycle_chunk(cpos2d: Vector2):
var mmi = _chunks[cpos2d]
_chunks.erase(cpos2d)
mmi.set_visible(false)
_multimesh_instance_pool.append(mmi)
func _get_ambient_wind_params() -> Vector2:
var aw = 0.0
var terrain = _get_terrain()
if terrain != null:
aw = terrain.ambient_wind
# amplitude, time
return Vector2(aw, _ambient_wind_time)
func _update_material():
# Sets API shader properties. Custom properties are assumed to be set already
_logger.debug("Updating detail layer material")
var terrain_data = null
var terrain = _get_terrain()
var it = Transform3D()
var normal_basis = Basis()
if terrain != null:
var gt = terrain.get_internal_transform()
it = gt.affine_inverse()
terrain_data = terrain.get_data()
# This is needed to properly transform normals if the terrain is scaled.
# However we don't want to pick up rotation because it's already factored in the instance
#normal_basis = gt.basis.inverse().transposed()
normal_basis = Basis().scaled(terrain.map_scale).inverse().transposed()
var mat = _material
mat.set_shader_parameter("u_terrain_inverse_transform", it)
mat.set_shader_parameter("u_terrain_normal_basis", normal_basis)
mat.set_shader_parameter("u_albedo_alpha", texture)
mat.set_shader_parameter("u_view_distance", view_distance)
mat.set_shader_parameter("u_ambient_wind", _get_ambient_wind_params())
var heightmap_texture = null
var normalmap_texture = null
var detailmap_texture = null
var globalmap_texture = null
if terrain_data != null:
if terrain_data.is_locked():
_logger.error("Terrain data locked, can't update detail layer now")
return
heightmap_texture = terrain_data.get_texture(HTerrainData.CHANNEL_HEIGHT)
normalmap_texture = terrain_data.get_texture(HTerrainData.CHANNEL_NORMAL)
if layer_index < terrain_data.get_map_count(HTerrainData.CHANNEL_DETAIL):
detailmap_texture = terrain_data.get_texture(HTerrainData.CHANNEL_DETAIL, layer_index)
if terrain_data.get_map_count(HTerrainData.CHANNEL_GLOBAL_ALBEDO) > 0:
globalmap_texture = terrain_data.get_texture(HTerrainData.CHANNEL_GLOBAL_ALBEDO)
else:
_logger.error("Terrain data is null, can't update detail layer completely")
mat.set_shader_parameter("u_terrain_heightmap", heightmap_texture)
mat.set_shader_parameter("u_terrain_detailmap", detailmap_texture)
mat.set_shader_parameter("u_terrain_normalmap", normalmap_texture)
mat.set_shader_parameter("u_terrain_globalmap", globalmap_texture)
func _add_debug_cube(terrain: Node3D, aabb: AABB):
var world : World3D = terrain.get_world_3d()
if _debug_wirecube_mesh == null:
_debug_wirecube_mesh = HT_Util.create_wirecube_mesh()
var mat := StandardMaterial3D.new()
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
_debug_wirecube_mesh.surface_set_material(0, mat)
var debug_cube := HT_DirectMeshInstance.new()
debug_cube.set_mesh(_debug_wirecube_mesh)
debug_cube.set_world(world)
#aabb.position.y += 0.2*randf()
debug_cube.set_transform(Transform3D(Basis().scaled(aabb.size), aabb.position))
_debug_cubes.append(debug_cube)
func _regen_multimesh():
# We modify the existing multimesh instead of replacing it.
# DirectMultiMeshInstance does not keep a strong reference to them,
# so replacing would break pooled instances.
_generate_multimesh(CHUNK_SIZE, density, _get_used_mesh(), _multimesh)
func is_layer_index_valid() -> bool:
var terrain = _get_terrain()
if terrain == null:
return false
var data = terrain.get_data()
if data == null:
return false
return layer_index >= 0 and layer_index < data.get_map_count(HTerrainData.CHANNEL_DETAIL)
func _get_configuration_warnings() -> PackedStringArray:
var warnings := PackedStringArray()
var terrain = _get_terrain()
if not (is_instance_of(terrain, HTerrain)):
warnings.append("This node must be child of an HTerrain node")
return warnings
var data = terrain.get_data()
if data == null:
warnings.append("The terrain has no data")
return warnings
if data.get_map_count(HTerrainData.CHANNEL_DETAIL) == 0:
warnings.append("The terrain does not have any detail map")
return warnings
if layer_index < 0 or layer_index >= data.get_map_count(HTerrainData.CHANNEL_DETAIL):
warnings.append("Layer index is out of bounds")
return warnings
var tex = data.get_texture(HTerrainData.CHANNEL_DETAIL, layer_index)
if tex == null:
warnings.append("The terrain does not have a map assigned in slot {0}" \
.format([layer_index]))
return warnings
# Compat
func set_cast_shadow(option: int):
cast_shadow = option
# Compat
func get_cast_shadow() -> int:
return cast_shadow
static func _generate_multimesh(resolution: int, density: float, mesh: Mesh, multimesh: MultiMesh):
assert(multimesh != null)
var position_randomness := 0.5
var scale_randomness := 0.0
#var color_randomness = 0.5
var cell_count := resolution * resolution
var idensity := int(density)
var random_instance_count := int(cell_count * (density - floorf(density)))
var total_instance_count := cell_count * idensity + random_instance_count
multimesh.instance_count = total_instance_count
multimesh.mesh = mesh
# First pass ensures uniform spread
var i := 0
for z in resolution:
for x in resolution:
for j in idensity:
var pos := Vector3(x, 0, z)
pos.x += randf_range(-position_randomness, position_randomness)
pos.z += randf_range(-position_randomness, position_randomness)
multimesh.set_instance_color(i, Color(1, 1, 1))
multimesh.set_instance_transform(i, \
Transform3D(_get_random_instance_basis(scale_randomness), pos))
i += 1
# Second pass adds the rest
for j in random_instance_count:
var pos = Vector3(randf_range(0, resolution), 0, randf_range(0, resolution))
multimesh.set_instance_color(i, Color(1, 1, 1))
multimesh.set_instance_transform(i, \
Transform3D(_get_random_instance_basis(scale_randomness), pos))
i += 1
static func _get_random_instance_basis(scale_randomness: float) -> Basis:
var sr := randf_range(0, scale_randomness)
var s := 1.0 + (sr * sr * sr * sr * sr) * 50.0
var basis := Basis()
basis = basis.scaled(Vector3(1, s, 1))
basis = basis.rotated(Vector3(0, 1, 0), randf_range(0, PI))
return basis

View File

@@ -0,0 +1 @@
uid://dwbt4sf3xfwna

View File

@@ -0,0 +1,358 @@
@tool
#const HT_Logger = preload("./util/logger.gd")
const HTerrainData = preload("./hterrain_data.gd")
const SEAM_LEFT = 1
const SEAM_RIGHT = 2
const SEAM_BOTTOM = 4
const SEAM_TOP = 8
const SEAM_CONFIG_COUNT = 16
# [seams_mask][lod]
var _mesh_cache := []
var _chunk_size_x := 16
var _chunk_size_y := 16
func configure(chunk_size_x: int, chunk_size_y: int, lod_count: int):
assert(typeof(chunk_size_x) == TYPE_INT)
assert(typeof(chunk_size_y) == TYPE_INT)
assert(typeof(lod_count) == TYPE_INT)
assert(chunk_size_x >= 2 or chunk_size_y >= 2)
_mesh_cache.resize(SEAM_CONFIG_COUNT)
if chunk_size_x == _chunk_size_x \
and chunk_size_y == _chunk_size_y and lod_count == len(_mesh_cache):
return
_chunk_size_x = chunk_size_x
_chunk_size_y = chunk_size_y
# TODO Will reduce the size of this cache, but need index buffer swap feature
for seams in SEAM_CONFIG_COUNT:
var slot := []
slot.resize(lod_count)
_mesh_cache[seams] = slot
for lod in lod_count:
slot[lod] = make_flat_chunk(_chunk_size_x, _chunk_size_y, 1 << lod, seams)
func get_chunk(lod: int, seams: int) -> Mesh:
return _mesh_cache[seams][lod] as Mesh
static func make_flat_chunk(quad_count_x: int, quad_count_y: int, stride: int, seams: int) -> Mesh:
var positions = PackedVector3Array()
positions.resize((quad_count_x + 1) * (quad_count_y + 1))
var i = 0
for y in quad_count_y + 1:
for x in quad_count_x + 1:
positions[i] = Vector3(x * stride, 0, y * stride)
i += 1
var indices := make_indices(quad_count_x, quad_count_y, seams)
var arrays := []
arrays.resize(Mesh.ARRAY_MAX);
arrays[Mesh.ARRAY_VERTEX] = positions
arrays[Mesh.ARRAY_INDEX] = indices
var mesh := ArrayMesh.new()
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
return mesh
# size: chunk size in quads (there are N+1 vertices)
# seams: Bitfield for which seams are present
static func make_indices(chunk_size_x: int, chunk_size_y: int, seams: int) -> PackedInt32Array:
var output_indices := PackedInt32Array()
if seams != 0:
# LOD seams can't be made properly on uneven chunk sizes
assert(chunk_size_x % 2 == 0 and chunk_size_y % 2 == 0)
var reg_origin_x := 0
var reg_origin_y := 0
var reg_size_x := chunk_size_x
var reg_size_y := chunk_size_y
var reg_hstride := 1
if seams & SEAM_LEFT:
reg_origin_x += 1;
reg_size_x -= 1;
reg_hstride += 1
if seams & SEAM_BOTTOM:
reg_origin_y += 1
reg_size_y -= 1
if seams & SEAM_RIGHT:
reg_size_x -= 1
reg_hstride += 1
if seams & SEAM_TOP:
reg_size_y -= 1
# Regular triangles
var ii := reg_origin_x + reg_origin_y * (chunk_size_x + 1)
for y in reg_size_y:
for x in reg_size_x:
var i00 := ii
var i10 := ii + 1
var i01 := ii + chunk_size_x + 1
var i11 := i01 + 1
# 01---11
# | /|
# | / |
# |/ |
# 00---10
# This flips the pattern to make the geometry orientation-free.
# Not sure if it helps in any way though
var flip = ((x + reg_origin_x) + (y + reg_origin_y) % 2) % 2 != 0
if flip:
output_indices.push_back( i00 )
output_indices.push_back( i10 )
output_indices.push_back( i01 )
output_indices.push_back( i10 )
output_indices.push_back( i11 )
output_indices.push_back( i01 )
else:
output_indices.push_back( i00 )
output_indices.push_back( i11 )
output_indices.push_back( i01 )
output_indices.push_back( i00 )
output_indices.push_back( i10 )
output_indices.push_back( i11 )
ii += 1
ii += reg_hstride
# Left seam
if seams & SEAM_LEFT:
# 4 . 5
# |\ .
# | \ .
# | \.
# (2)| 3
# | /.
# | / .
# |/ .
# 0 . 1
var i := 0
var n := chunk_size_y / 2
for j in n:
var i0 := i
var i1 := i + 1
var i3 := i + chunk_size_x + 2
var i4 := i + 2 * (chunk_size_x + 1)
var i5 := i4 + 1
output_indices.push_back( i0 )
output_indices.push_back( i3 )
output_indices.push_back( i4 )
if j != 0 or (seams & SEAM_BOTTOM) == 0:
output_indices.push_back( i0 )
output_indices.push_back( i1 )
output_indices.push_back( i3 )
if j != n - 1 or (seams & SEAM_TOP) == 0:
output_indices.push_back( i3 )
output_indices.push_back( i5 )
output_indices.push_back( i4 )
i = i4
if seams & SEAM_RIGHT:
# 4 . 5
# . /|
# . / |
# ./ |
# 2 |(3)
# .\ |
# . \ |
# . \|
# 0 . 1
var i := chunk_size_x - 1
var n := chunk_size_y / 2
for j in n:
var i0 := i
var i1 := i + 1
var i2 := i + chunk_size_x + 1
var i4 := i + 2 * (chunk_size_x + 1)
var i5 := i4 + 1
output_indices.push_back( i1 )
output_indices.push_back( i5 )
output_indices.push_back( i2 )
if j != 0 or (seams & SEAM_BOTTOM) == 0:
output_indices.push_back( i0 )
output_indices.push_back( i1 )
output_indices.push_back( i2 )
if j != n - 1 or (seams & SEAM_TOP) == 0:
output_indices.push_back( i2 )
output_indices.push_back( i5 )
output_indices.push_back( i4 )
i = i4;
if seams & SEAM_BOTTOM:
# 3 . 4 . 5
# . / \ .
# . / \ .
# ./ \.
# 0-------2
# (1)
var i := 0;
var n := chunk_size_x / 2;
for j in n:
var i0 := i
var i2 := i + 2
var i3 := i + chunk_size_x + 1
var i4 := i3 + 1
var i5 := i4 + 1
output_indices.push_back( i0 )
output_indices.push_back( i2 )
output_indices.push_back( i4 )
if j != 0 or (seams & SEAM_LEFT) == 0:
output_indices.push_back( i0 )
output_indices.push_back( i4 )
output_indices.push_back( i3 )
if j != n - 1 or (seams & SEAM_RIGHT) == 0:
output_indices.push_back( i2 )
output_indices.push_back( i5 )
output_indices.push_back( i4 )
i = i2
if seams & SEAM_TOP:
# (4)
# 3-------5
# .\ /.
# . \ / .
# . \ / .
# 0 . 1 . 2
var i := (chunk_size_y - 1) * (chunk_size_x + 1)
var n := chunk_size_x / 2
for j in n:
var i0 := i
var i1 := i + 1
var i2 := i + 2
var i3 := i + chunk_size_x + 1
var i5 := i3 + 2
output_indices.push_back( i3 )
output_indices.push_back( i1 )
output_indices.push_back( i5 )
if j != 0 or (seams & SEAM_LEFT) == 0:
output_indices.push_back( i0 )
output_indices.push_back( i1 )
output_indices.push_back( i3 )
if j != n - 1 or (seams & SEAM_RIGHT) == 0:
output_indices.push_back( i1 )
output_indices.push_back( i2 )
output_indices.push_back( i5 )
i = i2
return output_indices
static func get_mesh_size(width: int, height: int) -> Dictionary:
return {
"vertices": width * height,
"triangles": (width - 1) * (height - 1) * 2
}
# Makes a full mesh from a heightmap, without any LOD considerations.
# Using this mesh for rendering is very expensive on large terrains.
# Initially used as a workaround for Godot to use for navmesh generation.
static func make_heightmap_mesh(heightmap: Image, stride: int, scale: Vector3,
logger = null) -> Mesh:
var size_x := heightmap.get_width() / stride
var size_z := heightmap.get_height() / stride
assert(size_x >= 2)
assert(size_z >= 2)
var positions := PackedVector3Array()
positions.resize(size_x * size_z)
var i := 0
if heightmap.get_format() == Image.FORMAT_RH or heightmap.get_format() == Image.FORMAT_RF:
for mz in size_z:
for mx in size_x:
var x := mx * stride
var z := mz * stride
var y := heightmap.get_pixel(x, z).r
positions[i] = Vector3(x, y, z) * scale
i += 1
elif heightmap.get_format() == Image.FORMAT_RGB8:
for mz in size_z:
for mx in size_x:
var x := mx * stride
var z := mz * stride
var c := heightmap.get_pixel(x, z)
var y := HTerrainData.decode_height_from_rgb8_unorm(c)
positions[i] = Vector3(x, y, z) * scale
i += 1
else:
logger.error("Unknown heightmap format!")
return null
var indices := make_indices(size_x - 1, size_z - 1, 0)
var arrays := []
arrays.resize(Mesh.ARRAY_MAX);
arrays[Mesh.ARRAY_VERTEX] = positions
arrays[Mesh.ARRAY_INDEX] = indices
if logger != null:
logger.debug(str("Generated mesh has ", len(positions),
" vertices and ", len(indices) / 3, " triangles"))
var mesh := ArrayMesh.new()
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
return mesh

View File

@@ -0,0 +1 @@
uid://ddsorgybud6bc

View File

@@ -0,0 +1,35 @@
@tool
class_name HTerrainDataLoader
extends ResourceFormatLoader
const HTerrainData = preload("./hterrain_data.gd")
func _get_recognized_extensions() -> PackedStringArray:
return PackedStringArray([HTerrainData.META_EXTENSION])
func _get_resource_type(path: String) -> String:
var ext := path.get_extension().to_lower()
if ext == HTerrainData.META_EXTENSION:
return "Resource"
return ""
# TODO Handle UIDs?
# By default Godot will return INVALID_ID,
# which makes this resource only tracked by path, like scripts
#
# func _get_resource_uid(path: String) -> int:
# return ???
func _handles_type(typename: StringName) -> bool:
return typename == &"Resource"
func _load(path: String, original_path: String, use_sub_threads: bool, cache_mode: int):
var res = HTerrainData.new()
res.load_data(path.get_base_dir())
return res

View File

@@ -0,0 +1 @@
uid://b1yim3ag28vka

View File

@@ -0,0 +1,29 @@
@tool
class_name HTerrainDataSaver
extends ResourceFormatSaver
const HTerrainData = preload("./hterrain_data.gd")
func _get_recognized_extensions(res: Resource) -> PackedStringArray:
if res != null and res is HTerrainData:
return PackedStringArray([HTerrainData.META_EXTENSION])
return PackedStringArray()
func _recognize(res: Resource) -> bool:
return res is HTerrainData
func _save(resource: Resource, path: String, flags: int) -> Error:
if resource.save_data(path.get_base_dir()):
return OK
# This can occur if at least one map of the terrain fails to save.
# It doesnt necessarily mean the entire terrain failed to save.
return FAILED
# TODO Handle UIDs
# func _set_uid(path: String, uid: int) -> int:
# ???

View File

@@ -0,0 +1 @@
uid://bpkenpp1gvmhy

View File

@@ -0,0 +1,253 @@
@tool
extends Resource
const MODE_TEXTURES = 0
const MODE_TEXTURE_ARRAYS = 1
const MODE_COUNT = 2
const _mode_names = ["Textures", "TextureArrays"]
const SRC_TYPE_ALBEDO = 0
const SRC_TYPE_BUMP = 1
const SRC_TYPE_NORMAL = 2
const SRC_TYPE_ROUGHNESS = 3
const SRC_TYPE_COUNT = 4
const _src_texture_type_names = ["albedo", "bump", "normal", "roughness"]
# Ground texture types (used by the terrain system)
const TYPE_ALBEDO_BUMP = 0
const TYPE_NORMAL_ROUGHNESS = 1
const TYPE_COUNT = 2
const _texture_type_names = ["albedo_bump", "normal_roughness"]
const _type_to_src_types = [
[SRC_TYPE_ALBEDO, SRC_TYPE_BUMP],
[SRC_TYPE_NORMAL, SRC_TYPE_ROUGHNESS]
]
const _src_default_color_codes = [
"#ff000000",
"#ff888888",
"#ff8888ff",
"#ffffffff"
]
# TODO We may get rid of modes in the future, and only use TextureArrays.
# It exists for now for backward compatibility, but it makes the API a bit confusing
var _mode := MODE_TEXTURES
# [type][slot] -> StreamTexture or TextureArray
var _textures := [[], []]
static func get_texture_type_name(tt: int) -> String:
return _texture_type_names[tt]
static func get_source_texture_type_name(tt: int) -> String:
return _src_texture_type_names[tt]
static func get_source_texture_default_color_code(tt: int) -> String:
return _src_default_color_codes[tt]
static func get_import_mode_name(mode: int) -> String:
return _mode_names[mode]
static func get_src_types_from_type(t: int) -> Array:
return _type_to_src_types[t]
static func get_max_slots_for_mode(mode: int) -> int:
match mode:
MODE_TEXTURES:
# This is a legacy mode, where shaders can only have up to 4
return 4
MODE_TEXTURE_ARRAYS:
# Will probably be lifted some day
return 16
return 0
func _get_property_list() -> Array:
return [
{
"name": "mode",
"type": TYPE_INT,
"usage": PROPERTY_USAGE_STORAGE
},
{
"name": "textures",
"type": TYPE_ARRAY,
"usage": PROPERTY_USAGE_STORAGE
}
]
func _get(key: StringName):
if key == &"mode":
return _mode
if key == &"textures":
return _textures
func _set(key: StringName, value):
if key == &"mode":
# Not using set_mode() here because otherwise it could reset stuff set before...
_mode = value
if key == &"textures":
_textures = value
func get_slots_count() -> int:
if _mode == MODE_TEXTURES:
return get_texture_count()
elif _mode == MODE_TEXTURE_ARRAYS:
# TODO What if there are two texture arrays of different size?
var texarray : TextureLayered = _textures[TYPE_ALBEDO_BUMP][0]
if texarray == null:
texarray = _textures[TYPE_NORMAL_ROUGHNESS][0]
if texarray == null:
return 0
return texarray.get_layers()
else:
assert(false)
return 0
func get_texture_count() -> int:
var texs = _textures[TYPE_ALBEDO_BUMP]
return len(texs)
func get_texture(slot_index: int, ground_texture_type: int) -> Texture2D:
if _mode == MODE_TEXTURE_ARRAYS:
# Can't get a single texture at once
return null
elif _mode == MODE_TEXTURES:
var texs = _textures[ground_texture_type]
if slot_index >= len(texs):
return null
return texs[slot_index]
else:
assert(false)
return null
func set_texture(slot_index: int, ground_texture_type: int, texture: Texture2D):
assert(_mode == MODE_TEXTURES)
var texs = _textures[ground_texture_type]
if texs[slot_index] != texture:
texs[slot_index] = texture
emit_changed()
func get_texture_array(ground_texture_type: int) -> TextureLayered:
if _mode != MODE_TEXTURE_ARRAYS:
return null
var texs = _textures[ground_texture_type]
return texs[0]
func set_texture_array(ground_texture_type: int, texarray: TextureLayered):
assert(_mode == MODE_TEXTURE_ARRAYS)
var texs = _textures[ground_texture_type]
if texs[0] != texarray:
texs[0] = texarray
emit_changed()
# TODO This function only exists because of a flaw in UndoRedo
# See https://github.com/godotengine/godot/issues/36895
func set_texture_null(slot_index: int, ground_texture_type: int):
set_texture(slot_index, ground_texture_type, null)
# TODO This function only exists because of a flaw in UndoRedo
# See https://github.com/godotengine/godot/issues/36895
func set_texture_array_null(ground_texture_type: int):
set_texture_array(ground_texture_type, null)
func get_mode() -> int:
return _mode
func set_mode(mode: int):
# This effectively clears slots
_mode = mode
clear()
func clear():
match _mode:
MODE_TEXTURES:
for type in TYPE_COUNT:
_textures[type] = []
MODE_TEXTURE_ARRAYS:
for type in TYPE_COUNT:
_textures[type] = [null]
_:
assert(false)
emit_changed()
func insert_slot(i: int) -> int:
assert(_mode == MODE_TEXTURES)
if i == -1:
i = get_texture_count()
for type in TYPE_COUNT:
_textures[type].insert(i, null)
emit_changed()
return i
func remove_slot(i: int):
assert(_mode == MODE_TEXTURES)
if i == -1:
i = get_slots_count() - 1
for type in TYPE_COUNT:
_textures[type].remove_at(i)
emit_changed()
func has_any_textures() -> bool:
for type in len(_textures):
var texs = _textures[type]
for i in len(texs):
if texs[i] != null:
return true
return false
#func set_textures(textures: Array):
# _textures = textures
# Cannot type hint because it would cause circular dependency
#func migrate_from_1_4(terrain):
# var textures := []
# for type in TYPE_COUNT:
# textures.append([])
#
# if terrain.is_using_texture_array():
# for type in TYPE_COUNT:
# var tex : TextureArray = terrain.get_ground_texture_array(type)
# textures[type] = [tex]
# _mode = MODE_TEXTURE_ARRAYS
#
# else:
# for index in terrain.get_max_ground_texture_slot_count():
# for type in TYPE_COUNT:
# var tex : Texture = terrain.get_ground_texture(type, index)
# textures[type].append(tex)
# _mode = MODE_TEXTURES
#
# _textures = textures

View File

@@ -0,0 +1 @@
uid://be6xnjpqubbdd

View File

@@ -0,0 +1,14 @@
# Blender v2.80 (sub 75) OBJ File: 'grass.blend'
# www.blender.org
o Cube
v 0.000000 1.000000 -0.500000
v 0.000000 0.000000 -0.500000
v 0.000000 1.000000 0.500000
v 0.000000 0.000000 0.500000
vt 0.999900 0.000100
vt 0.999900 0.999900
vt 0.000100 0.999900
vt 0.000100 0.000100
vn 1.0000 0.0000 0.0000
s off
f 2/1/1 1/2/1 3/3/1 4/4/1

View File

@@ -0,0 +1,25 @@
[remap]
importer="wavefront_obj"
importer_version=1
type="Mesh"
uid="uid://c1k6wgnjlvxpm"
path="res://.godot/imported/grass_quad.obj-da067750350fe98ec466261b2aeaf486.mesh"
[deps]
files=["res://.godot/imported/grass_quad.obj-da067750350fe98ec466261b2aeaf486.mesh"]
source_file="res://addons/zylann.hterrain/models/grass_quad.obj"
dest_files=["res://.godot/imported/grass_quad.obj-da067750350fe98ec466261b2aeaf486.mesh", "res://.godot/imported/grass_quad.obj-da067750350fe98ec466261b2aeaf486.mesh"]
[params]
generate_tangents=true
generate_lods=true
generate_shadow_mesh=true
generate_lightmap_uv2=false
generate_lightmap_uv2_texel_size=0.2
scale_mesh=Vector3(1, 1, 1)
offset_mesh=Vector3(0, 0, 0)
force_disable_mesh_compression=false

View File

@@ -0,0 +1,24 @@
# Blender v2.80 (sub 75) OBJ File: 'grass_x2.blend'
# www.blender.org
o Cube
v 0.000000 1.000000 -0.500000
v 0.000000 0.000000 -0.500000
v 0.000000 1.000000 0.500000
v 0.000000 0.000000 0.500000
v -0.500000 1.000000 0.000000
v -0.500000 0.000000 0.000000
v 0.500000 1.000000 0.000000
v 0.500000 0.000000 0.000000
vt 0.999900 0.000100
vt 0.999900 0.999900
vt 0.000100 0.999900
vt 0.000100 0.000100
vt 0.999900 0.000100
vt 0.999900 0.999900
vt 0.000100 0.999900
vt 0.000100 0.000100
vn 1.0000 0.0000 0.0000
vn 0.0000 0.0000 -1.0000
s off
f 2/1/1 1/2/1 3/3/1 4/4/1
f 6/5/2 5/6/2 7/7/2 8/8/2

View File

@@ -0,0 +1,25 @@
[remap]
importer="wavefront_obj"
importer_version=1
type="Mesh"
uid="uid://dpef2d0qcn5d4"
path="res://.godot/imported/grass_quad_x2.obj-2054c140f543f2a80e2eb921f865ea49.mesh"
[deps]
files=["res://.godot/imported/grass_quad_x2.obj-2054c140f543f2a80e2eb921f865ea49.mesh"]
source_file="res://addons/zylann.hterrain/models/grass_quad_x2.obj"
dest_files=["res://.godot/imported/grass_quad_x2.obj-2054c140f543f2a80e2eb921f865ea49.mesh", "res://.godot/imported/grass_quad_x2.obj-2054c140f543f2a80e2eb921f865ea49.mesh"]
[params]
generate_tangents=true
generate_lods=true
generate_shadow_mesh=true
generate_lightmap_uv2=false
generate_lightmap_uv2_texel_size=0.2
scale_mesh=Vector3(1, 1, 1)
offset_mesh=Vector3(0, 0, 0)
force_disable_mesh_compression=false

View File

@@ -0,0 +1,34 @@
# Blender v2.80 (sub 75) OBJ File: 'grass_x3.blend'
# www.blender.org
o Cube
v 0.000000 1.000000 -0.500000
v 0.000000 0.000000 -0.500000
v 0.000000 1.000000 0.500000
v 0.000000 0.000000 0.500000
v -0.433013 1.000000 -0.250000
v -0.433013 0.000000 -0.250000
v 0.433013 1.000000 0.250000
v 0.433013 0.000000 0.250000
v -0.433013 1.000000 0.250000
v -0.433013 0.000000 0.250000
v 0.433013 1.000000 -0.250000
v 0.433013 0.000000 -0.250000
vt 0.999900 0.000100
vt 0.999900 0.999900
vt 0.000100 0.999900
vt 0.000100 0.000100
vt 0.999900 0.000100
vt 0.999900 0.999900
vt 0.000100 0.999900
vt 0.000100 0.000100
vt 0.999900 0.000100
vt 0.999900 0.999900
vt 0.000100 0.999900
vt 0.000100 0.000100
vn 1.0000 0.0000 0.0000
vn 0.5000 0.0000 -0.8660
vn -0.5000 0.0000 -0.8660
s off
f 2/1/1 1/2/1 3/3/1 4/4/1
f 6/5/2 5/6/2 7/7/2 8/8/2
f 10/9/3 9/10/3 11/11/3 12/12/3

View File

@@ -0,0 +1,25 @@
[remap]
importer="wavefront_obj"
importer_version=1
type="Mesh"
uid="uid://bhjb8bijf1ql3"
path="res://.godot/imported/grass_quad_x3.obj-8691724bc5006b6f65d4e8742ffc84dc.mesh"
[deps]
files=["res://.godot/imported/grass_quad_x3.obj-8691724bc5006b6f65d4e8742ffc84dc.mesh"]
source_file="res://addons/zylann.hterrain/models/grass_quad_x3.obj"
dest_files=["res://.godot/imported/grass_quad_x3.obj-8691724bc5006b6f65d4e8742ffc84dc.mesh", "res://.godot/imported/grass_quad_x3.obj-8691724bc5006b6f65d4e8742ffc84dc.mesh"]
[params]
generate_tangents=true
generate_lods=true
generate_shadow_mesh=true
generate_lightmap_uv2=false
generate_lightmap_uv2_texel_size=0.2
scale_mesh=Vector3(1, 1, 1)
offset_mesh=Vector3(0, 0, 0)
force_disable_mesh_compression=false

View File

@@ -0,0 +1,42 @@
# Blender v2.80 (sub 75) OBJ File: 'grass_x4.blend'
# www.blender.org
o Cube
v 0.250000 1.000000 -0.500000
v 0.250000 0.000000 -0.500000
v 0.250000 1.000000 0.500000
v 0.250000 0.000000 0.500000
v 0.500000 0.000000 -0.250000
v 0.500000 1.000000 -0.250000
v -0.500000 0.000000 -0.250000
v -0.500000 1.000000 -0.250000
v -0.250000 0.000000 0.500000
v -0.250000 1.000000 0.500000
v -0.250000 0.000000 -0.500000
v -0.250000 1.000000 -0.500000
v 0.500000 0.000000 0.250000
v 0.500000 1.000000 0.250000
v -0.500000 0.000000 0.250000
v -0.500000 1.000000 0.250000
vt 0.999900 0.000100
vt 0.999900 0.999900
vt 0.000100 0.999900
vt 0.000100 0.000100
vt 0.999900 0.000100
vt 0.999900 0.999900
vt 0.000100 0.999900
vt 0.000100 0.000100
vt 0.999900 0.000100
vt 0.999900 0.999900
vt 0.000100 0.999900
vt 0.000100 0.000100
vt 0.999900 0.000100
vt 0.999900 0.999900
vt 0.000100 0.999900
vt 0.000100 0.000100
vn 1.0000 0.0000 0.0000
vn 0.0000 0.0000 -1.0000
s off
f 2/1/1 1/2/1 3/3/1 4/4/1
f 7/5/2 8/6/2 6/7/2 5/8/2
f 11/9/1 12/10/1 10/11/1 9/12/1
f 15/13/2 16/14/2 14/15/2 13/16/2

View File

@@ -0,0 +1,25 @@
[remap]
importer="wavefront_obj"
importer_version=1
type="Mesh"
uid="uid://cism8qe63t4tk"
path="res://.godot/imported/grass_quad_x4.obj-c449a7d6c810ba1595ed30df9fbf3d28.mesh"
[deps]
files=["res://.godot/imported/grass_quad_x4.obj-c449a7d6c810ba1595ed30df9fbf3d28.mesh"]
source_file="res://addons/zylann.hterrain/models/grass_quad_x4.obj"
dest_files=["res://.godot/imported/grass_quad_x4.obj-c449a7d6c810ba1595ed30df9fbf3d28.mesh", "res://.godot/imported/grass_quad_x4.obj-c449a7d6c810ba1595ed30df9fbf3d28.mesh"]
[params]
generate_tangents=true
generate_lods=true
generate_shadow_mesh=true
generate_lightmap_uv2=false
generate_lightmap_uv2_texel_size=0.2
scale_mesh=Vector3(1, 1, 1)
offset_mesh=Vector3(0, 0, 0)
force_disable_mesh_compression=false

View File

@@ -0,0 +1,127 @@
# Commented out parameters are those with the same value as base LLVM style
# We can uncomment them if we want to change their value, or enforce the
# chosen value in case the base style changes (last sync: Clang 6.0.1).
---
### General config, applies to all languages ###
BasedOnStyle: LLVM
AccessModifierOffset: -4
AlignAfterOpenBracket: DontAlign
# AlignConsecutiveAssignments: false
# AlignConsecutiveDeclarations: false
# AlignEscapedNewlines: Right
# AlignOperands: true
AlignTrailingComments: false
AllowAllParametersOfDeclarationOnNextLine: false
# AllowShortBlocksOnASingleLine: false
AllowShortCaseLabelsOnASingleLine: true
AllowShortFunctionsOnASingleLine: Inline
AllowShortIfStatementsOnASingleLine: true
# AllowShortLoopsOnASingleLine: false
# AlwaysBreakAfterDefinitionReturnType: None
# AlwaysBreakAfterReturnType: None
# AlwaysBreakBeforeMultilineStrings: false
# AlwaysBreakTemplateDeclarations: false
# BinPackArguments: true
# BinPackParameters: true
# BraceWrapping:
# AfterClass: false
# AfterControlStatement: false
# AfterEnum: false
# AfterFunction: false
# AfterNamespace: false
# AfterObjCDeclaration: false
# AfterStruct: false
# AfterUnion: false
# AfterExternBlock: false
# BeforeCatch: false
# BeforeElse: false
# IndentBraces: false
# SplitEmptyFunction: true
# SplitEmptyRecord: true
# SplitEmptyNamespace: true
# BreakBeforeBinaryOperators: None
# BreakBeforeBraces: Attach
# BreakBeforeInheritanceComma: false
BreakBeforeTernaryOperators: false
# BreakConstructorInitializersBeforeComma: false
BreakConstructorInitializers: AfterColon
# BreakStringLiterals: true
ColumnLimit: 0
# CommentPragmas: '^ IWYU pragma:'
# CompactNamespaces: false
ConstructorInitializerAllOnOneLineOrOnePerLine: true
ConstructorInitializerIndentWidth: 8
ContinuationIndentWidth: 8
Cpp11BracedListStyle: false
# DerivePointerAlignment: false
# DisableFormat: false
# ExperimentalAutoDetectBinPacking: false
# FixNamespaceComments: true
# ForEachMacros:
# - foreach
# - Q_FOREACH
# - BOOST_FOREACH
# IncludeBlocks: Preserve
IncludeCategories:
- Regex: '".*"'
Priority: 1
- Regex: '^<.*\.h>'
Priority: 2
- Regex: '^<.*'
Priority: 3
# IncludeIsMainRegex: '(Test)?$'
IndentCaseLabels: true
# IndentPPDirectives: None
IndentWidth: 4
# IndentWrappedFunctionNames: false
# JavaScriptQuotes: Leave
# JavaScriptWrapImports: true
# KeepEmptyLinesAtTheStartOfBlocks: true
# MacroBlockBegin: ''
# MacroBlockEnd: ''
# MaxEmptyLinesToKeep: 1
# NamespaceIndentation: None
# PenaltyBreakAssignment: 2
# PenaltyBreakBeforeFirstCallParameter: 19
# PenaltyBreakComment: 300
# PenaltyBreakFirstLessLess: 120
# PenaltyBreakString: 1000
# PenaltyExcessCharacter: 1000000
# PenaltyReturnTypeOnItsOwnLine: 60
# PointerAlignment: Right
# RawStringFormats:
# - Delimiter: pb
# Language: TextProto
# BasedOnStyle: google
# ReflowComments: true
# SortIncludes: true
# SortUsingDeclarations: true
# SpaceAfterCStyleCast: false
# SpaceAfterTemplateKeyword: true
# SpaceBeforeAssignmentOperators: true
# SpaceBeforeParens: ControlStatements
# SpaceInEmptyParentheses: false
# SpacesBeforeTrailingComments: 1
# SpacesInAngles: false
# SpacesInContainerLiterals: true
# SpacesInCStyleCastParentheses: false
# SpacesInParentheses: false
# SpacesInSquareBrackets: false
TabWidth: 4
UseTab: Always
---
### C++ specific config ###
Language: Cpp
Standard: Cpp03
---
### ObjC specific config ###
Language: ObjC
Standard: Cpp03
ObjCBlockIndentWidth: 4
# ObjCSpaceAfterProperty: false
# ObjCSpaceBeforeProtocolList: true
---
### Java specific config ###
Language: Java
# BreakAfterJavaFieldAnnotations: false
...

4
addons/zylann.hterrain/native/.gitignore vendored Executable file
View File

@@ -0,0 +1,4 @@
# Build
# Ignored locally because there are other folders in which we want to version OBJ files
*.obj

View File

@@ -0,0 +1,119 @@
#!python
import os
opts = Variables([], ARGUMENTS)
# Gets the standard flags CC, CCX, etc.
env = Environment(ENV = os.environ)
# Define our options
opts.Add(EnumVariable('target', "Compilation target", 'debug', ['debug', 'release']))
opts.Add(EnumVariable('platform', "Compilation platform", '', ['', 'windows', 'linux', 'osx']))
opts.Add(BoolVariable('use_llvm', "Use the LLVM / Clang compiler", 'no'))
opts.Add(EnumVariable('macos_arch', "Target macOS architecture", 'universal', ['universal', 'x86_64', 'arm64']))
# Hardcoded ones
target_path = "bin/"
TARGET_NAME = "hterrain_native"
# Local dependency paths
godot_headers_path = "godot-cpp/godot-headers/"
cpp_bindings_path = "godot-cpp/"
cpp_bindings_library = "libgodot-cpp"
# only support 64 at this time
bits = 64
# Updates the environment with the option variables.
opts.Update(env)
# Process some arguments
if env['use_llvm']:
env['CC'] = 'clang'
env['CXX'] = 'clang++'
if env['platform'] == '':
print("No valid target platform selected.")
quit()
# For the reference:
# - CCFLAGS are compilation flags shared between C and C++
# - CFLAGS are for C-specific compilation flags
# - CXXFLAGS are for C++-specific compilation flags
# - CPPFLAGS are for pre-processor flags
# - CPPDEFINES are for pre-processor defines
# - LINKFLAGS are for linking flags
# Check our platform specifics
if env['platform'] == "osx":
target_path += 'osx/'
cpp_bindings_library += '.osx'
if env['target'] == 'debug':
env.Append(CCFLAGS = ['-g', '-O2', '-arch', 'x86_64'])
env.Append(CXXFLAGS = ['-std=c++17'])
env.Append(LINKFLAGS = ['-arch', 'x86_64'])
else:
env.Append(CCFLAGS = ['-g', '-O3', '-arch', 'x86_64'])
env.Append(CXXFLAGS = ['-std=c++17'])
env.Append(LINKFLAGS = ['-arch', 'x86_64'])
elif env['platform'] == "linux":
target_path += 'linux/'
cpp_bindings_library += '.linux'
if env['target'] == 'debug':
# -g3 means we want plenty of debug info, more than default
env.Append(CCFLAGS = ['-fPIC', '-g3', '-Og'])
env.Append(CXXFLAGS = ['-std=c++17'])
else:
env.Append(CCFLAGS = ['-fPIC', '-O3'])
env.Append(CXXFLAGS = ['-std=c++17'])
env.Append(LINKFLAGS = ['-s'])
elif env['platform'] == "windows":
target_path += 'win64/'
cpp_bindings_library += '.windows'
# This makes sure to keep the session environment variables on windows,
# that way you can run scons in a vs 2017 prompt and it will find all the required tools
#env.Append(ENV = os.environ)
env.Append(CPPDEFINES = ['WIN32', '_WIN32', '_WINDOWS', '_CRT_SECURE_NO_WARNINGS'])
env.Append(CCFLAGS = ['-W3', '-GR'])
if env['target'] == 'debug':
env.Append(CPPDEFINES = ['_DEBUG'])
env.Append(CCFLAGS = ['-EHsc', '-MDd', '-ZI'])
env.Append(LINKFLAGS = ['-DEBUG'])
else:
env.Append(CPPDEFINES = ['NDEBUG'])
env.Append(CCFLAGS = ['-O2', '-EHsc', '-MD'])
if env['target'] == 'debug':
cpp_bindings_library += '.debug'
else:
cpp_bindings_library += '.release'
if env['macos_arch'] == 'universal':
cpp_bindings_library += '.' + str(bits)
else:
cpp_bindings_library += '.' + env['macos_arch']
# make sure our binding library is properly included
env.Append(CPPPATH = [
'.',
godot_headers_path,
cpp_bindings_path + 'include/',
cpp_bindings_path + 'include/core/',
cpp_bindings_path + 'include/gen/'
])
env.Append(LIBPATH = [cpp_bindings_path + 'bin/'])
env.Append(LIBS = [cpp_bindings_library])
# Add source files of our library
env.Append(CPPPATH = ['src/'])
sources = Glob('src/*.cpp')
library = env.SharedLibrary(target = target_path + TARGET_NAME , source = sources)
Default(library)
# Generates help for the -h scons option.
Help(opts.GenerateHelpText(env))

Binary file not shown.

View File

@@ -0,0 +1,55 @@
@tool
const NATIVE_PATH = "res://addons/zylann.hterrain/native/"
const HT_ImageUtilsGeneric = preload("./image_utils_generic.gd")
const HT_QuadTreeLodGeneric = preload("./quad_tree_lod_generic.gd")
# No native code was ported when moving to Godot 4.
# It may be changed too using GDExtension.
# See https://docs.godotengine.org/en/stable/classes/class_os.html#class-os-method-get-name
const _supported_os = {
# "Windows": true,
# "X11": true,
# "OSX": true
}
# See https://docs.godotengine.org/en/stable/tutorials/export/feature_tags.html
const _supported_archs = ["x86_64"]
static func _supports_current_arch() -> bool:
for arch in _supported_archs:
# This is misleading, we are querying features of the ENGINE, not the OS
if OS.has_feature(arch):
return true
return false
static func is_native_available() -> bool:
if not _supports_current_arch():
return false
var os = OS.get_name()
if not _supported_os.has(os):
return false
# API changes can cause binary incompatibility
var v = Engine.get_version_info()
return v.major == 4 and v.minor == 0
static func get_image_utils():
if is_native_available():
var HT_ImageUtilsNative = load(NATIVE_PATH + "image_utils.gdns")
# TODO Godot doesn't always return `null` when it fails so that `if` doesn't always help...
# See https://github.com/Zylann/godot_heightmap_plugin/issues/331
if HT_ImageUtilsNative != null:
return HT_ImageUtilsNative.new()
return HT_ImageUtilsGeneric.new()
static func get_quad_tree_lod():
if is_native_available():
var HT_QuadTreeLod = load(NATIVE_PATH + "quad_tree_lod.gdns")
if HT_QuadTreeLod != null:
return HT_QuadTreeLod.new()
return HT_QuadTreeLodGeneric.new()

View File

@@ -0,0 +1 @@
uid://cupyw3v6cqa4n

View File

@@ -0,0 +1,316 @@
@tool
# These functions are the same as the ones found in the GDNative library.
# They are used if the user's platform is not supported.
const HT_Util = preload("../util/util.gd")
var _blur_buffer : Image
func get_red_range(im: Image, rect: Rect2) -> Vector2:
rect = rect.intersection(Rect2(0, 0, im.get_width(), im.get_height()))
var min_x := int(rect.position.x)
var min_y := int(rect.position.y)
var max_x := min_x + int(rect.size.x)
var max_y := min_y + int(rect.size.y)
var min_height := im.get_pixel(min_x, min_y).r
var max_height := min_height
for y in range(min_y, max_y):
for x in range(min_x, max_x):
var h = im.get_pixel(x, y).r
if h < min_height:
min_height = h
elif h > max_height:
max_height = h
return Vector2(min_height, max_height)
func get_red_sum(im: Image, rect: Rect2) -> float:
rect = rect.intersection(Rect2(0, 0, im.get_width(), im.get_height()))
var min_x := int(rect.position.x)
var min_y := int(rect.position.y)
var max_x := min_x + int(rect.size.x)
var max_y := min_y + int(rect.size.y)
var sum := 0.0
for y in range(min_y, max_y):
for x in range(min_x, max_x):
sum += im.get_pixel(x, y).r
return sum
func get_red_sum_weighted(im: Image, brush: Image, pos: Vector2, factor: float) -> float:
var min_x = int(pos.x)
var min_y = int(pos.y)
var max_x = min_x + brush.get_width()
var max_y = min_y + brush.get_height()
var min_noclamp_x = min_x
var min_noclamp_y = min_y
min_x = clampi(min_x, 0, im.get_width())
min_y = clampi(min_y, 0, im.get_height())
max_x = clampi(max_x, 0, im.get_width())
max_y = clampi(max_y, 0, im.get_height())
var sum = 0.0
for y in range(min_y, max_y):
var by = y - min_noclamp_y
for x in range(min_x, max_x):
var bx = x - min_noclamp_x
var shape_value = brush.get_pixel(bx, by).r
sum += im.get_pixel(x, y).r * shape_value * factor
return sum
func add_red_brush(im: Image, brush: Image, pos: Vector2, factor: float):
var min_x = int(pos.x)
var min_y = int(pos.y)
var max_x = min_x + brush.get_width()
var max_y = min_y + brush.get_height()
var min_noclamp_x = min_x
var min_noclamp_y = min_y
min_x = clampi(min_x, 0, im.get_width())
min_y = clampi(min_y, 0, im.get_height())
max_x = clampi(max_x, 0, im.get_width())
max_y = clampi(max_y, 0, im.get_height())
for y in range(min_y, max_y):
var by = y - min_noclamp_y
for x in range(min_x, max_x):
var bx = x - min_noclamp_x
var shape_value = brush.get_pixel(bx, by).r
var r = im.get_pixel(x, y).r + shape_value * factor
im.set_pixel(x, y, Color(r, r, r))
func lerp_channel_brush(im: Image, brush: Image, pos: Vector2,
factor: float, target_value: float, channel: int):
var min_x = int(pos.x)
var min_y = int(pos.y)
var max_x = min_x + brush.get_width()
var max_y = min_y + brush.get_height()
var min_noclamp_x = min_x
var min_noclamp_y = min_y
min_x = clampi(min_x, 0, im.get_width())
min_y = clampi(min_y, 0, im.get_height())
max_x = clampi(max_x, 0, im.get_width())
max_y = clampi(max_y, 0, im.get_height())
for y in range(min_y, max_y):
var by = y - min_noclamp_y
for x in range(min_x, max_x):
var bx = x - min_noclamp_x
var shape_value = brush.get_pixel(bx, by).r
var c = im.get_pixel(x, y)
c[channel] = lerp(c[channel], target_value, shape_value * factor)
im.set_pixel(x, y, c)
func lerp_color_brush(im: Image, brush: Image, pos: Vector2,
factor: float, target_value: Color):
var min_x = int(pos.x)
var min_y = int(pos.y)
var max_x = min_x + brush.get_width()
var max_y = min_y + brush.get_height()
var min_noclamp_x = min_x
var min_noclamp_y = min_y
min_x = clampi(min_x, 0, im.get_width())
min_y = clampi(min_y, 0, im.get_height())
max_x = clampi(max_x, 0, im.get_width())
max_y = clampi(max_y, 0, im.get_height())
for y in range(min_y, max_y):
var by = y - min_noclamp_y
for x in range(min_x, max_x):
var bx = x - min_noclamp_x
var shape_value = brush.get_pixel(bx, by).r
var c = im.get_pixel(x, y).lerp(target_value, factor * shape_value)
im.set_pixel(x, y, c)
func generate_gaussian_brush(im: Image) -> float:
var sum := 0.0
var center := Vector2(im.get_width() / 2, im.get_height() / 2)
var radius := minf(im.get_width(), im.get_height()) / 2.0
for y in im.get_height():
for x in im.get_width():
var d := Vector2(x, y).distance_to(center) / radius
var v := clampf(1.0 - d * d * d, 0.0, 1.0)
im.set_pixel(x, y, Color(v, v, v))
sum += v;
return sum
func blur_red_brush(im: Image, brush: Image, pos: Vector2, factor: float):
factor = clampf(factor, 0.0, 1.0)
if _blur_buffer == null:
_blur_buffer = Image.new()
var buffer := _blur_buffer
var buffer_width := brush.get_width() + 2
var buffer_height := brush.get_height() + 2
if buffer_width != buffer.get_width() or buffer_height != buffer.get_height():
buffer.create(buffer_width, buffer_height, false, Image.FORMAT_RF)
var min_x := int(pos.x) - 1
var min_y := int(pos.y) - 1
var max_x := min_x + buffer.get_width()
var max_y := min_y + buffer.get_height()
var im_clamp_w = im.get_width() - 1
var im_clamp_h = im.get_height() - 1
# Copy pixels to temporary buffer
for y in range(min_y, max_y):
for x in range(min_x, max_x):
var ix := clampi(x, 0, im_clamp_w)
var iy := clampi(y, 0, im_clamp_h)
var c = im.get_pixel(ix, iy)
buffer.set_pixel(x - min_x, y - min_y, c)
min_x = int(pos.x)
min_y = int(pos.y)
max_x = min_x + brush.get_width()
max_y = min_y + brush.get_height()
var min_noclamp_x := min_x
var min_noclamp_y := min_y
min_x = clampi(min_x, 0, im.get_width())
min_y = clampi(min_y, 0, im.get_height())
max_x = clampi(max_x, 0, im.get_width())
max_y = clampi(max_y, 0, im.get_height())
# Apply blur
for y in range(min_y, max_y):
var by := y - min_noclamp_y
for x in range(min_x, max_x):
var bx := x - min_noclamp_x
var shape_value := brush.get_pixel(bx, by).r * factor
var p10 = buffer.get_pixel(bx + 1, by ).r
var p01 = buffer.get_pixel(bx, by + 1).r
var p11 = buffer.get_pixel(bx + 1, by + 1).r
var p21 = buffer.get_pixel(bx + 2, by + 1).r
var p12 = buffer.get_pixel(bx + 1, by + 2).r
var m = (p10 + p01 + p11 + p21 + p12) * 0.2
var p = lerpf(p11, m, shape_value * factor)
im.set_pixel(x, y, Color(p, p, p))
func paint_indexed_splat(index_map: Image, weight_map: Image, brush: Image, pos: Vector2, \
texture_index: int, factor: float):
var min_x := pos.x
var min_y := pos.y
var max_x := min_x + brush.get_width()
var max_y := min_y + brush.get_height()
var min_noclamp_x := min_x
var min_noclamp_y := min_y
min_x = clampi(min_x, 0, index_map.get_width())
min_y = clampi(min_y, 0, index_map.get_height())
max_x = clampi(max_x, 0, index_map.get_width())
max_y = clampi(max_y, 0, index_map.get_height())
var texture_index_f := float(texture_index) / 255.0
var all_texture_index_f := Color(texture_index_f, texture_index_f, texture_index_f)
var ci := texture_index % 3
var cm := Color(-1, -1, -1)
cm[ci] = 1
for y in range(min_y, max_y):
var by := y - min_noclamp_y
for x in range(min_x, max_x):
var bx := x - min_noclamp_x
var shape_value := brush.get_pixel(bx, by).r * factor
if shape_value == 0.0:
continue
var i := index_map.get_pixel(x, y)
var w := weight_map.get_pixel(x, y)
# Decompress third weight to make computations easier
w[2] = 1.0 - w[0] - w[1]
# The index map tells which textures to blend.
# The weight map tells their blending amounts.
# This brings the limitation that up to 3 textures can blend at a time in a given pixel.
# Painting this in real time can be a challenge.
# The approach here is a compromise for simplicity.
# Each texture is associated a fixed component of the index map (R, G or B),
# so two neighbor pixels having the same component won't be guaranteed to blend.
# In other words, texture T will not be able to blend with T + N * k,
# where k is an integer, and N is the number of components in the index map (up to 4).
# It might still be able to blend due to a special case when an area is uniform,
# but not otherwise.
# Dynamic component assignment sounds like the alternative, however I wasn't able
# to find a painting algorithm that wasn't confusing, at least the current one is
# predictable.
# Need to use approximation because Color is float but GDScript uses doubles...
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] > shape_value:
w -= cm * shape_value
elif w[ci] >= 0.0:
w[ci] = 0.0
i[ci] = texture_index_f
else:
# Pixel has our texture index, increase its weight
if w[ci] + shape_value < 1.0:
w += cm * shape_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 = Color(0, 0, 0)
w[ci] = 1.0
i = all_texture_index_f
# No `saturate` function in Color??
w[0] = clampf(w[0], 0.0, 1.0)
w[1] = clampf(w[1], 0.0, 1.0)
w[2] = clampf(w[2], 0.0, 1.0)
# Renormalize
w /= w[0] + w[1] + w[2]
index_map.set_pixel(x, y, i)
weight_map.set_pixel(x, y, w)

View File

@@ -0,0 +1 @@
uid://2f87sp1jmfe1

View File

@@ -0,0 +1,188 @@
@tool
# Independent quad tree designed to handle LOD
class HT_QTLQuad:
# Optional array of 4 HT_QTLQuad
var children = null
# TODO Use Vector2i
var origin_x : int = 0
var origin_y : int = 0
var data = null
func _init():
pass
func clear():
clear_children()
data = null
func clear_children():
children = null
func has_children() -> bool:
return children != null
var _tree := HT_QTLQuad.new()
var _max_depth : int = 0
var _base_size : int = 16
var _split_scale : float = 2.0
var _make_func : Callable
var _recycle_func : Callable
var _vertical_bounds_func : Callable
func set_callbacks(make_cb: Callable, recycle_cb: Callable, vbounds_cb: Callable):
_make_func = make_cb
_recycle_func = recycle_cb
_vertical_bounds_func = vbounds_cb
func clear():
_join_all_recursively(_tree, _max_depth)
_max_depth = 0
_base_size = 0
static func compute_lod_count(base_size: int, full_size: int) -> int:
var po : int = 0
while full_size > base_size:
full_size = full_size >> 1
po += 1
return po
func create_from_sizes(base_size: int, full_size: int):
clear()
_base_size = base_size
_max_depth = compute_lod_count(base_size, full_size)
func get_lod_count() -> int:
# TODO _max_depth is a maximum, not a count. Would be better for it to be a count (+1)
return _max_depth + 1
# The higher, the longer LODs will spread and higher the quality.
# The lower, the shorter LODs will spread and lower the quality.
func set_split_scale(p_split_scale: float):
var MIN := 2.0
var MAX := 5.0
# Split scale must be greater than a threshold,
# otherwise lods will decimate too fast and it will look messy
_split_scale = clampf(p_split_scale, MIN, MAX)
func get_split_scale() -> float:
return _split_scale
func update(view_pos: Vector3):
_update(_tree, _max_depth, view_pos)
# This makes sure we keep seeing the lowest LOD,
# if the tree is cleared while we are far away
if not _tree.has_children() and _tree.data == null:
_tree.data = _make_chunk(_max_depth, 0, 0)
func get_lod_factor(lod: int) -> int:
return 1 << lod
func _update(quad: HT_QTLQuad, lod: int, view_pos: Vector3):
# This function should be called regularly over frames.
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))
if _vertical_bounds_func.is_valid():
var vbounds : Vector2 = _vertical_bounds_func.call(quad.origin_x, quad.origin_y, lod)
world_center.y = (vbounds.x + vbounds.y) / 2.0
var split_distance := _base_size * lod_factor * _split_scale
if not quad.has_children():
if lod > 0 and world_center.distance_to(view_pos) < split_distance:
# Split
quad.children = [null, null, null, null]
for i in 4:
var child := HT_QTLQuad.new()
child.origin_x = quad.origin_x * 2 + (i & 1)
child.origin_y = quad.origin_y * 2 + ((i & 2) >> 1)
quad.children[i] = child
child.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.data != null:
_recycle_chunk(quad.data, quad.origin_x, quad.origin_y, lod)
quad.data = null
else:
var no_split_child := true
for child in quad.children:
_update(child, lod - 1, view_pos)
if child.has_children():
no_split_child = false
if no_split_child and world_center.distance_to(view_pos) > split_distance:
# Join
for i in 4:
var child : HT_QTLQuad = 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: HT_QTLQuad, lod: int):
if quad.has_children():
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
func _make_chunk(lod: int, origin_x: int, origin_y: int):
var chunk = null
if _make_func.is_valid():
chunk = _make_func.call(origin_x, origin_y, lod)
return chunk
func _recycle_chunk(chunk, origin_x: int, origin_y: int, lod: int):
if _recycle_func.is_valid():
_recycle_func.call(chunk, origin_x, origin_y, lod)
func debug_draw_tree(ci: CanvasItem):
var quad := _tree
_debug_draw_tree_recursive(ci, quad, _max_depth, 0)
func _debug_draw_tree_recursive(ci: CanvasItem, quad: HT_QTLQuad, lod_index: int, child_index: int):
if quad.has_children():
for i in 4:
_debug_draw_tree_recursive(ci, quad.children[i], lod_index - 1, i)
else:
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 : int = 0
if quad.data != null:
chunk_indicator = 1
var r := Rect2(Vector2(quad.origin_x, quad.origin_y) * size, Vector2(size, size))
ci.draw_rect(r, Color(1.0 - lod_index * 0.2, 0.2 * checker, chunk_indicator, 1))

View File

@@ -0,0 +1 @@
uid://ci5njrvvrqucr

View File

View File

@@ -0,0 +1,30 @@
#include "image_utils.h"
#include "quad_tree_lod.h"
extern "C" {
void GDN_EXPORT godot_gdnative_init(godot_gdnative_init_options *o) {
#ifdef _DEBUG
printf("godot_gdnative_init hterrain_native\n");
#endif
godot::Godot::gdnative_init(o);
}
void GDN_EXPORT godot_gdnative_terminate(godot_gdnative_terminate_options *o) {
#ifdef _DEBUG
printf("godot_gdnative_terminate hterrain_native\n");
#endif
godot::Godot::gdnative_terminate(o);
}
void GDN_EXPORT godot_nativescript_init(void *handle) {
#ifdef _DEBUG
printf("godot_nativescript_init hterrain_native\n");
#endif
godot::Godot::nativescript_init(handle);
godot::register_tool_class<godot::ImageUtils>();
godot::register_tool_class<godot::QuadTreeLod>();
}
} // extern "C"

View File

@@ -0,0 +1,364 @@
#include "image_utils.h"
#include "int_range_2d.h"
#include "math_funcs.h"
namespace godot {
template <typename F>
inline void generic_brush_op(Image &image, Image &brush, Vector2 p_pos, float factor, F op) {
IntRange2D range = IntRange2D::from_min_max(p_pos, brush.get_size());
int min_x_noclamp = range.min_x;
int min_y_noclamp = range.min_y;
range.clip(Vector2i(image.get_size()));
image.lock();
brush.lock();
for (int y = range.min_y; y < range.max_y; ++y) {
int by = y - min_y_noclamp;
for (int x = range.min_x; x < range.max_x; ++x) {
int bx = x - min_x_noclamp;
float b = brush.get_pixel(bx, by).r * factor;
op(image, x, y, b);
}
}
image.unlock();
brush.unlock();
}
ImageUtils::ImageUtils() {
#ifdef _DEBUG
Godot::print("Constructing ImageUtils");
#endif
}
ImageUtils::~ImageUtils() {
#ifdef _DEBUG
// TODO Cannot print shit here, see https://github.com/godotengine/godot/issues/37417
// Means only the console will print this
//Godot::print("Destructing ImageUtils");
printf("Destructing ImageUtils\n");
#endif
}
void ImageUtils::_init() {
}
Vector2 ImageUtils::get_red_range(Ref<Image> image_ref, Rect2 rect) const {
ERR_FAIL_COND_V(image_ref.is_null(), Vector2());
Image &image = **image_ref;
IntRange2D range(rect);
range.clip(Vector2i(image.get_size()));
image.lock();
float min_value = image.get_pixel(range.min_x, range.min_y).r;
float max_value = min_value;
for (int y = range.min_y; y < range.max_y; ++y) {
for (int x = range.min_x; x < range.max_x; ++x) {
float v = image.get_pixel(x, y).r;
if (v > max_value) {
max_value = v;
} else if (v < min_value) {
min_value = v;
}
}
}
image.unlock();
return Vector2(min_value, max_value);
}
float ImageUtils::get_red_sum(Ref<Image> image_ref, Rect2 rect) const {
ERR_FAIL_COND_V(image_ref.is_null(), 0.f);
Image &image = **image_ref;
IntRange2D range(rect);
range.clip(Vector2i(image.get_size()));
image.lock();
float sum = 0.f;
for (int y = range.min_y; y < range.max_y; ++y) {
for (int x = range.min_x; x < range.max_x; ++x) {
sum += image.get_pixel(x, y).r;
}
}
image.unlock();
return sum;
}
float ImageUtils::get_red_sum_weighted(Ref<Image> image_ref, Ref<Image> brush_ref, Vector2 p_pos, float factor) const {
ERR_FAIL_COND_V(image_ref.is_null(), 0.f);
ERR_FAIL_COND_V(brush_ref.is_null(), 0.f);
Image &image = **image_ref;
Image &brush = **brush_ref;
float sum = 0.f;
generic_brush_op(image, brush, p_pos, factor, [&sum](Image &image, int x, int y, float b) {
sum += image.get_pixel(x, y).r * b;
});
return sum;
}
void ImageUtils::add_red_brush(Ref<Image> image_ref, Ref<Image> brush_ref, Vector2 p_pos, float factor) const {
ERR_FAIL_COND(image_ref.is_null());
ERR_FAIL_COND(brush_ref.is_null());
Image &image = **image_ref;
Image &brush = **brush_ref;
generic_brush_op(image, brush, p_pos, factor, [](Image &image, int x, int y, float b) {
float r = image.get_pixel(x, y).r + b;
image.set_pixel(x, y, Color(r, r, r));
});
}
void ImageUtils::lerp_channel_brush(Ref<Image> image_ref, Ref<Image> brush_ref, Vector2 p_pos, float factor, float target_value, int channel) const {
ERR_FAIL_COND(image_ref.is_null());
ERR_FAIL_COND(brush_ref.is_null());
Image &image = **image_ref;
Image &brush = **brush_ref;
generic_brush_op(image, brush, p_pos, factor, [target_value, channel](Image &image, int x, int y, float b) {
Color c = image.get_pixel(x, y);
c[channel] = Math::lerp(c[channel], target_value, b);
image.set_pixel(x, y, c);
});
}
void ImageUtils::lerp_color_brush(Ref<Image> image_ref, Ref<Image> brush_ref, Vector2 p_pos, float factor, Color target_value) const {
ERR_FAIL_COND(image_ref.is_null());
ERR_FAIL_COND(brush_ref.is_null());
Image &image = **image_ref;
Image &brush = **brush_ref;
generic_brush_op(image, brush, p_pos, factor, [target_value](Image &image, int x, int y, float b) {
const Color c = image.get_pixel(x, y).linear_interpolate(target_value, b);
image.set_pixel(x, y, c);
});
}
// TODO Smooth (each pixel being box-filtered, contrary to the existing smooth)
float ImageUtils::generate_gaussian_brush(Ref<Image> image_ref) const {
ERR_FAIL_COND_V(image_ref.is_null(), 0.f);
Image &image = **image_ref;
int w = static_cast<int>(image.get_width());
int h = static_cast<int>(image.get_height());
Vector2 center(w / 2, h / 2);
float radius = Math::min(w, h) / 2;
ERR_FAIL_COND_V(radius <= 0.1f, 0.f);
float sum = 0.f;
image.lock();
for (int y = 0; y < h; ++y) {
for (int x = 0; x < w; ++x) {
float d = Vector2(x, y).distance_to(center) / radius;
float v = Math::clamp(1.f - d * d * d, 0.f, 1.f);
image.set_pixel(x, y, Color(v, v, v));
sum += v;
}
}
image.unlock();
return sum;
}
void ImageUtils::blur_red_brush(Ref<Image> image_ref, Ref<Image> brush_ref, Vector2 p_pos, float factor) {
ERR_FAIL_COND(image_ref.is_null());
ERR_FAIL_COND(brush_ref.is_null());
Image &image = **image_ref;
Image &brush = **brush_ref;
factor = Math::clamp(factor, 0.f, 1.f);
// Relative to the image
IntRange2D buffer_range = IntRange2D::from_pos_size(p_pos, brush.get_size());
buffer_range.pad(1);
const int image_width = static_cast<int>(image.get_width());
const int image_height = static_cast<int>(image.get_height());
const int buffer_width = static_cast<int>(buffer_range.get_width());
const int buffer_height = static_cast<int>(buffer_range.get_height());
_blur_buffer.resize(buffer_width * buffer_height);
image.lock();
// Cache pixels, because they will be queried more than once and written to later
int buffer_i = 0;
for (int y = buffer_range.min_y; y < buffer_range.max_y; ++y) {
for (int x = buffer_range.min_x; x < buffer_range.max_x; ++x) {
const int ix = Math::clamp(x, 0, image_width - 1);
const int iy = Math::clamp(y, 0, image_height - 1);
_blur_buffer[buffer_i] = image.get_pixel(ix, iy).r;
++buffer_i;
}
}
IntRange2D range = IntRange2D::from_min_max(p_pos, brush.get_size());
const int min_x_noclamp = range.min_x;
const int min_y_noclamp = range.min_y;
range.clip(Vector2i(image.get_size()));
const int buffer_offset_left = -1;
const int buffer_offset_right = 1;
const int buffer_offset_top = -buffer_width;
const int buffer_offset_bottom = buffer_width;
brush.lock();
// Apply blur
for (int y = range.min_y; y < range.max_y; ++y) {
const int brush_y = y - min_y_noclamp;
for (int x = range.min_x; x < range.max_x; ++x) {
const int brush_x = x - min_x_noclamp;
const float brush_value = brush.get_pixel(brush_x, brush_y).r * factor;
buffer_i = (brush_x + 1) + (brush_y + 1) * buffer_width;
const float p10 = _blur_buffer[buffer_i + buffer_offset_top];
const float p01 = _blur_buffer[buffer_i + buffer_offset_left];
const float p11 = _blur_buffer[buffer_i];
const float p21 = _blur_buffer[buffer_i + buffer_offset_right];
const float p12 = _blur_buffer[buffer_i + buffer_offset_bottom];
// Average
float m = (p10 + p01 + p11 + p21 + p12) * 0.2f;
float p = Math::lerp(p11, m, brush_value);
image.set_pixel(x, y, Color(p, p, p));
}
}
image.unlock();
brush.unlock();
}
void ImageUtils::paint_indexed_splat(Ref<Image> index_map_ref, Ref<Image> weight_map_ref,
Ref<Image> brush_ref, Vector2 p_pos, int texture_index, float factor) {
ERR_FAIL_COND(index_map_ref.is_null());
ERR_FAIL_COND(weight_map_ref.is_null());
ERR_FAIL_COND(brush_ref.is_null());
Image &index_map = **index_map_ref;
Image &weight_map = **weight_map_ref;
Image &brush = **brush_ref;
ERR_FAIL_COND(index_map.get_size() != weight_map.get_size());
factor = Math::clamp(factor, 0.f, 1.f);
IntRange2D range = IntRange2D::from_min_max(p_pos, brush.get_size());
const int min_x_noclamp = range.min_x;
const int min_y_noclamp = range.min_y;
range.clip(Vector2i(index_map.get_size()));
const float texture_index_f = float(texture_index) / 255.f;
const Color all_texture_index_f(texture_index_f, texture_index_f, texture_index_f);
const int ci = texture_index % 3;
Color cm(-1, -1, -1);
cm[ci] = 1;
brush.lock();
index_map.lock();
weight_map.lock();
for (int y = range.min_y; y < range.max_y; ++y) {
const int brush_y = y - min_y_noclamp;
for (int x = range.min_x; x < range.max_x; ++x) {
const int brush_x = x - min_x_noclamp;
const float brush_value = brush.get_pixel(brush_x, brush_y).r * factor;
if (brush_value == 0.f) {
continue;
}
Color i = index_map.get_pixel(x, y);
Color w = weight_map.get_pixel(x, y);
// Decompress third weight to make computations easier
w[2] = 1.f - w[0] - w[1];
if (std::abs(i[ci] - texture_index_f) > 0.001f) {
// 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 = Color(0, 0, 0);
w[ci] = 1.0;
i = all_texture_index_f;
}
}
// No `saturate` function in Color??
w[0] = Math::clamp(w[0], 0.f, 1.f);
w[1] = Math::clamp(w[1], 0.f, 1.f);
w[2] = Math::clamp(w[2], 0.f, 1.f);
// Renormalize
const float sum = w[0] + w[1] + w[2];
w[0] /= sum;
w[1] /= sum;
w[2] /= sum;
index_map.set_pixel(x, y, i);
weight_map.set_pixel(x, y, w);
}
}
brush.lock();
index_map.unlock();
weight_map.unlock();
}
void ImageUtils::_register_methods() {
register_method("get_red_range", &ImageUtils::get_red_range);
register_method("get_red_sum", &ImageUtils::get_red_sum);
register_method("get_red_sum_weighted", &ImageUtils::get_red_sum_weighted);
register_method("add_red_brush", &ImageUtils::add_red_brush);
register_method("lerp_channel_brush", &ImageUtils::lerp_channel_brush);
register_method("lerp_color_brush", &ImageUtils::lerp_color_brush);
register_method("generate_gaussian_brush", &ImageUtils::generate_gaussian_brush);
register_method("blur_red_brush", &ImageUtils::blur_red_brush);
register_method("paint_indexed_splat", &ImageUtils::paint_indexed_splat);
}
} // namespace godot

View File

@@ -0,0 +1,38 @@
#ifndef IMAGE_UTILS_H
#define IMAGE_UTILS_H
#include <core/Godot.hpp>
#include <gen/Image.hpp>
#include <gen/Reference.hpp>
#include <vector>
namespace godot {
class ImageUtils : public Reference {
GODOT_CLASS(ImageUtils, Reference)
public:
static void _register_methods();
ImageUtils();
~ImageUtils();
void _init();
Vector2 get_red_range(Ref<Image> image_ref, Rect2 rect) const;
float get_red_sum(Ref<Image> image_ref, Rect2 rect) const;
float get_red_sum_weighted(Ref<Image> image_ref, Ref<Image> brush_ref, Vector2 p_pos, float factor) const;
void add_red_brush(Ref<Image> image_ref, Ref<Image> brush_ref, Vector2 p_pos, float factor) const;
void lerp_channel_brush(Ref<Image> image_ref, Ref<Image> brush_ref, Vector2 p_pos, float factor, float target_value, int channel) const;
void lerp_color_brush(Ref<Image> image_ref, Ref<Image> brush_ref, Vector2 p_pos, float factor, Color target_value) const;
float generate_gaussian_brush(Ref<Image> image_ref) const;
void blur_red_brush(Ref<Image> image_ref, Ref<Image> brush_ref, Vector2 p_pos, float factor);
void paint_indexed_splat(Ref<Image> index_map_ref, Ref<Image> weight_map_ref, Ref<Image> brush_ref, Vector2 p_pos, int texture_index, float factor);
//void erode_red_brush(Ref<Image> image_ref, Ref<Image> brush_ref, Vector2 p_pos, float factor);
private:
std::vector<float> _blur_buffer;
};
} // namespace godot
#endif // IMAGE_UTILS_H

View File

@@ -0,0 +1,59 @@
#ifndef INT_RANGE_2D_H
#define INT_RANGE_2D_H
#include "math_funcs.h"
#include "vector2i.h"
#include <core/Rect2.hpp>
struct IntRange2D {
int min_x;
int min_y;
int max_x;
int max_y;
static inline IntRange2D from_min_max(godot::Vector2 min_pos, godot::Vector2 max_pos) {
return IntRange2D(godot::Rect2(min_pos, max_pos));
}
static inline IntRange2D from_pos_size(godot::Vector2 min_pos, godot::Vector2 size) {
return IntRange2D(godot::Rect2(min_pos, size));
}
IntRange2D(godot::Rect2 rect) {
min_x = static_cast<int>(rect.position.x);
min_y = static_cast<int>(rect.position.y);
max_x = static_cast<int>(rect.position.x + rect.size.x);
max_y = static_cast<int>(rect.position.y + rect.size.y);
}
inline bool is_inside(Vector2i size) const {
return min_x >= size.x &&
min_y >= size.y &&
max_x <= size.x &&
max_y <= size.y;
}
inline void clip(Vector2i size) {
min_x = Math::clamp(min_x, 0, size.x);
min_y = Math::clamp(min_y, 0, size.y);
max_x = Math::clamp(max_x, 0, size.x);
max_y = Math::clamp(max_y, 0, size.y);
}
inline void pad(int p) {
min_x -= p;
min_y -= p;
max_x += p;
max_y += p;
}
inline int get_width() const {
return max_x - min_x;
}
inline int get_height() const {
return max_y - min_y;
}
};
#endif // INT_RANGE_2D_H

View File

@@ -0,0 +1,28 @@
#ifndef MATH_FUNCS_H
#define MATH_FUNCS_H
namespace Math {
inline float lerp(float minv, float maxv, float t) {
return minv + t * (maxv - minv);
}
template <typename T>
inline T clamp(T x, T minv, T maxv) {
if (x < minv) {
return minv;
}
if (x > maxv) {
return maxv;
}
return x;
}
template <typename T>
inline T min(T a, T b) {
return a < b ? a : b;
}
} // namespace Math
#endif // MATH_FUNCS_H

Some files were not shown because too many files have changed in this diff Show More