跳转至

借助DeepSeek实现在MkDocs-Material网页上使用Three.js展示3D模型

约 5052 个字 • 1227 行代码

之前在查看kinect摄像头github仓库时,发现其中有一个摄像头的3d模型文件,于是就产生了把这个3d模型文件展示在文档首页的想法。

之后搜索了一下如何在html中显示3d模型,发现主流是使用three.js。查看了three.js的说明文档,但是感觉太复杂了(有很多东西需要设置),于是就直接开始询问deepseek,最后一步步将idea实现成了实际的效果。

大致的过程是:

  1. 询问deepseek如何在(空白的)html中使用three.js加载glb模型文件(一种3d模型的文件格式)
  2. 参照deepseek的回答创建本地的文件,尝试运行并查看效果
  3. 继续跟deepseek说要添加或修改调整的效果
  4. 效果基本满意之后,询问deepseek如何将这个东西移植到mkdocs构建的网页中
  5. 按照deepseek的回答修改原有的文件,并mkdocs serve预览查看效果
  6. 询问deepseek如何修改实现自己想要的效果
  7. 最终实现
第1段对话记录

You:

我想在html中使用three.js来加载glb 3d模型文件,你能帮我编写一个这样的main.js文件吗?

 

DeepSeek:

已深度思考(用时 23 秒)

嗯,用户想在HTML里用Three.js加载GLB模型,我得帮他写个main.js。首先,我得回想一下Three.js的基本结构。通常需要初始化场景、相机、渲染器,然后加载模型。

用户可能不太熟悉Three.js的步骤,所以代码要注释清楚。首先,创建场景,设置背景色,比如灰色。然后透视相机,设置好视场、宽高比、近远平面。渲染器要用WebGL,并调整大小到窗口。

接下来是光照,环境光和方向光,这样模型看起来不会太暗。然后GLTF加载器,Three.js自带了吗?可能需要从examples/jsm/loaders里导入,不过如果用户用的是CDN,可能已经包括进去了。不过用户可能没正确引入加载器,所以代码里需要确认使用GLTFLoader。

加载模型的时候,用loader.load,传入路径,成功回调里把模型加入场景。可能需要调整模型的位置或缩放,特别是如果模型太大或太小的话。还要考虑动画循环,如果有动画的话,但用户没说,可能先不加。不过如果有的话,可能需要调用update方法。

然后是窗口调整事件,确保相机和渲染器能自适应。最后动画循环用requestAnimationFrame,渲染场景。

可能的问题点:GLB文件的路径是否正确,服务器是否支持这种MIME类型。另外,可能需要添加一些错误处理的回调,比如加载出错时的处理。还有,相机的位置可能需要调整,不然模型可能在视野外。比如设置相机的位置z为5,或者根据模型大小调整。

用户可能没有提到控制部分,比如轨道控制器,这样用户无法旋转查看模型。所以最好加上OrbitControls,这需要从examples/jsm/controls导入,或者CDN链接。但用户可能没安装,所以代码里要提醒他们引入。

总结步骤:初始化Three.js基础,设置光照,加载模型,处理窗口变化,添加动画循环,可能的话添加控制器。代码要注释清楚,方便用户理解每个部分的作用,并提醒他们注意路径和额外需要的模块。

当然可以!以下是一个使用Three.js在HTML中加载和显示GLB模型文件的main.js示例代码。我会包含详细的注释说明每个步骤的作用:

// main.js
// 初始化Three.js核心组件
let scene, camera, renderer, controls;

function init() {
    // 创建场景
    scene = new THREE.Scene();
    scene.background = new THREE.Color(0xeeeeee);  // 设置背景颜色

    // 创建透视相机
    camera = new THREE.PerspectiveCamera(
        75,  // 视野角度
        window.innerWidth / window.innerHeight,  // 宽高比
        0.1,  // 近平面
        1000  // 远平面
    );
    camera.position.set(5, 5, 5);  // 设置相机位置

    // 创建渲染器
    renderer = new THREE.WebGLRenderer({ antialias: true });
    renderer.setSize(window.innerWidth, window.innerHeight);
    document.body.appendChild(renderer.domElement);

    // 添加灯光
    const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
    scene.add(ambientLight);

    const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
    directionalLight.position.set(10, 10, 10);
    scene.add(directionalLight);

    // 添加轨道控制器(需要额外引入OrbitControls.js)
    controls = new THREE.OrbitControls(camera, renderer.domElement);
    controls.enableDamping = true;  // 启用阻尼效果
    controls.dampingFactor = 0.05;

    // 加载GLB模型
    const loader = new THREE.GLTFLoader();
    loader.load(
        'model.glb',  // 模型文件路径
        (gltf) => {
            scene.add(gltf.scene);
            console.log('模型加载完成');

            // 可选:自动调整相机视角到模型
            const box = new THREE.Box3().setFromObject(gltf.scene);
            const center = box.getCenter(new THREE.Vector3());
            const size = box.getSize(new THREE.Vector3());
            camera.position.copy(center).add(new THREE.Vector3(0, 0, size.length() * 1.5));
            controls.update();
        },
        (xhr) => {
            // 加载进度回调
            console.log((xhr.loaded / xhr.total * 100) + '% loaded');
        },
        (error) => {
            console.error('加载错误:', error);
        }
    );

    // 处理窗口大小变化
    window.addEventListener('resize', onWindowResize, false);
}

// 窗口大小变化处理函数
function onWindowResize() {
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(window.innerWidth, window.innerHeight);
}

// 动画循环
function animate() {
    requestAnimationFrame(animate);
    controls.update();  // 更新控制器
    renderer.render(scene, camera);
}

// 初始化并启动
init();
animate();

需要使用以下HTML结构(需放在同一目录下):

<!DOCTYPE html>
<html>
<head>
    <title>Three.js GLB Viewer</title>
    <style>
        body { margin: 0; }
        canvas { display: block; }
    </style>
</head>
<body>
    <!-- Three.js 依赖 -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
    <!-- GLTF加载器 -->
    <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/loaders/GLTFLoader.js"></script>
    <!-- 轨道控制器 -->
    <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script>

    <!-- 主程序 -->
    <script src="main.js"></script>
