File size: 9,159 Bytes
e937288
f4b5e4a
2b5b6bf
 
 
 
06a5ef5
 
f4b5e4a
2b5b6bf
39806b8
06a5ef5
 
 
2b5b6bf
 
39806b8
f4b5e4a
06a5ef5
 
5c0d072
06a5ef5
1578be5
 
06a5ef5
 
1578be5
 
 
0eacd6a
06a5ef5
 
 
 
1578be5
 
06a5ef5
0eacd6a
 
 
06a5ef5
 
 
 
 
 
2b5b6bf
1578be5
0eacd6a
 
 
 
 
 
06a5ef5
1578be5
0eacd6a
 
 
 
 
 
 
 
 
 
 
 
 
 
1578be5
06a5ef5
 
 
 
 
1578be5
06a5ef5
2b5b6bf
06a5ef5
1578be5
06a5ef5
1578be5
06a5ef5
 
 
1578be5
f4b5e4a
0eacd6a
2b5b6bf
 
 
 
 
 
 
 
 
 
 
 
e937288
2b5b6bf
39806b8
2b5b6bf
 
 
 
06a5ef5
 
2b5b6bf
06a5ef5
5c0d072
 
 
 
 
 
 
 
 
 
39806b8
 
1578be5
2b5b6bf
 
 
 
39806b8
2b5b6bf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
06a5ef5
2b5b6bf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e937288
2b5b6bf
39806b8
2b5b6bf
 
 
 
 
06a5ef5
39806b8
 
06a5ef5
39806b8
 
 
 
 
 
 
06a5ef5
2b5b6bf
 
e937288
2b5b6bf
8364922
2b5b6bf
39806b8
 
2b5b6bf
 
 
 
06a5ef5
39806b8
06a5ef5
2b5b6bf
 
06a5ef5
2b5b6bf
0d7e91b
39806b8
e50a35e
0d7e91b
e50a35e
 
8364922
e50a35e
39806b8
 
0d7e91b
 
e50a35e
 
39806b8
e50a35e
39806b8
2b5b6bf
39806b8
0d7e91b
e50a35e
2b5b6bf
39806b8
 
 
 
 
 
 
 
2b5b6bf
 
e937288
2b5b6bf
 
8364922
 
 
 
 
 
 
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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
import gradio as gr
import numpy as np
import trimesh
import tempfile
import os
import struct
from pathlib import Path
from typing import Tuple

class VoxParser:
    """MagicaVoxel .vox file parser"""
    
    def __init__(self, file_path):
        self.file_path = file_path
        
    def parse(self) -> dict:
        """Parse the .vox file structure"""
        try:
            with open(self.file_path, 'rb') as f:
                data = f.read()
                
            offset = 0
            
            # Read header
            header = data[offset:offset+4].decode('ascii', errors='ignore')
            offset += 4
            if header != 'VOX ':
                raise ValueError("Invalid VOX file header")
            
            offset += 4  # Skip version
            
            voxels = []
            palette = []
            size = {}
            
            # Parse chunks
            while offset < len(data):
                if offset + 12 > len(data):
                    break
                    
                chunk_id = data[offset:offset+4].decode('ascii', errors='ignore')
                offset += 4
                chunk_size = struct.unpack('<I', data[offset:offset+4])[0]
                offset += 4
                child_size = struct.unpack('<I', data[offset:offset+4])[0]
                offset += 4
                
                if chunk_id == 'SIZE':
                    if offset + 12 <= len(data):
                        size = {
                            'x': struct.unpack('<I', data[offset:offset+4])[0],
                            'y': struct.unpack('<I', data[offset+4:offset+8])[0],
                            'z': struct.unpack('<I', data[offset+8:offset+12])[0]
                        }
                    offset += chunk_size
                elif chunk_id == 'XYZI':
                    if offset + 4 <= len(data):
                        num_voxels = struct.unpack('<I', data[offset:offset+4])[0]
                        offset += 4
                        
                        for i in range(num_voxels):
                            if offset + 4 <= len(data):
                                x, y, z, color_index = struct.unpack('BBBB', data[offset:offset+4])
                                voxels.append({
                                    'x': x,
                                    'y': y,
                                    'z': z,
                                    'color_index': color_index
                                })
                                offset += 4
                elif chunk_id == 'RGBA':
                    for i in range(256):
                        if offset + 4 <= len(data):
                            r, g, b, a = struct.unpack('BBBB', data[offset:offset+4])
                            palette.append({'r': r, 'g': g, 'b': b, 'a': a})
                            offset += 4
                else:
                    offset += chunk_size
                
                if offset >= len(data):
                    break
            
            return {
                'voxels': voxels,
                'palette': palette or self._default_palette(),
                'size': size
            }
        except Exception as e:
            return {'voxels': [], 'palette': self._default_palette(), 'size': {'x': 0, 'y': 0, 'z': 0}}
    
    def _default_palette(self) -> list:
        colors = []
        for i in range(256):
            intensity = i / 255.0
            colors.append({
                'r': int(intensity * 255),
                'g': int(intensity * 255),
                'b': int(intensity * 255),
                'a': 255
            })
        return colors

