three.js 预览

<!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>
posted @ 2025-12-10 09:01  7c89  阅读(8)  评论(0)    收藏  举报