voxcity 0.7.0__py3-none-any.whl → 1.0.13__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. voxcity/__init__.py +14 -14
  2. voxcity/downloader/ocean.py +559 -0
  3. voxcity/exporter/__init__.py +12 -12
  4. voxcity/exporter/cityles.py +633 -633
  5. voxcity/exporter/envimet.py +733 -728
  6. voxcity/exporter/magicavoxel.py +333 -333
  7. voxcity/exporter/netcdf.py +238 -238
  8. voxcity/exporter/obj.py +1480 -1480
  9. voxcity/generator/__init__.py +47 -44
  10. voxcity/generator/api.py +727 -675
  11. voxcity/generator/grids.py +394 -379
  12. voxcity/generator/io.py +94 -94
  13. voxcity/generator/pipeline.py +582 -282
  14. voxcity/generator/update.py +429 -0
  15. voxcity/generator/voxelizer.py +18 -6
  16. voxcity/geoprocessor/__init__.py +75 -75
  17. voxcity/geoprocessor/draw.py +1494 -1219
  18. voxcity/geoprocessor/merge_utils.py +91 -91
  19. voxcity/geoprocessor/mesh.py +806 -806
  20. voxcity/geoprocessor/network.py +708 -708
  21. voxcity/geoprocessor/raster/__init__.py +2 -0
  22. voxcity/geoprocessor/raster/buildings.py +435 -428
  23. voxcity/geoprocessor/raster/core.py +31 -0
  24. voxcity/geoprocessor/raster/export.py +93 -93
  25. voxcity/geoprocessor/raster/landcover.py +178 -51
  26. voxcity/geoprocessor/raster/raster.py +1 -1
  27. voxcity/geoprocessor/utils.py +824 -824
  28. voxcity/models.py +115 -113
  29. voxcity/simulator/solar/__init__.py +66 -43
  30. voxcity/simulator/solar/integration.py +336 -336
  31. voxcity/simulator/solar/sky.py +668 -0
  32. voxcity/simulator/solar/temporal.py +792 -434
  33. voxcity/simulator_gpu/__init__.py +115 -0
  34. voxcity/simulator_gpu/common/__init__.py +9 -0
  35. voxcity/simulator_gpu/common/geometry.py +11 -0
  36. voxcity/simulator_gpu/core.py +322 -0
  37. voxcity/simulator_gpu/domain.py +262 -0
  38. voxcity/simulator_gpu/environment.yml +11 -0
  39. voxcity/simulator_gpu/init_taichi.py +154 -0
  40. voxcity/simulator_gpu/integration.py +15 -0
  41. voxcity/simulator_gpu/kernels.py +56 -0
  42. voxcity/simulator_gpu/radiation.py +28 -0
  43. voxcity/simulator_gpu/raytracing.py +623 -0
  44. voxcity/simulator_gpu/sky.py +9 -0
  45. voxcity/simulator_gpu/solar/__init__.py +178 -0
  46. voxcity/simulator_gpu/solar/core.py +66 -0
  47. voxcity/simulator_gpu/solar/csf.py +1249 -0
  48. voxcity/simulator_gpu/solar/domain.py +561 -0
  49. voxcity/simulator_gpu/solar/epw.py +421 -0
  50. voxcity/simulator_gpu/solar/integration.py +2953 -0
  51. voxcity/simulator_gpu/solar/radiation.py +3019 -0
  52. voxcity/simulator_gpu/solar/raytracing.py +686 -0
  53. voxcity/simulator_gpu/solar/reflection.py +533 -0
  54. voxcity/simulator_gpu/solar/sky.py +907 -0
  55. voxcity/simulator_gpu/solar/solar.py +337 -0
  56. voxcity/simulator_gpu/solar/svf.py +446 -0
  57. voxcity/simulator_gpu/solar/volumetric.py +1151 -0
  58. voxcity/simulator_gpu/solar/voxcity.py +2953 -0
  59. voxcity/simulator_gpu/temporal.py +13 -0
  60. voxcity/simulator_gpu/utils.py +25 -0
  61. voxcity/simulator_gpu/view.py +32 -0
  62. voxcity/simulator_gpu/visibility/__init__.py +109 -0
  63. voxcity/simulator_gpu/visibility/geometry.py +278 -0
  64. voxcity/simulator_gpu/visibility/integration.py +808 -0
  65. voxcity/simulator_gpu/visibility/landmark.py +753 -0
  66. voxcity/simulator_gpu/visibility/view.py +944 -0
  67. voxcity/utils/__init__.py +11 -0
  68. voxcity/utils/classes.py +194 -0
  69. voxcity/utils/lc.py +80 -39
  70. voxcity/utils/shape.py +230 -0
  71. voxcity/visualizer/__init__.py +24 -24
  72. voxcity/visualizer/builder.py +43 -43
  73. voxcity/visualizer/grids.py +141 -141
  74. voxcity/visualizer/maps.py +187 -187
  75. voxcity/visualizer/renderer.py +1146 -928
  76. {voxcity-0.7.0.dist-info → voxcity-1.0.13.dist-info}/METADATA +56 -52
  77. voxcity-1.0.13.dist-info/RECORD +116 -0
  78. voxcity-0.7.0.dist-info/RECORD +0 -77
  79. {voxcity-0.7.0.dist-info → voxcity-1.0.13.dist-info}/WHEEL +0 -0
  80. {voxcity-0.7.0.dist-info → voxcity-1.0.13.dist-info}/licenses/AUTHORS.rst +0 -0
  81. {voxcity-0.7.0.dist-info → voxcity-1.0.13.dist-info}/licenses/LICENSE +0 -0