</body>
</html>

使用说明:

  1. 将模型文件命名为 model.glb 并放在与HTML文件同目录下
  2. 按需调整以下参数:
    • 相机初始位置(camera.position.set()
    • 背景颜色(scene.background
    • 灯光参数(颜色和强度)
  3. 可选功能:
    • 添加加载进度条
    • 添加模型点击交互
    • 添加自动旋转功能

注意事项:

  1. 需要从网络服务器运行(本地直接打开可能会有CORS问题)
  2. 模型尺寸过大时需要调整相机位置
  3. 不同版本的Three.js可能需要调整加载器路径

如果需要更复杂的功能(如模型交互、动画控制等),可以在加载完成的回调函数中添加相关逻辑。

 

You:

  • 添加加载进度条

  • 添加自动旋转功能

这两个功能要如何添加呢?

 

DeepSeek:

已深度思考(用时 26 秒)

好的,用户之前已经得到了一个使用Three.js加载GLB模型的示例代码,现在他们想进一步添加加载进度条和自动旋转功能。我需要仔细分析这两个需求,并给出具体的实现步骤。

首先,关于加载进度条。原来的代码中已经有一个加载进度的回调函数,但只是将进度打印到控制台。用户想要一个可视化的进度条,这意味着需要在页面上添加HTML元素来显示进度。通常,进度条可以用一个div容器,里面包含一个表示进度的内层div,通过调整内层div的宽度来显示进度百分比。在加载过程中,GLTFLoader的load方法会持续调用进度回调,这里需要更新进度条的宽度。加载完成后,可能需要隐藏进度条或者将其设置为完成状态。

接下来是自动旋转功能。自动旋转通常指的是让模型或相机围绕某个中心点旋转。在Three.js中,可以通过在动画循环中调整模型或相机的旋转属性来实现。另一种方法是使用OrbitControls的autoRotate属性,将其设置为true即可启用控制器的自动旋转,这样用户就不需要手动编写旋转逻辑,而且可以与用户交互(如拖动)结合使用。用户可能希望控制旋转速度,所以需要添加一个参数来调整autoRotateSpeed。

然后,我需要考虑如何将这两个功能集成到现有的代码中。对于进度条,需要在HTML中添加相应的结构,并在CSS中定义样式,然后在JavaScript中更新进度条的宽度。加载完成后,可能需要隐藏进度条。对于自动旋转,只需在初始化OrbitControls时设置autoRotate和autoRotateSpeed属性,并在动画循环中确保controls.update()被调用,因为OrbitControls需要每帧更新。

此外,用户可能没有提到但需要注意的细节:比如进度条的样式是否需要过渡效果,自动旋转的速度是否可调,以及如何确保在窗口大小变化时进度条仍然正确显示。另外,如果模型加载失败,是否需要处理错误情况并更新进度条的状态。

现在需要检查示例代码中是否已经包含OrbitControls,因为自动旋转功能依赖于它。原来的代码确实有引入OrbitControls,并且启用了阻尼效果,因此在设置autoRotate后,应该可以正常工作。同时,需要确认Three.js的版本是否支持这些属性,但一般来说,较新版本的Three.js都支持。

最后,要将这些修改步骤清晰地解释给用户,并提供完整的代码示例,确保用户能够复制并运行。同时,提醒用户注意路径和版本问题,确保所有依赖正确加载。

以下是添加加载进度条自动旋转功能的完整实现方案,我在之前的代码基础上进行了功能扩展,新增的代码用注释标出:

<!-- 修改后的HTML(新增进度条容器) -->
<!DOCTYPE html>
<html>
<head>
    <title>Three.js GLB Viewer</title>
    <style>
        body { margin: 0; }
        canvas { display: block; }
        /* 新增进度条样式 */
        #loading-container {
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            width: 300px;
            height: 20px;
            background: #555;
            border-radius: 10px;
        }
        #progress-bar {
            width: 0%;
            height: 100%;
            background: #00ff88;
            border-radius: 10px;
            transition: width 0.3s ease;
        }
    </style>
</head>
<body>
    <!-- 新增加载进度条 -->
    <div id="loading-container">
        <div id="progress-bar"></div>
    </div>

    <!-- Three.js 依赖保持不变 -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/loaders/GLTFLoader.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script>

    <script src="main.js"></script>
</body>
</html>
// 修改后的main.js
let scene, camera, renderer, controls, model;

// 新增变量
let isLoading = true;
const autoRotateSpeed = 1; // 自动旋转速度 (1=正常速度)

function init() {
    // ...保持之前的初始化代码不变...

    // 修改后的模型加载部分
    const loader = new THREE.GLTFLoader();
    loader.load(
        'model.glb',
        (gltf) => {
            model = gltf.scene; // 保存模型引用
            scene.add(model);

            // 隐藏进度条
            isLoading = false;
            document.getElementById('loading-container').style.display = 'none';

            // 自动调整视角
            const box = new THREE.Box3().setFromObject(model);
            const center = box.getCenter(new THREE.Vector3());
            const size = box.getSize(new THREE.Vector3());
            camera.position.copy(center).add(new THREE.Vector3(0, 0, size.length() * 1.5));
            controls.target.copy(center); // 设置控制器的焦点
            controls.update();
        },
        (xhr) => {
            // 更新进度条
            const progress = (xhr.loaded / xhr.total) * 100;
            document.getElementById('progress-bar').style.width = `${progress}%`;
        },
        (error) => {
            console.error('加载错误:', error);
            document.getElementById('progress-bar').style.background = '#ff0000';
        }
    );

    // 配置自动旋转
    controls.autoRotate = true; // 启用自动旋转
    controls.autoRotateSpeed = autoRotateSpeed; // 设置旋转速度

    // ...保持其他代码不变...
}

function animate() {
    requestAnimationFrame(animate);

    // 只在加载完成后执行自动旋转
    if (!isLoading) {
        controls.update();
    }

    renderer.render(scene, camera);
}

// ...其他保持不变的代码...

