Skip to content

Commit

Permalink
Merge pull request #369 from MJacred/godot4
Browse files Browse the repository at this point in the history
Godot4: support importing and exporting 32-bit raw files
  • Loading branch information
Zylann authored Aug 28, 2023
2 parents 47ae27e + 2018f3f commit 73d9d1d
Show file tree
Hide file tree
Showing 6 changed files with 125 additions and 56 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ For a more detailed list of past and incoming changes, see the commit history.
- Used absolute path in shader #includes, so forking doesn't fail to find them by default
- Added `HTerrainData.reload` function to allow reloading a terrain while the game is running (after saving in the editor)
- Update properties in the inspector when a custom shader is modified
- Added support for importing and exporting 32-bit raw files


1.7.2
Expand Down
2 changes: 1 addition & 1 deletion addons/zylann.hterrain/doc/docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -692,7 +692,7 @@ This window allows you to import several kinds of data, such as heightmap but al
There are a few things to check before you can successfully import a terrain though:

- The resolution should be power of two + 1, and square. If it isn't, the plugin will attempt to crop it, which might be OK or not if you can deal with map borders that this will produce.
- If you import a RAW heightmap, it has to be encoded using 16-bit unsigned integer format.
- If you import a RAW heightmap, it has to be encoded using either 16-bit or 32-bit unsigned integer format. Upon selecting a file via the file chooser dialog, the importer will attempt to deduce the bit depth.
- If you import a PNG heightmap, Godot can only load it as 8-bit depth, so it is not recommended for high-range terrains because it doesn't have enough height precision.

This feature also can't be undone when executed, as all terrain data will be overwritten with the new one. If anything isn't correct, the tool will warn you before to prevent data loss.
Expand Down
64 changes: 44 additions & 20 deletions addons/zylann.hterrain/hterrain_data.gd
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ const HT_Logger = preload("./util/logger.gd")
const HT_ImageFileCache = preload("./util/image_file_cache.gd")
const HT_XYZFormat = preload("./util/xyz_format.gd")

enum {
BIT_DEPTH_UNDEFINED = 0,
BIT_DEPTH_16 = 16,
BIT_DEPTH_32 = 32
}

# Note: indexes matters for saving, don't re-order
# TODO Rename "CHANNEL" to "MAP", makes more sense and less confusing with RGBA channels
const CHANNEL_HEIGHT = 0
Expand Down Expand Up @@ -1393,7 +1399,7 @@ func _edit_import_maps(input: Dictionary) -> bool:
if input.has(CHANNEL_HEIGHT):
var params = input[CHANNEL_HEIGHT]
if not _import_heightmap(
params.path, params.min_height, params.max_height, params.big_endian):
params.path, params.min_height, params.max_height, params.big_endian, params.bit_depth):
return false

# TODO Import indexed maps?
Expand All @@ -1418,7 +1424,7 @@ static func get_adjusted_map_size(width: int, height: int) -> int:
return size_po2


func _import_heightmap(fpath: String, min_y: float, max_y: float, big_endian: bool) -> bool:
func _import_heightmap(fpath: String, min_y: float, max_y: float, big_endian: bool, bit_depth: int) -> bool:
var ext := fpath.get_extension().to_lower()

