3d-model-viewer / index.html
Sal-ONE's picture
Add 2 files
85fa8b8 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D Model Viewer</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/loaders/OBJLoader.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/loaders/GLTFLoader.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/controls/OrbitControls.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/controls/DragControls.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/controls/TrackballControls.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
.model-container {
position: relative;
width: 100%;
height: 70vh;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
border-radius: 10px;
overflow: hidden;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
}
.loading-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 100;
color: white;
font-size: 1.5rem;
flex-direction: column;
}
.spinner {
border: 5px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top: 5px solid #4f46e5;
width: 50px;
height: 50px;
animation: spin 1s linear infinite;
margin-bottom: 20px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.control-panel {
position: absolute;
top: 20px;
right: 20px;
background: rgba(30, 30, 60, 0.8);
backdrop-filter: blur(10px);
border-radius: 8px;
padding: 15px;
z-index: 10;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
max-height: 80vh;
overflow-y: auto;
}
.control-panel::-webkit-scrollbar {
width: 6px;
}
.control-panel::-webkit-scrollbar-thumb {
background: #4f46e5;
border-radius: 3px;
}
.control-panel h3 {
color: white;
margin-bottom: 15px;
font-weight: 600;
display: flex;
align-items: center;
gap: 8px;
}
.control-section {
margin-bottom: 20px;
}
.control-section:last-child {
margin-bottom: 0;
}
.control-label {
color: #d1d5db;
margin-bottom: 8px;
display: block;
font-size: 0.9rem;
}
.slider-container {
display: flex;
align-items: center;
gap: 10px;
}
.slider {
flex-grow: 1;
-webkit-appearance: none;
height: 6px;
background: #4b5563;
border-radius: 3px;
outline: none;
}
.slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 16px;
height: 16px;
border-radius: 50%;
background: #4f46e5;
cursor: pointer;
}
.slider-value {
color: white;
width: 40px;
text-align: center;
font-size: 0.85rem;
}
.toggle-switch {
position: relative;
display: inline-block;
width: 50px;
height: 24px;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #4b5563;
transition: .4s;
border-radius: 24px;
}
.toggle-slider:before {
position: absolute;
content: "";
height: 16px;
width: 16px;
left: 4px;
bottom: 4px;
background-color: white;
transition: .4s;
border-radius: 50%;
}
input:checked + .toggle-slider {
background-color: #4f46e5;
}
input:checked + .toggle-slider:before {
transform: translateX(26px);
}
.btn {
background: #4f46e5;
color: white;
border: none;
padding: 8px 15px;
border-radius: 6px;
cursor: pointer;
font-size: 0.9rem;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 8px;
}
.btn:hover {
background: #4338ca;
transform: translateY(-1px);
}
.btn-secondary {
background: #374151;
}
.btn-secondary:hover {
background: #4b5563;
}
.btn-group {
display: flex;
gap: 10px;
margin-top: 10px;
}
.file-input {
display: none;
}
.file-label {
display: block;
background: #4f46e5;
color: white;
padding: 10px 15px;
border-radius: 6px;
cursor: pointer;
text-align: center;
transition: all 0.3s ease;
margin-bottom: 15px;
}
.file-label:hover {
background: #4338ca;
}
.model-info {
position: absolute;
bottom: 20px;
left: 20px;
background: rgba(30, 30, 60, 0.8);
backdrop-filter: blur(10px);
border-radius: 8px;
padding: 12px 15px;
color: white;
font-size: 0.85rem;
z-index: 10;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
}
.model-info h4 {
margin-bottom: 5px;
font-weight: 600;
color: #d1d5db;
}
.model-info p {
margin: 3px 0;
}
.controls-hint {
position: absolute;
bottom: 20px;
right: 20px;
background: rgba(30, 30, 60, 0.8);
backdrop-filter: blur(10px);
border-radius: 8px;
padding: 12px 15px;
color: white;
font-size: 0.85rem;
z-index: 10;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
}
.controls-hint h4 {
margin-bottom: 8px;
font-weight: 600;
color: #d1d5db;
}
.controls-hint ul {
list-style-type: none;
padding: 0;
margin: 0;
}
.controls-hint li {
margin-bottom: 5px;
display: flex;
align-items: center;
gap: 8px;
}
.controls-hint li:last-child {
margin-bottom: 0;
}
.tab-container {
display: flex;
border-bottom: 1px solid #374151;
margin-bottom: 15px;
}
.tab {
padding: 8px 15px;
cursor: pointer;
color: #9ca3af;
font-size: 0.9rem;
border-bottom: 2px solid transparent;
transition: all 0.3s ease;
}
.tab.active {
color: white;
border-bottom: 2px solid #4f46e5;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
.preset-btn {
background: #374151;
color: white;
border: none;
padding: 6px 12px;
border-radius: 6px;
cursor: pointer;
font-size: 0.8rem;
transition: all 0.3s ease;
margin-right: 8px;
margin-bottom: 8px;
}
.preset-btn:hover {
background: #4b5563;
}
.preset-btn.active {
background: #4f46e5;
}
.color-picker {
width: 30px;
height: 30px;
border-radius: 50%;
border: 2px solid #4b5563;
cursor: pointer;
transition: all 0.3s ease;
}
.color-picker:hover {
transform: scale(1.1);
}
.color-picker.active {
border-color: white;
box-shadow: 0 0 0 2px #4f46e5;
}
.color-palette {
display: flex;
gap: 10px;
margin-top: 10px;
}
.dropdown {
position: relative;
display: inline-block;
width: 100%;
}
.dropdown-btn {
background: #374151;
color: white;
border: none;
padding: 8px 15px;
border-radius: 6px;
cursor: pointer;
font-size: 0.9rem;
width: 100%;
text-align: left;
display: flex;
justify-content: space-between;
align-items: center;
}
.dropdown-content {
display: none;
position: absolute;
background: #1f2937;
min-width: 100%;
box-shadow: 0 8px 16px rgba(0,0,0,0.2);
z-index: 1;
border-radius: 6px;
overflow: hidden;
margin-top: 5px;
}
.dropdown-content a {
color: white;
padding: 10px 15px;
text-decoration: none;
display: block;
font-size: 0.85rem;
transition: background 0.3s;
}
.dropdown-content a:hover {
background: #374151;
}
.dropdown:hover .dropdown-content {
display: block;
}
.checkbox-container {
display: flex;
align-items: center;
margin-bottom: 10px;
}
.checkbox-container input {
margin-right: 10px;
}
.checkbox-label {
color: #d1d5db;
font-size: 0.9rem;
}
</style>
</head>
<body class="bg-gray-900 text-white min-h-screen">
<div class="container mx-auto px-4 py-8">
<header class="mb-8">
<h1 class="text-3xl font-bold text-center mb-2 bg-gradient-to-r from-purple-500 to-blue-500 bg-clip-text text-transparent">3D Model Viewer</h1>
<p class="text-center text-gray-400 max-w-2xl mx-auto">Upload and interact with your 3D models. Supports OBJ, GLTF, and more formats with advanced rendering controls.</p>
</header>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div class="lg:col-span-2">
<div class="model-container" id="model-container">
<div class="loading-overlay" id="loading-overlay">
<div class="spinner"></div>
<p>Loading model...</p>
</div>
<div class="model-info" id="model-info">
<h4>Model Information</h4>
<p id="model-name">No model loaded</p>
<p id="model-vertices">Vertices: 0</p>
<p id="model-faces">Faces: 0</p>
</div>
<div class="controls-hint">
<h4>Controls</h4>
<ul>
<li><i class="fas fa-arrows-alt"></i> Left click + drag: Rotate</li>
<li><i class="fas fa-hand-paper"></i> Right click + drag: Pan</li>
<li><i class="fas fa-search-plus"></i> Scroll: Zoom</li>
</ul>
</div>
</div>
<div class="mt-4 flex flex-wrap gap-3">
<label class="file-label">
<i class="fas fa-upload mr-2"></i> Upload Model
<input type="file" id="file-input" class="file-input" accept=".obj,.gltf,.glb,.fbx,.dae,.stl,.ply" multiple>
</label>
<button id="reset-view" class="btn btn-secondary">
<i class="fas fa-sync-alt mr-2"></i> Reset View
</button>
<button id="screenshot" class="btn btn-secondary">
<i class="fas fa-camera mr-2"></i> Screenshot
</button>
<button id="fullscreen" class="btn btn-secondary">
<i class="fas fa-expand mr-2"></i> Fullscreen
</button>
</div>
</div>
<div class="control-panel">
<div class="tab-container">
<div class="tab active" data-tab="display">Display</div>
<div class="tab" data-tab="lighting">Lighting</div>
<div class="tab" data-tab="materials">Materials</div>
</div>
<div class="tab-content active" id="display-tab">
<div class="control-section">
<h3><i class="fas fa-eye"></i> Rendering Mode</h3>
<div class="btn-group">
<button class="preset-btn active" data-mode="solid">Solid</button>
<button class="preset-btn" data-mode="wireframe">Wireframe</button>
<button class="preset-btn" data-mode="points">Points</button>
</div>
</div>
<div class="control-section">
<h3><i class="fas fa-palette"></i> Background</h3>
<div class="color-palette">
<div class="color-picker bg-gray-900 active" data-color="#111827"></div>
<div class="color-picker bg-gray-800" data-color="#1f2937"></div>
<div class="color-picker bg-gray-700" data-color="#374151"></div>
<div class="color-picker bg-black" data-color="#000000"></div>
<div class="color-picker bg-white" data-color="#ffffff"></div>
</div>
</div>
<div class="control-section">
<label class="control-label">Model Opacity</label>
<div class="slider-container">
<input type="range" min="0" max="100" value="100" class="slider" id="opacity-slider">
<span class="slider-value" id="opacity-value">100%</span>
</div>
</div>
<div class="control-section">
<label class="control-label">Model Scale</label>
<div class="slider-container">
<input type="range" min="10" max="500" value="100" class="slider" id="scale-slider">
<span class="slider-value" id="scale-value">100%</span>
</div>
</div>
<div class="control-section">
<div class="checkbox-container">
<input type="checkbox" id="grid-toggle" checked>
<label class="checkbox-label">Show Grid</label>
</div>
<div class="checkbox-container">
<input type="checkbox" id="axes-toggle" checked>
<label class="checkbox-label">Show Axes</label>
</div>
<div class="checkbox-container">
<input type="checkbox" id="bounding-box-toggle">
<label class="checkbox-label">Show Bounding Box</label>
</div>
</div>
<div class="control-section">
<h3><i class="fas fa-sliders-h"></i> Quality Presets</h3>
<div>
<button class="preset-btn active" data-quality="high">High</button>
<button class="preset-btn" data-quality="medium">Medium</button>
<button class="preset-btn" data-quality="low">Low</button>
</div>
</div>
</div>
<div class="tab-content" id="lighting-tab">
<div class="control-section">
<h3><i class="fas fa-lightbulb"></i> Lighting</h3>
<div class="checkbox-container">
<input type="checkbox" id="ambient-light-toggle" checked>
<label class="checkbox-label">Ambient Light</label>
</div>
<div class="checkbox-container">
<input type="checkbox" id="directional-light-toggle" checked>
<label class="checkbox-label">Directional Light</label>
</div>
<div class="checkbox-container">
<input type="checkbox" id="hemisphere-light-toggle">
<label class="checkbox-label">Hemisphere Light</label>
</div>
</div>
<div class="control-section">
<label class="control-label">Light Intensity</label>
<div class="slider-container">
<input type="range" min="0" max="200" value="100" class="slider" id="light-intensity-slider">
<span class="slider-value" id="light-intensity-value">100%</span>
</div>
</div>
<div class="control-section">
<label class="control-label">Light Color</label>
<input type="color" id="light-color-picker" value="#ffffff" class="w-full h-10">
</div>
<div class="control-section">
<label class="control-label">Light Position X</label>
<div class="slider-container">
<input type="range" min="-10" max="10" value="1" step="0.1" class="slider" id="light-x-slider">
<span class="slider-value" id="light-x-value">1.0</span>
</div>
</div>
<div class="control-section">
<label class="control-label">Light Position Y</label>
<div class="slider-container">
<input type="range" min="-10" max="10" value="1" step="0.1" class="slider" id="light-y-slider">
<span class="slider-value" id="light-y-value">1.0</span>
</div>
</div>
<div class="control-section">
<label class="control-label">Light Position Z</label>
<div class="slider-container">
<input type="range" min="-10" max="10" value="1" step="0.1" class="slider" id="light-z-slider">
<span class="slider-value" id="light-z-value">1.0</span>
</div>
</div>
<div class="control-section">
<h3><i class="fas fa-moon"></i> Shadows</h3>
<div class="checkbox-container">
<input type="checkbox" id="shadow-toggle">
<label class="checkbox-label">Enable Shadows</label>
</div>
</div>
</div>
<div class="tab-content" id="materials-tab">
<div class="control-section">
<h3><i class="fas fa-paint-brush"></i> Material Type</h3>
<div class="dropdown">
<button class="dropdown-btn">
<span id="current-material">Standard</span>
<i class="fas fa-chevron-down"></i>
</button>
<div class="dropdown-content">
<a href="#" data-material="standard">Standard</a>
<a href="#" data-material="phong">Phong</a>
<a href="#" data-material="lambert">Lambert</a>
<a href="#" data-material="basic">Basic</a>
<a href="#" data-material="physical">Physical</a>
</div>
</div>
</div>
<div class="control-section">
<label class="control-label">Material Color</label>
<input type="color" id="material-color-picker" value="#888888" class="w-full h-10">
</div>
<div class="control-section">
<label class="control-label">Roughness</label>
<div class="slider-container">
<input type="range" min="0" max="100" value="50" class="slider" id="roughness-slider">
<span class="slider-value" id="roughness-value">50%</span>
</div>
</div>
<div class="control-section">
<label class="control-label">Metalness</label>
<div class="slider-container">
<input type="range" min="0" max="100" value="0" class="slider" id="metalness-slider">
<span class="slider-value" id="metalness-value">0%</span>
</div>
</div>
<div class="control-section">
<label class="control-label">Emissive Intensity</label>
<div class="slider-container">
<input type="range" min="0" max="100" value="0" class="slider" id="emissive-slider">
<span class="slider-value" id="emissive-value">0%</span>
</div>
</div>
<div class="control-section">
<h3><i class="fas fa-texture"></i> Textures</h3>
<label for="texture-upload" class="file-label">
<i class="fas fa-image mr-2"></i> Upload Texture
<input type="file" id="texture-upload" class="file-input" accept="image/*">
</label>
</div>
</div>
</div>
</div>
</div>
<script>
// Initialize Three.js scene
let scene, camera, renderer, controls, model, gridHelper, axesHelper, boundingBox;
let ambientLight, directionalLight, hemisphereLight;
let isModelLoaded = false;
let currentFileType = '';
// Initialize the 3D viewer
function init() {
// Get container dimensions
const container = document.getElementById('model-container');
const width = container.clientWidth;
const height = container.clientHeight;
// Create scene
scene = new THREE.Scene();
scene.background = new THREE.Color(0x111827);
// Create camera
camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000);
camera.position.set(5, 5, 5);
// Create renderer
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(width, height);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
container.appendChild(renderer.domElement);
// Add controls
controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.screenSpacePanning = false;
controls.minDistance = 1;
controls.maxDistance = 100;
// Add lights
setupLights();
// Add helpers
setupHelpers();
// Event listeners
setupEventListeners();
// Start animation loop
animate();
}
function setupLights() {
// Ambient light
ambientLight = new THREE.AmbientLight(0x404040, 0.5);
scene.add(ambientLight);
// Directional light
directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(1, 1, 1);
directionalLight.castShadow = true;
directionalLight.shadow.mapSize.width = 1024;
directionalLight.shadow.mapSize.height = 1024;
scene.add(directionalLight);
// Hemisphere light
hemisphereLight = new THREE.HemisphereLight(0xffffbb, 0x080820, 0.5);
hemisphereLight.visible = false;
scene.add(hemisphereLight);
}
function setupHelpers() {
// Grid helper
gridHelper = new THREE.GridHelper(10, 10);
gridHelper.position.y = -0.5;
scene.add(gridHelper);
// Axes helper
axesHelper = new THREE.AxesHelper(5);
scene.add(axesHelper);
// Bounding box (initially hidden)
boundingBox = new THREE.Box3Helper(new THREE.Box3(), 0xffff00);
boundingBox.visible = false;
scene.add(boundingBox);
}
function setupEventListeners() {
// File input
const fileInput = document.getElementById('file-input');
fileInput.addEventListener('change', handleFileUpload);
// Texture upload
const textureUpload = document.getElementById('texture-upload');
textureUpload.addEventListener('change', handleTextureUpload);
// Reset view
document.getElementById('reset-view').addEventListener('click', resetView);
// Screenshot
document.getElementById('screenshot').addEventListener('click', takeScreenshot);
// Fullscreen
document.getElementById('fullscreen').addEventListener('click', toggleFullscreen);
// Tabs
document.querySelectorAll('.tab').forEach(tab => {
tab.addEventListener('click', () => {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
tab.classList.add('active');
document.getElementById(`${tab.dataset.tab}-tab`).classList.add('active');
});
});
// Rendering mode buttons
document.querySelectorAll('[data-mode]').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('[data-mode]').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
changeRenderingMode(btn.dataset.mode);
});
});
// Background color pickers
document.querySelectorAll('.color-picker').forEach(picker => {
picker.addEventListener('click', () => {
document.querySelectorAll('.color-picker').forEach(p => p.classList.remove('active'));
picker.classList.add('active');
scene.background = new THREE.Color(picker.dataset.color);
});
});
// Quality presets
document.querySelectorAll('[data-quality]').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('[data-quality]').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
applyQualityPreset(btn.dataset.quality);
});
});
// Material dropdown
document.querySelectorAll('.dropdown-content a').forEach(item => {
item.addEventListener('click', (e) => {
e.preventDefault();
document.getElementById('current-material').textContent = item.textContent;
changeMaterialType(item.dataset.material);
});
});
// Sliders
document.getElementById('opacity-slider').addEventListener('input', updateOpacity);
document.getElementById('scale-slider').addEventListener('input', updateScale);
document.getElementById('light-intensity-slider').addEventListener('input', updateLightIntensity);
document.getElementById('light-x-slider').addEventListener('input', updateLightPosition);
document.getElementById('light-y-slider').addEventListener('input', updateLightPosition);
document.getElementById('light-z-slider').addEventListener('input', updateLightPosition);
document.getElementById('roughness-slider').addEventListener('input', updateMaterialProperties);
document.getElementById('metalness-slider').addEventListener('input', updateMaterialProperties);
document.getElementById('emissive-slider').addEventListener('input', updateMaterialProperties);
// Color pickers
document.getElementById('light-color-picker').addEventListener('input', updateLightColor);
document.getElementById('material-color-picker').addEventListener('input', updateMaterialColor);
// Toggles
document.getElementById('grid-toggle').addEventListener('change', toggleGrid);
document.getElementById('axes-toggle').addEventListener('change', toggleAxes);
document.getElementById('bounding-box-toggle').addEventListener('change', toggleBoundingBox);
document.getElementById('ambient-light-toggle').addEventListener('change', toggleAmbientLight);
document.getElementById('directional-light-toggle').addEventListener('change', toggleDirectionalLight);
document.getElementById('hemisphere-light-toggle').addEventListener('change', toggleHemisphereLight);
document.getElementById('shadow-toggle').addEventListener('change', toggleShadows);
// Window resize
window.addEventListener('resize', onWindowResize);
}
function handleFileUpload(event) {
const files = event.target.files;
if (files.length === 0) return;
const file = files[0];
const fileName = file.name;
const fileExtension = fileName.split('.').pop().toLowerCase();
// Show loading overlay
const loadingOverlay = document.getElementById('loading-overlay');
loadingOverlay.style.display = 'flex';
// Remove previous model if exists
if (model) {
scene.remove(model);
if (boundingBox) {
boundingBox.box = new THREE.Box3();
}
}
// Create file reader
const reader = new FileReader();
reader.onload = function(e) {
const contents = e.target.result;
try {
// Load based on file type
switch(fileExtension) {
case 'obj':
loadOBJModel(contents, fileName);
currentFileType = 'obj';
break;
case 'gltf':
case 'glb':
loadGLTFModel(contents, fileName);
currentFileType = 'gltf';
break;
default:
alert('Unsupported file format. Please upload an OBJ or GLTF file.');
loadingOverlay.style.display = 'none';
return;
}
} catch (error) {
console.error('Error loading model:', error);
alert('Error loading model. Please check the console for details.');
loadingOverlay.style.display = 'none';
}
};
reader.onerror = function() {
alert('Error reading file');
loadingOverlay.style.display = 'none';
};
if (fileExtension === 'glb') {
reader.readAsArrayBuffer(file);
} else {
reader.readAsText(file);
}
}
function loadOBJModel(objContents, fileName) {
const loader = new THREE.OBJLoader();
try {
model = loader.parse(objContents);
// Center and scale the model
centerAndScaleModel(model);
// Add to scene
scene.add(model);
// Update model info
updateModelInfo(model, fileName);
// Hide loading overlay
document.getElementById('loading-overlay').style.display = 'none';
isModelLoaded = true;
// Update bounding box
updateBoundingBox(model);
} catch (error) {
console.error('Error parsing OBJ:', error);
document.getElementById('loading-overlay').style.display = 'none';
alert('Error parsing OBJ file. Please check the console for details.');
}
}
function loadGLTFModel(gltfContents, fileName) {
const loader = new THREE.GLTFLoader();
try {
let data;
if (currentFileType === 'glb') {
data = new Uint8Array(gltfContents);
} else {
data = gltfContents;
}
loader.parse(data, '', (gltf) => {
model = gltf.scene;
// Center and scale the model
centerAndScaleModel(model);
// Add to scene
scene.add(model);
// Update model info
updateModelInfo(model, fileName);
// Hide loading overlay
document.getElementById('loading-overlay').style.display = 'none';
isModelLoaded = true;
// Update bounding box
updateBoundingBox(model);
}, (error) => {
console.error('Error loading GLTF:', error);
document.getElementById('loading-overlay').style.display = 'none';
alert('Error loading GLTF file. Please check the console for details.');
});
} catch (error) {
console.error('Error parsing GLTF:', error);
document.getElementById('loading-overlay').style.display = 'none';
alert('Error parsing GLTF file. Please check the console for details.');
}
}
function centerAndScaleModel(model) {
// Compute bounding box
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.x += (model.position.x - center.x);
model.position.y += (model.position.y - center.y);
model.position.z += (model.position.z - center.z);
// Scale to fit in view
const maxDim = Math.max(size.x, size.y, size.z);
const scale = 5 / maxDim;
model.scale.set(scale, scale, scale);
}
function updateModelInfo(model, fileName) {
let vertices = 0;
let faces = 0;
model.traverse(function(child) {
if (child.isMesh) {
if (child.geometry) {
vertices += child.geometry.attributes.position.count;
if (child.geometry.index) {
faces += child.geometry.index.count / 3;
} else {
faces += child.geometry.attributes.position.count / 3;
}
}
}
});
document.getElementById('model-name').textContent = fileName;
document.getElementById('model-vertices').textContent = `Vertices: ${vertices.toLocaleString()}`;
document.getElementById('model-faces').textContent = `Faces: ${faces.toLocaleString()}`;
}
function updateBoundingBox(model) {
if (!model || !boundingBox) return;
const box = new THREE.Box3().setFromObject(model);
boundingBox.box = box;
if (document.getElementById('bounding-box-toggle').checked) {
boundingBox.visible = true;
}
}
function handleTextureUpload(event) {
if (!isModelLoaded) {
alert('Please load a model first');
return;
}
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(e) {
const texture = new THREE.TextureLoader().load(e.target.result, () => {
// Apply texture to all materials in the model
model.traverse(function(child) {
if (child.isMesh && child.material) {
child.material.map = texture;
child.material.needsUpdate = true;
}
});
});
};
reader.readAsDataURL(file);
}
function resetView() {
if (!isModelLoaded) return;
// Reset camera position
camera.position.set(5, 5, 5);
controls.target.set(0, 0, 0);
controls.update();
}
function takeScreenshot() {
if (!isModelLoaded) {
alert('Please load a model first');
return;
}
// Render the scene to a data URL
renderer.render(scene, camera);
const dataURL = renderer.domElement.toDataURL('image/png');
// Create a download link
const link = document.createElement('a');
link.href = dataURL;
link.download = '3d-model-screenshot.png';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
function toggleFullscreen() {
const container = document.getElementById('model-container');
if (!document.fullscreenElement) {
container.requestFullscreen().catch(err => {
alert(`Error attempting to enable fullscreen: ${err.message}`);
});
} else {
document.exitFullscreen();
}
}
function changeRenderingMode(mode) {
if (!isModelLoaded) return;
model.traverse(function(child) {
if (child.isMesh) {
switch(mode) {
case 'solid':
child.material.wireframe = false;
child.material.pointCloud = false;
break;
case 'wireframe':
child.material.wireframe = true;
child.material.pointCloud = false;
break;
case 'points':
child.material.wireframe = false;
child.material.pointCloud = true;
break;
}
}
});
}
function applyQualityPreset(quality) {
switch(quality) {
case 'high':
renderer.setPixelRatio(window.devicePixelRatio);
renderer.antialias = true;
break;
case 'medium':
renderer.setPixelRatio(1);
renderer.antialias = true;
break;
case 'low':
renderer.setPixelRatio(1);
renderer.antialias = false;
break;
}
// Force resize to apply changes
onWindowResize();
}
function changeMaterialType(type) {
if (!isModelLoaded) return;
model.traverse(function(child) {
if (child.isMesh) {
const currentMaterial = child.material;
let newMaterial;
// Preserve some properties
const color = currentMaterial.color || new THREE.Color(0x888888);
const map = currentMaterial.map || null;
const opacity = currentMaterial.opacity || 1;
switch(type) {
case 'standard':
newMaterial = new THREE.MeshStandardMaterial({
color: color,
map: map,
roughness: 0.5,
metalness: 0,
opacity: opacity,
transparent: opacity < 1
});
break;
case 'phong':
newMaterial = new THREE.MeshPhongMaterial({
color: color,
map: map,
shininess: 30,
opacity: opacity,
transparent: opacity < 1
});
break;
case 'lambert':
newMaterial = new THREE.MeshLambertMaterial({
color: color,
map: map,
opacity: opacity,
transparent: opacity < 1
});
break;
case 'basic':
newMaterial = new THREE.MeshBasicMaterial({
color: color,
map: map,
opacity: opacity,
transparent: opacity < 1
});
break;
case 'physical':
newMaterial = new THREE.MeshPhysicalMaterial({
color: color,
map: map,
roughness: 0.5,
metalness: 0,
clearcoat: 1,
clearcoatRoughness: 0.1,
opacity: opacity,
transparent: opacity < 1
});
break;
}
child.material = newMaterial;
}
});
// Update material controls to match new material
updateMaterialControls();
}
function updateMaterialControls() {
if (!isModelLoaded) return;
// Get the first material in the model to sync controls
let firstMaterial = null;
model.traverse(function(child) {
if (child.isMesh && !firstMaterial) {
firstMaterial = child.material;
}
});
if (!firstMaterial) return;
// Update color picker
if (firstMaterial.color) {
document.getElementById('material-color-picker').value = `#${firstMaterial.color.getHexString()}`;
}
// Update sliders based on material type
if (firstMaterial.isMeshStandardMaterial || firstMaterial.isMeshPhysicalMaterial) {
document.getElementById('roughness-slider').value = firstMaterial.roughness * 100;
document.getElementById('roughness-value').textContent = `${Math.round(firstMaterial.roughness * 100)}%`;
document.getElementById('metalness-slider').value = firstMaterial.metalness * 100;
document.getElementById('metalness-value').textContent = `${Math.round(firstMaterial.metalness * 100)}%`;
}
if (firstMaterial.emissive) {
document.getElementById('emissive-slider').value = firstMaterial.emissiveIntensity * 100;
document.getElementById('emissive-value').textContent = `${Math.round(firstMaterial.emissiveIntensity * 100)}%`;
}
}
function updateOpacity() {
if (!isModelLoaded) return;
const opacity = document.getElementById('opacity-slider').value / 100;
document.getElementById('opacity-value').textContent = `${document.getElementById('opacity-slider').value}%`;
model.traverse(function(child) {
if (child.isMesh) {
child.material.opacity = opacity;
child.material.transparent = opacity < 1;
}
});
}
function updateScale() {
if (!isModelLoaded) return;
const scale = document.getElementById('scale-slider').value / 100;
document.getElementById('scale-value').textContent = `${document.getElementById('scale-slider').value}%`;
model.scale.set(scale, scale, scale);
updateBoundingBox(model);
}
function updateLightIntensity() {
const intensity = document.getElementById('light-intensity-slider').value / 100;
document.getElementById('light-intensity-value').textContent = `${document.getElementById('light-intensity-slider').value}%`;
if (ambientLight) ambientLight.intensity = intensity * 0.5;
if (directionalLight) directionalLight.intensity = intensity;
if (hemisphereLight) hemisphereLight.intensity = intensity * 0.5;
}
function updateLightPosition() {
const x = parseFloat(document.getElementById('light-x-slider').value);
const y = parseFloat(document.getElementById('light-y-slider').value);
const z = parseFloat(document.getElementById('light-z-slider').value);
document.getElementById('light-x-value').textContent = x.toFixed(1);
document.getElementById('light-y-value').textContent = y.toFixed(1);
document.getElementById('light-z-value').textContent = z.toFixed(1);
if (directionalLight) directionalLight.position.set(x, y, z);
}
function updateLightColor() {
const color = new THREE.Color(document.getElementById('light-color-picker').value);
if (directionalLight) directionalLight.color = color;
if (hemisphereLight) hemisphereLight.color = color;
}
function updateMaterialColor() {
if (!isModelLoaded) return;
const color = new THREE.Color(document.getElementById('material-color-picker').value);
model.traverse(function(child) {
if (child.isMesh) {
child.material.color = color;
}
});
}
function updateMaterialProperties() {
if (!isModelLoaded) return;
const roughness = document.getElementById('roughness-slider').value / 100;
const metalness = document.getElementById('metalness-slider').value / 100;
const emissive = document.getElementById('emissive-slider').value / 100;
document.getElementById('roughness-value').textContent = `${document.getElementById('roughness-slider').value}%`;
document.getElementById('metalness-value').textContent = `${document.getElementById('metalness-slider').value}%`;
document.getElementById('emissive-value').textContent = `${document.getElementById('emissive-slider').value}%`;
model.traverse(function(child) {
if (child.isMesh) {
if (child.material.isMeshStandardMaterial || child.material.isMeshPhysicalMaterial) {
child.material.roughness = roughness;
child.material.metalness = metalness;
}
if (child.material.emissive) {
child.material.emissiveIntensity = emissive;
}
}
});
}
function toggleGrid() {
gridHelper.visible = document.getElementById('grid-toggle').checked;
}
function toggleAxes() {
axesHelper.visible = document.getElementById('axes-toggle').checked;
}
function toggleBoundingBox() {
boundingBox.visible = document.getElementById('bounding-box-toggle').checked;
}
function toggleAmbientLight() {
ambientLight.visible = document.getElementById('ambient-light-toggle').checked;
}
function toggleDirectionalLight() {
directionalLight.visible = document.getElementById('directional-light-toggle').checked;
}
function toggleHemisphereLight() {
hemisphereLight.visible = document.getElementById('hemisphere-light-toggle').checked;
}
function toggleShadows() {
renderer.shadowMap.enabled = document.getElementById('shadow-toggle').checked;
if (directionalLight) directionalLight.castShadow = document.getElementById('shadow-toggle').checked;
model.traverse(function(child) {
if (child.isMesh) {
child.castShadow = document.getElementById('shadow-toggle').checked;
child.receiveShadow = document.getElementById('shadow-toggle').checked;
}
});
}
function onWindowResize() {
const container = document.getElementById('model-container');
const width = container.clientWidth;
const height = container.clientHeight;
camera.aspect = width / height;
camera.updateProjectionMatrix();
renderer.setSize(width, height);
}
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}
// Initialize the app when the page loads
window.addEventListener('load', init);
</script>
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=Sal-ONE/3d-model-viewer" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
</html>