功能说明:

  1. 加载进度条:

    • 添加了包含动态进度条的HTML/CSS结构
    • 在加载过程中实时更新进度条宽度
    • 加载完成后隐藏进度条
    • 加载错误时进度条变红提示
  2. 自动旋转功能:

    • 启用了OrbitControls的自动旋转(autoRotate)
    • 可通过autoRotateSpeed调整旋转速度(值越大旋转越快)
    • 保持原有控制器功能(鼠标交互仍然有效)

高级定制建议:

  1. 要修改自动旋转速度,调整autoRotateSpeed变量的值
  2. 要改变旋转方向,可以设置负值:autoRotateSpeed = -1
  3. 进度条样式可以通过修改CSS自定义:
    • 修改#progress-bar的背景色改变进度条颜色
    • 调整border-radius值改变圆角大小
    • 修改transition参数调整动画速度

效果预览:

  • 加载时会显示绿色进度条从左到右填充
  • 加载完成后模型会自动缓慢旋转
  • 仍然可以用鼠标:
    • 左键拖动旋转视角
    • 右键拖动平移视角
    • 滚轮缩放

如果需要强制面向某个方向旋转,可以在animate()函数中添加手动旋转逻辑(示例):

function animate() {
    // ...其他代码...

    // 手动旋转示例(绕Y轴旋转)
    if (model && !isLoading) {
        model.rotation.y += 0.005;
    }
}
第2段对话记录

You:

现在我在一个空白的index.html中实现了使用three.js加载glb 3d模型文件(整个页面都是这个模型的展示),

index.html
<!DOCTYPE html>
<html>
<head>
    <title>Three.js GLB Viewer</title>
    <style>
        body { margin: 0; }
        canvas { display: block; }
        /* 新增进度条样式 */
        #loading-container {
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            width: 300px;
            height: 20px;
            background: #555;
            border-radius: 10px;
        }
        #progress-bar {
            width: 0%;
            height: 100%;
            background: #00ff88;
            border-radius: 10px;
            transition: width 0.3s ease;
        }
    </style>
</head>
<body>
    <!-- 新增加载进度条 -->
    <div id="loading-container">
        <div id="progress-bar"></div>
    </div>

    <!-- Three.js 依赖 -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
    <!-- GLTF加载器 -->
    <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/loaders/GLTFLoader.js"></script>
    <!-- 轨道控制器 -->
    <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script>

    <!-- 主程序 -->
    <script src="main.js"></script>
</body>
</html>

main.js如下

main.js
// main.js
// 初始化Three.js核心组件
let scene, camera, renderer, controls;

// 新增变量
let isLoading = true;
const autoRotateSpeed = 1; // 自动旋转速度 (1=正常速度)

function init() {
    // 创建场景
    scene = new THREE.Scene();
    scene.background = new THREE.Color(0xeeeeee);  // 设置背景颜色

    // 创建透视相机
    camera = new THREE.PerspectiveCamera(
        45,  // 视野角度
        window.innerWidth / window.innerHeight,  // 宽高比
        0.1,  // 近平面
        1000  // 远平面
    );
    camera.position.set(5, 5, 5);  // 设置相机位置

    // 创建渲染器
    renderer = new THREE.WebGLRenderer({ antialias: true });
    renderer.setSize(window.innerWidth, window.innerHeight);
    document.body.appendChild(renderer.domElement);

    // 添加灯光
    const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
    scene.add(ambientLight);

    const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
    directionalLight.position.set(10, 10, 10);
    scene.add(directionalLight);

    // 添加轨道控制器(需要额外引入OrbitControls.js)
    controls = new THREE.OrbitControls(camera, renderer.domElement);
    controls.enableDamping = true;  // 启用阻尼效果
    controls.dampingFactor = 0.05;

    // 修改后的模型加载部分
    const loader = new THREE.GLTFLoader();
    loader.load(
        'model.glb',
        (gltf) => {
            model = gltf.scene; // 保存模型引用
            scene.add(model);

            // 隐藏进度条
            isLoading = false;
            document.getElementById('loading-container').style.display = 'none';

            // 自动调整视角
            const box = new THREE.Box3().setFromObject(model);
            const center = box.getCenter(new THREE.Vector3());
            const size = box.getSize(new THREE.Vector3());
            camera.position.copy(center).add(new THREE.Vector3(size.length(), size.length(), size.length() * 1.5));
            controls.target.copy(center); // 设置控制器的焦点
            controls.update();
        },
        (xhr) => {
            // 更新进度条
            const progress = (xhr.loaded / xhr.total) * 100;
            document.getElementById('progress-bar').style.width = `${progress}%`;
        },
        (error) => {
            console.error('加载错误:', error);
            document.getElementById('progress-bar').style.background = '#ff0000';
        }
    );

    // 配置自动旋转
    controls.autoRotate = true; // 启用自动旋转
    controls.autoRotateSpeed = autoRotateSpeed; // 设置旋转速度

    // 处理窗口大小变化
    window.addEventListener('resize', onWindowResize, false);
}

// 窗口大小变化处理函数
function onWindowResize() {
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(window.innerWidth, window.innerHeight);
}

// 动画循环
function animate() {
    requestAnimationFrame(animate);

    // 只在加载完成后执行自动旋转
    if (!isLoading) {
        controls.update();  // 更新控制器
    }

    renderer.render(scene, camera);
}

// 初始化并启动
init();
animate();

我现在想要在我的另一个网页中,把这个3d模型作为一个部分放进去(类似于插入一张图片),

home.html
<!-- Custom HTML site displayed as the Home chapter -->