if ext == "png":
Expand Down Expand Up @@ -1502,15 +1508,15 @@ func _import_heightmap(fpath: String, min_y: float, max_y: float, big_endian: bo
_logger.error(str("Invalid heightmap format ", im.get_format()))

elif ext == "raw":
# RAW files don't contain size, so we have to deduce it from 16-bit size.
# RAW files don't contain size, so we take the user's bit depth import choice.
# We also need to bring it back to float in the wanted range.

var f := FileAccess.open(fpath, FileAccess.READ)
if f == null:
return false

var file_len := f.get_length()
var file_res := HT_Util.integer_square_root(file_len / 2)
var file_res := HT_Util.integer_square_root(file_len / (bit_depth/8))
if file_res == -1:
# Can't deduce size
return false
Expand Down Expand Up @@ -1545,22 +1551,40 @@ func _import_heightmap(fpath: String, min_y: float, max_y: float, big_endian: bo

# Convert to internal format
var h := 0.0
for y in rh:
for x in rw:
var gs := float(f.get_16()) / 65535.0
h = min_y + hrange * float(gs)
match im.get_format():
Image.FORMAT_RF:
im.set_pixel(x, y, Color(h, 0, 0))
Image.FORMAT_RGB8:
im.set_pixel(x, y, encode_height_to_rgb8_unorm(h))
_:
_logger.error(str("Invalid heightmap format ", im.get_format()))
return false

# Skip next pixels if the file is bigger than the accepted resolution
for x in range(rw, file_res):
f.get_16()
if bit_depth == BIT_DEPTH_32:
for y in rh:
for x in rw:
var gs := float(f.get_32()) / 4294967295.0
h = min_y + hrange * float(gs)
match im.get_format():
Image.FORMAT_RF:
im.set_pixel(x, y, Color(h, 0, 0))
Image.FORMAT_RGB8:
im.set_pixel(x, y, encode_height_to_rgb8_unorm(h))
_:
_logger.error(str("Invalid heightmap format ", im.get_format()))
return false

# Skip next pixels if the file is bigger than the accepted resolution
for x in range(rw, file_res):
f.get_32()
else:
for y in rh:
for x in rw:
var gs := float(f.get_16()) / 65535.0
h = min_y + hrange * float(gs)
match im.get_format():
Image.FORMAT_RF:
im.set_pixel(x, y, Color(h, 0, 0))
Image.FORMAT_RGB8:
im.set_pixel(x, y, encode_height_to_rgb8_unorm(h))
_:
_logger.error(str("Invalid heightmap format ", im.get_format()))
return false

# Skip next pixels if the file is bigger than the accepted resolution
for x in range(rw, file_res):
f.get_16()

elif ext == "xyz":
var f := FileAccess.open(fpath, FileAccess.READ)
Expand Down
32 changes: 18 additions & 14 deletions addons/zylann.hterrain/tools/exporter/export_image_dialog.gd
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ const HT_Logger = preload("../../util/logger.gd")
const FORMAT_RH = 0
const FORMAT_RF = 1
const FORMAT_R16 = 2
const FORMAT_PNG8 = 3
const FORMAT_EXRH = 4
const FORMAT_EXRF = 5
const FORMAT_COUNT = 6
const FORMAT_R32 = 3
const FORMAT_PNG8 = 4
const FORMAT_EXRH = 5
const FORMAT_EXRF = 6
const FORMAT_COUNT = 7

@onready var _output_path_line_edit := $VB/Grid/OutputPath/HeightmapPathLineEdit as LineEdit
@onready var _format_selector := $VB/Grid/FormatSelector as OptionButton
Expand Down Expand Up @@ -41,14 +42,16 @@ func _ready():

_format_names[FORMAT_RH] = "16-bit RAW float"
_format_names[FORMAT_RF] = "32-bit RAW float"
_format_names[FORMAT_R16] = "16-bit RAW unsigned"
_format_names[FORMAT_R16] = "16-bit RAW int unsigned (little endian)"
_format_names[FORMAT_R32] = "32-bit RAW int unsigned (little endian)"
_format_names[FORMAT_PNG8] = "8-bit PNG greyscale"
_format_names[FORMAT_EXRH] = "16-bit float greyscale EXR"
_format_names[FORMAT_EXRF] = "32-bit float greyscale EXR"

_format_extensions[FORMAT_RH] = "raw"
_format_extensions[FORMAT_RF] = "raw"
_format_extensions[FORMAT_R16] = "raw"
_format_extensions[FORMAT_R32] = "raw"
_format_extensions[FORMAT_PNG8] = "png"
_format_extensions[FORMAT_EXRH] = "exr"
_format_extensions[FORMAT_EXRF] = "exr"
Expand Down Expand Up @@ -142,7 +145,7 @@ func _export() -> bool:
var err := FileAccess.get_open_error()
_print_file_error(fpath, err)
return false

if format == FORMAT_RH:
float_heightmap.convert(Image.FORMAT_RH)
f.store_buffer(float_heightmap.get_data())
Expand All @@ -155,14 +158,15 @@ func _export() -> bool:
for y in float_heightmap.get_height():
for x in float_heightmap.get_width():
var h := int((float_heightmap.get_pixel(x, y).r - height_min) * hscale)
if h < 0:
h = 0
elif h > 65535:
h = 65535
if x % 50 == 0:
_logger.debug(str(h))
f.store_16(h)

f.store_16(clampi(h, 0, 65535))

elif format == FORMAT_R32:
var hscale := 4294967295.0 / (height_max - height_min)
for y in float_heightmap.get_height():
for x in float_heightmap.get_width():
var h := int((float_heightmap.get_pixel(x, y).r - height_min) * hscale)
f.store_32(clampi(h, 0, 4294967295))

if save_error == OK:
_logger.debug("Exported heightmap as \"{0}\"".format([fpath]))
return true
Expand Down
65 changes: 50 additions & 15 deletions addons/zylann.hterrain/tools/importer/importer_dialog.gd
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,11 @@ signal permanent_change_performed(message)
@onready var _warnings_label : Label = \
$VBoxContainer/ColorRect/ScrollContainer/VBoxContainer/Warnings

const RAW_LITTLE_ENDIAN = 0
const RAW_BIG_ENDIAN = 1

enum {
RAW_LITTLE_ENDIAN,
RAW_BIG_ENDIAN
}

var _terrain : HTerrain = null
var _logger = HT_Logger.get_for(self)
Expand All @@ -37,7 +40,13 @@ func _ready():
"raw_endianess": {
"type": TYPE_INT,
"usage": "enum",
"enum_items": ["Little Endian", "Big Endian"],
"enum_items": [[RAW_LITTLE_ENDIAN, "Little Endian"], [RAW_BIG_ENDIAN, "Big Endian"]],
"enabled": false
},
"bit_depth": {
"type": TYPE_INT,
"usage": "enum",
"enum_items": [[HTerrainData.BIT_DEPTH_16, "16-bit"], [HTerrainData.BIT_DEPTH_32, "32-bit"]],
"enabled": false
},
"min_height": {
Expand All @@ -61,7 +70,7 @@ func _ready():
"exts": ["png"]
}
})