class VoxToGlbConverter:
    """MagicaVoxel to GLB converter"""
    
    def __init__(self):
        self.voxel_size = 1.0
        
    def vox_to_glb(self, vox_file_path: str) -> Tuple[str, str]:
        """Convert .vox file to .glb file"""
        try:
            parser = VoxParser(vox_file_path)
            voxel_data = parser.parse()
            
            if not voxel_data['voxels']:
                return "", "No voxels found in the file"
            
            mesh = self.create_mesh_from_voxels(voxel_data)
            
            output_path = str(Path(tempfile.gettempdir()) / f"converted_model.glb")
            mesh.export(output_path)
            
            voxel_count = len(voxel_data['voxels'])
            return output_path, f"Converted {voxel_count} voxels to GLB format"
                
        except Exception as e:
            return "", f"Error converting file: {str(e)}"
    
    def create_mesh_from_voxels(self, voxel_data: dict) -> trimesh.Trimesh:
        """Create mesh from voxel data"""
        voxels = voxel_data['voxels']
        palette = voxel_data['palette']
        
        color_groups = {}
        for voxel in voxels:
            color_idx = voxel['color_index']
            if color_idx not in color_groups:
                color_groups[color_idx] = []
            color_groups[color_idx].append(voxel)
        
        meshes = []
        
        for color_idx, voxels in color_groups.items():
            color = palette[color_idx] if color_idx < len(palette) else {'r': 255, 'g': 255, 'b': 255, 'a': 255}
            
            cube = trimesh.creation.box(extents=[self.voxel_size, self.voxel_size, self.voxel_size])
            
            for voxel in voxels:
                translation = trimesh.transformations.translation_matrix([
                    voxel['x'] * self.voxel_size,
                    voxel['z'] * self.voxel_size,
                    voxel['y'] * self.voxel_size
                ])
                
                transformed_cube = cube.copy()
                transformed_cube.apply_transform(translation)
                
                vertex_colors = np.tile([color['r']/255, color['g']/255, color['b']/255, color['a']/255], 
                                      (len(transformed_cube.vertices), 1))
                transformed_cube.visual.vertex_colors = vertex_colors
                
                meshes.append(transformed_cube)
        
        if meshes:
            combined = trimesh.util.concatenate(meshes)
            return combined
        else:
            return trimesh.creation.box(extents=[self.voxel_size, self.voxel_size, self.voxel_size])

def process_vox_file(vox_file) -> Tuple[str, str]:
    """Process uploaded .vox file and convert to .glb"""
    if vox_file is None:
        return "", "Please upload a .vox file"
    
    try:
        converter = VoxToGlbConverter()
        
        if hasattr(vox_file, 'name'):
            real_file_path = vox_file.name
            
            if os.path.isfile(real_file_path):
                glb_path, message = converter.vox_to_glb(real_file_path)
                return glb_path, message
            else:
                return "", "Could not access uploaded file"
        else:
            return "", "Invalid file format"
            
    except Exception as e:
        return "", f"Error: {str(e)}"

def create_gradio_interface():
    with gr.Blocks(title="VOX to GLB Converter with Preview") as app:
        gr.Markdown("""
        # ๐ŸงŠ MagicaVoxel VOX to GLB Converter with 3D Preview
        Convert your MagicaVoxel `.vox` files to `.glb` format and preview them in 3D
        """)
        
        with gr.Row():
            with gr.Column():
                vox_input = gr.File(label="Upload VOX File", file_types=[".vox"], file_count="single")
                convert_btn = gr.Button("๐Ÿ”„ Convert to GLB", variant="primary")
                status_output = gr.Textbox(label="Status", interactive=False, placeholder="Ready...")
                
            with gr.Column():
                glb_output = gr.File(label="Download GLB File", file_types=[".glb"], interactive=False)
        
        # 3D Preview Section - Using Gradio's built-in 3D viewer
        gr.Markdown("### ๐ŸŽฎ 3D Preview")
        
        # FIXED: Use correct variable name
        model_3d = gr.Model3D(
            label="GLB Preview",
            height=300
        )
        
        # Connect conversion to preview
        def convert_with_preview(vox_input):
            glb_path, message = process_vox_file(vox_input)
            if glb_path and os.path.exists(glb_path):
                return glb_path, message, glb_path  # Return path for both file and 3D viewer
            else:
                return None, message, None
        
        convert_btn.click(
            fn=convert_with_preview,
            inputs=[vox_input],  # FIXED: Use correct variable name
            outputs=[glb_output, status_output, model_3d]
        )
        
        gr.Markdown("""
        ### ๐Ÿ“‹ How to Use
        1. Upload your `.vox` file
        2. Click "Convert to GLB"
        3. Download the GLB file
        4. Preview your voxel model in 3D above
        """)
    
    return app

if __name__ == "__main__":
    app = create_gradio_interface()
    app.launch(
        server_name="0.0.0.0",
        server_port=7860,
        share=True,
        show_error=True,
        theme=gr.themes.Soft()
    )