{% extends "main.html" %}
{% block tabs %}
{{ super() }}
<style>

    .md-main {
        flex-grow: 0
    }

    .md-main__inner {
        display: flex;
        height: 100%;
    }

    .tx-container {
        padding-top: .0rem;
        background: linear-gradient(to bottom, var(--md-primary-fg-color), var(--md-link-color) 99%)
    }

    .tx-hero {
        margin: 32px 2.8rem;
        color: var(--md-primary-bg-color);
        justify-content: center;
    }

    .tx-hero h1 {
        margin-bottom: 1rem;
        color: currentColor;
        font-weight: 700
    }

    .tx-hero__content {
        padding-bottom: 1rem;
        margin: 0 auto;
    }

    .tx-hero__image{
        width:17rem;
        height:17rem;
        order:1;
        padding-right: 2.5rem;
    }

    .feature-item h3 svg {
        fill: var(--md-default-fg-color);
        height: 30px;
        float: left;
        margin-right: 10px;
        transform: translateY(10%);
    }

    .top-hr {
        margin-top: 42px;
    }

    .feature-item {
        font-family: var(--md-text-font);
        font-weight: 300;
        box-sizing: border-box;
        padding: 0 15px;
        word-break: break-word
    }

    @media screen and (max-width:30em) {
        .tx-hero h1 {
            font-size: 1.4rem
        }
    }

    @media screen and (min-width:60em) {
        .md-sidebar--secondary {
            display: none
        }

        .tx-hero {
            display: flex;
            align-items: center;
            justify-content: center;
        }

        .tx-hero__content {
            max-width: 22rem;
            margin-top: 3.5rem;
            margin-bottom: 3.5rem;
            margin-left: 1.0rem;
            margin-right: 4.0rem;
            align-items: center;
        }
    }

    @media screen and (min-width:76.25em) {
        .md-sidebar--primary {
            display: none
        }

        .top-hr {
            width: 100%;
            display: flex;
            max-width: 61rem;
            margin-right: auto;
            margin-left: auto;
            padding: 0 .2rem;
        }

        .bottom-hr {
            margin-top: 10px;
            width: 100%;
            display: flex;
            max-width: 61rem;
            margin-right: auto;
            margin-left: auto;
            padding: 0 .2rem;
        }

        .feature-item {
            flex: 1;
            min-width: 0;
        }
    }

    .hr {
        border-bottom: 1px solid #eee;
        width: 100%;
        margin: 20px 0;
    }

    .text-center {
        text-align: center;
        padding-right: 15px;
        padding-left: 15px;
        margin-right: auto;
        margin-left: auto;
        margin-top: 15px;
        font-family: 'Lato', sans-serif;
        font-size: 23px;
        font-weight: 300;
        padding-bottom: 10px;
    }

    .logos {
        display: flex;
        align-items: center;
        justify-content: center;
        flex-flow: row wrap;
        margin: 0 auto;
    }

    .logos img {
        flex: 1 1 auto;
        padding: 25px;
        max-height: 130px;
        vertical-align: middle;
    }

    .hr-logos {
        margin-top: 0;
        margin-bottom: 30px;
    }

    .md-footer-meta__inner {
        display: flex;
        flex-wrap: wrap;
        justify-content: space-between;
        margin-top: 1.0rem;
    }

    .md-footer-social {
        padding-top: 20px;
    }

    .md-typeset img, .md-typeset video {
        box-shadow: initial;
        border: initial;
        max-width: 110%;
    }
</style>

<!-- Main site Entry button descriptions -->
<section class="tx-container">
    <div class="md-grid md-typeset">
    <div class="tx-hero">
        <div class="tx-hero__image">
        <img src="assets/azure-kinect-sensor.webp" draggable="false">
        </div>
        <div class="tx-hero__content">
        <h1>
            视觉技术文档
            <br>
            <p align="center">
                <span class="twemoji">
                    {% include ".icons/material/camera-iris.svg" %} 
                </span>·<span class="twemoji">
                    {% include ".icons/material/eye.svg" %} 
                </span>·<span class="twemoji">
                    {% include ".icons/material/camera.svg" %} 
                </span>·<span class="twemoji">
                    {% include ".icons/material/video-image.svg" %} 
                </span>
            </p>
        </h1>
        <a href="{{ page.next_page.url | url }}" class="md-button md-button--primary">
            开始查看文档
        </a>
        <a href="blog/" class="md-button">
            查看记录博客
        </a>
        </div>
    </div>
    </div>
</section>

<!-- Main site box descriptions -->
<div class="top-hr">
    <div class="feature-item">
        <div class="md-grid md-typeset">
            <h3>基地网站</h3>
            <p>
                <a href="https://npu-home.github.io" class="md-button" id="width-left">
                    <span class="twemoji">
                        {% include ".icons/material/home.svg" %} 
                    </span>
                    基地官网:NPU-舞蹈机器人基地
                </a>
                <a href="https://github.com/NPU-Home" class="md-button" id="width-left">
                    <span class="twemoji">
                        {% include ".icons/material/github.svg" %} 
                    </span>
                    家政组软件仓库:NPU-Home
                </a>
            </p>
        </div>
    </div>
    <div class="feature-item">
        <div class="md-grid md-typeset">
            <h3>文档仓库</h3>
            <p>
                <a href="https://github.com/NPU-Home/vision-docs" class="md-button" id="width-left">
                    <span class="twemoji">
                        {% include ".icons/material/book.svg" %} 
                    </span>
                    文档仓库:vision-docs
                    <span class="twemoji">
                        {% include ".icons/octicons/lock-24.svg" %} 
                    </span>
                </a>
                <a href="https://github.com/NPU-Home/vision-docs-site" class="md-button" id="width-left">
                    <span class="twemoji">
                        {% include ".icons/material/web.svg" %} 
                    </span>
                    网站源代码:vision-docs-site
                </a>
            </p>
        </div>
    </div>
</div>


{% endblock %}
{% block content %}{% endblock %}
{% block footer %}{% endblock %}

我想要的效果是这样,<img src="assets/azure-kinect-sensor.webp" draggable="false">这个图片被点击后,图片取消显示,开始加载3d的模型。

我现在应该怎么做才能实现这个效果呢?

 

DeepSeek:

已深度思考(用时 43 秒)

嗯,用户想要在他们现有的另一个网页中嵌入一个3D模型,当点击原本显示的图片时,图片隐藏并开始加载3D模型。这需要结合现有的Three.js代码和网页中的元素交互。我需要逐步分析如何实现这个效果。

首先,用户当前的网页结构是使用MkDocs的模板,其中有一个图片元素,当点击该图片时需要替换为3D模型。所以,第一步应该是在图片被点击时触发一个事件,隐藏图片并启动Three.js的场景加载。