# Testing
# _errors_label.text = "- Hello World!"
# _warnings_label.text = "- Yolo Jesus!"
Expand Down Expand Up @@ -137,7 +146,8 @@ func _on_ImportButton_pressed():
"path": heightmap_path,
"min_height": _inspector.get_value("min_height"),
"max_height": _inspector.get_value("max_height"),
"big_endian": endianess == RAW_BIG_ENDIAN
"big_endian": endianess == RAW_BIG_ENDIAN,
"bit_depth": _inspector.get_value("bit_depth"),
}

var colormap_path = _inspector.get_value("colormap")
Expand Down Expand Up @@ -168,6 +178,31 @@ func _on_Inspector_property_changed(key: String, value):
if key == "heightmap":
var is_raw = value.get_extension().to_lower() == "raw"
_inspector.set_property_enabled("raw_endianess", is_raw)
_inspector.set_property_enabled("bit_depth", is_raw)
if is_raw:
var bit_depth:int = _estimate_bit_depth_for_raw_file(value)
if bit_depth == HTerrainData.BIT_DEPTH_UNDEFINED:
bit_depth = HTerrainData.BIT_DEPTH_16 # fallback depth value
_inspector.set_value("bit_depth", bit_depth)


# _estimate_bit_depth_for_raw_file returns the file's identified bit depth, or 0.
static func _estimate_bit_depth_for_raw_file(path: String) -> int:
var ext := path.get_extension().to_lower()
if ext == "raw":
var f := FileAccess.open(path, FileAccess.READ)
if f == null:
return HTerrainData.BIT_DEPTH_UNDEFINED

var file_len := f.get_length()
f = null # close file

for bit_depth in [HTerrainData.BIT_DEPTH_16, HTerrainData.BIT_DEPTH_32]:
var file_res := HT_Util.integer_square_root(file_len / (bit_depth/8))
if file_res > 0:
return bit_depth

return HTerrainData.BIT_DEPTH_UNDEFINED


