MySafeCode commited on
Commit
2b5b6bf
·
verified ·
1 Parent(s): e5b87f1

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +280 -61
app.py CHANGED
@@ -1,72 +1,291 @@
1
- """
2
- Gradio app: robust .vox → .glb conversion + built-in Model3D viewer.
3
- Handles small and large voxel files, centers and colors mesh to avoid 'paper icon'.
4
- """
5
-
6
  import gradio as gr
7
- import tempfile
8
- import trimesh
9
  import numpy as np
 
 
 
10
  from pathlib import Path
11
- from pyvox.parser import VoxParser
12
-
13
-
14
- def vox_to_glb(file_path):
15
- vox = VoxParser(file_path).parse()
16
- model = vox.models[0]
17
- voxels = model.voxels
18
- size_x, size_y, size_z = model.size.x, model.size.y, model.size.z
19
 
20
- if not voxels:
21
- raise ValueError("No voxels found")
 
22
 
23
- # Small grid: cube per voxel, Large grid: marching cubes
24
- if max(size_x, size_y, size_z) <= 16:
25
- cubes = []
26
- for v in voxels:
27
- cube = trimesh.creation.box(extents=(1,1,1))
28
- cube.apply_translation([v.x, v.y, v.z])
29
- cubes.append(cube)
30
- mesh = trimesh.util.concatenate(cubes)
31
- else:
32
- grid = np.zeros((size_x, size_y, size_z), dtype=bool)
33
- for v in voxels:
34
- grid[v.x, v.y, v.z] = True
35
- mesh = trimesh.voxel.ops.matrix_to_marching_cubes(grid)
36
-
37
- # Center mesh, scale, add color
38
- mesh.apply_translation(-mesh.centroid)
39
- mesh.apply_scale(1.0)
40
- mesh.visual.vertex_colors = [200, 100, 50, 255]
41
-
42
- tmp = tempfile.NamedTemporaryFile(suffix=".glb", delete=False)
43
- mesh.export(tmp.name)
44
- return tmp.name
45
-
46
-
47
- def handle_upload(file):
48
- if file is None:
49
- return None
50
- ext = Path(file.name).suffix.lower()
51
- if ext == '.vox':
52
  try:
53
- return vox_to_glb(file.name)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  except Exception as e:
55
- print(f"Failed to convert vox: {e}")
56
- return None
57
- elif ext in ['.glb', '.gltf']:
58
- return file.name
59
- else:
60
- return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
 
63
- with gr.Blocks() as demo:
64
- gr.Markdown("# VOX / GLB Viewer\nUpload a `.vox` or `.glb/.gltf` file to preview.")
65
- upload = gr.File(file_types=['.vox','.glb','.gltf'], label="Upload File")
66
- viewer = gr.Model3D(label="Preview")
67
-
68
- upload.change(handle_upload, upload, viewer)
 
 
 
 
 
69
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
 
71
- if __name__ == '__main__':
72
- demo.launch()
 
 
 
 
 
 
 
 
 
 
 
 
1
  import gradio as gr
 
 
2
  import numpy as np
3
+ import trimesh
4
+ import tempfile
5
+ import os
6
  from pathlib import Path
7
+ import logging
8
+ from typing import Tuple, Optional
9
+ import struct
 
 
 
 
 
10
 
11
+ # Configure logging
12
+ logging.basicConfig(level=logging.INFO)
13
+ logger = logging.getLogger(__name__)
14
 