接下来需要考虑如何将原有的独立index.html中的Three.js代码整合到当前的页面中。原来的index.html包含HTML结构和main.js脚本,但现在需要将这些内容嵌入到现有的页面结构中,特别是作为图片点击后的替换内容。

可能的步骤包括:

  1. 事件监听:给图片元素添加点击事件监听器,当点击时执行相应的函数。

  2. 动态创建容器:在图片的位置或附近创建一个容器元素,用于承载Three.js的canvas和加载进度条。

  3. 加载Three.js相关资源:确保页面已经加载了Three.js、GLTFLoader和OrbitControls等必要的库。

  4. 初始化3D场景:在点击事件中初始化Three.js的场景、相机、渲染器等,并开始加载模型。

  5. 隐藏图片:点击后隐藏原始图片,显示3D模型容器。

  6. 处理加载进度和完成后的操作:显示加载进度条,加载完成后隐藏进度条并启动自动旋转。

需要考虑的问题:

  • 库的引入:现有的模板可能没有引入Three.js及其相关组件,需要确认在页面中正确引入这些脚本。

  • 容器尺寸:3D模型的渲染区域需要合适的尺寸,可能需要设置为与图片相同的大小,或者根据父容器调整。

  • 防止重复加载:用户可能多次点击图片,需要确保Three.js的初始化只执行一次,或者正确处理重复加载的情况。

  • 页面布局:插入的canvas元素可能会影响现有布局,需要使用CSS确保其正确放置,比如绝对定位或覆盖在图片的位置。

  • 性能问题:在已有页面中嵌入3D模型需要注意性能,避免影响其他部分的交互和加载。

可能的实现方式:

  1. 修改HTML结构:在图片的父容器中添加一个用于3D模型的div,设置其样式为隐藏,点击图片时显示该div并隐藏图片。

  2. 动态加载脚本:如果原页面没有加载Three.js相关脚本,可以使用动态脚本加载的方式,在点击时加载所需库,但需要注意异步加载的问题。

  3. 整合原有代码:将main.js中的初始化函数和动画循环整合到点击事件的处理函数中,确保正确的作用域和变量访问。

具体步骤:

  • 在图片的HTML元素中添加onclick事件,例如onclick="load3DModel()"。

  • 在图片附近添加一个容器div,设置其ID(如model-container),初始状态隐藏(display: none)。

  • 在load3DModel函数中,隐藏图片,显示model-container,并执行Three.js的初始化。

  • 确保Three.js所需的脚本已经在页面中存在,如果没有,需要在HTML头部添加相应的script标签。

  • 调整Three.js的渲染器尺寸以适应容器的大小,可能需要根据容器的尺寸设置renderer的大小,并处理窗口resize事件。

  • 确保模型加载后的自动旋转和控制器正确工作,同时处理可能的交互冲突,比如页面滚动或其它事件。

另外,需要考虑用户可能已经存在的样式冲突,比如z-index问题,确保canvas能够正确显示在页面的正确位置。进度条的位置可能需要调整,以适应新的容器大小。

测试过程中需要注意的问题:

  • 点击图片后,是否成功隐藏图片并显示3D容器。

  • Three.js是否正确初始化,模型是否加载,是否有控制台报错。

  • 进度条是否正常显示和隐藏。

  • 窗口调整大小时,3D场景是否自适应。

  • 多次点击是否导致重复初始化或内存泄漏。

最后,可能需要调整原有Three.js代码中的部分参数,比如相机的初始位置和自动旋转速度,以适应嵌入后的容器尺寸和布局。

要在现有页面中实现点击图片替换为3D模型的效果,可以按照以下步骤实现:

  1. 首先在现有页面中添加3D模型容器和必要的脚本引用:

    <!-- 在.tx-hero__image区块中添加模型容器 -->
    <div class="tx-hero__image">
      <img id="model-preview" src="assets/azure-kinect-sensor.webp" draggable="false" style="cursor: pointer">
      <div id="model-container" style="display: none; width: 100%; height: 100%; position: relative;"></div>
    </div>
    
    <!-- 在现有脚本引用之后添加 -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/loaders/GLTFLoader.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script>
    
  2. 添加新的样式到现有样式中:

    #model-container canvas {
      width: 100% !important;
      height: 100% !important;
    }
    
    .model-loading {
      position: absolute;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
      width: 80%;
      height: 3px;
      background: rgba(255,255,255,0.2);
      z-index: 100;
    }
    
    .model-progress {
      height: 100%;
      background: #00ff88;
      transition: width 0.3s ease;
    }
    
  3. 在页面底部添加JavaScript代码:

    <script>
    // 初始化变量
    let scene, camera, renderer, controls, model;
    let isModelLoaded = false;
    
    function init3DViewer(container) {
      // 初始化场景
      scene = new THREE.Scene();
      scene.background = new THREE.Color(0xeeeeee);
    
      // 相机设置
      camera = new THREE.PerspectiveCamera(45, container.offsetWidth / container.offsetHeight, 0.1, 1000);
      camera.position.set(5, 5, 5);
    
      // 渲染器
      renderer = new THREE.WebGLRenderer({ antialias: true });
      renderer.setSize(container.offsetWidth, container.offsetHeight);
      container.appendChild(renderer.domElement);
    
      // 灯光
      const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
      scene.add(ambientLight);
      const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
      directionalLight.position.set(10, 10, 10);
      scene.add(directionalLight);
    
      // 控制器
      controls = new THREE.OrbitControls(camera, renderer.domElement);
      controls.enableDamping = true;
      controls.dampingFactor = 0.05;
      controls.autoRotate = true;
    }
    
    function loadModel() {
      const container = document.getElementById('model-container');
      const progressBar = document.createElement('div');
      progressBar.className = 'model-loading';
      progressBar.innerHTML = '<div class="model-progress"></div>';
      container.appendChild(progressBar);
    
      const loader = new THREE.GLTFLoader();
      loader.load(
        'model.glb',
        (gltf) => {
          model = gltf.scene;
          scene.add(model);
    
          // 移除进度条
          container.removeChild(progressBar);
    
          // 调整视角
          const box = new THREE.Box3().setFromObject(model);
          const center = box.getCenter(new THREE.Vector3());
          const size = box.getSize(new THREE.Vector3());
          camera.position.copy(center).add(new THREE.Vector3(size.length(), size.length(), size.length() * 1.5));
          controls.target.copy(center);
          controls.update();
        },
        (xhr) => {
          const progress = (xhr.loaded / xhr.total) * 100;
          progressBar.querySelector('.model-progress').style.width = `${progress}%`;
        },
        (error) => {
          console.error('加载失败:', error);
          progressBar.querySelector('.model-progress').style.background = '#ff0000';
        }
      );
    }
    
    // 点击图片事件处理
    document.getElementById('model-preview').addEventListener('click', function() {
      if (!isModelLoaded) {
        const container = document.getElementById('model-container');
        this.style.display = 'none';
        container.style.display = 'block';
    
        if (!scene) {
          init3DViewer(container);
          loadModel();
        }
    
        isModelLoaded = true;
    
        // 启动动画循环
        function animate() {
          requestAnimationFrame(animate);
          controls.update();
          renderer.render(scene, camera);
        }
        animate();
      }
    });
    
    // 窗口大小调整处理
    window.addEventListener('resize', () => {
      if (camera && renderer) {
        const container = document.getElementById('model-container');
        camera.aspect = container.offsetWidth / container.offsetHeight;
        camera.updateProjectionMatrix();
        renderer.setSize(container.offsetWidth, container.offsetHeight);
      }
    });
    </script>
    
  4. 需要修改的部分说明:

    • 将原来的独立页面功能封装成按需加载
    • 添加点击事件处理来触发3D模型加载
    • 动态创建进度条而不是使用固定HTML
    • 自动处理容器尺寸变化
    • 保持原有页面布局结构不变

