voxandgltf / app.py
MySafeCode's picture
Update app.py
a679d69 verified
raw
history blame
13.8 kB
import gradio as gr
import numpy as np
import trimesh
import tempfile
import os
import struct
import logging
from pathlib import Path
from typing import Tuple
import shutil
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
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")
version = struct.unpack('<I', data[offset:offset+4])[0]
offset += 4
voxels = []
palette = []
size = {}
# Parse chunks
while offset < len(data):
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':
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':
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:
raise ValueError(f"Error parsing VOX file: {e}")
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:
logger.error(f"Conversion error: {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_glb_preview(glb_file):
"""Create HTML preview for GLB file - FIXED SYNTAX ERROR"""
if glb_file is None:
return "<p>No GLB file to preview</p>"
if hasattr(glb_file, 'name'):
glb_path = glb_file.name
else:
glb_path = str(glb_file)
if not os.path.exists(glb_path):
return "<p>GLB file not found</p>"
# Create HTML with Three.js viewer - FIXED THE SYNTAX ERROR
html_content = f"""
<div style="width: 100%; height: 400px; position: relative; background: #1a1a1a; border-radius: 8px; overflow: hidden;">
<div id="preview-container" style="width: 100%; height: 100%;"></div>
<div style="position: absolute; top: 10px; left: 10px; color: white; font-size: 12px; background: rgba(0,0,0,0.7); padding: 5px 10px; border-radius: 4px;">
๐ŸŽฎ Drag to rotate โ€ข Scroll to zoom โ€ข Right-click to pan
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/controls/OrbitControls.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/loaders/GLTFLoader.js"></script>
<script>
// Initialize Three.js scene
const container = document.getElementById('preview-container');
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x2a2a2a);
const camera = new THREE.PerspectiveCamera(75, container.clientWidth / container.clientHeight, 0.1, 1000);
camera.position.set(5, 5, 5);
const renderer = new THREE.WebGLRenderer({{ antialias: true }});
renderer.setSize(container.clientWidth, container.clientHeight);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
container.appendChild(renderer.domElement);
// Controls
const controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.target.set(0, 0, 0);
// Lights
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(10, 10, 10);
directionalLight.castShadow = true;
scene.add(directionalLight);
// Grid helper
const gridHelper = new THREE.GridHelper(20, 20, 0x444444, 0x333333);
scene.add(gridHelper);
// Load GLB file
const loader = new THREE.GLTFLoader();
const glbPath = '{glb_path}';
loader.load(
glbPath,
function (gltf) {{
const model = gltf.scene;
// Center and scale the model
const box = new THREE.Box3().setFromObject(model);
const center = box.getCenter(new THREE.Vector3());
const size = box.getSize(new THREE.Vector3());
// Center the model
model.position.sub(center);
// Auto-scale to fit view
const maxDim = Math.max(size.x, size.y, size.z);
const scale = 10 / maxDim;
model.scale.multiplyScalar(scale);
scene.add(model);
// Auto-rotate camera
function animate() {{
requestAnimationFrame(animate);
controls.update();
// Gentle auto-rotation
const time = Date.now() * 0.001;
camera.position.x = Math.cos(time * 0.1) * 8;
camera.position.z = Math.sin(time * 0.1) * 8;
camera.lookAt(0, 0, 0);
renderer.render(scene, camera);
}}
animate();
}},
function (progress) {{
console.log('Loading:', progress);
}},
function (error) {{
console.error('Error loading GLB:', error);
container.innerHTML = '<p style="color: white; text-align: center; padding: 20px;">Error loading GLB file</p>';
}}
);
// Handle resize
window.addEventListener('resize', function() {{
camera.aspect = container.clientWidth / container.clientHeight;
camera.updateProjectionMatrix();
renderer.setSize(container.clientWidth, container.clientHeight);
}});
</script>
"""
return html_content
def create_gradio_interface():
with gr.Blocks(title="VOX to GLB Converter with Preview", theme=gr.themes.Soft()) 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
gr.Markdown("### ๐ŸŽฎ 3D Preview")
preview_html = gr.HTML(label="GLB Preview")
# Connect conversion to preview
def convert_with_preview(vox_file):
glb_path, message = process_vox_file(vox_file)
if glb_path:
preview = create_glb_preview(glb_path)
return glb_path, message, preview
else:
return None, message, "<p>โŒ Conversion failed - no preview available</p>"
convert_btn.click(
fn=convert_with_preview,
inputs=[vox_input],
outputs=[glb_output, status_output, preview_html]
)
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)