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

View File

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

View File

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

View File

@@ -0,0 +1,19 @@
#ifndef VECTOR2I_H
#define VECTOR2I_H
#include <core/Vector2.hpp>
struct Vector2i {
int x;
int y;
Vector2i(godot::Vector2 v) :
x(static_cast<int>(v.x)),
y(static_cast<int>(v.y)) {}
bool any_zero() const {
return x == 0 || y == 0;
}
};
#endif // VECTOR2I_H