这个实现方案有以下特点:

  1. 保持原有页面结构,仅在点击时初始化3D场景
  2. 自动适应容器尺寸
  3. 显示加载进度条
  4. 支持窗口大小调整
  5. 保持OrbitControls的交互功能
  6. 模型加载后自动旋转

注意需要确保:

  • 模型文件路径正确
  • Three.js相关CDN链接可访问
  • 容器尺寸与图片显示区域匹配
  • 模型复杂度适合在网页中展示

如果遇到性能问题,可以考虑:

  1. 降低模型多边形数量
  2. 添加加载完成前的旋转指示器
  3. 限制最大渲染分辨率
  4. 添加模型卸载功能(如果需要)

 

You:

现在3d模型成功显示在我的预览网页上了。我现在想要把这个3d模型显示窗口中,背景的部分改成透明的(现在背景是灰色的,会遮住网页原有的背景)

 

DeepSeek:

已深度思考(用时 17 秒)

好的,用户现在成功将3D模型嵌入到他们的网页中,但希望将模型的背景改为透明,这样就不会遮挡网页原有的背景。首先,我需要回顾之前的对话,确认他们是如何设置Three.js场景的。之前他们提到场景的背景颜色是灰色(0xeeeeee),所以第一步应该是修改这个颜色设置,使其透明。

然后,我需要考虑如何正确配置Three.js的渲染器以支持透明背景。Three.js的WebGLRenderer有一个alpha参数,设置为true可以让canvas的背景透明。同时,可能还需要调整渲染器的clearAlpha值,确保渲染器在清除画布时不会覆盖背景颜色。

接下来,用户可能希望场景中的物体本身不要有背景色,因此需要检查是否有其他元素(如灯光或材质)可能影响透明度。例如,环境光和方向光的颜色和强度是否合适,材质是否使用透明属性等。如果模型材质本身不支持透明,可能需要调整材质设置,但用户的问题主要集中在背景透明,所以可能不需要改动材质。

另外,考虑到用户可能在集成到现有网页时遇到层叠问题,需要确认canvas元素的CSS样式是否正确,特别是z-index和position属性,确保模型不会遮挡其他内容,或者被其他元素遮挡。

还有,用户在点击图片后加载模型,可能需要确保在初始化渲染器时已经启用了alpha通道。检查他们的init3DViewer函数中创建WebGLRenderer的参数是否正确设置了alpha:true。

最后,测试这些修改是否有效,确保背景透明后,模型的交互功能(如轨道控制)仍然正常工作,并且网页的其他部分如预期显示。如果有问题,可能需要进一步调整渲染器的设置或CSS样式。

要实现Three.js场景透明背景,需要修改以下四个关键位置:

  1. 修改渲染器初始化配置(在init3DViewer函数中):

    renderer = new THREE.WebGLRenderer({ 
      antialias: true,
      alpha: true  // 启用alpha通道
    });
    renderer.setClearAlpha(0);  // 设置完全透明
    
  2. 移除场景背景色设置(删除或注释掉这行):

    // scene.background = new THREE.Color(0xeeeeee);  // 注释或删除这行
    
  3. 在CSS中添加模型容器的透明处理:

    #model-container {
      background: transparent !important;
    }
    
    #model-container canvas {
      background: transparent !important;
      mix-blend-mode: multiply;  /* 可选,根据背景调整混合模式 */
    }
    
  4. 如果模型需要透明材质支持,在加载后添加:

    loader.load((gltf) => {
      model = gltf.scene;
      // 遍历所有材质设置透明属性
      model.traverse((child) => {
        if (child.material) {
          child.material.transparent = true;
          child.material.depthWrite = false;  // 改善透明渲染
        }
      });
    });
    

完整修改后的init3DViewer函数应该像这样:

function init3DViewer(container) {
  // 初始化场景
  scene = new THREE.Scene();
  // 移除原有的背景色设置

  // 相机设置
  camera = new THREE.PerspectiveCamera(45, container.offsetWidth / container.offsetHeight, 0.1, 1000);
  camera.position.set(5, 5, 5);

  // 渲染器配置(关键修改)
  renderer = new THREE.WebGLRenderer({ 
    antialias: true,
    alpha: true  // 启用alpha通道
  });
  renderer.setClearColor(0x000000, 0); // 设置透明背景
  renderer.setSize(container.offsetWidth, container.offsetHeight);
  container.appendChild(renderer.domElement);

  // 保持其他灯光和控制器设置不变...
}

