<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Three.js GLB模型预览器</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #1a2a6c, #b21f1f, #fdbb2d);
color: #fff;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
overflow-x: hidden;
}
header {
text-align: center;
margin-bottom: 30px;
width: 100%;
max-width: 1200px;
}
h1 {
font-size: 2.8rem;
margin-bottom: 10px;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
}
.subtitle {
font-size: 1.2rem;
opacity: 0.9;
margin-bottom: 30px;
}
.container {
display: flex;
flex-wrap: wrap;
gap: 30px;
justify-content: center;
width: 100%;
max-width: 1200px;
}
.preview-section {
flex: 1;
min-width: 300px;
max-width: 700px;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 15px;
padding: 20px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
}
.canvas-container {
position: relative;
width: 100%;
height: 400px;
border-radius: 10px;
overflow: hidden;
margin-bottom: 20px;
background: rgba(0, 0, 0, 0.2);
}
#model-viewer {
width: 100%;
height: 100%;
display: block;
}
.controls {
display: flex;
flex-wrap: wrap;
gap: 15px;
justify-content: center;
margin-top: 20px;
}
button {
padding: 12px 25px;
background: #ff6b6b;
color: white;
border: none;
border-radius: 50px;
cursor: pointer;
font-weight: bold;
transition: all 0.3s ease;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
button:hover {
background: #ff8e8e;
transform: translateY(-2px);
box-shadow: 0 6px 8px rgba(0, 0, 0, 0.15);
}
button:active {
transform: translateY(0);
}
.instructions {
flex: 1;
min-width: 300px;
max-width: 400px;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 15px;
padding: 25px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
}
h2 {
font-size: 1.8rem;
margin-bottom: 20px;
color: #ffd166;
}
.instructions ul {
list-style-type: none;
padding-left: 20px;
}
.instructions li {
margin-bottom: 15px;
font-size: 1.1rem;
line-height: 1.5;
position: relative;
padding-left: 30px;
}
.instructions li:before {
content: "•";
color: #ffd166;
font-size: 1.5rem;
position: absolute;
left: 0;
top: -3px;
}
.loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 1.2rem;
background: rgba(0, 0, 0, 0.7);
padding: 15px 30px;
border-radius: 8px;
display: none;
}
.model-info {
text-align: center;
margin-top: 15px;
font-size: 1.1rem;
}
footer {
margin-top: 40px;
text-align: center;
font-size: 0.9rem;
opacity: 0.7;
}
@media (max-width: 768px) {
.container {
flex-direction: column;
align-items: center;
}
.preview-section,
.instructions {
width: 100%;
}
h1 {
font-size: 2.2rem;
}
}
</style>
</head>
<body>
<header>
<h1>Three.js GLB模型预览器</h1>
<p class="subtitle">上传并预览您的GLB模型,自动适应不同大小</p>
</header>
<div class="container">
<div class="preview-section">
<div class="canvas-container">
<canvas id="model-viewer"></canvas>
<div class="loading" id="loading">加载中...</div>
</div>
<div class="model-info" id="model-info">
等待模型加载...
</div>
<div class="controls">
<input type="file" id="model-upload" accept=".glb" style="display: none;">
<button id="upload-btn">上传GLB模型</button>
<button id="reset-btn">重置视图</button>
<button id="rotate-toggle">自动旋转: 开</button>
</div>
</div>
<div class="instructions">
<h2>使用说明</h2>
<ul>
<li>点击"上传GLB模型"按钮选择GLB格式的3D模型文件</li>
<li>模型加载后将自动调整大小和位置以适应视图</li>
<li>拖动鼠标可以旋转模型查看不同角度</li>
<li>滚动鼠标可以缩放模型</li>
<li>使用"重置视图"按钮恢复初始视角</li>
<li>切换"自动旋转"按钮控制模型是否自动旋转</li>
<li>支持各种大小的GLB模型,系统会自动进行缩放适配</li>
</ul>
</div>
</div>
<footer>
<p>基于Three.js构建的GLB模型预览器 | 2023</p>
</footer>
<!-- Three.js库 -->
<script src="./three.min.js"></script>
<script src="./OrbitControls.js"></script>
<script src="./GLTFLoader.js"></script>
<script>
// 全局变量
let scene, camera, renderer, controls;
let model = null;
let autoRotate = true;
const clock = new THREE.Clock();
let mixer = null;
function test(url) {
// 清除现有模型
if (model) {
scene.remove(model);
model = null;
}
const loadingElement = document.getElementById('loading');
loadingElement.style.display = 'block';
document.getElementById('model-info').textContent = '加载中...';
const loader = new THREE.GLTFLoader();
loader.load(
url,
function (gltf) {
model = gltf.scene;
gltf.scene.traverse(function (child) {
if (child.name.includes("boundbox_")) {
child.visible = false;
}
});
scene.add(model);
// animateModel(gltf)
mixer = new THREE.AnimationMixer(model);
if (gltf.animations[0]) {
mixer.clipAction(gltf.animations[0]).play();
}
// renderer.setAnimationLoop( animate2 );
// 调整模型位置和大小
centerAndScaleModel();
// 更新模型信息
document.getElementById('model-info').textContent =
`模型名称: | 格式: GLB`;
// 隐藏加载提示
loadingElement.style.display = 'none';
},
// onProgress callback
function (xhr) {
const percent = Math.round((xhr.loaded / xhr.total) * 100);
loadingElement.textContent = `加载中... ${percent}%`;
},
// onError callback
function (error) {
console.error('Error loading model:', error);
loadingElement.textContent = '加载失败,请重试';
document.getElementById('model-info').textContent = '加载失败';
}
);
}
// 初始化Three.js场景
function init() {
// 创建场景
scene = new THREE.Scene();
scene.background = new THREE.Color(0xbfe3dd);
scene.add(new THREE.AmbientLight(0xffffff, 0.6));
// 创建相机
const canvas = document.getElementById('model-viewer');
camera = new THREE.PerspectiveCamera(45, canvas.clientWidth / canvas.clientHeight, 0.1, 1000);
camera.position.set(0, 0, 5);
// 创建渲染器
renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
renderer.setSize(canvas.clientWidth, canvas.clientHeight);
renderer.setPixelRatio(window.devicePixelRatio);
// 添加轨道控制
controls = new THREE.OrbitControls(camera, canvas);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.autoRotate = autoRotate;
controls.autoRotateSpeed = 1.0;
// 添加方向光
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(5, 10, 7);
scene.add(directionalLight);
// 添加辅助网格
const gridHelper = new THREE.GridHelper(10, 10);
gridHelper.visible = false;
scene.add(gridHelper);
// 监听窗口大小变化
window.addEventListener('resize', onWindowResize);
// 设置文件上传事件
document.getElementById('upload-btn').addEventListener('click', () => {
document.getElementById('model-upload').click();
});
document.getElementById('model-upload').addEventListener('change', handleFileUpload);
document.getElementById('reset-btn').addEventListener('click', resetView);
document.getElementById('rotate-toggle').addEventListener('click', toggleAutoRotate);
// 开始动画循环
animate();
}
// 处理文件上传
function handleFileUpload(event) {
const file = event.target.files[0];
if (!file) return;
// 显示加载提示
const loadingElement = document.getElementById('loading');
loadingElement.style.display = 'block';
document.getElementById('model-info').textContent = '加载中...';
// 清除现有模型
if (model) {
scene.remove(model);
model = null;
}
// 读取文件
const reader = new FileReader();
reader.onload = function (e) {
const loader = new THREE.GLTFLoader();
loader.load(
// resource URL
URL.createObjectURL(file),
// onLoad callback
function (gltf) {
model = gltf.scene;
gltf.scene.traverse(function (child) {
if (child.name.includes("boundbox_")) {
child.visible = false;
}
});
scene.add(model);
// animateModel(gltf)
mixer = new THREE.AnimationMixer(model);
if (gltf.animations[0]) {
mixer.clipAction(gltf.animations[0]).play();
}
// renderer.setAnimationLoop( animate2 );
// 调整模型位置和大小
centerAndScaleModel();
// 更新模型信息
document.getElementById('model-info').textContent =
`模型名称: ${file.name} | 格式: GLB`;
// 隐藏加载提示
loadingElement.style.display = 'none';
},
// onProgress callback
function (xhr) {
const percent = Math.round((xhr.loaded / xhr.total) * 100);
loadingElement.textContent = `加载中... ${percent}%`;
},
// onError callback
function (error) {
console.error('Error loading model:', error);
loadingElement.textContent = '加载失败,请重试';
document.getElementById('model-info').textContent = '加载失败';
}
);
};
reader.readAsArrayBuffer(file);
}
function animate2() {
if (mixer) {
const delta = clock.getDelta();
mixer.update(delta);
}
// controls.update();
// stats.update();
// renderer.render( scene, camera );
}
function animateModel(gltf) {
const mixer = new THREE.AnimationMixer(gltf.scene); // 创建动画混合器
const action = mixer.clipAction(gltf.animations[0]); // 获取第一个动画剪辑并创建动作
action.play(); // 播放动作
function animate() {
requestAnimationFrame(animate);
mixer.update(clock.getDelta()); // 更新混合器,传递时间增量以实现平滑动画
renderer.render(scene, camera); // 渲染场景和相机
}
animate(); // 开始动画循环
}
// 居中并缩放模型
function centerAndScaleModel() {
if (!model) return;
// 计算模型的边界框
const bbox = new THREE.Box3().setFromObject(model);
const center = bbox.getCenter(new THREE.Vector3());
const size = bbox.getSize(new THREE.Vector3());
// 计算缩放比例,确保模型完全可见
const maxDim = Math.max(size.x, size.y, size.z);
console.log(maxDim);
const canvas = renderer.domElement;
const fov = camera.fov * (Math.PI / 180);
const cameraZ = Math.abs((maxDim / 2) / Math.tan(fov / 2));
// 重置模型位置和缩放
model.position.set(-center.x, -center.y, -center.z);
camera.position.set(0, 0, cameraZ * 1.5);
if (maxDim < 1) {
const scaleFactor = 1 / maxDim;
model.scale.multiplyScalar(scaleFactor);
// console.log('scaleFactor', scaleFactor);
let positionZ = 2;
let positionY = center.y - (scaleFactor * size.y);
camera.position.set(center.x, positionY, positionZ);
}
// 设置相机位置
camera.near = 0.1;
camera.far = Math.max(1000, cameraZ * 5);
camera.updateProjectionMatrix();
// 更新控制器
controls.target.set(0, 0, 0);
controls.update();
controls.saveState();
}
// 重置视图
function resetView() {
controls.reset();
if (model) {
centerAndScaleModel();
}
}
// 切换自动旋转
function toggleAutoRotate() {
autoRotate = !autoRotate;
controls.autoRotate = autoRotate;
document.getElementById('rotate-toggle').textContent =
`自动旋转: ${autoRotate ? '开' : '关'}`;
}
// 处理窗口大小变化
function onWindowResize() {
const canvas = renderer.domElement;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
if (canvas.width !== width || canvas.height !== height) {
camera.aspect = width / height;
camera.updateProjectionMatrix();
renderer.setSize(width, height, false);
// 如果模型已加载,重新调整视图
if (model) {
centerAndScaleModel();
}
}
}
// 动画循环
function animate() {
requestAnimationFrame(animate);
controls.update();
animate2()
// mixer.update(clock.getDelta());
renderer.render(scene, camera);
}
// 页面加载完成后初始化
window.addEventListener('load', init);
</script>
</body>
</html>