@@ -1,334 +1,334 @@
1
- """
2
- Module for handling MagicaVoxel .vox files.
3
-
4
- This module provides functionality for converting 3D numpy arrays to MagicaVoxel .vox files,
5
- including color mapping and splitting large models into smaller chunks.
6
-
7
- The module handles:
8
- - Color map conversion and optimization
9
- - Large model splitting into MagicaVoxel-compatible chunks
10
- - Custom palette creation
11
- - Coordinate system transformation
12
- - Batch export of multiple .vox files
13
-
14
- Key Features:
15
- - Supports models larger than MagicaVoxel's 256³ size limit
16
- - Automatic color palette optimization
17
- - Preserves color mapping across chunks
18
- - Handles coordinate system differences between numpy and MagicaVoxel
19
- """
20
-
21
- # Required imports for voxel file handling and array manipulation
22
- import numpy as np
23
- from pyvox.models import Vox
24
- from pyvox.writer import VoxWriter
25
- import os
26
- from ..visualizer import get_voxel_color_map
27
-
28
- def convert_colormap_and_array(original_map, original_array):
29
- """
30
- Convert a color map with arbitrary indices to sequential indices starting from 0
31
- and update the corresponding 3D numpy array.
32
-
33
- This function optimizes the color mapping by:
34
- 1. Converting arbitrary color indices to sequential ones
35
- 2. Creating a new mapping that preserves color relationships
36
- 3. Updating the voxel array to use the new sequential indices
37
-
38
- Args:
39
- original_map (dict): Dictionary with integer keys and RGB color value lists.
40
- Each key is a color index, and each value is a list of [R,G,B] values.
41
- original_array (numpy.ndarray): 3D array with integer values corresponding to color map keys.
42
- The array contains indices that match the keys in original_map.
43
-
44
- Returns:
45
- tuple: (new_color_map, new_array)
46
- - new_color_map (dict): Color map with sequential indices starting from 0
47
- - new_array (numpy.ndarray): Updated array with new sequential indices
48
-
49
- Example:
50
- >>> color_map = {5: [255,0,0], 10: [0,255,0]}
51
- >>> array = np.array([[[5,10],[10,5]]])
52
- >>> new_map, new_array = convert_colormap_and_array(color_map, array)
53
- >>> print(new_map)
54
- {0: [255,0,0], 1: [0,255,0]}
55
- """
56
- # Get all the keys and sort them
57
- keys = sorted(original_map.keys())
58
-
59
- # Create mapping from old indices to new indices
60
- old_to_new = {old_idx: new_idx for new_idx, old_idx in enumerate(keys)}
61
-
62
- # Create new color map with sequential indices
63
- new_map = {}
64
- for new_idx, old_idx in enumerate(keys):
65
- new_map[new_idx] = original_map[old_idx]
66
-
67
- # Create a copy of the original array
68
- new_array = original_array.copy()
69
-
70
- # Replace old indices with new ones in the array
71
- for old_idx, new_idx in old_to_new.items():
72
- new_array[original_array == old_idx] = new_idx
73
-
74
- return new_map, new_array
75
-
76
- def create_custom_palette(color_map):
77
- """
78
- Create a palette array from a color map dictionary suitable for MagicaVoxel format.
79
-
80
- This function:
81
- 1. Creates a 256x4 RGBA palette array
82
- 2. Sets full opacity (alpha=255) for all colors by default
83
- 3. Reserves index 0 for transparent black (void)
84
- 4. Maps colors sequentially starting from index 1
85
-
86
- Args:
87
- color_map (dict): Dictionary mapping indices to RGB color values.
88
- Each value should be a list of 3 integers [R,G,B] in range 0-255.
89
-
90
- Returns:
91
- numpy.ndarray: 256x4 array containing RGBA color values.
92
- - Shape: (256, 4)
93
- - Type: uint8
94
- - Format: [R,G,B,A] for each color
95
- - Index 0: [0,0,0,0] (transparent)
96
- - Indices 1-255: Colors from color_map with alpha=255
97
- """
98
- # Initialize empty palette with alpha channel
99
- palette = np.zeros((256, 4), dtype=np.uint8)
100
- palette[:, 3] = 255 # Set alpha to 255 for all colors
101
- palette[0] = [0, 0, 0, 0] # Set the first color to transparent black
102
-
103
- # Fill palette with RGB values from color map
104
- for i, color in enumerate(color_map.values(), start=1):
105
- palette[i, :3] = color
106
- return palette
107
-
108
- def create_mapping(color_map):
109
- """
110
- Create a mapping from color map keys to sequential indices for MagicaVoxel compatibility.
111
-
112
- Creates a mapping that:
113
- - Reserves index 0 for void space
114
- - Reserves index 1 (typically for special use)
115
- - Maps colors sequentially starting from index 2
116
-
117
- Args:
118
- color_map (dict): Dictionary mapping indices to RGB color values.
119
- The keys can be any integers, they will be remapped sequentially.
120
-
121
- Returns:
122
- dict: Mapping from original indices to sequential indices starting at 2.
123
- Example: {original_index1: 2, original_index2: 3, ...}
124
- """
125
- # Create mapping starting at index 2 (0 is void, 1 is reserved)
126
- return {value: i+2 for i, value in enumerate(color_map.keys())}
127
-
128
- def split_array(array, max_size=255):
129
- """
130
- Split a 3D array into smaller chunks that fit within MagicaVoxel size limits.
131
-
132
- This function handles large voxel models by:
133
- 1. Calculating required splits in each dimension
134
- 2. Dividing the model into chunks of max_size or smaller
135
- 3. Yielding each chunk with its position information
136
-
137
- Args:
138
- array (numpy.ndarray): 3D array to split.
139
- Can be any size, will be split into chunks of max_size or smaller.
140
- max_size (int, optional): Maximum size allowed for each dimension.
141
- Defaults to 255 (MagicaVoxel's limit is 256).
142
-
143
- Yields:
144
- tuple: (sub_array, (i,j,k))
145
- - sub_array: numpy.ndarray of size <= max_size in each dimension
146
- - (i,j,k): tuple of indices indicating chunk position in the original model
147
-
148
- Example:
149
- >>> array = np.ones((300, 300, 300))
150
- >>> for chunk, (i,j,k) in split_array(array):
151
- ... print(f"Chunk at position {i},{j},{k} has shape {chunk.shape}")
152
- """
153
- # Calculate number of splits needed in each dimension
154
- x, y, z = array.shape
155
- x_splits = (x + max_size - 1) // max_size
156
- y_splits = (y + max_size - 1) // max_size
157
- z_splits = (z + max_size - 1) // max_size
158
-
159
- # Iterate through all possible chunk positions
160
- for i in range(x_splits):
161
- for j in range(y_splits):
162
- for k in range(z_splits):
163
- # Calculate chunk boundaries
164
- x_start, x_end = i * max_size, min((i + 1) * max_size, x)
165
- y_start, y_end = j * max_size, min((j + 1) * max_size, y)
166
- z_start, z_end = k * max_size, min((k + 1) * max_size, z)
167
- yield (
168
- array[x_start:x_end, y_start:y_end, z_start:z_end],
169
- (i, j, k)
170
- )
171
-
172
- def numpy_to_vox(array, color_map, output_file):
173
- """
174
- Convert a numpy array to a MagicaVoxel .vox file.
175
-
176
- This function handles the complete conversion process:
177
- 1. Creates a custom color palette from the color map
178
- 2. Generates value mapping for voxel indices
179
- 3. Transforms coordinates to match MagicaVoxel's system
180
- 4. Saves the model in .vox format
181
-
182
- Args:
183
- array (numpy.ndarray): 3D array containing voxel data.
184
- Values should correspond to keys in color_map.
185
- color_map (dict): Dictionary mapping indices to RGB color values.
186
- Each value should be a list of [R,G,B] values (0-255).
187
- output_file (str): Path to save the .vox file.
188
- Will overwrite if file exists.
189
-
190
- Returns:
191
- tuple: (value_mapping, palette, shape)
192
- - value_mapping: dict mapping original indices to MagicaVoxel indices
193
- - palette: numpy.ndarray of shape (256,4) containing RGBA values
194
- - shape: tuple of (width, height, depth) of the output model
195
-
196
- Note:
197
- - Coordinates are transformed to match MagicaVoxel's coordinate system
198
- - Z-axis is flipped and axes are reordered in the process
199
- """
200
- # Create color palette and value mapping
201
- palette = create_custom_palette(color_map)
202
- value_mapping = create_mapping(color_map)
203
- value_mapping[0] = 0 # Ensure 0 maps to 0 (void)
204
-
205
- # Transform array to match MagicaVoxel coordinate system
206
- array_flipped = np.flip(array, axis=2) # Flip Z axis
207
- array_transposed = np.transpose(array_flipped, (1, 2, 0)) # Reorder axes
208
- mapped_array = np.vectorize(value_mapping.get)(array_transposed, 0)
209
-
210
- # Create and save vox file
211
- vox = Vox.from_dense(mapped_array.astype(np.uint8))
212
- vox.palette = palette
213
- VoxWriter(output_file, vox).write()
214
-
215
- return value_mapping, palette, array_transposed.shape
216
-
217
- def export_large_voxel_model(array, color_map, output_prefix, max_size=255, base_filename='chunk'):
218
- """
219
- Export a large voxel model by splitting it into multiple .vox files.
220
-
221
- This function handles models of any size by:
222
- 1. Creating the output directory if needed
223
- 2. Splitting the model into manageable chunks
224
- 3. Saving each chunk as a separate .vox file
225
- 4. Maintaining consistent color mapping across all chunks
226
-
227
- Args:
228
- array (numpy.ndarray): 3D array containing voxel data.
229
- Can be any size, will be split into chunks if needed.
230
- color_map (dict): Dictionary mapping indices to RGB color values.
231
- Each value should be a list of [R,G,B] values (0-255).
232
- output_prefix (str): Directory to save the .vox files.
233
- Will be created if it doesn't exist.
234
- max_size (int, optional): Maximum size allowed for each dimension.
235
- Defaults to 255 (MagicaVoxel's limit is 256).
236
- base_filename (str, optional): Base name for the output files.
237
- Defaults to 'chunk'. Final filenames will be {base_filename}_{i}_{j}_{k}.vox
238
-
239
- Returns:
240
- tuple: (value_mapping, palette)
241
- - value_mapping: dict mapping original indices to MagicaVoxel indices
242
- - palette: numpy.ndarray of shape (256,4) containing RGBA values
243
-
244
- Example:
245
- >>> array = np.ones((500,500,500))
246
- >>> color_map = {1: [255,0,0]}
247
- >>> export_large_voxel_model(array, color_map, "output/model")
248
- # Creates files like: output/model/chunk_0_0_0.vox, chunk_0_0_1.vox, etc.
249
- """
250
- # Create output directory if it doesn't exist
251
- os.makedirs(output_prefix, exist_ok=True)
252
-
253
- # Process each chunk of the model
254
- for sub_array, (i, j, k) in split_array(array, max_size):
255
- output_file = f"{output_prefix}/{base_filename}_{i}_{j}_{k}.vox"
256
- value_mapping, palette, shape = numpy_to_vox(sub_array, color_map, output_file)
257
- print(f"Chunk {i}_{j}_{k} saved as {output_file}")
258
- print(f"Shape: {shape}")
259
-
260
- return value_mapping, palette
261
-
262
- def export_magicavoxel_vox(array, output_dir, base_filename='chunk', voxel_color_map=None):
263
- """
264
- Export a voxel model to MagicaVoxel .vox format.
265
-
266
- This is the main entry point for voxel model export. It handles:
267
- 1. Color map management (using default if none provided)
268
- 2. Color index optimization
269
- 3. Large model splitting and export
270
- 4. Progress reporting
271
-
272
- Args:
273
- array (numpy.ndarray | VoxCity): 3D array containing voxel data or a VoxCity instance.
274
- When a VoxCity is provided, its voxel classes are exported.
275
- output_dir (str): Directory to save the .vox files.
276
- Will be created if it doesn't exist.
277
- base_filename (str, optional): Base name for the output files.
278
- Defaults to 'chunk'. Used when model is split into multiple files.
279
- voxel_color_map (dict, optional): Dictionary mapping indices to RGB color values.
280
- If None, uses default color map from visualizer.
281
- Each value should be a list of [R,G,B] values (0-255).
282
-
283
- Note:
284
- - Large models are automatically split into multiple files
285
- - Color mapping is optimized and made sequential
286
- - Progress information is printed to stdout
287
- """
288
- # Accept VoxCity instance as first argument
289
- try:
290
- from ..models import VoxCity as _VoxCity
291
- if isinstance(array, _VoxCity):
292
- array = array.voxels.classes
293
- except Exception:
294
- pass
295
-
296
- # Use default color map if none provided
297
- if voxel_color_map is None:
298
- voxel_color_map = get_voxel_color_map()
299
-
300
- # Convert color map and array to sequential indices
301
- converted_voxel_color_map, converted_array = convert_colormap_and_array(voxel_color_map, array)
302
-
303
- # Export the model and print confirmation
304
- value_mapping, palette = export_large_voxel_model(converted_array, converted_voxel_color_map, output_dir, base_filename=base_filename)
305
- print(f"\tvox files was successfully exported in {output_dir}")
306
-
307
-
308
- class MagicaVoxelExporter:
309
- """Exporter adapter to write VoxCity voxels as MagicaVoxel .vox chunks.
310
-
311
- Accepts either a VoxCity instance (uses `voxels.classes`) or a raw 3D numpy array.
312
- """
313
-
314
- def export(self, obj, output_directory: str, base_filename: str, **kwargs):
315
- import numpy as _np
316
- os.makedirs(output_directory, exist_ok=True)
317
- try:
318
- from ..models import VoxCity as _VoxCity
319
- if isinstance(obj, _VoxCity):
320
- export_magicavoxel_vox(
321
- obj.voxels.classes,
322
- output_directory,
323
- base_filename,
324
- voxel_color_map=kwargs.get("voxel_color_map"),
325
- )
326
- return output_directory
327
- except Exception:
328
- pass
329
-
330
- if isinstance(obj, _np.ndarray) and obj.ndim == 3:
331
- export_magicavoxel_vox(obj, output_directory, base_filename, voxel_color_map=kwargs.get("voxel_color_map"))
332
- return output_directory
333
-
1
+ """
2
+ Module for handling MagicaVoxel .vox files.
3
+
4
+ This module provides functionality for converting 3D numpy arrays to MagicaVoxel .vox files,
5
+ including color mapping and splitting large models into smaller chunks.
6
+
7
+ The module handles:
8
+ - Color map conversion and optimization
9
+ - Large model splitting into MagicaVoxel-compatible chunks
10
+ - Custom palette creation
11
+ - Coordinate system transformation
12
+ - Batch export of multiple .vox files
13
+
14
+ Key Features:
15
+ - Supports models larger than MagicaVoxel's 256³ size limit
16
+ - Automatic color palette optimization
17
+ - Preserves color mapping across chunks
18
+ - Handles coordinate system differences between numpy and MagicaVoxel
19
+ """
20
+
21
+ # Required imports for voxel file handling and array manipulation
22
+ import numpy as np
23
+ from pyvox.models import Vox
24
+ from pyvox.writer import VoxWriter
25
+ import os
26
+ from ..visualizer import get_voxel_color_map
27
+
28
+ def convert_colormap_and_array(original_map, original_array):
29
+ """
30
+ Convert a color map with arbitrary indices to sequential indices starting from 0
31
+ and update the corresponding 3D numpy array.
32
+
33
+ This function optimizes the color mapping by:
34
+ 1. Converting arbitrary color indices to sequential ones
35
+ 2. Creating a new mapping that preserves color relationships
36
+ 3. Updating the voxel array to use the new sequential indices
37
+
38
+ Args:
39
+ original_map (dict): Dictionary with integer keys and RGB color value lists.
40
+ Each key is a color index, and each value is a list of [R,G,B] values.
41
+ original_array (numpy.ndarray): 3D array with integer values corresponding to color map keys.
42
+ The array contains indices that match the keys in original_map.
43
+
44
+ Returns:
45
+ tuple: (new_color_map, new_array)
46
+ - new_color_map (dict): Color map with sequential indices starting from 0
47
+ - new_array (numpy.ndarray): Updated array with new sequential indices
48
+
49
+ Example:
50
+ >>> color_map = {5: [255,0,0], 10: [0,255,0]}
51
+ >>> array = np.array([[[5,10],[10,5]]])
52
+ >>> new_map, new_array = convert_colormap_and_array(color_map, array)
53
+ >>> print(new_map)
54
+ {0: [255,0,0], 1: [0,255,0]}
55
+ """
56
+ # Get all the keys and sort them
57
+ keys = sorted(original_map.keys())
58
+
59
+ # Create mapping from old indices to new indices
60
+ old_to_new = {old_idx: new_idx for new_idx, old_idx in enumerate(keys)}
61
+
62
+ # Create new color map with sequential indices
63
+ new_map = {}
64
+ for new_idx, old_idx in enumerate(keys):
65
+ new_map[new_idx] = original_map[old_idx]
66
+
67
+ # Create a copy of the original array
68
+ new_array = original_array.copy()
69
+
70
+ # Replace old indices with new ones in the array
71
+ for old_idx, new_idx in old_to_new.items():
72
+ new_array[original_array == old_idx] = new_idx
73
+
74
+ return new_map, new_array
75
+
76
+ def create_custom_palette(color_map):
77
+ """
78
+ Create a palette array from a color map dictionary suitable for MagicaVoxel format.
79
+
80
+ This function:
81
+ 1. Creates a 256x4 RGBA palette array
82
+ 2. Sets full opacity (alpha=255) for all colors by default
83
+ 3. Reserves index 0 for transparent black (void)
84
+ 4. Maps colors sequentially starting from index 1
85
+
86
+ Args:
87
+ color_map (dict): Dictionary mapping indices to RGB color values.
88
+ Each value should be a list of 3 integers [R,G,B] in range 0-255.
89
+
90
+ Returns:
91
+ numpy.ndarray: 256x4 array containing RGBA color values.
92
+ - Shape: (256, 4)
93
+ - Type: uint8
94
+ - Format: [R,G,B,A] for each color
95
+ - Index 0: [0,0,0,0] (transparent)
96
+ - Indices 1-255: Colors from color_map with alpha=255
97
+ """
98
+ # Initialize empty palette with alpha channel
99
+ palette = np.zeros((256, 4), dtype=np.uint8)
100
+ palette[:, 3] = 255 # Set alpha to 255 for all colors
101
+ palette[0] = [0, 0, 0, 0] # Set the first color to transparent black
102
+
103
+ # Fill palette with RGB values from color map
104
+ for i, color in enumerate(color_map.values(), start=1):
105
+ palette[i, :3] = color
106
+ return palette
107
+
108
+ def create_mapping(color_map):
109
+ """
110
+ Create a mapping from color map keys to sequential indices for MagicaVoxel compatibility.
111
+
112
+ Creates a mapping that:
113
+ - Reserves index 0 for void space
114
+ - Reserves index 1 (typically for special use)
115
+ - Maps colors sequentially starting from index 2
116
+
117
+ Args:
118
+ color_map (dict): Dictionary mapping indices to RGB color values.
119
+ The keys can be any integers, they will be remapped sequentially.
120
+
121
+ Returns:
122
+ dict: Mapping from original indices to sequential indices starting at 2.
123
+ Example: {original_index1: 2, original_index2: 3, ...}
124
+ """
125
+ # Create mapping starting at index 2 (0 is void, 1 is reserved)
126
+ return {value: i+2 for i, value in enumerate(color_map.keys())}
127
+
128
+ def split_array(array, max_size=255):
129
+ """
130
+ Split a 3D array into smaller chunks that fit within MagicaVoxel size limits.
131
+
132
+ This function handles large voxel models by:
133
+ 1. Calculating required splits in each dimension
134
+ 2. Dividing the model into chunks of max_size or smaller
135
+ 3. Yielding each chunk with its position information
136
+
137
+ Args:
138
+ array (numpy.ndarray): 3D array to split.
139
+ Can be any size, will be split into chunks of max_size or smaller.
140
+ max_size (int, optional): Maximum size allowed for each dimension.
141
+ Defaults to 255 (MagicaVoxel's limit is 256).
142
+
143
+ Yields:
144
+ tuple: (sub_array, (i,j,k))
145
+ - sub_array: numpy.ndarray of size <= max_size in each dimension
146
+ - (i,j,k): tuple of indices indicating chunk position in the original model
147
+
148
+ Example:
149
+ >>> array = np.ones((300, 300, 300))
150
+ >>> for chunk, (i,j,k) in split_array(array):
151
+ ... print(f"Chunk at position {i},{j},{k} has shape {chunk.shape}")
152
+ """
153
+ # Calculate number of splits needed in each dimension
154
+ x, y, z = array.shape
155
+ x_splits = (x + max_size - 1) // max_size
156
+ y_splits = (y + max_size - 1) // max_size
157
+ z_splits = (z + max_size - 1) // max_size
158
+
159
+ # Iterate through all possible chunk positions
160
+ for i in range(x_splits):
161
+ for j in range(y_splits):
162
+ for k in range(z_splits):
163
+ # Calculate chunk boundaries
164
+ x_start, x_end = i * max_size, min((i + 1) * max_size, x)
165
+ y_start, y_end = j * max_size, min((j + 1) * max_size, y)
166
+ z_start, z_end = k * max_size, min((k + 1) * max_size, z)
167
+ yield (
168
+ array[x_start:x_end, y_start:y_end, z_start:z_end],
169
+ (i, j, k)
170
+ )
171
+
172
+ def numpy_to_vox(array, color_map, output_file):
173
+ """
174
+ Convert a numpy array to a MagicaVoxel .vox file.
175
+
176
+ This function handles the complete conversion process:
177
+ 1. Creates a custom color palette from the color map
178
+ 2. Generates value mapping for voxel indices
179
+ 3. Transforms coordinates to match MagicaVoxel's system
180
+ 4. Saves the model in .vox format
181
+
182
+ Args:
183
+ array (numpy.ndarray): 3D array containing voxel data.
184
+ Values should correspond to keys in color_map.
185
+ color_map (dict): Dictionary mapping indices to RGB color values.
186
+ Each value should be a list of [R,G,B] values (0-255).
187
+ output_file (str): Path to save the .vox file.
188
+ Will overwrite if file exists.
189
+
190
+ Returns:
191
+ tuple: (value_mapping, palette, shape)
192
+ - value_mapping: dict mapping original indices to MagicaVoxel indices
193
+ - palette: numpy.ndarray of shape (256,4) containing RGBA values
194
+ - shape: tuple of (width, height, depth) of the output model
195
+
196
+ Note:
197
+ - Coordinates are transformed to match MagicaVoxel's coordinate system
198
+ - Z-axis is flipped and axes are reordered in the process
199
+ """
200
+ # Create color palette and value mapping
201
+ palette = create_custom_palette(color_map)
202
+ value_mapping = create_mapping(color_map)
203
+ value_mapping[0] = 0 # Ensure 0 maps to 0 (void)
204
+
205
+ # Transform array to match MagicaVoxel coordinate system
206
+ array_flipped = np.flip(array, axis=2) # Flip Z axis
207
+ array_transposed = np.transpose(array_flipped, (1, 2, 0)) # Reorder axes
208
+ mapped_array = np.vectorize(value_mapping.get)(array_transposed, 0)
209
+
210
+ # Create and save vox file
211
+ vox = Vox.from_dense(mapped_array.astype(np.uint8))
212
+ vox.palette = palette
213
+ VoxWriter(output_file, vox).write()
214
+
215
+ return value_mapping, palette, array_transposed.shape
216
+
217
+ def export_large_voxel_model(array, color_map, output_prefix, max_size=255, base_filename='chunk'):
218
+ """
219
+ Export a large voxel model by splitting it into multiple .vox files.
220
+
221
+ This function handles models of any size by:
222
+ 1. Creating the output directory if needed
223
+ 2. Splitting the model into manageable chunks
224
+ 3. Saving each chunk as a separate .vox file
225
+ 4. Maintaining consistent color mapping across all chunks
226
+
227
+ Args:
228
+ array (numpy.ndarray): 3D array containing voxel data.
229
+ Can be any size, will be split into chunks if needed.
230
+ color_map (dict): Dictionary mapping indices to RGB color values.
231
+ Each value should be a list of [R,G,B] values (0-255).
232
+ output_prefix (str): Directory to save the .vox files.
233
+ Will be created if it doesn't exist.
234
+ max_size (int, optional): Maximum size allowed for each dimension.
235
+ Defaults to 255 (MagicaVoxel's limit is 256).
236
+ base_filename (str, optional): Base name for the output files.
237
+ Defaults to 'chunk'. Final filenames will be {base_filename}_{i}_{j}_{k}.vox
238
+
239
+ Returns:
240
+ tuple: (value_mapping, palette)
241
+ - value_mapping: dict mapping original indices to MagicaVoxel indices
242
+ - palette: numpy.ndarray of shape (256,4) containing RGBA values
243
+
244
+ Example:
245
+ >>> array = np.ones((500,500,500))
246
+ >>> color_map = {1: [255,0,0]}
247
+ >>> export_large_voxel_model(array, color_map, "output/model")
248
+ # Creates files like: output/model/chunk_0_0_0.vox, chunk_0_0_1.vox, etc.
249
+ """
250
+ # Create output directory if it doesn't exist
251
+ os.makedirs(output_prefix, exist_ok=True)
252
+
253
+ # Process each chunk of the model
254
+ for sub_array, (i, j, k) in split_array(array, max_size):
255
+ output_file = f"{output_prefix}/{base_filename}_{i}_{j}_{k}.vox"
256
+ value_mapping, palette, shape = numpy_to_vox(sub_array, color_map, output_file)
257
+ print(f"Chunk {i}_{j}_{k} saved as {output_file}")
258
+ print(f"Shape: {shape}")
259
+
260
+ return value_mapping, palette
261
+
262
+ def export_magicavoxel_vox(array, output_dir, base_filename='chunk', voxel_color_map=None):
263
+ """
264
+ Export a voxel model to MagicaVoxel .vox format.
265
+
266
+ This is the main entry point for voxel model export. It handles:
267
+ 1. Color map management (using default if none provided)
268
+ 2. Color index optimization
269
+ 3. Large model splitting and export
270
+ 4. Progress reporting
271
+
272
+ Args:
273
+ array (numpy.ndarray | VoxCity): 3D array containing voxel data or a VoxCity instance.
274
+ When a VoxCity is provided, its voxel classes are exported.
275
+ output_dir (str): Directory to save the .vox files.
276
+ Will be created if it doesn't exist.
277
+ base_filename (str, optional): Base name for the output files.
278
+ Defaults to 'chunk'. Used when model is split into multiple files.
279
+ voxel_color_map (dict, optional): Dictionary mapping indices to RGB color values.
280
+ If None, uses default color map from visualizer.
281
+ Each value should be a list of [R,G,B] values (0-255).
282
+
283
+ Note:
284
+ - Large models are automatically split into multiple files
285
+ - Color mapping is optimized and made sequential
286
+ - Progress information is printed to stdout
287
+ """
288
+ # Accept VoxCity instance as first argument
289
+ try:
290
+ from ..models import VoxCity as _VoxCity
291
+ if isinstance(array, _VoxCity):
292
+ array = array.voxels.classes
293
+ except Exception:
294
+ pass
295
+
296
+ # Use default color map if none provided
297
+ if voxel_color_map is None:
298
+ voxel_color_map = get_voxel_color_map()
299
+
300
+ # Convert color map and array to sequential indices
301
+ converted_voxel_color_map, converted_array = convert_colormap_and_array(voxel_color_map, array)
302
+
303
+ # Export the model and print confirmation
304
+ value_mapping, palette = export_large_voxel_model(converted_array, converted_voxel_color_map, output_dir, base_filename=base_filename)
305
+ print(f"\tvox files was successfully exported in {output_dir}")
306
+
307
+
308
+ class MagicaVoxelExporter:
309
+ """Exporter adapter to write VoxCity voxels as MagicaVoxel .vox chunks.
310
+
311
+ Accepts either a VoxCity instance (uses `voxels.classes`) or a raw 3D numpy array.
312
+ """
313
+
314
+ def export(self, obj, output_directory: str, base_filename: str, **kwargs):
315
+ import numpy as _np
316
+ os.makedirs(output_directory, exist_ok=True)
317
+ try:
318
+ from ..models import VoxCity as _VoxCity
319
+ if isinstance(obj, _VoxCity):
320
+ export_magicavoxel_vox(
321
+ obj.voxels.classes,
322
+ output_directory,
323
+ base_filename,
324
+ voxel_color_map=kwargs.get("voxel_color_map"),
325
+ )
326
+ return output_directory
327
+ except Exception:
328
+ pass
329
+
330
+ if isinstance(obj, _np.ndarray) and obj.ndim == 3:
331
+ export_magicavoxel_vox(obj, output_directory, base_filename, voxel_color_map=kwargs.get("voxel_color_map"))
332
+ return output_directory
333
+
334
334
  raise TypeError("MagicaVoxelExporter.export expects VoxCity or a 3D numpy array")