注意几个可能需要额外调整的地方:

  1. 如果模型有半透明材质,需要设置material.transparent = true
  2. 调整灯光强度避免过曝:

    const ambientLight = new THREE.AmbientLight(0xffffff, 0.3);  // 降低环境光强度
    const directionalLight = new THREE.DirectionalLight(0xffffff, 0.6);  // 降低直射光强度
    
  3. 在CSS中确保父容器没有背景:

    .tx-hero__image {
      background: transparent !important;
      position: relative;
    }
    

修改完成后,模型背景会显示为透明,但可能会观察到以下现象需要进一步优化:

  • 模型边缘可能有抗锯齿白边(可添加renderer.antialias = true
  • 透明区域可能出现深度混合问题(尝试设置renderer.sortObjects = true
  • 环境反射可能丢失(需要添加场景环境贴图)

如果希望保留场景照明但保持背景透明,可以考虑添加环境贴图:

new THREE.RGBELoader()
  .setDataType( THREE.UnsignedByteType )
  .load( 'environment.hdr', (texture) => {
    scene.environment = texture;
    scene.background = texture;
});

到这里按照deepseek的回答,已经能够实现把3d模型嵌入到网页中显示,并且背景设置成透明了。但deepseek还把模型的材质给设置成透明了,预览效果之后我感觉还是不透明的好看,所以将这部分的javascript代码注释掉了

// 遍历所有材质设置透明属性
// model.traverse((child) => {
//   if (child.material) {
//     child.material.transparent = true;
//     child.material.depthWrite = false;  // 改善透明渲染
//   }
// });

然后由于在展示3d模型的那个框中,显示的模型太小了,之前查看官方说明文档的时候,发现

// 相机设置
camera = new THREE.PerspectiveCamera(45, container.offsetWidth / container.offsetHeight, 0.1, 1000);

这里的45是视野角度,将它调小,模型就会(在框中)显示得更大。但是如果设置的太小,模型的部分会在框外(显示不出来),最后调到了32刚刚好不会让模型超出框外。

home.html
<!-- Custom HTML site displayed as the Home chapter -->

{% extends "main.html" %}
{% block tabs %}
{{ super() }}
<style>

    .md-main {
        flex-grow: 0
    }

    .md-main__inner {
        display: flex;
        height: 100%;
    }

    .tx-container {
        padding-top: .0rem;
        background: linear-gradient(to bottom, var(--md-primary-fg-color), var(--md-link-color) 99%)
    }

    .tx-hero {
        margin: 32px 2.8rem;
        color: var(--md-primary-bg-color);
        justify-content: center;
    }

    .tx-hero h1 {
        margin-bottom: 1rem;
        color: currentColor;
        font-weight: 700
    }

    .tx-hero__content {
        padding-bottom: 1rem;
        margin: 0 auto;
    }

    .tx-hero__image{
        width:17rem;
        height:17rem;
        order:1;
        padding-right: 2.5rem;
    }

    .feature-item h3 svg {
        fill: var(--md-default-fg-color);
        height: 30px;
        float: left;
        margin-right: 10px;
        transform: translateY(10%);
    }

    .top-hr {
        margin-top: 42px;
    }

    .feature-item {
        font-family: var(--md-text-font);
        font-weight: 300;
        box-sizing: border-box;
        padding: 0 15px;
        word-break: break-word
    }

    @media screen and (max-width:30em) {
        .tx-hero h1 {
            font-size: 1.4rem
        }
    }

    @media screen and (min-width:60em) {
        .md-sidebar--secondary {
            display: none
        }

        .tx-hero {
            display: flex;
            align-items: center;
            justify-content: center;
        }

        .tx-hero__content {
            max-width: 22rem;
            margin-top: 3.5rem;
            margin-bottom: 3.5rem;
            margin-left: 1.0rem;
            margin-right: 4.0rem;
            align-items: center;
        }
    }

    @media screen and (min-width:76.25em) {
        .md-sidebar--primary {
            display: none
        }

        .top-hr {
            width: 100%;
            display: flex;
            max-width: 61rem;
            margin-right: auto;
            margin-left: auto;
            padding: 0 .2rem;
        }

        .bottom-hr {
            margin-top: 10px;
            width: 100%;
            display: flex;
            max-width: 61rem;
            margin-right: auto;
            margin-left: auto;
            padding: 0 .2rem;
        }

        .feature-item {
            flex: 1;
            min-width: 0;
        }
    }

    .hr {
        border-bottom: 1px solid #eee;
        width: 100%;
        margin: 20px 0;
    }

    .text-center {
        text-align: center;
        padding-right: 15px;
        padding-left: 15px;
        margin-right: auto;
        margin-left: auto;
        margin-top: 15px;
        font-family: 'Lato', sans-serif;
        font-size: 23px;
        font-weight: 300;
        padding-bottom: 10px;
    }

    .logos {
        display: flex;
        align-items: center;
        justify-content: center;
        flex-flow: row wrap;
        margin: 0 auto;
    }

    .logos img {
        flex: 1 1 auto;
        padding: 25px;
        max-height: 130px;
        vertical-align: middle;
    }

    .hr-logos {
        margin-top: 0;
        margin-bottom: 30px;
    }

    .md-footer-meta__inner {
        display: flex;
        flex-wrap: wrap;
        justify-content: space-between;
        margin-top: 1.0rem;
    }

    .md-footer-social {
        padding-top: 20px;
    }

    .md-typeset img, .md-typeset video {
        box-shadow: initial;
        border: initial;
        max-width: 110%;
    }

    #model-container {
        background: transparent !important;
    }

    #model-container canvas {
        width: 100% !important;
        height: 100% !important;
        background: transparent !important;
        mix-blend-mode: multiply;  /* 可选,根据背景调整混合模式 */
    }

    .model-loading {
        position: absolute;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
        width: 80%;
        height: 3px;
        background: rgba(255,255,255,0.2);
        z-index: 100;
    }

    .model-progress {
        height: 100%;
        background: #00ff88;
        transition: width 0.3s ease;
    }
</style>

