voxcity 0.7.0__py3-none-any.whl → 1.0.2__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.
- voxcity/__init__.py +14 -14
- voxcity/exporter/__init__.py +12 -12
- voxcity/exporter/cityles.py +633 -633
- voxcity/exporter/envimet.py +733 -728
- voxcity/exporter/magicavoxel.py +333 -333
- voxcity/exporter/netcdf.py +238 -238
- voxcity/exporter/obj.py +1480 -1480
- voxcity/generator/__init__.py +47 -44
- voxcity/generator/api.py +721 -675
- voxcity/generator/grids.py +381 -379
- voxcity/generator/io.py +94 -94
- voxcity/generator/pipeline.py +282 -282
- voxcity/generator/update.py +429 -0
- voxcity/generator/voxelizer.py +18 -6
- voxcity/geoprocessor/__init__.py +75 -75
- voxcity/geoprocessor/draw.py +1488 -1219
- voxcity/geoprocessor/merge_utils.py +91 -91
- voxcity/geoprocessor/mesh.py +806 -806
- voxcity/geoprocessor/network.py +708 -708
- voxcity/geoprocessor/raster/buildings.py +435 -428
- voxcity/geoprocessor/raster/export.py +93 -93
- voxcity/geoprocessor/raster/landcover.py +5 -2
- voxcity/geoprocessor/utils.py +824 -824
- voxcity/models.py +113 -113
- voxcity/simulator/solar/__init__.py +66 -43
- voxcity/simulator/solar/integration.py +336 -336
- voxcity/simulator/solar/sky.py +668 -0
- voxcity/simulator/solar/temporal.py +792 -434
- voxcity/utils/__init__.py +11 -0
- voxcity/utils/classes.py +194 -0
- voxcity/utils/lc.py +80 -39
- voxcity/utils/shape.py +230 -0
- voxcity/visualizer/__init__.py +24 -24
- voxcity/visualizer/builder.py +43 -43
- voxcity/visualizer/grids.py +141 -141
- voxcity/visualizer/maps.py +187 -187
- voxcity/visualizer/renderer.py +1145 -928
- {voxcity-0.7.0.dist-info → voxcity-1.0.2.dist-info}/METADATA +90 -49
- {voxcity-0.7.0.dist-info → voxcity-1.0.2.dist-info}/RECORD +42 -38
- {voxcity-0.7.0.dist-info → voxcity-1.0.2.dist-info}/WHEEL +0 -0
- {voxcity-0.7.0.dist-info → voxcity-1.0.2.dist-info}/licenses/AUTHORS.rst +0 -0
- {voxcity-0.7.0.dist-info → voxcity-1.0.2.dist-info}/licenses/LICENSE +0 -0
voxcity/exporter/magicavoxel.py
CHANGED
|
@@ -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")
|