aboutsummaryrefslogtreecommitdiff
path: root/game/addons/zylann.hterrain/util/xyz_format.gd
blob: 86e4a1ab69525348d789142679fcb497c0b401fd (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
@tool

# XYZ files are text files containing a list of 3D points.
# They can be found in GIS software as an export format for heightmaps.
# In order to turn it into a heightmap we may calculate bounds first
# to find the origin and then set points in an image.


class HT_XYZBounds:
   # Note: it is important for these to be double-precision floats,
   # GIS data can have large coordinates
   var min_x := 0.0
   var min_y := 0.0

   var max_x := 0.0
   var max_y := 0.0

   var line_count := 0

   var image_width := 0
   var image_height := 0


# TODO `split_float` returns 32-bit floats, despite internally parsing doubles... 
# Despite that, I still use it here because it doesn't seem to cause issues and is faster.
# If it becomes an issue, we'll have to switch to `split` and casting to `float`.

static func load_bounds(f: FileAccess) -> HT_XYZBounds:
   # It is faster to get line and split floats than using CSV functions
   var line := f.get_line()
   var floats := line.split_floats(" ")
   
   # We only care about X and Y, it makes less operations to do in the loop.
   # Z is the height and will remain as-is at the end.
   var min_pos_x := floats[0]
   var min_pos_y := floats[1]

   var max_pos_x := min_pos_x
   var max_pos_y := min_pos_y

   # Start at 1 because we just read the first line
   var line_count := 1
   
   # We know the file is a series of float triplets
   while not f.eof_reached():
      line = f.get_line()

      # The last line can be empty
      if len(line) < 2:
         break

      floats = line.split_floats(" ")

      var pos_x := floats[0]
      var pos_y := floats[1]
      
      min_pos_x = minf(min_pos_x, pos_x)
      min_pos_y = minf(min_pos_y, pos_y)

      max_pos_x = maxf(max_pos_x, pos_x)
      max_pos_y = maxf(max_pos_y, pos_y)

      line_count += 1

   var bounds := HT_XYZBounds.new()
   bounds.min_x = min_pos_x
   bounds.min_y = min_pos_y
   bounds.max_x = max_pos_x
   bounds.max_y = max_pos_y
   bounds.line_count = line_count
   bounds.image_width = int(max_pos_x - min_pos_x) + 1
   bounds.image_height = int(max_pos_y - min_pos_y) + 1
   return bounds


# Loads points into an image with existing dimensions and format.
# `f` must be positioned at the beginning of the series of points.
# If `bounds` is `null`, it will be computed.
static func load_heightmap(f: FileAccess, dst_image: Image, bounds: HT_XYZBounds):
   # We are not going to read the entire file directly in memory, because it can be really big.
   # Instead we'll parse it directly and the only thing we retain in memory is the heightmap.
   # This can be really slow on big files. If we can assume the file is square and points
   # separated by 1 unit each in a grid pattern, it could be a bit faster, but
   # parsing points from text really is the main bottleneck (40 seconds to load a 2000x2000 file!).
   
   # Bounds can be precalculated
   if bounds == null:
      var file_begin := f.get_position()
      bounds = load_bounds(f)
      f.seek(file_begin)
   
   # Put min coordinates on the GDScript stack so they are faster to access
   var min_pos_x := bounds.min_x
   var min_pos_y := bounds.min_y
   var line_count := bounds.line_count

   for i in line_count:
      var line := f.get_line()
      var floats := line.split_floats(" ")
      var x := int(floats[0] - min_pos_x)
      var y := int(floats[1] - min_pos_y)
      
      # Make sure the coordinate is inside the image,
      # due to float imprecision or potentially non-grid-aligned points.
      # Could use `Rect2` to check faster but it uses floats.
      # `Rect2i` would be better but is only available in Godot 4.
      if x >= 0 and y >= 0 and x < dst_image.get_width() and y < dst_image.get_height():
         dst_image.set_pixel(x, y, Color(floats[2], 0, 0))