<!-- Main site Entry button descriptions -->
<section class="tx-container">
    <div class="md-grid md-typeset">
      <div class="tx-hero">
        <div class="tx-hero__image">
          <img id="model-preview" src="assets/azure-kinect-sensor.webp" draggable="false" style="cursor: pointer">
          <div id="model-container" style="display: none; width: 100%; height: 100%; position: relative;"></div>
        </div>
        <div class="tx-hero__content">
          <h1>
            视觉技术文档
            <br>
            <p align="center">
                <span class="twemoji">
                    {% include ".icons/material/camera-iris.svg" %} 
                </span>·<span class="twemoji">
                    {% include ".icons/material/eye.svg" %} 
                </span>·<span class="twemoji">
                    {% include ".icons/material/camera.svg" %} 
                </span>·<span class="twemoji">
                    {% include ".icons/material/video-image.svg" %} 
                </span>
            </p>
          </h1>
          <a href="{{ page.next_page.url | url }}" class="md-button md-button--primary">
            开始查看文档
          </a>
          <a href="blog/" class="md-button">
            查看记录博客
          </a>
        </div>
      </div>
    </div>
</section>

<!-- Main site box descriptions -->
<div class="top-hr">
    <div class="feature-item">
        <div class="md-grid md-typeset">
            <h3>基地网站</h3>
            <p>
                <a href="https://npu-home.github.io" class="md-button" id="width-left">
                    <span class="twemoji">
                        {% include ".icons/material/home.svg" %} 
                    </span>
                    基地官网:NPU-舞蹈机器人基地
                </a>
                <a href="https://github.com/NPU-Home" class="md-button" id="width-left">
                    <span class="twemoji">
                        {% include ".icons/material/github.svg" %} 
                    </span>
                    家政组软件仓库:NPU-Home
                </a>
            </p>
        </div>
    </div>
    <div class="feature-item">
        <div class="md-grid md-typeset">
            <h3>文档仓库</h3>
            <p>
                <a href="https://github.com/NPU-Home/vision-docs" class="md-button" id="width-left">
                    <span class="twemoji">
                        {% include ".icons/material/book.svg" %} 
                    </span>
                    文档仓库:vision-docs
                    <span class="twemoji">
                        {% include ".icons/octicons/lock-24.svg" %} 
                    </span>
                </a>
                <a href="https://github.com/NPU-Home/vision-docs-site" class="md-button" id="width-left">
                    <span class="twemoji">
                        {% include ".icons/material/web.svg" %} 
                    </span>
                    网站源代码:vision-docs-site
                </a>
            </p>
        </div>
    </div>
</div>

<!-- 在现有脚本引用之后添加 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/loaders/GLTFLoader.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script>
<script>
    // 初始化变量
    let scene, camera, renderer, controls, model;
    let isModelLoaded = false;

    function init3DViewer(container) {
      // 初始化场景
      scene = new THREE.Scene();
      // scene.background = new THREE.Color(0xeeeeee);

      // 相机设置
      camera = new THREE.PerspectiveCamera(32, container.offsetWidth / container.offsetHeight, 0.1, 1000);  // 视角大小
      camera.position.set(5, 5, 5);

      // 渲染器
      renderer = new THREE.WebGLRenderer({ 
        antialias: true,
        alpha: true  // 启用alpha通道
      });
      renderer.setClearAlpha(0x000000, 0);  // 设置完全透明
      renderer.setSize(container.offsetWidth, container.offsetHeight);
      container.appendChild(renderer.domElement);

      // 灯光
      const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
      scene.add(ambientLight);
      const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
      directionalLight.position.set(10, 10, 10);
      scene.add(directionalLight);

      // 控制器
      controls = new THREE.OrbitControls(camera, renderer.domElement);
      controls.enableDamping = true;
      controls.dampingFactor = 0.05;
      controls.autoRotate = true;
    }

    function loadModel() {
      const container = document.getElementById('model-container');
      const progressBar = document.createElement('div');
      progressBar.className = 'model-loading';
      progressBar.innerHTML = '<div class="model-progress"></div>';
      container.appendChild(progressBar);

      const loader = new THREE.GLTFLoader();
      loader.load(
        'assets/akdk_camera_cad.glb',
        (gltf) => {
          model = gltf.scene;
          // 遍历所有材质设置透明属性
          // model.traverse((child) => {
          //   if (child.material) {
          //     child.material.transparent = true;
          //     child.material.depthWrite = false;  // 改善透明渲染
          //   }
          // });
          scene.add(model);

          // 移除进度条
          container.removeChild(progressBar);

          // 调整视角
          const box = new THREE.Box3().setFromObject(model);
          const center = box.getCenter(new THREE.Vector3());
          const size = box.getSize(new THREE.Vector3());
          camera.position.copy(center).add(new THREE.Vector3(size.length(), size.length(), size.length() * 1.5));
          controls.target.copy(center);
          controls.update();
        },
        (xhr) => {
          const progress = (xhr.loaded / xhr.total) * 100;
          progressBar.querySelector('.model-progress').style.width = `${progress}%`;
        },
        (error) => {
          console.error('加载失败:', error);
          progressBar.querySelector('.model-progress').style.background = '#ff0000';
        }
      );
    }

    // 点击图片事件处理
    document.getElementById('model-preview').addEventListener('click', function() {
      if (!isModelLoaded) {
        const container = document.getElementById('model-container');
        this.style.display = 'none';
        container.style.display = 'block';

        if (!scene) {
          init3DViewer(container);
          loadModel();
        }

        isModelLoaded = true;

        // 启动动画循环
        function animate() {
          requestAnimationFrame(animate);
          controls.update();
          renderer.render(scene, camera);
        }
        animate();
      }
    });

    // 窗口大小调整处理
    window.addEventListener('resize', () => {
      if (camera && renderer) {
        const container = document.getElementById('model-container');
        camera.aspect = container.offsetWidth / container.offsetHeight;
        camera.updateProjectionMatrix();
        renderer.setSize(container.offsetWidth, container.offsetHeight);
      }
    });
</script>

{% endblock %}
{% block content %}{% endblock %}
{% block footer %}{% endblock %}

最后更新: 2025-04-08
创建日期: 2025-04-07

评论