15
+ class VoxParser:
16
+ """Parse MagicaVoxel .vox files"""
17
+ def __init__(self, file_path):
18
+ self.file_path = file_path
19
+ self.voxels = []
20
+ self.palette = []
21
+ self.size = {}
22
+
23
+ def parse(self) -> dict:
24
+ """Parse the .vox file and extract voxel data"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  try:
26
+ with open(self.file_path, 'rb') as f:
27
+ # Read header
28
+ header = f.read(4).decode('ascii')
29
+ if header != 'VOX ':
30
+ raise ValueError("Invalid VOX file header")
31
+
32
+ version = struct.unpack('<I', f.read(4))[0]
33
+
34
+ # Parse chunks
35
+ while True:
36
+ chunk_data = f.read(8)
37
+ if len(chunk_data) < 8:
38
+ break
39
+
40
+ chunk_id = chunk_data[:4].decode('ascii')
41
+ chunk_size, child_size = struct.unpack('<II', chunk_data[4:])
42
+
43
+ chunk_content = f.read(chunk_size)
44
+
45
+ if chunk_id == 'SIZE':
46
+ self.parse_size(chunk_content)
47
+ elif chunk_id == 'XYZI':
48
+ self.parse_voxels(chunk_content)
49
+ elif chunk_id == 'RGBA':
50
+ self.parse_palette(chunk_content)
51
+ else:
52
+ # Skip unknown chunks
53
+ if chunk_size > 0:
54
+ f.seek(chunk_size, 1)
55
+
56
+ return {
57
+ 'voxels': self.voxels,
58
+ 'palette': self.palette or self._default_palette(),
59
+ 'size': self.size
60
+ }
61
  except Exception as e:
62
+ logger.error(f"Error parsing VOX file: {e}")
63
+ raise
64
+
65
+ def parse_size(self, data: bytes):
66
+ """Parse size chunk"""
67
+ self.size = {
68
+ 'x': struct.unpack('<I', data[:4])[0],
69
+ 'y': struct.unpack('<I', data[4:8])[0],
70
+ 'z': struct.unpack('<I', data[8:12])[0]
71
+ }
72
+
73
+ def parse_voxels(self, data: bytes):
74
+ """Parse voxel data"""
75
+ num_voxels = struct.unpack('<I', data[:4])[0]
76
+ offset = 4
77
+
78
+ for i in range(num_voxels):
79
+ if offset + 4 <= len(data):
80
+ x, y, z, color_index = struct.unpack('BBBB', data[offset:offset+4])
81
+ self.voxels.append({
82
+ 'x': x,
83
+ 'y': y,
84
+ 'z': z,
85
+ 'color_index': color_index
86
+ })
87
+ offset += 4
88
+
89
+ def parse_palette(self, data: bytes):
90
+ """Parse palette data"""
91
+ offset = 0
92
+ for i in range(256):
93
+ if offset + 4 <= len(data):
94
+ r, g, b, a = struct.unpack('BBBB', data[offset:offset+4])
95
+ self.palette.append({
96
+ 'r': r,
97
+ 'g': g,
98
+ 'b': b,
99
+ 'a': a
100
+ })
101
+ offset += 4
102
+
103
+ def _default_palette(self) -> list:
104
+ """Default MagicaVoxel palette if none found"""
105
+ colors = []
106
+ # Create a simple gradient palette
107
+ for i in range(256):
108
+ intensity = i / 255.0
109
+ colors.append({
110
+ 'r': int(intensity * 255),
111
+ 'g': int(intensity * 255),
112
+ 'b': int(intensity * 255),
113
+ 'a': 255
114
+ })
115
+ return colors
116
 
117
+ class VoxToGlbConverter:
118
+ """Convert MagicaVoxel files to GLB format"""
119
+
120
+ def __init__(self):
121
+ self.voxel_size = 1.0
122
+
123
+ def vox_to_glb(self, vox_file_path: str) -> Tuple[str, str]:
124
+ """
125
+ Convert .vox file to .glb file
126
+
127
+ Args:
128
+ vox_file_path: Path to the .vox file
129
+
130
+ Returns:
131
+ Tuple of (glb_file_path, status_message)
132
+ """
133
+ try:
134
+ # Parse the .vox file
135
+ parser = VoxParser(vox_file_path)
136
+ voxel_data = parser.parse()
137
+
138
+ if not voxel_data['voxels']:
139
+ return "", "No voxels found in the file"
140
+
141
+ # Create mesh from voxels
142
+ mesh = self.create_mesh_from_voxels(voxel_data)
143
+
144
+ # Save as GLB
145
+ output_path = str(Path(vox_file_path).with_suffix('.glb'))
146
+ mesh.export(output_path)
147
+
148
+ return output_path, f"Successfully converted {len(voxel_data['voxels'])} voxels to GLB format"
149
+
150
+ except Exception as e:
151
+ logger.error(f"Conversion error: {e}")
152
+ return "", f"Error converting file: {str(e)}"
153
+
154
+ def create_mesh_from_voxels(self, voxel_data: dict) -> trimesh.Trimesh:
155
+ """Create a trimesh from voxel data"""
156
+ voxels = voxel_data['voxels']
157
+ palette = voxel_data['palette']
158
+
159
+ # Group voxels by color for better performance
160
+ color_groups = {}
161
+ for voxel in voxels:
162
+ color_idx = voxel['color_index']
163
+ if color_idx not in color_groups:
164
+ color_groups[color_idx] = []
165
+ color_groups[color_idx].append(voxel)
166
+
167
+ # Create meshes for each color group
168
+ meshes = []
169
+
170
+ for color_idx, voxels in color_groups.items():
171
+ color = palette[color_idx] if color_idx < len(palette) else {'r': 255, 'g': 255, 'b': 255, 'a': 255}
172
+
173
+ # Create instanced cubes for this color
174
+ cube = trimesh.creation.box(extents=[self.voxel_size, self.voxel_size, self.voxel_size])
175
+
176
+ for voxel in voxels:
177
+ # Position the cube
178
+ translation = trimesh.transformations.translation_matrix([
179
+ voxel['x'] * self.voxel_size,
180
+ voxel['z'] * self.voxel_size, # Swap Y and Z for proper orientation
181
+ voxel['y'] * self.voxel_size
182
+ ])
183
+
184
+ transformed_cube = cube.copy()
185
+ transformed_cube.apply_transform(translation)
186
+
187
+ # Set vertex colors
188
+ vertex_colors = np.tile([color['r']/255, color['g']/255, color['b']/255, color['a']/255],
189
+ (len(transformed_cube.vertices), 1))
190
+ transformed_cube.visual.vertex_colors = vertex_colors
191
+
192
+ meshes.append(transformed_cube)
193
+
194
+ # Combine all meshes
195
+ if meshes:
196
+ combined = trimesh.util.concatenate(meshes)
197
+ return combined
198
+ else:
199
+ # Fallback: create a simple cube
200
+ return trimesh.creation.box(extents=[self.voxel_size, self.voxel_size, self.voxel_size])
201
 
202
+ def process_vox_file(vox_file) -> Tuple[str, str]:
203
+ """Process uploaded .vox file and convert to .glb"""
204
+ if vox_file is None:
205
+ return "", "Please upload a .vox file"
206
+
207
+ try:
208
+ converter = VoxToGlbConverter()
209
+ glb_path, message = converter.vox_to_glb(vox_file.name)
210
+ return glb_path, message
211
+ except Exception as e:
212
+ return "", f"Error: {str(e)}"
213
 
214
+ def create_gradio_interface():
215
+ """Create the Gradio interface"""
216
+
217
+ with gr.Blocks(title="VOX to GLB Converter", theme=gr.themes.Soft()) as app:
218
+ gr.Markdown("""
219
+ # 🧊 MagicaVoxel to GLB Converter
220
+
221
+ Convert your MagicaVoxel `.vox` files to `.glb` format for preview and use in 3D applications.
222
+
223
+ **Features:**
224
+ - ✅ Preserves voxel colors and structure
225
+ - ✅ Optimized for 3D preview
226
+ - ✅ Handles legacy VOX formats (2019-2020)
227
+ - ✅ Downloads as GLB file
228
+ """)
229
+
230
+ with gr.Row():
231
+ with gr.Column():
232
+ vox_input = gr.File(
233
+ label="Upload VOX File",
234
+ file_types=[".vox"],
235
+ file_count="single"
236
+ )
237
+
238
+ convert_btn = gr.Button("Convert to GLB", variant="primary")
239
+
240
+ status_output = gr.Textbox(
241
+ label="Status",
242
+ interactive=False,
243
+ placeholder="Ready to convert..."
244
+ )
245
+
246
+ with gr.Column():
247
+ glb_output = gr.File(
248
+ label="Download GLB File",
249
+ file_types=[".glb"],
250
+ interactive=False
251
+ )
252
+
253
+ preview_info = gr.Markdown("""
254
+ **Preview Info:**
255
+ - Download the GLB file
256
+ - Drag into any 3D viewer
257
+ - Compatible with Three.js, Babylon.js, Unity, Blender
258
+ """)
259
+
260
+ # Event handlers
261
+ convert_btn.click(
262
+ fn=process_vox_file,
263
+ inputs=[vox_input],
264
+ outputs=[glb_output, status_output]
265
+ )
266
+
267
+ # Examples
268
+ gr.Markdown("### 📁 Example Usage")
269
+ gr.Markdown("""
270
+ **How to use:**
271
+ 1. Click "Upload VOX File" and select your `.vox` file
272
+ 2. Click "Convert to GLB"
273
+ 3. Download the converted `.glb` file
274
+ 4. Preview in any 3D viewer or use in your projects
275
+
276
+ **Supported formats:**
277
+ - MagicaVoxel .vox files (all versions)
278
+ - Preserves colors and voxel positions
279
+ - Optimized mesh output
280
+ """)
281
+
282
+ return app
283
 
284
+ if __name__ == "__main__":
285
+ app = create_gradio_interface()
286
+ app.launch(
287
+ server_name="0.0.0.0",
288
+ server_port=7860,
289
+ share=True,
290
+ show_error=True
291
+ )