threejsGLBandVoxels / index.html
MySafeCode's picture
Update index.html
9ba4a16 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VOX Color Fix - Amplified Viewer</title>
<style>
body {
margin: 0;
padding: 0;
overflow: hidden;
font-family: Arial, sans-serif;
background: #1a1a1a;
}
#container {
width: 100vw;
height: 100vh;
}
#controls {
position: absolute;
top: 10px;
left: 10px;
color: white;
background: rgba(0,0,0,0.8);
padding: 15px;
border-radius: 8px;
max-width: 400px;
}
#controls h3 {
margin-top: 0;
color: #4CAF50;
}
.control-group {
margin: 12px 0;
}
label {
display: inline-block;
width: 160px;
font-size: 12px;
}
input[type="file"] {
display: none;
}
.btn {
background: #4CAF50;
color: white;
border: none;
padding: 8px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 11px;
margin: 2px;
}
.btn:hover {
background: #45a049;
}
.btn.active {
background: #2196F3;
}
.btn:disabled {
background: #666;
cursor: not-allowed;
}
#info {
position: absolute;
bottom: 10px;
left: 10px;
color: white;
background: rgba(0,0,0,0.7);
padding: 10px;
border-radius: 5px;
font-size: 12px;
}
#loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
font-size: 18px;
display: none;
}
.model-info {
font-size: 11px;
color: #ccc;
margin-top: 8px;
min-height: 30px;
}
.render-mode {
display: flex;
gap: 5px;
margin: 8px 0;
}
.render-mode button {
flex: 1;
}
.file-types {
font-size: 10px;
color: #aaa;
margin-top: 4px;
}
.status-indicator {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 6px;
}
.status-indicator.glb { background: #FF9800; }
.status-indicator.vox { background: #4CAF50; }
.status-indicator.none { background: #666; }
.rotation-controls {
background: rgba(255,255,255,0.1);
padding: 8px;
border-radius: 4px;
margin: 8px 0;
}
</style>
</head>
<body>
<div id="container"></div>
<div id="controls">
<h3>VOX Color Amplifier</h3>
<div class="control-group">
<button class="btn" onclick="document.getElementById('glbFileInput').click()">Load GLB/GLTF</button>
<input type="file" id="glbFileInput" accept=".glb,.gltf">
<div class="file-types">GLB, GLTF files</div>
</div>
<div class="control-group">
<button class="btn" onclick="document.getElementById('voxFileInput').click()">Load VOX File</button>
<input type="file" id="voxFileInput" accept=".vox">
<div class="file-types">MagicaVoxel files</div>
</div>
<div class="control-group" id="voxControls" style="display: none;">
<label>VOX Render Mode:</label>
<div class="render-mode">
<button class="btn active" id="boxMode" onclick="setRenderMode('box')">Boxes</button>
<button class="btn" id="ballMode" onclick="setRenderMode('ball')">Balls</button>
</div>
</div>
<div class="control-group" id="voxelSizeControl" style="display: none;">
<label>Voxel Size:</label>
<input type="range" id="voxelSize" min="0.3" max="1.5" step="0.1" value="0.8">
<span id="voxelSizeValue">0.8</span>
</div>
<div class="control-group" id="ballSegmentsControl" style="display: none;">
<label>Ball Segments:</label>
<input type="range" id="ballSegments" min="4" max="16" step="1" value="8">
<span id="ballSegmentsValue">8</span>
</div>
<div class="control-group" id="colorAmplifyControl" style="display: none;">
<label>Color Amplify:</label>
<input type="range" id="colorAmplify" min="0.1" max="50" step="0.1" value="1.0">
<span id="colorAmplifyValue">1.0</span>x
</div>
<div class="rotation-controls">
<div class="control-group">
<label>Rotation Center:</label>
<button class="btn" onclick="centerRotationOnModel()">Center on Model</button>
</div>
<div class="control-group">
<label>Default Position:</label>
<button class="btn" onclick="resetToDefaultPosition()">Reset Position</button>
</div>
</div>
<div class="control-group">
<label>Rotation Speed:</label>
<input type="range" id="rotationSpeed" min="0" max="0.02" step="0.001" value="0.005">
</div>
<div class="control-group">
<label>Auto Rotate:</label>
<input type="checkbox" id="autoRotate" checked>
</div>
<div class="control-group">
<button class="btn" onclick="resetView()">Reset View</button>
<button class="btn" onclick="loadSampleModel('chest')">Load Sample Chest</button>
</div>
<div class="model-info">
<span class="status-indicator" id="statusIndicator"></span>
<span id="modelInfo">No model loaded</span>
</div>
</div>
<div id="loading">Loading...</div>
<div id="info">Click and drag to rotate • Scroll to zoom • Use color amplifier for VOX files!</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/loaders/GLTFLoader.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/controls/OrbitControls.js"></script>
<script>
// Fixed VOX parser with proper color handling and amplification
class VOXParser {
static parse(buffer) {
const view = new DataView(buffer);
let offset = 0;
const header = new TextDecoder().decode(buffer.slice(0, 4));
if (header !== 'VOX ') {
throw new Error('Invalid VOX file');
}
offset = 8; // Skip version
const chunks = {};
while (offset < buffer.byteLength) {
const chunkId = new TextDecoder().decode(buffer.slice(offset, offset + 4));
const chunkSize = view.getUint32(offset + 4, true);
const childSize = view.getUint32(offset + 8, true);
offset += 12;
if (chunkId === 'SIZE') {
const sizeX = view.getUint32(offset, true);
const sizeY = view.getUint32(offset + 4, true);
const sizeZ = view.getUint32(offset + 8, true);
chunks.size = { x: sizeX, y: sizeY, z: sizeZ };
} else if (chunkId === 'XYZI') {
const numVoxels = view.getUint32(offset, true);
const voxels = [];
for (let i = 0; i < numVoxels; i++) {
const x = view.getUint8(offset + 4 + i * 4);
const y = view.getUint8(offset + 4 + i * 4 + 1);
const z = view.getUint8(offset + 4 + i * 4 + 2);
const colorIndex = view.getUint8(offset + 4 + i * 4 + 3);
voxels.push({ x, y, z, colorIndex });
}
chunks.voxels = voxels;
} else if (chunkId === 'RGBA') {
const colors = [];
for (let i = 0; i < 256; i++) {
const r = view.getUint8(offset + i * 4);
const g = view.getUint8(offset + i * 4 + 1);
const b = view.getUint8(offset + i * 4 + 2);
const a = view.getUint8(offset + i * 4 + 3);
colors.push({ r, g, b, a });
}
chunks.palette = colors;
}
offset += chunkSize;
}
return chunks;
}
static createInstancedGeometry(voxData, renderMode = 'box', voxelSize = 0.8, ballSegments = 8, colorAmplify = 1.0) {
if (!voxData.voxels || !voxData.size) return null;
const voxels = voxData.voxels;
const palette = voxData.palette || this.generateDefaultPalette();
// Create base geometry based on render mode
let baseGeometry;
if (renderMode === 'ball') {
baseGeometry = new THREE.SphereGeometry(voxelSize * 0.5, ballSegments, ballSegments);
} else {
baseGeometry = new THREE.BoxGeometry(voxelSize, voxelSize, voxelSize);
}
// Create materials for different colors instead of using vertex colors
const materials = [];
const colorMap = new Map();
// Create materials for each unique color in the palette
for (let i = 0; i < palette.length; i++) {
const color = palette[i];
const material = new THREE.MeshPhongMaterial({
color: new THREE.Color(
Math.min(1.0, (color.r / 255) * colorAmplify),
Math.min(1.0, (color.g / 255) * colorAmplify),
Math.min(1.0, (color.b / 255) * colorAmplify)
),
shininess: 30
});
materials.push(material);
colorMap.set(i, materials.length - 1);
}
// Create groups of meshes by material for better performance
const meshGroups = new Map();
// Calculate offsets to center the model
const offsetX = voxData.size.x / 2;
const offsetY = voxData.size.y / 2;
const offsetZ = voxData.size.z / 2;
// Group voxels by color
for (let i = 0; i < voxels.length; i++) {
const voxel = voxels[i];
const colorIndex = voxel.colorIndex - 1; // VOX colors start at 1
if (colorIndex >= 0 && colorIndex < palette.length) {
const materialIndex = colorIndex;
if (!meshGroups.has(materialIndex)) {
meshGroups.set(materialIndex, []);
}
meshGroups.get(materialIndex).push({
x: voxel.x - offsetX,
y: voxel.y - offsetY,
z: voxel.z - offsetZ
});
}
}
// Create a group to hold all the voxel meshes
const voxelGroup = new THREE.Group();
// Create instanced meshes for each color group
for (const [materialIndex, positions] of meshGroups) {
if (positions.length === 0) continue;
const instancedMesh = new THREE.InstancedMesh(
baseGeometry,
materials[materialIndex],
positions.length
);
const dummy = new THREE.Object3D();
for (let i = 0; i < positions.length; i++) {
const pos = positions[i];
dummy.position.set(pos.x, pos.y, pos.z);
dummy.updateMatrix();
instancedMesh.setMatrixAt(i, dummy.matrix);
}
instancedMesh.castShadow = true;
instancedMesh.receiveShadow = true;
voxelGroup.add(instancedMesh);
}
return voxelGroup;
}
static generateDefaultPalette() {
const palette = [];
for (let i = 0; i < 256; i++) {
palette.push({
r: Math.random() * 255,
g: Math.random() * 255,
b: Math.random() * 255,
a: 255
});
}
return palette;
}
}
</script>
<script>
// Scene setup
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x2a2a2a);
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(8, 8, 12);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.getElementById('container').appendChild(renderer.domElement);
// Controls
const controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
// Store original camera position for reset
const defaultCameraPosition = new THREE.Vector3(8, 8, 12);
const defaultCameraLookAt = new THREE.Vector3(0, 0, 0);
// Lighting setup - make it brighter for better color visibility
const ambientLight = new THREE.AmbientLight(0xffffff, 0.8);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.0);
directionalLight.position.set(10, 10, 5);
directionalLight.castShadow = true;
directionalLight.shadow.mapSize.width = 2048;
directionalLight.shadow.mapSize.height = 2048;
scene.add(directionalLight);
// Current state
let currentModel = null;
let currentVOXData = null;
let currentFileType = 'none'; // 'none', 'glb', 'vox'
let autoRotateEnabled = true;
let currentRenderMode = 'box';
let currentColorAmplify = 1.0;
let modelCenter = new THREE.Vector3(0, 0, 0); // Store model center for rotation
// File input handlers
document.getElementById('glbFileInput').addEventListener('change', handleGLBLoad);
document.getElementById('voxFileInput').addEventListener('change', handleVOXLoad);
function handleGLBLoad(event) {
const file = event.target.files[0];
if (!file) return;
showLoading(true);
const reader = new FileReader();
reader.onload = function(e) {
const loader = new THREE.GLTFLoader();
loader.parse(e.target.result, '', function(gltf) {
const model = gltf.scene;
// Calculate model center
const box = new THREE.Box3().setFromObject(model);
modelCenter = box.getCenter(new THREE.Vector3());
loadModel(model, 'glb');
showVOXControls(false);
showLoading(false);
updateModelInfo(`GLB/GLTF: ${file.name}`, 'glb');
}, function(error) {
console.error('Error loading GLTF:', error);
showLoading(false);
});
};
reader.readAsArrayBuffer(file);
}
function handleVOXLoad(event) {
const file = event.target.files[0];
if (!file) return;
showLoading(true);
const reader = new FileReader();
reader.onload = function(e) {
try {
const voxData = VOXParser.parse(e.target.result);
currentVOXData = voxData;
// Calculate model center
modelCenter.set(
voxData.size.x / 2 - voxData.size.x / 2,
voxData.size.y / 2 - voxData.size.y / 2,
voxData.size.z / 2 - voxData.size.z / 2
);
const voxelGroup = VOXParser.createInstancedGeometry(
voxData,
currentRenderMode,
parseFloat(document.getElementById('voxelSize').value),
parseInt(document.getElementById('ballSegments').value),
currentColorAmplify
);
if (voxelGroup) {
loadModel(voxelGroup, 'vox');
showVOXControls(true);
updateModelInfo(`VOX: ${file.name} (${voxData.voxels.length} voxels)`, 'vox');
}
} catch (error) {
console.error('Error loading VOX:', error);
}
showLoading(false);
};
reader.readAsArrayBuffer(file);
}
function loadModel(model, fileType) {
if (currentModel) {
scene.remove(currentModel);
if (currentModel.dispose) currentModel.dispose();
}
currentModel = model;
currentFileType = fileType;
scene.add(model);
// Center and scale model
const box = new THREE.Box3().setFromObject(model);
const center = box.getCenter(new THREE.Vector3());
const size = box.getSize(new THREE.Vector3());
// Update model center for rotation
modelCenter.copy(center);
model.position.sub(center);
const maxDim = Math.max(size.x, size.y, size.z);
const scale = 6 / maxDim;
model.scale.multiplyScalar(scale);
// Enable shadows
model.traverse(function(child) {
if (child.isMesh) {
child.castShadow = true;
child.receiveShadow = true;
}
});
}
function showVOXControls(show) {
const voxControls = document.getElementById('voxControls');
const voxelSizeControl = document.getElementById('voxelSizeControl');
const ballSegmentsControl = document.getElementById('ballSegmentsControl');
const colorAmplifyControl = document.getElementById('colorAmplifyControl');
voxControls.style.display = show ? 'block' : 'none';
voxelSizeControl.style.display = show ? 'block' : 'none';
ballSegmentsControl.style.display = show && currentRenderMode === 'ball' ? 'block' : 'none';
colorAmplifyControl.style.display = show ? 'block' : 'none';
}
function setRenderMode(mode) {
currentRenderMode = mode;
// Update button states
document.getElementById('boxMode').classList.toggle('active', mode === 'box');
document.getElementById('ballMode').classList.toggle('active', mode === 'ball');
// Show/hide ball segments control
const ballSegmentsControl = document.getElementById('ballSegmentsControl');
ballSegmentsControl.style.display = mode === 'ball' ? 'block' : 'none';
// Rebuild geometry if we have VOX data
if (currentVOXData && currentFileType === 'vox') {
const voxelGroup = VOXParser.createInstancedGeometry(
currentVOXData,
mode,
parseFloat(document.getElementById('voxelSize').value),
parseInt(document.getElementById('ballSegments').value),
currentColorAmplify
);
if (voxelGroup) {
loadModel(voxelGroup, 'vox');
}
}
}
function updateVoxelSize() {
const value = parseFloat(document.getElementById('voxelSize').value);
document.getElementById('voxelSizeValue').textContent = value.toFixed(1);
if (currentVOXData && currentFileType === 'vox') {
setRenderMode(currentRenderMode);
}
}
function updateBallSegments() {
const value = parseInt(document.getElementById('ballSegments').value);
document.getElementById('ballSegmentsValue').textContent = value;
if (currentVOXData && currentFileType === 'vox' && currentRenderMode === 'ball') {
setRenderMode('ball');
}
}
function updateColorAmplify() {
const value = parseFloat(document.getElementById('colorAmplify').value);
document.getElementById('colorAmplifyValue').textContent = value.toFixed(1);
currentColorAmplify = value;
if (currentVOXData && currentFileType === 'vox') {
setRenderMode(currentRenderMode);
}
}
function centerRotationOnModel() {
if (!currentModel) return;
// Move controls target to model center
controls.target.copy(modelCenter);
controls.update();
// Optional: Move camera to look at the center
camera.lookAt(modelCenter);
}
function resetToDefaultPosition() {
// Reset camera to default position
camera.position.copy(defaultCameraPosition);
camera.lookAt(defaultCameraLookAt);
// Reset controls target to origin
controls.target.copy(defaultCameraLookAt);
controls.update();
}
function loadSampleModel(type) {
if (type === 'chest') {
const chestGroup = new THREE.Group();
const woodMaterial = new THREE.MeshPhongMaterial({ color: 0x8B4513 });
const metalMaterial = new THREE.MeshPhongMaterial({ color: 0x444444 });
const baseGeometry = new THREE.BoxGeometry(2, 1, 1.2);
const base = new THREE.Mesh(baseGeometry, woodMaterial);
base.position.y = -0.3;
base.castShadow = true;
chestGroup.add(base);
const lidGeometry = new THREE.BoxGeometry(2, 0.8, 1.2);
const lid = new THREE.Mesh(lidGeometry, woodMaterial);
lid.position.y = 0.2;
lid.castShadow = true;
chestGroup.add(lid);
const bandGeometry = new THREE.BoxGeometry(2.05, 0.08, 1.25);
const band = new THREE.Mesh(bandGeometry, metalMaterial);
band.position.y = -0.1;
chestGroup.add(band);
// Calculate center
const box = new THREE.Box3().setFromObject(chestGroup);
modelCenter = box.getCenter(new THREE.Vector3());
loadModel(chestGroup, 'glb');
showVOXControls(false);
updateModelInfo('Sample Chest Model (GLB)', 'glb');
}
}
function showLoading(show) {
document.getElementById('loading').style.display = show ? 'block' : 'none';
}
function updateModelInfo(info, fileType) {
const statusIndicator = document.getElementById('statusIndicator');
const modelInfo = document.getElementById('modelInfo');
statusIndicator.className = `status-indicator ${fileType || 'none'}`;
modelInfo.textContent = info;
}
function resetView() {
resetToDefaultPosition();
}
// Control event listeners
document.getElementById('autoRotate').addEventListener('change', function(e) {
autoRotateEnabled = e.target.checked;
});
document.getElementById('voxelSize').addEventListener('input', updateVoxelSize);
document.getElementById('ballSegments').addEventListener('input', updateBallSegments);
document.getElementById('colorAmplify').addEventListener('input', updateColorAmplify);
// Animation loop
function animate() {
requestAnimationFrame(animate);
controls.update();
if (currentModel && autoRotateEnabled) {
currentModel.rotation.y += parseFloat(document.getElementById('rotationSpeed').value);
}
renderer.render(scene, camera);
}
// Handle window resize
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// Initialize
updateVoxelSize();
updateBallSegments();
updateColorAmplify();
updateModelInfo('Ready to load models', 'none');
animate();
</script>
</body>
</html>