func _validate_form() -> HT_ErrorCheckReport:
Expand All @@ -176,6 +211,7 @@ func _validate_form() -> HT_ErrorCheckReport:
var heightmap_path : String = _inspector.get_value("heightmap")
var splatmap_path : String = _inspector.get_value("splatmap")
var colormap_path : String = _inspector.get_value("colormap")
var bit_depth = _inspector.get_value("bit_depth")

if colormap_path == "" and heightmap_path == "" and splatmap_path == "":
res.errors.append("No maps specified.")
Expand All @@ -195,7 +231,7 @@ func _validate_form() -> HT_ErrorCheckReport:
# so we avoid loading other maps every time to do further checks.
return res

var image_size_result = _load_image_size(heightmap_path, _logger)
var image_size_result = _load_image_size(heightmap_path, _logger, bit_depth)
if image_size_result.error_code != OK:
res.errors.append(str("Cannot open heightmap file: ", image_size_result.to_string()))
return res
Expand All @@ -211,18 +247,18 @@ func _validate_form() -> HT_ErrorCheckReport:
heightmap_size = adjusted_size

if splatmap_path != "":
_check_map_size(splatmap_path, "splatmap", heightmap_size, res, _logger)
_check_map_size(splatmap_path, "splatmap", heightmap_size, bit_depth, res, _logger)

if colormap_path != "":
_check_map_size(colormap_path, "colormap", heightmap_size, res, _logger)
_check_map_size(colormap_path, "colormap", heightmap_size, bit_depth, res, _logger)

return res


static func _check_map_size(path: String, map_name: String, heightmap_size: int,
static func _check_map_size(path: String, map_name: String, heightmap_size: int, bit_depth: int,
res: HT_ErrorCheckReport, logger):

var size_result := _load_image_size(path, logger)
var size_result := _load_image_size(path, logger, bit_depth)
if size_result.error_code != OK:
res.errors.append(str("Cannot open splatmap file: ", size_result.to_string()))
return
Expand Down Expand Up @@ -251,7 +287,7 @@ class HT_ImageSizeResult:
return HT_Errors.get_message(error_code)


static func _load_image_size(path: String, logger) -> HT_ImageSizeResult:
static func _load_image_size(path: String, logger, bit_depth: int) -> HT_ImageSizeResult:
var ext := path.get_extension().to_lower()
var result := HT_ImageSizeResult.new()

Expand All @@ -276,14 +312,13 @@ static func _load_image_size(path: String, logger) -> HT_ImageSizeResult:
result.error_code = err
return result

# Assume the raw data is square in 16-bit format,
# so its size is function of file length
# Assume the raw data is square, so its size is function of file length
var flen := f.get_length()
f = null
var size_px = HT_Util.integer_square_root(flen / 2)
var size_px = HT_Util.integer_square_root(flen / (bit_depth/8))
if size_px == -1:
result.error_code = ERR_INVALID_DATA
result.error_message = "RAW image is not square"
result.error_message = "RAW image is not square or your bit depth choice is incorrect…"
return result

logger.debug("Deduced RAW heightmap resolution: {0}*{1}, for a length of {2}" \
Expand Down
17 changes: 11 additions & 6 deletions addons/zylann.hterrain/tools/inspector/inspector.gd
Original file line number Diff line number Diff line change
Expand Up @@ -209,16 +209,21 @@ func _make_editor(key: String, prop: Dictionary) -> HT_InspectorEditor:
var option_button := OptionButton.new()

for i in len(prop.enum_items):
var item = prop.enum_items[i]
option_button.add_item(item)

# TODO We assume index, actually
var item:Array = prop.enum_items[i]
var value:int = item[0]
var text:String = item[1]
option_button.add_item(text, value)

getter = option_button.get_selected_id
setter = option_button.select
setter = func select_id(id: int):
var index:int = option_button.get_item_index(id)
assert(index >= 0)
option_button.select(index)

option_button.item_selected.connect(_property_edited.bind(key))

editor = option_button

else:
# Numeric value
var spinbox := SpinBox.new()
Expand Down

0 comments on commit 73d9d1d

Please sign in to comment.