二(1)、3D 可旋转电脑网页实现教程

启发来自B站Up主:ai超级个人的视频。

一、准备工作


1.1 你需要准备什么

物品 用途 获取方式
电脑 写代码 你的电脑
代码编辑器 写代码 下载 VS Code(免费)
浏览器 看效果 Chrome 或 Edge
3D 电脑模型 显示在网页上 Sketchfab 免费下载
模型贴图 给模型上色 和模型一起下载

1.2 下载 3D 模型

步骤:

  1. 打开网站:https://sketchfab.com
  2. 搜索关键词:laptop 或 notebook
  3. 筛选免费模型(Price → Free)
  4. 下载格式选择 GLB 或 GLTF
  5. 解压后你会得到:
    • laptop.glb(模型文件)
    • texture.jpg(贴图文件,可能没有)

提示:如果没有贴图,模型可能是纯色的,也能用。

搜索框搜索laptop,我选择的是Berk Gedik'sCyberPunk Laptop Concept Design

image
然后点击下载3D模型(Download 3D Model)——> 会弹出下载格式,我最终选择了GLB最小的一个, 因为GitHub 有 100MB 的单文件限制, 而且小的话网页加载快点。(注意:一定是要使用free的)
image

会得到一个名为cyberpunk_laptop_concept_design.glb文件,然后重命名为laptop.glb


二、创建项目文件夹

2.1 创建文件夹结构

在你的电脑上创建如下文件夹:

my-3d-computer/          ← 项目总文件夹
├── index.html            ← 网页入口
├── style.css             ← 样式文件
└── js/                   ← JavaScript 文件夹
    └── main.js           ← 主要代码
└── assets/               ← 资源文件夹
    ├── models/           ← 放 3D 模型
    │   └── laptop.glb
    └── textures/         ← 放贴图
        └── laptop.jpg

我使用的是VSCode的替代版VSCodium
image


2.2 创建基础文件

index.html(v1:复制以下内容):

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>3D 可旋转电脑</title>
    <!-- 引入 Three.js -->
    <script type="importmap">
    {
        "imports": {
            "three": "https://unpkg.com/three@0.160.0/build/three.module.js",
            "three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
        }
    }
    </script>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        
        body {
            overflow: hidden;  /* 防止滚动条 */
            background: #1a1a1a;
        }
        
        #canvas-container {
            width: 100vw;
            height: 100vh;
            position: relative;
        }
        
        /* 加载提示 */
        #loading {
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            color: white;
            font-size: 20px;
            font-family: Arial, sans-serif;
        }
    </style>
</head>
<body>
    <div id="canvas-container">
        <div id="loading">加载中...</div>
    </div>
    
    <script type="module" src="./js/main.js"></script>
</body>
</html>

三、核心代码实现


3.1 创建 main.js 文件

在 js/ 文件夹中创建 main.js(第一版v1),复制以下完整代码:

// ============================================
// 3D 可旋转电脑 - 完整代码
// ============================================

import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { CSS3DRenderer, CSS3DObject } from 'three/addons/renderers/CSS3DRenderer.js';

// ============================================
// 第 1 步:获取容器
// ============================================
const container = document.getElementById('canvas-container');

// ============================================
// 第 2 步:创建场景(Scene)
// ============================================
// 场景就像一个大舞台,所有东西都要放在里面
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x222222);  // 深灰色背景

// CSS3D 需要单独的场景
const cssScene = new THREE.Scene();

// ============================================
// 第 3 步:创建相机(Camera)
// ============================================
// 相机就像你的眼睛,决定你能看到什么
const width = window.innerWidth;
const height = window.innerHeight;

const camera = new THREE.PerspectiveCamera(
    35,           // 视野角度(FOV)
    width / height, // 宽高比
    10,           // 近裁剪面
    100000        // 远裁剪面
);

// 初始相机位置(后面会自动调整)
camera.position.set(0, 0, 5000);

// ============================================
// 第 4 步:创建两个渲染器(重点!)
// ============================================

// 4.1 WebGL 渲染器 - 画 3D 模型(底层)
const renderer = new THREE.WebGLRenderer({
    antialias: true,    // 抗锯齿,让边缘更平滑
    alpha: true         // 支持透明
});

renderer.setSize(width, height);  // 设置大小为全屏
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));  // 限制像素比

// 设置样式:绝对定位,放在底层
renderer.domElement.style.position = 'absolute';
renderer.domElement.style.top = '0';
renderer.domElement.style.left = '0';
renderer.domElement.style.zIndex = '1';

container.appendChild(renderer.domElement);

// 4.2 CSS3D 渲染器 - 画网页(顶层)
const cssRenderer = new CSS3DRenderer();

cssRenderer.setSize(width, height);

// 设置样式:绝对定位,放在上层
cssRenderer.domElement.style.position = 'absolute';
cssRenderer.domElement.style.top = '0';
cssRenderer.domElement.style.left = '0';
cssRenderer.domElement.style.zIndex = '2';

container.appendChild(cssRenderer.domElement);

// ============================================
// 第 5 步:创建轨道控制器(OrbitControls)
// ============================================
// 这让你可以用鼠标旋转、缩放、平移

const controls = new OrbitControls(camera, cssRenderer.domElement);
controls.enableDamping = true;      // 开启阻尼(惯性效果)
controls.dampingFactor = 0.05;      // 阻尼系数

// ============================================
// 第 6 步:加载 3D 模型
// ============================================

const loader = new GLTFLoader();

// 模型路径(根据你的实际路径修改)
const modelPath = './assets/models/laptop.glb';

loader.load(
    modelPath,
    function (gltf) {
        // 加载成功后的回调
        const model = gltf.scene;
        
        // 6.1 计算模型大小,自动调整相机
        autoFitCamera(model);
        
        // 6.2 添加模型到场景
        scene.add(model);
        
        // 6.3 创建电脑屏幕上的网页
        createScreen();
        
        // 6.4 隐藏加载提示
        document.getElementById('loading').style.display = 'none';
    },
    function (progress) {
        // 加载进度(可选)
        console.log('加载进度:', (progress.loaded / progress.total * 100) + '%');
    },
    function (error) {
        // 加载失败
        console.error('模型加载失败:', error);
        alert('模型加载失败,请检查路径是否正确');
    }
);

// ============================================
// 第 7 步:自动对焦函数
// ============================================

function autoFitCamera(object) {
    // 创建一个包围盒,计算模型的大小
    const box = new THREE.Box3().setFromObject(object);
    
    // 获取模型的尺寸
    const size = new THREE.Vector3();
    box.getSize(size);
    
    // 获取模型的中心点
    const center = new THREE.Vector3();
    box.getCenter(center);
    
    console.log('模型尺寸:', size);
    console.log('模型中心:', center);
    
    // 计算相机应该放多远
    const maxDim = Math.max(size.x, size.y, size.z);
    const fov = camera.fov * (Math.PI / 180);
    const cameraDistance = Math.abs(maxDim / 2 / Math.tan(fov / 2)) * 1.5;
    
    // 设置相机位置
    camera.position.set(
        center.x + maxDim * 0.2,
        center.y + maxDim * 0.3,
        center.z + cameraDistance
    );
    
    // 让相机看向模型中心
    controls.target.copy(center);
    controls.update();
    
    console.log('相机位置:', camera.position);
}

// ============================================
// 第 8 步:创建电脑屏幕(核心!)
// ============================================

function createScreen() {
    // 8.1 创建 iframe 元素
    const iframe = document.createElement('iframe');
    
    // 设置要显示的网页(改成你的博客地址)
    iframe.src = 'https://www.example.com';
    
    // 设置 iframe 大小(逻辑分辨率)
    iframe.style.width = '1480px';
    iframe.style.height = '1100px';
    
    // 样式设置
    iframe.style.border = 'none';
    iframe.style.backgroundColor = 'white';
    
    // 防止穿模:背面不可见
    iframe.style.backfaceVisibility = 'hidden';
    
    // ========================================
    // 8.2 将 iframe 转为 3D 对象
    // ========================================
    const screenObject = new CSS3DObject(iframe);
    
    // ========================================
    // 8.3 调整位置和旋转(需要手动调试!)
    // ========================================
    
    // 位置(x, y, z)- 根据你的模型调整
    screenObject.position.set(0, 100, 0);
    
    // 旋转(x, y, z)- 单位是弧度
    // -Math.PI / 2 表示 90度
    screenObject.rotation.x = -0.2;
    
    // 缩放(如果网页太大或太小)
    screenObject.scale.set(0.5, 0.5, 0.5);
    
    // 添加到 CSS3D 场景
    cssScene.add(screenObject);
    
    console.log('屏幕已创建');
    
    // ========================================
    // 8.4 添加调试功能(按键盘调整位置)
    // ========================================
    setupDebugControls(screenObject);
}

// ============================================
// 第 9 步:键盘调试功能
// ============================================

function setupDebugControls(screenObject) {
    console.log('=== 调试模式已开启 ===');
    console.log('按键说明:');
    console.log('方向键 ↑↓←→ : 移动屏幕位置');
    console.log('W/S : 前后移动');
    console.log('Q/E : 旋转 X 轴');
    console.log('A/D : 旋转 Y 轴');
    console.log('Z/X : 旋转 Z 轴');
    console.log('+/- : 缩放');
    console.log('P : 打印当前坐标');
    
    const step = 10;        // 移动步长
    const rotStep = 0.05;   // 旋转步长
    
    window.addEventListener('keydown', (e) => {
        switch(e.key) {
            // 位置移动
            case 'ArrowUp':
                screenObject.position.y += step;
                break;
            case 'ArrowDown':
                screenObject.position.y -= step;
                break;
            case 'ArrowLeft':
                screenObject.position.x -= step;
                break;
            case 'ArrowRight':
                screenObject.position.x += step;
                break;
            case 'w':
            case 'W':
                screenObject.position.z -= step;
                break;
            case 's':
            case 'S':
                screenObject.position.z += step;
                break;
                
            // 旋转
            case 'q':
            case 'Q':
                screenObject.rotation.x += rotStep;
                break;
            case 'e':
            case 'E':
                screenObject.rotation.x -= rotStep;
                break;
            case 'a':
            case 'A':
                screenObject.rotation.y += rotStep;
                break;
            case 'd':
            case 'D':
                screenObject.rotation.y -= rotStep;
                break;
            case 'z':
            case 'Z':
                screenObject.rotation.z += rotStep;
                break;
            case 'x':
            case 'X':
                screenObject.rotation.z -= rotStep;
                break;
                
            // 缩放
            case '+':
            case '=':
                screenObject.scale.multiplyScalar(1.1);
                break;
            case '-':
                screenObject.scale.multiplyScalar(0.9);
                break;
                
            // 打印坐标
            case 'p':
            case 'P':
                console.log('========== 当前坐标 ==========');
                console.log(`position.set(${screenObject.position.x.toFixed(2)}, ${screenObject.position.y.toFixed(2)}, ${screenObject.position.z.toFixed(2)})`);
                console.log(`rotation.set(${screenObject.rotation.x.toFixed(2)}, ${screenObject.rotation.y.toFixed(2)}, ${screenObject.rotation.z.toFixed(2)})`);
                console.log(`scale.set(${screenObject.scale.x.toFixed(2)}, ${screenObject.scale.y.toFixed(2)}, ${screenObject.scale.z.toFixed(2)})`);
                console.log('==============================');
                break;
        }
    });
}

// ============================================
// 第 10 步:渲染循环
// ============================================

function animate() {
    requestAnimationFrame(animate);
    
    // 更新控制器
    controls.update();
    
    // 同时渲染两个场景
    renderer.render(scene, camera);      // 3D 模型
    cssRenderer.render(cssScene, camera); // 网页
}

// 启动渲染
animate();

// ============================================
// 第 11 步:响应窗口大小变化
// ============================================

window.addEventListener('resize', () => {
    const newWidth = window.innerWidth;
    const newHeight = window.innerHeight;
    
    // 更新相机
    camera.aspect = newWidth / newHeight;
    camera.updateProjectionMatrix();
    
    // 更新两个渲染器
    renderer.setSize(newWidth, newHeight);
    cssRenderer.setSize(newWidth, newHeight);
});

console.log('3D 电脑初始化完成');

注意:将8.1部分iframe.src = 'https://www.example.com';里面修改成自己的博客地址,比如我的博客地址https://rockosdev.github.io/


四、运行项目


4.1 本地运行方式

方式一:使用 VS Code 插件(推荐小白)
安装 VS Code
安装插件:Live Server
右键点击 index.html → Open with Live Server
浏览器会自动打开

image

image


方式二:使用 Python 临时服务器

# 打开终端,进入项目文件夹
cd my-3d-computer

# Python 3
python -m http.server 8000

# 然后浏览器访问 http://localhost:8000

4.2 你应该看到什么

阶段 现象
加载中 黑屏,显示"加载中..."
加载完成 出现 3D 电脑模型
鼠标操作 可以旋转、缩放、平移
屏幕位置 可能不对,需要调试

五、调试屏幕位置(关键步骤!)

5.1 调试方法

因为每个 3D 模型的屏幕位置和大小都不一样,你需要手动调整。
操作步骤:

  1. 打开浏览器控制台(按 F12)
  2. 查看按键说明(控制台会打印)
  3. 使用键盘调整:
按键 功能
↑ ↓ ← → 上下左右移动屏幕
W / S 前后移动
Q / E 绕 X 轴旋转
A / D 绕 Y 轴旋转
Z / X 绕 Z 轴旋转
+ / - 放大 / 缩小
P 打印当前坐标

调整之后按下P打印当前坐标
image

然后在main.js中8.3“调整位置和旋转”部分替换成自己刚才测试的坐标

    // 完美坐标(3/17/2026)
    position.set(0.00, 52.00, -8.00)
    rotation.set(-0.37, 0.00, 0.00)
    scale.set(0.125, 0.11, 0.20)

完整的第八部分为

// ============================================
// 第 8 步:创建电脑屏幕(核心!)
// ============================================

function createScreen() {
    // 8.1 创建 iframe 元素
    const iframe = document.createElement('iframe');
    
    // 设置要显示的网页(改成你的博客地址)
    iframe.src = 'https://rockosdev.github.io/';
    
    // 设置 iframe 大小(逻辑分辨率)
    iframe.style.width = '1480px';
    iframe.style.height = '1100px';
    
    // 样式设置
    iframe.style.border = 'none';
    iframe.style.backgroundColor = 'white';
    
    // 防止穿模:背面不可见
    iframe.style.backfaceVisibility = 'hidden';
    
    // ========================================
    // 8.2 将 iframe 转为 3D 对象
    // ========================================
    const screenObject = new CSS3DObject(iframe);
    
    // ========================================
    // 8.3 调整位置和旋转(需要手动调试!)
    // ========================================
    // 完美坐标(3/17/2026)
    //screenObject.position.set(0.00, 52.00, -8.00);
    //screenObject.rotation.set(-0.37, 0.00, 0.00);
    //screenObject.scale.set(0.12, 0.11, 0.20);
    
    // 位置(x, y, z)- 根据你的模型调整
    screenObject.position.set(0.00, 52.00, -8.00);
    
    // 旋转(x, y, z)- 单位是弧度
    // -Math.PI / 2 表示 90度
    screenObject.rotation.x = -0.37;
    
    // 缩放(如果网页太大或太小)
    screenObject.scale.set(0.125, 0.11, 0.20);
    
    // 添加到 CSS3D 场景
    cssScene.add(screenObject);
    
    console.log('屏幕已创建');
    
    // ========================================
    // 8.4 添加调试功能(按键盘调整位置)
    // ========================================
    setupDebugControls(screenObject);
}

5.2 调试流程

第 1 步:看屏幕是否在电脑附近
    ↓ 不在?用方向键和 W/S 移动
    ↓
第 2 步:看屏幕角度是否贴合
    ↓ 不贴合?用 Q/E/A/D/Z/X 旋转
    ↓
第 3 步:看屏幕大小是否合适
    ↓ 不合适?用 +/- 缩放
    ↓
第 4 步:按 P 打印坐标
    ↓
第 5 步:修改代码中的坐标值
    ↓
第 6 步:刷新页面看效果

5.3 修改固定坐标

当你找到合适的坐标后,修改 createScreen 函数中的:

// 修改前(初始值)
screenObject.position.set(0, 100, 0);
screenObject.rotation.x = -0.2;
screenObject.scale.set(0.5, 0.5, 0.5);

// 修改后(你调试好的值,示例)
screenObject.position.set(900, 458, 765);
screenObject.rotation.x = -1.0;
screenObject.rotation.y = 0;
screenObject.rotation.z = 0;
screenObject.scale.set(1, 1, 1);

六、常见问题解决


6.1 黑屏/看不到模型

原因:相机在模型内部或模型太大/太小
解决:

  • 检查 modelPath 路径是否正确
  • 打开 F12 控制台看错误信息
  • 确保 autoFitCamera 函数被调用

在VSCodium使用Live Server插件默认打开的http://127.0.0.1:5500/浏览器页面,按下F12键————>到控制台发现

Failed to load resource: the server responded with a status of 404 (Not Found)

刷新了一遍,又没啥问题了, 原来是那个飘浮的网页挡住了模型,使用Ctrl和+(or-)可以缩小漂浮的网页
image


6.2 网页显示在模型后面(无法点击)

原因:层级不对或控制器绑定错误
解决:

  • 确保 cssRenderer.domElement.style.zIndex = '2'
  • 确保 renderer.domElement.style.zIndex = '1'
  • 确保 OrbitControls 绑定到 cssRenderer.domElement

6.3 屏幕位置调不准

原因:需要耐心调试
解决:

  • 使用小步长(把 step 改成 1)
  • 先调位置,再调旋转,最后调缩放
  • 一次只调一个轴

6.4 模型加载慢

原因:模型文件太大
解决:

  • 使用 Blender 简化模型面数
  • 压缩贴图图片
  • 添加加载进度条

完工,但是和原来的相比,总怪还是差点东西,好像比较暗淡。我决定优化一下。
image


七、进阶优化(可选)


7.1 添加加载动画

在 index.html 的 #loading 中添加:

<div id="loading">
    <div>加载中...</div>
    <div id="progress">0%</div>
</div>

7.2 添加光影效果

在 main.js 中添加:

// 环境光
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);

// 方向光
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(10, 10, 10);
scene.add(directionalLight);

7.3 添加辅助线

在 main.js 中添加:

// 网格辅助线
const gridHelper = new THREE.GridHelper(10000, 100);
scene.add(gridHelper);

// 坐标轴辅助线
const axesHelper = new THREE.AxesHelper(5000);
scene.add(axesHelper);

7.4 强制材质并保留贴图(145MB 大模型需要)

在 main.js 的 第 6 步 loader.load 成功回调 中添加:

// 遍历模型,强制使用标准材质并保留贴图
model.traverse((child) => {
    if (child.isMesh) {
        const oldMaterial = child.material;
        
        // 创建新标准材质
        const newMaterial = new THREE.MeshStandardMaterial({
            color: 0xffffff,
            roughness: 0.5,
            metalness: 0.1,
        });
        
        // 如果有贴图,复制过来
        if (oldMaterial && oldMaterial.map) {
            newMaterial.map = oldMaterial.map;
            newMaterial.map.flipY = false;
            newMaterial.map.colorSpace = THREE.SRGBColorSpace;
            console.log('贴图已复制:', child.name);
        } else {
            console.log('无贴图:', child.name);
        }
        
        child.material = newMaterial;
    }
});

7.5 优化加载进度显示(显示 MB)

在 main.js 的 第 6 步 progress 回调 中替换:

const percent = (progress.loaded / progress.total * 100).toFixed(1);
const mb = (progress.loaded / 1024 / 1024).toFixed(1);
const totalMb = (progress.total / 1024 / 1024).toFixed(1);

console.log(`加载中: ${percent}% (${mb}MB / ${totalMb}MB)`);

const loading = document.getElementById('loading');
if (loading) {
    loading.innerHTML = `加载模型中...<br>${percent}%<br>(${mb}MB / ${totalMb}MB)<br>145MB 较大,请耐心等待`;
}

7.6 修正背面剔除位置(防止穿模)

在 main.js 的 第 8 步 中:
删除原来错误的:

// 删除这行!
// iframe.style.backfaceVisibility = 'hidden';

在正确位置添加:

// 在 screenObject.scale.set() 之后,cssScene.add() 之前添加
screenObject.element.style.backfaceVisibility = 'hidden';

7.7 增强灯光(解决模型发黑)

在 main.js 的 第 4 步后,第 5 步前 添加(替换或补充 7.2):

// 环境光(提高亮度)
const ambientLight = new THREE.AmbientLight(0xffffff, 0.8);
scene.add(ambientLight);

// 主光源
const dirLight = new THREE.DirectionalLight(0xffffff, 1.5);
dirLight.position.set(10, 20, 10);
scene.add(dirLight);

// 补光
const fillLight = new THREE.DirectionalLight(0xffffff, 0.5);
fillLight.position.set(-10, 10, -10);
scene.add(fillLight);

// 底部反光
const bottomLight = new THREE.DirectionalLight(0xffffff, 0.3);
bottomLight.position.set(0, -10, 0);
scene.add(bottomLight);

第二版index.html(v2) 和 main.js(v3)添加内容:

编号 位置 添加内容 标记
7.1 index.html 的 #loading 加载动画 + 进度百分比 ⭐ 新增
7.2 第 5 步后 环境光 + 方向光 ⭐ 新增
7.3 第 5 步后 网格辅助线 + 坐标轴辅助线 ⭐ 新增
7.4 第 4 步后 灯光(环境光+方向光+补光+反光) ⭐ 新增
7.5 第 6 步 loader 成功回调 强制材质遍历 ⭐ 新增
7.6 第 6 步 progress 回调 MB 进度显示 ⭐ 新增
7.7 第 8 步 删除错误的 iframe.style.backfaceVisibility,添加正确的 screenObject.element.style.backfaceVisibility ⭐ 删除 + 新增

index.html(v2修改 #loading)

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>3D 可旋转电脑</title>
    <!-- 引入 Three.js -->
    <script type="importmap">
    {
        "imports": {
            "three": "https://unpkg.com/three@0.160.0/build/three.module.js",
            "three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
        }
    }
    </script>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        
        body {
            overflow: hidden;
            background: #1a1a1a;
        }
        
        #canvas-container {
            width: 100vw;
            height: 100vh;
            position: relative;
        }
        
        /* 7.1 加载动画样式 */
        #loading {
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            color: white;
            font-size: 20px;
            font-family: Arial, sans-serif;
            text-align: center;
            z-index: 10;
        }
        
        #progress {
            margin-top: 10px;
            font-size: 16px;
            color: #aaa;
        }
    </style>
</head>
<body>
    <div id="canvas-container">
        <!-- 7.1 添加加载动画 -->
        <div id="loading">
            <div>加载中...</div>
            <div id="progress">0%</div>
        </div>
    </div>
    
    <script type="module" src="./js/main.js"></script>
</body>
</html>

更新7.1~7.7之后的main.js(v2)

// ============================================
// 3D 可旋转电脑 - 完整代码(添加灯光+材质优化)
// ============================================

import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { CSS3DRenderer, CSS3DObject } from 'three/addons/renderers/CSS3DRenderer.js';

// ============================================
// 第 1 步:获取容器
// ============================================
const container = document.getElementById('canvas-container');

// ============================================
// 第 2 步:创建场景(Scene)
// ============================================
// 场景就像一个大舞台,所有东西都要放在里面
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x222222);  // 深灰色背景

// CSS3D 需要单独的场景
const cssScene = new THREE.Scene();

// ============================================
// 第 3 步:创建相机(Camera)
// ============================================
// 相机就像你的眼睛,决定你能看到什么
const width = window.innerWidth;
const height = window.innerHeight;

const camera = new THREE.PerspectiveCamera(
    35,           // 视野角度(FOV)
    width / height, // 宽高比
    10,           // 近裁剪面
    100000        // 远裁剪面
);

// 初始相机位置(后面会自动调整)
camera.position.set(0, 0, 5000);

// ============================================
// 第 4 步:创建两个渲染器(重点!)
// ============================================

// 4.1 WebGL 渲染器 - 画 3D 模型(底层)
const renderer = new THREE.WebGLRenderer({
    antialias: true,    // 抗锯齿,让边缘更平滑
    alpha: true         // 支持透明
});

renderer.setSize(width, height);  // 设置大小为全屏
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));  // 限制像素比

// 设置样式:绝对定位,放在底层
renderer.domElement.style.position = 'absolute';
renderer.domElement.style.top = '0';
renderer.domElement.style.left = '0';
renderer.domElement.style.zIndex = '1';

container.appendChild(renderer.domElement);

// 4.2 CSS3D 渲染器 - 画网页(顶层)
const cssRenderer = new CSS3DRenderer();

cssRenderer.setSize(width, height);

// 设置样式:绝对定位,放在上层
cssRenderer.domElement.style.position = 'absolute';
cssRenderer.domElement.style.top = '0';
cssRenderer.domElement.style.left = '0';
cssRenderer.domElement.style.zIndex = '2';

container.appendChild(cssRenderer.domElement);

// ============================================
// 第 4.5 步:添加灯光(新增!只添加不修改)
// ============================================

// 环境光(整体亮度)
const ambientLight = new THREE.AmbientLight(0xffffff, 0.8);
scene.add(ambientLight);

// 主光源
const dirLight = new THREE.DirectionalLight(0xffffff, 1.5);
dirLight.position.set(10, 20, 10);
scene.add(dirLight);

// 补光
const fillLight = new THREE.DirectionalLight(0xffffff, 0.5);
fillLight.position.set(-10, 10, -10);
scene.add(fillLight);

// 底部反光
const bottomLight = new THREE.DirectionalLight(0xffffff, 0.3);
bottomLight.position.set(0, -10, 0);
scene.add(bottomLight);

console.log('灯光已添加');

// ============================================
// 第 5 步:创建轨道控制器(OrbitControls)
// ============================================
// 这让你可以用鼠标旋转、缩放、平移

const controls = new OrbitControls(camera, cssRenderer.domElement);
controls.enableDamping = true;      // 开启阻尼(惯性效果)
controls.dampingFactor = 0.05;      // 阻尼系数

// ============================================
// 第 6 步:加载 3D 模型
// ============================================

const loader = new GLTFLoader();

// 模型路径(根据你的实际路径修改)
const modelPath = './assets/models/laptop.glb';

loader.load(
    modelPath,
    function (gltf) {
        // 加载成功后的回调
        
        // ========================================
        // 新增:强制使用标准材质并保留贴图(只添加不修改)
        // ========================================
        
        const model = gltf.scene;
        
        // 遍历模型,修复材质
        model.traverse((child) => {
            if (child.isMesh) {
                const oldMaterial = child.material;
                
                // 创建新标准材质
                const newMaterial = new THREE.MeshStandardMaterial({
                    color: 0xffffff,
                    roughness: 0.5,
                    metalness: 0.1,
                });
                
                // 如果有贴图,复制过来
                if (oldMaterial && oldMaterial.map) {
                    newMaterial.map = oldMaterial.map;
                    newMaterial.map.flipY = false;
                    newMaterial.map.colorSpace = THREE.SRGBColorSpace;
                    console.log('贴图已复制:', child.name);
                } else {
                    console.log('无贴图:', child.name);
                }
                
                child.material = newMaterial;
            }
        });
        
        // 原有代码不变
        autoFitCamera(model);
        
        // 6.2 添加模型到场景
        scene.add(model);
        
        // 6.3 创建电脑屏幕上的网页
        createScreen();
        
        // 6.4 隐藏加载提示
        document.getElementById('loading').style.display = 'none';
    },
    function (progress) {
        // ========================================
        // 新增:优化加载进度显示(只添加不修改)
        // ========================================
        
        const percent = (progress.loaded / progress.total * 100).toFixed(1);
        const mb = (progress.loaded / 1024 / 1024).toFixed(1);
        const totalMb = (progress.total / 1024 / 1024).toFixed(1);
        
        console.log(`加载中: ${percent}% (${mb}MB / ${totalMb}MB)`);
        
        const loading = document.getElementById('loading');
        if (loading) {
            loading.innerHTML = `加载模型中...<br>${percent}%<br>(${mb}MB / ${totalMb}MB)<br>145MB 较大,请耐心等待`;
        }
    },
    function (error) {
        // 加载失败
        console.error('模型加载失败:', error);
        alert('模型加载失败,请检查路径是否正确');
    }
);

// ============================================
// 第 7 步:自动对焦函数
// ============================================

function autoFitCamera(object) {
    // 创建一个包围盒,计算模型的大小
    const box = new THREE.Box3().setFromObject(object);
    
    // 获取模型的尺寸
    const size = new THREE.Vector3();
    box.getSize(size);
    
    // 获取模型的中心点
    const center = new THREE.Vector3();
    box.getCenter(center);
    
    console.log('模型尺寸:', size);
    console.log('模型中心:', center);
    
    // 计算相机应该放多远
    const maxDim = Math.max(size.x, size.y, size.z);
    const fov = camera.fov * (Math.PI / 180);
    const cameraDistance = Math.abs(maxDim / 2 / Math.tan(fov / 2)) * 1.5;
    
    // 设置相机位置
    camera.position.set(
        center.x + maxDim * 0.2,
        center.y + maxDim * 0.3,
        center.z + cameraDistance
    );
    
    // 让相机看向模型中心
    controls.target.copy(center);
    controls.update();
    
    console.log('相机位置:', camera.position);
}

// ============================================
// 第 7.2 步:添加光影效果
// ============================================

// 环境光 - 提供基础整体照明
const ambientLightExtra = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLightExtra);

// 方向光 - 模拟主光源,产生阴影效果
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(10, 10, 10);
scene.add(directionalLight);

// ============================================
// 第 7.3 步:添加辅助线
// ============================================

// 网格辅助线 - 显示地面网格,便于观察空间位置
const gridHelper = new THREE.GridHelper(10000, 100);
scene.add(gridHelper);

// 坐标轴辅助线 - 显示 X(红)、Y(绿)、Z(蓝) 三轴方向
const axesHelper = new THREE.AxesHelper(5000);
scene.add(axesHelper);

// ============================================
// 第 8 步:创建电脑屏幕(核心!)
// ============================================

function createScreen() {
    // 8.1 创建 iframe 元素
    const iframe = document.createElement('iframe');
    
    // 设置要显示的网页(改成你的博客地址)
    iframe.src = 'https://rockosdev.github.io/';
    
    // 设置 iframe 大小(逻辑分辨率)
    iframe.style.width = '1480px';
    iframe.style.height = '1100px';
    
    // 样式设置
    iframe.style.border = 'none';
    iframe.style.backgroundColor = 'white';
    
    // ========================================
    // 删除:原来错误的 backfaceVisibility 位置
    // iframe.style.backfaceVisibility = 'hidden';
    // ========================================
    
    // ========================================
    // 8.2 将 iframe 转为 3D 对象
    // ========================================
    const screenObject = new CSS3DObject(iframe);
    
    // ========================================
    // 8.3 调整位置和旋转(需要手动调试!)
    // ========================================
    // 完美坐标(3/17/2026)
    //screenObject.position.set(0.00, 52.00, -8.00);
    //screenObject.rotation.set(-0.37, 0.00, 0.00);
    //screenObject.scale.set(0.12, 0.11, 0.20);
    
    // 位置(x, y, z)- 根据你的模型调整
    screenObject.position.set(0.00, 52.00, -8.00);
    
    // 旋转(x, y, z)- 单位是弧度
    // -Math.PI / 2 表示 90度
    screenObject.rotation.x = -0.37;
    
    // 缩放(如果网页太大或太小)
    screenObject.scale.set(0.125, 0.11, 0.20);
    
    // ========================================
    // 新增:正确的 backfaceVisibility 位置(只添加)
    // ========================================
    screenObject.element.style.backfaceVisibility = 'hidden';
    
    // 添加到 CSS3D 场景
    cssScene.add(screenObject);
    
    console.log('屏幕已创建');
    
    // ========================================
    // 8.4 添加调试功能(按键盘调整位置)
    // ========================================
    setupDebugControls(screenObject);
}

// ============================================
// 第 9 步:键盘调试功能
// ============================================

function setupDebugControls(screenObject) {
    console.log('=== 调试模式已开启 ===');
    console.log('按键说明:');
    console.log('方向键 ↑↓←→ : 移动屏幕位置');
    console.log('W/S : 前后移动');
    console.log('Q/E : 旋转 X 轴');
    console.log('A/D : 旋转 Y 轴');
    console.log('Z/X : 旋转 Z 轴');
    console.log('+/- : 缩放');
    console.log('P : 打印当前坐标');
    
    const step = 2;        // 移动步长
    const rotStep = 0.01;   // 旋转步长
    
    window.addEventListener('keydown', (e) => {
        switch(e.key) {
            // 位置移动
            case 'ArrowUp':
                screenObject.position.y += step;
                break;
            case 'ArrowDown':
                screenObject.position.y -= step;
                break;
            case 'ArrowLeft':
                screenObject.position.x -= step;
                break;
            case 'ArrowRight':
                screenObject.position.x += step;
                break;
            case 'w':
            case 'W':
                screenObject.position.z -= step;
                break;
            case 's':
            case 'S':
                screenObject.position.z += step;
                break;
                
            // 旋转
            case 'q':
            case 'Q':
                screenObject.rotation.x += rotStep;
                break;
            case 'e':
            case 'E':
                screenObject.rotation.x -= rotStep;
                break;
            case 'a':
            case 'A':
                screenObject.rotation.y += rotStep;
                break;
            case 'd':
            case 'D':
                screenObject.rotation.y -= rotStep;
                break;
            case 'z':
            case 'Z':
                screenObject.rotation.z += rotStep;
                break;
            case 'x':
            case 'X':
                screenObject.rotation.z -= rotStep;
                break;
                
            // 缩放
            case '+':
            case '=':
                screenObject.scale.multiplyScalar(1.1);
                break;
            case '-':
                screenObject.scale.multiplyScalar(0.9);
                break;
                
            // 打印坐标
            case 'p':
            case 'P':
                console.log('========== 当前坐标 ==========');
                console.log(`position.set(${screenObject.position.x.toFixed(2)}, ${screenObject.position.y.toFixed(2)}, ${screenObject.position.z.toFixed(2)})`);
                console.log(`rotation.set(${screenObject.rotation.x.toFixed(2)}, ${screenObject.rotation.y.toFixed(2)}, ${screenObject.rotation.z.toFixed(2)})`);
                console.log(`scale.set(${screenObject.scale.x.toFixed(2)}, ${screenObject.scale.y.toFixed(2)}, ${screenObject.scale.z.toFixed(2)})`);
                console.log('==============================');
                break;
        }
    });
}

// ============================================
// 第 10 步:渲染循环
// ============================================

function animate() {
    requestAnimationFrame(animate);
    
    // 更新控制器
    controls.update();
    
    // 同时渲染两个场景
    renderer.render(scene, camera);      // 3D 模型
    cssRenderer.render(cssScene, camera); // 网页
}

// 启动渲染
animate();

// ============================================
// 第 11 步:响应窗口大小变化
// ============================================

window.addEventListener('resize', () => {
    const newWidth = window.innerWidth;
    const newHeight = window.innerHeight;
    
    // 更新相机
    camera.aspect = newWidth / newHeight;
    camera.updateProjectionMatrix();
    
    // 更新两个渲染器
    renderer.setSize(newWidth, newHeight);
    cssRenderer.setSize(newWidth, newHeight);
});

console.log('3D 电脑初始化完成');

这一版本我很满意:就它了。

image


main.js(v3)

// ============================================
// 3D 可旋转电脑 - 完整代码(添加灯光+材质优化+背面信息板)
// ============================================

import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { CSS3DRenderer, CSS3DObject } from 'three/addons/renderers/CSS3DRenderer.js';

// ============================================
// 第 1 步:获取容器
// ============================================
const container = document.getElementById('canvas-container');

// ============================================
// 第 2 步:创建场景(Scene)
// ============================================
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x222222);

const cssScene = new THREE.Scene();

// ============================================
// 第 3 步:创建相机(Camera)
// ============================================
const width = window.innerWidth;
const height = window.innerHeight;

const camera = new THREE.PerspectiveCamera(
    35,
    width / height,
    10,
    100000
);
camera.position.set(0, 0, 5000);

// ============================================
// 第 4 步:创建两个渲染器
// ============================================

// 4.1 WebGL 渲染器
const renderer = new THREE.WebGLRenderer({
    antialias: true,
    alpha: true
});
renderer.setSize(width, height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.domElement.style.position = 'absolute';
renderer.domElement.style.top = '0';
renderer.domElement.style.left = '0';
renderer.domElement.style.zIndex = '1';
container.appendChild(renderer.domElement);

// 4.2 CSS3D 渲染器
const cssRenderer = new CSS3DRenderer();
cssRenderer.setSize(width, height);
cssRenderer.domElement.style.position = 'absolute';
cssRenderer.domElement.style.top = '0';
cssRenderer.domElement.style.left = '0';
cssRenderer.domElement.style.zIndex = '2';
container.appendChild(cssRenderer.domElement);

// ============================================
// 第 4.5 步:添加灯光
// ============================================
const ambientLight = new THREE.AmbientLight(0xffffff, 0.8);
scene.add(ambientLight);

const dirLight = new THREE.DirectionalLight(0xffffff, 1.5);
dirLight.position.set(10, 20, 10);
scene.add(dirLight);

const fillLight = new THREE.DirectionalLight(0xffffff, 0.5);
fillLight.position.set(-10, 10, -10);
scene.add(fillLight);

const bottomLight = new THREE.DirectionalLight(0xffffff, 0.3);
bottomLight.position.set(0, -10, 0);
scene.add(bottomLight);

// ============================================
// 第 5 步:创建轨道控制器
// ============================================
const controls = new OrbitControls(camera, cssRenderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;

// ============================================
// 第 6 步:加载 3D 模型
// ============================================
const loader = new GLTFLoader();
const modelPath = './assets/models/laptop.glb';

// 存储电脑屏幕的四个角位置(用于连线)
let laptopScreenCorners = null;
let laptopBackPosition = null;

loader.load(
    modelPath,
    function (gltf) {
        const model = gltf.scene;
        
        // 修复材质
        model.traverse((child) => {
            if (child.isMesh) {
                const oldMaterial = child.material;
                const newMaterial = new THREE.MeshStandardMaterial({
                    color: 0xffffff,
                    roughness: 0.5,
                    metalness: 0.1,
                });
                
                if (oldMaterial && oldMaterial.map) {
                    newMaterial.map = oldMaterial.map;
                    newMaterial.map.flipY = false;
                    newMaterial.map.colorSpace = THREE.SRGBColorSpace;
                }
                
                child.material = newMaterial;
            }
        });
        
        autoFitCamera(model);
        scene.add(model);
        
        // ========================================
        // 新增:计算电脑屏幕四角位置和背面位置
        // ========================================
        calculateLaptopGeometry(model);
        
        // 创建背面信息板
        createBackPanel();
        
        // 创建连接线
        createConnectionLines();
        
        // 创建屏幕网页
        createScreen();
        
        document.getElementById('loading').style.display = 'none';
    },
    function (progress) {
        const percent = (progress.loaded / progress.total * 100).toFixed(1);
        const mb = (progress.loaded / 1024 / 1024).toFixed(1);
        const totalMb = (progress.total / 1024 / 1024).toFixed(1);
        
        const loading = document.getElementById('loading');
        if (loading) {
            loading.innerHTML = `加载模型中...<br>${percent}%<br>(${mb}MB / ${totalMb}MB)`;
        }
    },
    function (error) {
        console.error('模型加载失败:', error);
        alert('模型加载失败,请检查路径是否正确');
    }
);

// ============================================
// 第 6.5 步:计算电脑几何信息(新增)
// ============================================

function calculateLaptopGeometry(model) {
    // 创建包围盒获取电脑尺寸
    const box = new THREE.Box3().setFromObject(model);
    const size = new THREE.Vector3();
    const center = new THREE.Vector3();
    box.getSize(size);
    box.getCenter(center);
    
    console.log('电脑尺寸:', size);
    console.log('电脑中心:', center);
    
    // 估算屏幕位置(假设屏幕在电脑上半部分)
    // 根据你的模型可能需要调整这些值
    const screenWidth = size.x * 0.9;   // 屏幕宽度
    const screenHeight = size.y * 0.6;  // 屏幕高度
    const screenDepth = size.z * 0.1;   // 屏幕厚度
    
    // 屏幕中心位置(相对于电脑中心偏上)
    const screenCenter = new THREE.Vector3(
        center.x,
        center.y + size.y * 0.1,  // 偏上
        center.z - size.z * 0.1   // 偏前
    );
    
    // 计算屏幕四角位置(前面)
    const halfWidth = screenWidth / 2;
    const halfHeight = screenHeight / 2;
    
    laptopScreenCorners = {
        // 前面四角(屏幕四角)
        frontTopLeft: new THREE.Vector3(
            screenCenter.x - halfWidth,
            screenCenter.y + halfHeight,
            screenCenter.z
        ),
        frontTopRight: new THREE.Vector3(
            screenCenter.x + halfWidth,
            screenCenter.y + halfHeight,
            screenCenter.z
        ),
        frontBottomLeft: new THREE.Vector3(
            screenCenter.x - halfWidth,
            screenCenter.y - halfHeight,
            screenCenter.z
        ),
        frontBottomRight: new THREE.Vector3(
            screenCenter.x + halfWidth,
            screenCenter.y - halfHeight,
            screenCenter.z
        ),
        // 用于调试显示
        center: screenCenter
    };
    
    // 电脑背面位置(屏幕后方远处)
    laptopBackPosition = new THREE.Vector3(
        screenCenter.x,
        screenCenter.y,
        screenCenter.z - 300  // 向后 300 单位,可调整
    );
    
    console.log('屏幕四角:', laptopScreenCorners);
    console.log('背面位置:', laptopBackPosition);
}

// ============================================
// 第 6.6 步:创建背面信息板(新增)
// ============================================

function createBackPanel() {
    if (!laptopBackPosition) return;
    
    // 加载贴图
    const textureLoader = new THREE.TextureLoader();
    const backTexture = textureLoader.load('./assets/textures/laptop.jpg');
    backTexture.colorSpace = THREE.SRGBColorSpace;
    
    // 创建平面几何体(信息板)
    // 参数:宽度、高度、宽度分段、高度分段
    const panelWidth = 400;   // 信息板宽度
    const panelHeight = 300;  // 信息板高度
    
    const geometry = new THREE.PlaneGeometry(panelWidth, panelHeight);
    
    // 创建材质(使用贴图)
    const material = new THREE.MeshStandardMaterial({
        map: backTexture,
        side: THREE.DoubleSide,  // 双面显示
        transparent: true,
        opacity: 0.95
    });
    
    // 创建网格
    const backPanel = new THREE.Mesh(geometry, material);
    
    // 设置位置(电脑背面)
    backPanel.position.copy(laptopBackPosition);
    
    // 让面板朝向电脑(面向前方)
    backPanel.rotation.y = Math.PI;  // 旋转 180 度朝向电脑
    
    // 添加到场景
    scene.add(backPanel);
    
    // 给信息板添加发光边框效果
    const edges = new THREE.EdgesGeometry(geometry);
    const lineMaterial = new THREE.LineBasicMaterial({ 
        color: 0x00ffff,  // 青色边框
        linewidth: 2 
    });
    const wireframe = new THREE.LineSegments(edges, lineMaterial);
    backPanel.add(wireframe);
    
    // 添加外发光效果(点光源)
    const panelLight = new THREE.PointLight(0x00ffff, 1, 500);
    panelLight.position.set(0, 0, 50);  // 在面板前方
    backPanel.add(panelLight);
    
    console.log('背面信息板已创建');
}

// ============================================
// 第 6.7 步:创建连接线(新增)
// ============================================

function createConnectionLines() {
    if (!laptopScreenCorners || !laptopBackPosition) return;
    
    const corners = laptopScreenCorners;
    
    // 计算信息板的四角位置(与背面信息板对应)
    const panelWidth = 400;
    const panelHeight = 300;
    const halfW = panelWidth / 2;
    const halfH = panelHeight / 2;
    
    const backPos = laptopBackPosition;
    
    // 信息板四角(考虑旋转后实际位置)
    const backCorners = {
        topLeft: new THREE.Vector3(backPos.x - halfW, backPos.y + halfH, backPos.z),
        topRight: new THREE.Vector3(backPos.x + halfW, backPos.y + halfH, backPos.z),
        bottomLeft: new THREE.Vector3(backPos.x - halfW, backPos.y - halfH, backPos.z),
        bottomRight: new THREE.Vector3(backPos.x + halfW, backPos.y - halfH, backPos.z)
    };
    
    // 创建四条连接线
    const lineMaterial = new THREE.LineBasicMaterial({
        color: 0x00ffff,      // 青色线条
        transparent: true,
        opacity: 0.8,
        linewidth: 3
    });
    
    // 定义连接:屏幕四角 → 信息板四角
    const connections = [
        { from: corners.frontTopLeft, to: backCorners.topLeft },
        { from: corners.frontTopRight, to: backCorners.topRight },
        { from: corners.frontBottomLeft, to: backCorners.bottomLeft },
        { from: corners.frontBottomRight, to: backCorners.bottomRight }
    ];
    
    connections.forEach((conn, index) => {
        // 创建曲线路径(贝塞尔曲线,更有科技感)
        const midPoint = new THREE.Vector3().addVectors(conn.from, conn.to).multiplyScalar(0.5);
        // 让中点稍微向外弯曲
        midPoint.x += (index % 2 === 0) ? -30 : 30;
        
        const curve = new THREE.QuadraticBezierCurve3(
            conn.from,
            midPoint,
            conn.to
        );
        
        const points = curve.getPoints(50);  // 50 个点让曲线更平滑
        const geometry = new THREE.BufferGeometry().setFromPoints(points);
        
        const line = new THREE.Line(geometry, lineMaterial);
        scene.add(line);
        
        // 添加发光效果(用圆柱体模拟粗线条)
        const tubeGeometry = new THREE.TubeGeometry(curve, 20, 2, 8, false);
        const tubeMaterial = new THREE.MeshBasicMaterial({
            color: 0x00ffff,
            transparent: true,
            opacity: 0.3
        });
        const tube = new THREE.Mesh(tubeGeometry, tubeMaterial);
        scene.add(tube);
    });
    
    // 添加角点标记(小球)
    const sphereGeometry = new THREE.SphereGeometry(5, 16, 16);
    const sphereMaterial = new THREE.MeshBasicMaterial({ color: 0xff0000 });
    
    Object.values(corners).forEach(pos => {
        if (pos instanceof THREE.Vector3) {
            const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
            sphere.position.copy(pos);
            scene.add(sphere);
        }
    });
    
    console.log('连接线已创建');
}

// ============================================
// 第 7 步:自动对焦函数
// ============================================
function autoFitCamera(object) {
    const box = new THREE.Box3().setFromObject(object);
    const size = new THREE.Vector3();
    const center = new THREE.Vector3();
    box.getSize(size);
    box.getCenter(center);
    
    const maxDim = Math.max(size.x, size.y, size.z);
    const fov = camera.fov * (Math.PI / 180);
    const cameraDistance = Math.abs(maxDim / 2 / Math.tan(fov / 2)) * 2.5;  // 距离远一点,能看到背面
    
    camera.position.set(
        center.x + maxDim * 0.5,
        center.y + maxDim * 0.3,
        center.z + cameraDistance
    );
    
    controls.target.copy(center);
    controls.update();
}

// ============================================
// 第 7.2 步:添加光影效果
// ============================================
const ambientLightExtra = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLightExtra);

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

// ============================================
// 第 7.3 步:添加辅助线
// ============================================
const gridHelper = new THREE.GridHelper(10000, 100);
scene.add(gridHelper);

const axesHelper = new THREE.AxesHelper(5000);
scene.add(axesHelper);

// ============================================
// 第 8 步:创建电脑屏幕(原有)
// ============================================
function createScreen() {
    const iframe = document.createElement('iframe');
    iframe.src = 'https://rockosdev.github.io/';
    iframe.style.width = '1480px';
    iframe.style.height = '1100px';
    iframe.style.border = 'none';
    iframe.style.backgroundColor = 'white';
    
    const screenObject = new CSS3DObject(iframe);
    screenObject.position.set(0.00, 52.00, -8.00);
    screenObject.rotation.x = -0.37;
    screenObject.scale.set(0.125, 0.11, 0.20);
    screenObject.element.style.backfaceVisibility = 'hidden';
    
    cssScene.add(screenObject);
    
    console.log('屏幕已创建');
    setupDebugControls(screenObject);
}

// ============================================
// 第 9 步:键盘调试功能
// ============================================
function setupDebugControls(screenObject) {
    console.log('=== 调试模式已开启 ===');
    console.log('按键说明:');
    console.log('方向键 ↑↓←→ : 移动屏幕位置');
    console.log('W/S : 前后移动');
    console.log('Q/E : 旋转 X 轴');
    console.log('A/D : 旋转 Y 轴');
    console.log('Z/X : 旋转 Z 轴');
    console.log('+/- : 缩放');
    console.log('P : 打印当前坐标');
    
    const step = 2;
    const rotStep = 0.01;
    
    window.addEventListener('keydown', (e) => {
        switch(e.key) {
            case 'ArrowUp': screenObject.position.y += step; break;
            case 'ArrowDown': screenObject.position.y -= step; break;
            case 'ArrowLeft': screenObject.position.x -= step; break;
            case 'ArrowRight': screenObject.position.x += step; break;
            case 'w': case 'W': screenObject.position.z -= step; break;
            case 's': case 'S': screenObject.position.z += step; break;
            case 'q': case 'Q': screenObject.rotation.x += rotStep; break;
            case 'e': case 'E': screenObject.rotation.x -= rotStep; break;
            case 'a': case 'A': screenObject.rotation.y += rotStep; break;
            case 'd': case 'D': screenObject.rotation.y -= rotStep; break;
            case 'z': case 'Z': screenObject.rotation.z += rotStep; break;
            case 'x': case 'X': screenObject.rotation.z -= rotStep; break;
            case '+': case '=': screenObject.scale.multiplyScalar(1.1); break;
            case '-': screenObject.scale.multiplyScalar(0.9); break;
            case 'p': case 'P':
                console.log('========== 当前坐标 ==========');
                console.log(`position.set(${screenObject.position.x.toFixed(2)}, ${screenObject.position.y.toFixed(2)}, ${screenObject.position.z.toFixed(2)})`);
                console.log(`rotation.set(${screenObject.rotation.x.toFixed(2)}, ${screenObject.rotation.y.toFixed(2)}, ${screenObject.rotation.z.toFixed(2)})`);
                console.log(`scale.set(${screenObject.scale.x.toFixed(2)}, ${screenObject.scale.y.toFixed(2)}, ${screenObject.scale.z.toFixed(2)})`);
                console.log('==============================');
                break;
        }
    });
}

// ============================================
// 第 10 步:渲染循环
// ============================================
function animate() {
    requestAnimationFrame(animate);
    controls.update();
    renderer.render(scene, camera);
    cssRenderer.render(cssScene, camera);
}

animate();

// ============================================
// 第 11 步:响应窗口大小变化
// ============================================
window.addEventListener('resize', () => {
    const newWidth = window.innerWidth;
    const newHeight = window.innerHeight;
    camera.aspect = newWidth / newHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(newWidth, newHeight);
    cssRenderer.setSize(newWidth, newHeight);
});

console.log('3D 电脑初始化完成');

image


功能 实现
R 键触发 setupRKeyListener() 监听键盘事件
再次 R 键隐藏 支持切换显示/隐藏
视觉反馈 屏幕底部提示 + 右上角通知
震动反馈 相机轻微震动
科技音效 双振荡器合成音效
悬浮动画 显示后持续上下浮动

main.js(v4)

// ============================================
// 3D 可旋转电脑 - 完整代码(R键触发映射)
// ============================================

import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { CSS3DRenderer, CSS3DObject } from 'three/addons/renderers/CSS3DRenderer.js';

// ============================================
// 全局状态管理
// ============================================
const state = {
    isPanelVisible: false,      // 信息板是否显示
    isAnimating: false,           // 是否正在动画中
    panelTargetOpacity: 0,        // 目标透明度
    panelCurrentOpacity: 0,       // 当前透明度
    rKeyPressed: false            // R键是否被按下
};

// 存储需要动画的对象
let backPanel = null;
let backPanelMaterial = null;
let connectionLines = [];
let panelLight = null;
let rKeyHint = null;  // R键提示UI

// ============================================
// 第 1 步:获取容器
// ============================================
const container = document.getElementById('canvas-container');

// ============================================
// 第 2 步:创建场景
// ============================================
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x222222);

const cssScene = new THREE.Scene();

// ============================================
// 第 3 步:创建相机
// ============================================
const width = window.innerWidth;
const height = window.innerHeight;

const camera = new THREE.PerspectiveCamera(35, width / height, 10, 100000);
camera.position.set(0, 0, 5000);

// ============================================
// 第 4 步:创建两个渲染器
// ============================================

// 4.1 WebGL 渲染器
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setSize(width, height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.domElement.style.position = 'absolute';
renderer.domElement.style.top = '0';
renderer.domElement.style.left = '0';
renderer.domElement.style.zIndex = '1';
container.appendChild(renderer.domElement);

// 4.2 CSS3D 渲染器
const cssRenderer = new CSS3DRenderer();
cssRenderer.setSize(width, height);
cssRenderer.domElement.style.position = 'absolute';
cssRenderer.domElement.style.top = '0';
cssRenderer.domElement.style.left = '0';
cssRenderer.domElement.style.zIndex = '2';
container.appendChild(cssRenderer.domElement);

// ============================================
// 第 4.5 步:添加灯光
// ============================================
const ambientLight = new THREE.AmbientLight(0xffffff, 0.8);
scene.add(ambientLight);

const dirLight = new THREE.DirectionalLight(0xffffff, 1.5);
dirLight.position.set(10, 20, 10);
scene.add(dirLight);

const fillLight = new THREE.DirectionalLight(0xffffff, 0.5);
fillLight.position.set(-10, 10, -10);
scene.add(fillLight);

const bottomLight = new THREE.DirectionalLight(0xffffff, 0.3);
bottomLight.position.set(0, -10, 0);
scene.add(bottomLight);

// ============================================
// 第 5 步:创建轨道控制器
// ============================================
const controls = new OrbitControls(camera, cssRenderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;

// ============================================
// 第 5.5 步:创建 R 键提示 UI(新增)
// ============================================

function createRKeyHint() {
    const hint = document.createElement('div');
    hint.id = 'r-key-hint';
    hint.innerHTML = `
        <div style="
            position: fixed;
            bottom: 30px;
            left: 50%;
            transform: translateX(-50%);
            background: rgba(0, 0, 0, 0.7);
            border: 2px solid #00ffff;
            border-radius: 10px;
            padding: 15px 25px;
            color: #00ffff;
            font-family: 'Courier New', monospace;
            font-size: 16px;
            text-align: center;
            pointer-events: none;
            z-index: 1000;
            box-shadow: 0 0 20px rgba(0, 255, 255, 0.3);
            transition: all 0.3s ease;
        ">
            <div style="font-size: 24px; margin-bottom: 5px;">⌨️</div>
            <div>按 <span style="
                background: #00ffff;
                color: #000;
                padding: 2px 8px;
                border-radius: 4px;
                font-weight: bold;
            ">R</span> 键映射信息板</div>
            <div style="font-size: 12px; color: #aaa; margin-top: 5px;">Press R to reveal panel</div>
        </div>
    `;
    document.body.appendChild(hint);
    rKeyHint = hint;
    
    // 3秒后淡出提示
    setTimeout(() => {
        hint.style.opacity = '0.6';
    }, 3000);
}

// ============================================
// 第 6 步:加载 3D 模型
// ============================================
const loader = new GLTFLoader();
const modelPath = './assets/models/laptop.glb';

let laptopScreenCorners = null;
let laptopBackPosition = null;
let screenObject = null;

loader.load(
    modelPath,
    function (gltf) {
        const model = gltf.scene;
        
        // 修复材质
        model.traverse((child) => {
            if (child.isMesh) {
                const oldMaterial = child.material;
                const newMaterial = new THREE.MeshStandardMaterial({
                    color: 0xffffff,
                    roughness: 0.5,
                    metalness: 0.1,
                });
                
                if (oldMaterial && oldMaterial.map) {
                    newMaterial.map = oldMaterial.map;
                    newMaterial.map.flipY = false;
                    newMaterial.map.colorSpace = THREE.SRGBColorSpace;
                }
                
                child.material = newMaterial;
            }
        });
        
        autoFitCamera(model);
        scene.add(model);
        
        // 计算几何信息
        calculateLaptopGeometry(model);
        
        // 创建信息板(初始隐藏)
        createFloatingPanel();
        
        // 创建连接线(初始隐藏)
        createConnectionLines();
        
        // 创建屏幕
        createScreen();
        
        // 创建 R 键提示
        createRKeyHint();
        
        // 设置键盘监听(重点)
        setupRKeyListener();
        
        document.getElementById('loading').style.display = 'none';
        
        console.log('✅ 加载完成!按 R 键触发映射');
    },
    function (progress) {
        const percent = (progress.loaded / progress.total * 100).toFixed(1);
        const loading = document.getElementById('loading');
        if (loading) {
            loading.innerHTML = `加载中... ${percent}%<br><br>准备按 <span style="color:#00ffff">R</span> 键`;
        }
    },
    function (error) {
        console.error('模型加载失败:', error);
    }
);

// ============================================
// 第 6.5 步:计算电脑几何信息
// ============================================

function calculateLaptopGeometry(model) {
    const box = new THREE.Box3().setFromObject(model);
    const size = new THREE.Vector3();
    const center = new THREE.Vector3();
    box.getSize(size);
    box.getCenter(center);
    
    console.log('电脑尺寸:', size);
    console.log('电脑中心:', center);
    
    // 屏幕尺寸估算
    const screenWidth = size.x * 0.9;
    const screenHeight = size.y * 0.6;
    
    // 屏幕中心
    const screenCenter = new THREE.Vector3(
        center.x,
        center.y + size.y * 0.1,
        center.z - size.z * 0.1
    );
    
    const halfWidth = screenWidth / 2;
    const halfHeight = screenHeight / 2;
    
    laptopScreenCorners = {
        frontTopLeft: new THREE.Vector3(
            screenCenter.x - halfWidth,
            screenCenter.y + halfHeight,
            screenCenter.z
        ),
        frontTopRight: new THREE.Vector3(
            screenCenter.x + halfWidth,
            screenCenter.y + halfHeight,
            screenCenter.z
        ),
        frontBottomLeft: new THREE.Vector3(
            screenCenter.x - halfWidth,
            screenCenter.y - halfHeight,
            screenCenter.z
        ),
        frontBottomRight: new THREE.Vector3(
            screenCenter.x + halfWidth,
            screenCenter.y - halfHeight,
            screenCenter.z
        ),
        center: screenCenter
    };
    
    // ========================================
    // 信息板放在背面上方的空中
    // ========================================
    laptopBackPosition = new THREE.Vector3(
        screenCenter.x,
        screenCenter.y + size.y * 1.2,  // 更高的空中位置
        screenCenter.z - size.z * 0.3     // 稍微向后
    );
    
    console.log('信息板位置(空中):', laptopBackPosition);
}

// ============================================
// 第 6.6 步:创建空中信息板(初始完全隐藏)
// ============================================

function createFloatingPanel() {
    if (!laptopBackPosition) return;
    
    // 加载贴图
    const textureLoader = new THREE.TextureLoader();
    const backTexture = textureLoader.load('./assets/textures/laptop.jpg');
    backTexture.colorSpace = THREE.SRGBColorSpace;
    
    const panelWidth = 400;
    const panelHeight = 300;
    
    const geometry = new THREE.PlaneGeometry(panelWidth, panelHeight);
    
    // 初始完全隐藏
    backPanelMaterial = new THREE.MeshStandardMaterial({
        map: backTexture,
        side: THREE.DoubleSide,
        transparent: true,
        opacity: 0,
        emissive: 0x0044ff,
        emissiveIntensity: 0
    });
    
    backPanel = new THREE.Mesh(geometry, backPanelMaterial);
    backPanel.position.copy(laptopBackPosition);
    
    // 倾斜角度,更有悬浮感
    backPanel.rotation.x = -0.3;
    backPanel.rotation.y = Math.PI;
    
    // 初始缩放为 0
    backPanel.scale.set(0.1, 0.1, 0.1);
    
    scene.add(backPanel);
    
    // 发光边框(初始隐藏)
    const edges = new THREE.EdgesGeometry(geometry);
    const lineMaterial = new THREE.LineBasicMaterial({ 
        color: 0x00ffff,
        transparent: true,
        opacity: 0
    });
    const wireframe = new THREE.LineSegments(edges, lineMaterial);
    wireframe.name = 'wireframe';
    backPanel.add(wireframe);
    
    // 点光源(初始关闭)
    panelLight = new THREE.PointLight(0x00ffff, 0, 1000);
    panelLight.position.set(0, 0, 50);
    backPanel.add(panelLight);
    
    console.log('空中信息板已创建(按 R 键显示)');
}

// ============================================
// 第 6.7 步:创建连接线(初始隐藏)
// ============================================

function createConnectionLines() {
    if (!laptopScreenCorners || !laptopBackPosition) return;
    
    const corners = laptopScreenCorners;
    const backPos = laptopBackPosition;
    
    const panelWidth = 400;
    const panelHeight = 300;
    const halfW = panelWidth / 2;
    const halfH = panelHeight / 2;
    
    // 计算倾斜后的角点
    const tilt = -0.3;
    const cosTilt = Math.cos(tilt);
    const sinTilt = Math.sin(tilt);
    
    const backCorners = [
        new THREE.Vector3(backPos.x - halfW, backPos.y + halfH * cosTilt, backPos.z - halfH * sinTilt),
        new THREE.Vector3(backPos.x + halfW, backPos.y + halfH * cosTilt, backPos.z - halfH * sinTilt),
        new THREE.Vector3(backPos.x - halfW, backPos.y - halfH * cosTilt, backPos.z + halfH * sinTilt),
        new THREE.Vector3(backPos.x + halfW, backPos.y - halfH * cosTilt, backPos.z + halfH * sinTilt)
    ];
    
    const screenCorners = [
        corners.frontTopLeft,
        corners.frontTopRight,
        corners.frontBottomLeft,
        corners.frontBottomRight
    ];
    
    // 创建四条连接线
    const lineMaterial = new THREE.LineBasicMaterial({
        color: 0x00ffff,
        transparent: true,
        opacity: 0
    });
    
    const tubeMaterial = new THREE.MeshBasicMaterial({
        color: 0x00ffff,
        transparent: true,
        opacity: 0
    });
    
    for (let i = 0; i < 4; i++) {
        const from = screenCorners[i];
        const to = backCorners[i];
        
        // 创建上升曲线(更有科技感)
        const midPoint = new THREE.Vector3().addVectors(from, to).multiplyScalar(0.5);
        midPoint.y += 100;  // 向上弯曲
        
        const curve = new THREE.QuadraticBezierCurve3(from, midPoint, to);
        const points = curve.getPoints(50);
        const geometry = new THREE.BufferGeometry().setFromPoints(points);
        
        // 线条
        const line = new THREE.Line(geometry, lineMaterial.clone());
        line.name = `connectionLine_${i}`;
        scene.add(line);
        connectionLines.push(line);
        
        // 发光管
        const tubeGeometry = new THREE.TubeGeometry(curve, 20, 3, 8, false);
        const tube = new THREE.Mesh(tubeGeometry, tubeMaterial.clone());
        tube.name = `connectionTube_${i}`;
        scene.add(tube);
        connectionLines.push(tube);
    }
    
    console.log('连接线已创建(随 R 键显示)');
}

// ============================================
// 第 6.8 步:创建屏幕
// ============================================

function createScreen() {
    const iframe = document.createElement('iframe');
    iframe.src = 'https://rockosdev.github.io/';
    iframe.style.width = '1480px';
    iframe.style.height = '1100px';
    iframe.style.border = 'none';
    iframe.style.backgroundColor = 'white';
    
    screenObject = new CSS3DObject(iframe);
    screenObject.position.set(0.00, 52.00, -8.00);
    screenObject.rotation.x = -0.37;
    screenObject.scale.set(0.125, 0.11, 0.20);
    screenObject.element.style.backfaceVisibility = 'hidden';
    
    cssScene.add(screenObject);
    
    console.log('屏幕已创建');
    setupDebugControls(screenObject);
}

// ============================================
// 第 6.9 步:R 键监听(核心新增)
// ============================================

function setupRKeyListener() {
    console.log('⌨️ R 键监听已启用');
    
    window.addEventListener('keydown', (e) => {
        // 检测 R 键(不区分大小写)
        if (e.key === 'r' || e.key === 'R') {
            console.log('🎯 R 键被按下!');
            
            if (!state.isPanelVisible && !state.isAnimating) {
                triggerPanelReveal();
            } else if (state.isPanelVisible && !state.isAnimating) {
                // 再次按 R 可以隐藏
                hidePanel();
            }
        }
    });
    
    // 显示提示
    showRKeyNotification();
}

// R 键按下提示
function showRKeyNotification() {
    const notif = document.createElement('div');
    notif.innerHTML = `
        <div style="
            position: fixed;
            top: 20px;
            right: 20px;
            background: rgba(0, 255, 255, 0.1);
            border: 1px solid #00ffff;
            border-radius: 8px;
            padding: 10px 15px;
            color: #00ffff;
            font-family: monospace;
            font-size: 14px;
            z-index: 1001;
            animation: slideIn 0.5s ease;
        ">
            🎮 R 键控制映射
        </div>
    `;
    document.body.appendChild(notif);
    
    setTimeout(() => {
        notif.style.opacity = '0';
        notif.style.transition = 'opacity 1s';
        setTimeout(() => notif.remove(), 1000);
    }, 3000);
}

// ============================================
// 第 7.0 步:触发显示动画(R 键触发)
// ============================================

function triggerPanelReveal() {
    if (state.isAnimating) return;
    
    console.log('>>> 🚀 R 键映射启动!');
    state.isAnimating = true;
    state.isPanelVisible = true;
    state.panelTargetOpacity = 1;
    state.rKeyPressed = true;
    
    // 更新提示
    if (rKeyHint) {
        rKeyHint.innerHTML = `
            <div style="
                position: fixed;
                bottom: 30px;
                left: 50%;
                transform: translateX(-50%);
                background: rgba(0, 255, 0, 0.2);
                border: 2px solid #00ff00;
                border-radius: 10px;
                padding: 15px 25px;
                color: #00ff00;
                font-family: monospace;
                text-align: center;
                pointer-events: none;
                z-index: 1000;
            ">
                <div style="font-size: 20px;">✅ 已映射</div>
                <div style="font-size: 12px;">按 R 隐藏</div>
            </div>
        `;
    }
    
    // 播放音效
    playRevealSound();
    
    // 相机轻微震动效果(反馈)
    cameraShake();
}

// 隐藏信息板
function hidePanel() {
    console.log('>>> 隐藏信息板');
    state.isPanelVisible = false;
    state.panelTargetOpacity = 0;
    
    // 更新提示
    if (rKeyHint) {
        rKeyHint.innerHTML = `
            <div style="
                position: fixed;
                bottom: 30px;
                left: 50%;
                transform: translateX(-50%);
                background: rgba(0, 0, 0, 0.7);
                border: 2px solid #00ffff;
                border-radius: 10px;
                padding: 15px 25px;
                color: #00ffff;
                font-family: monospace;
                text-align: center;
                pointer-events: none;
                z-index: 1000;
            ">
                <div>按 <span style="background:#00ffff;color:#000;padding:2px 8px;border-radius:4px;font-weight:bold;">R</span> 键映射</div>
            </div>
        `;
    }
}

// 相机震动反馈
function cameraShake() {
    const originalPos = camera.position.clone();
    let shakeTime = 0;
    const duration = 0.3;
    
    function shake() {
        shakeTime += 0.016;
        if (shakeTime < duration) {
            const intensity = (1 - shakeTime / duration) * 2;
            camera.position.x = originalPos.x + (Math.random() - 0.5) * intensity;
            camera.position.y = originalPos.y + (Math.random() - 0.5) * intensity;
            requestAnimationFrame(shake);
        } else {
            camera.position.copy(originalPos);
        }
    }
    shake();
}

// ============================================
// 第 7.1 步:动画更新
// ============================================

function updatePanelAnimation() {
    if (!backPanel || !backPanelMaterial) return;
    
    const dt = 0.016;
    const speed = 1.5;  // 动画速度
    
    // 透明度渐变
    const diff = state.panelTargetOpacity - state.panelCurrentOpacity;
    if (Math.abs(diff) > 0.001) {
        state.panelCurrentOpacity += diff * speed * dt;
        backPanelMaterial.opacity = Math.max(0, Math.min(1, state.panelCurrentOpacity));
    }
    
    // 缩放动画
    const targetScale = state.isPanelVisible ? 1 : 0.1;
    const currentScale = backPanel.scale.x;
    if (Math.abs(currentScale - targetScale) > 0.001) {
        const newScale = currentScale + (targetScale - currentScale) * speed * dt;
        backPanel.scale.set(newScale, newScale, newScale);
    }
    
    // 自发光
    const targetEmissive = state.isPanelVisible ? 0.4 : 0;
    backPanelMaterial.emissiveIntensity += (targetEmissive - backPanelMaterial.emissiveIntensity) * speed * dt;
    
    // 边框发光
    const wireframe = backPanel.getObjectByName('wireframe');
    if (wireframe && wireframe.material) {
        wireframe.material.opacity = state.panelCurrentOpacity * 0.8;
    }
    
    // 连接线(延迟显示,有流动感)
    connectionLines.forEach((line, index) => {
        if (line.material) {
            const delay = index * 0.05;
            let effectiveOpacity;
            if (state.isPanelVisible) {
                effectiveOpacity = Math.max(0, state.panelCurrentOpacity - delay);
            } else {
                effectiveOpacity = state.panelCurrentOpacity;
            }
            line.material.opacity = effectiveOpacity * 0.6;
        }
    });
    
    // 点光源
    if (panelLight) {
        const targetIntensity = state.isPanelVisible ? 2 : 0;
        panelLight.intensity += (targetIntensity - panelLight.intensity) * speed * dt;
    }
    
    // 悬浮动画(显示后)
    if (state.isPanelVisible && state.panelCurrentOpacity > 0.9) {
        const time = Date.now() * 0.001;
        backPanel.position.y = laptopBackPosition.y + Math.sin(time) * 8;
        backPanel.rotation.z = Math.sin(time * 0.5) * 0.03;
    }
    
    // 动画结束检测
    if (state.isAnimating) {
        const opacityDone = Math.abs(state.panelCurrentOpacity - state.panelTargetOpacity) < 0.01;
        const scaleDone = Math.abs(backPanel.scale.x - targetScale) < 0.01;
        
        if (opacityDone && scaleDone) {
            state.isAnimating = false;
            console.log(state.isPanelVisible ? '>>> ✅ 映射完成' : '>>> ✅ 隐藏完成');
        }
    }
}

// ============================================
// 第 7.2 步:音效
// ============================================

function playRevealSound() {
    try {
        const AudioContext = window.AudioContext || window.webkitAudioContext;
        const ctx = new AudioContext();
        
        // 创建科技感音效
        const osc1 = ctx.createOscillator();
        const osc2 = ctx.createOscillator();
        const gain = ctx.createGain();
        
        osc1.connect(gain);
        osc2.connect(gain);
        gain.connect(ctx.destination);
        
        // 频率滑升
        osc1.type = 'sine';
        osc1.frequency.setValueAtTime(220, ctx.currentTime);
        osc1.frequency.exponentialRampToValueAtTime(880, ctx.currentTime + 0.5);
        
        osc2.type = 'triangle';
        osc2.frequency.setValueAtTime(330, ctx.currentTime);
        osc2.frequency.exponentialRampToValueAtTime(660, ctx.currentTime + 0.5);
        
        // 音量包络
        gain.gain.setValueAtTime(0.1, ctx.currentTime);
        gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.6);
        
        osc1.start(ctx.currentTime);
        osc2.start(ctx.currentTime);
        osc1.stop(ctx.currentTime + 0.6);
        osc2.stop(ctx.currentTime + 0.6);
        
    } catch (e) {
        console.log('音效播放失败', e);
    }
}

// ============================================
// 第 8 步:自动对焦
// ============================================

function autoFitCamera(object) {
    const box = new THREE.Box3().setFromObject(object);
    const size = new THREE.Vector3();
    const center = new THREE.Vector3();
    box.getSize(size);
    box.getCenter(center);
    
    const maxDim = Math.max(size.x, size.y, size.z);
    const fov = camera.fov * (Math.PI / 180);
    const cameraDistance = Math.abs(maxDim / 2 / Math.tan(fov / 2)) * 3;
    
    camera.position.set(
        center.x + maxDim * 0.5,
        center.y + maxDim * 0.8,
        center.z + cameraDistance
    );
    
    controls.target.copy(center);
    controls.update();
}

// ============================================
// 第 9 步:键盘调试
// ============================================

function setupDebugControls(screenObject) {
    console.log('=== 调试控制 ===');
    console.log('R     - 触发/隐藏信息板映射');
    console.log('↑↓←→  - 移动屏幕');
    console.log('W/S   - 前后移动');
    console.log('P     - 打印坐标');
    
    const step = 2;
    const rotStep = 0.01;
    
    window.addEventListener('keydown', (e) => {
        // R 键已在 setupRKeyListener 中处理
        
        switch(e.key) {
            case 'ArrowUp': screenObject.position.y += step; break;
            case 'ArrowDown': screenObject.position.y -= step; break;
            case 'ArrowLeft': screenObject.position.x -= step; break;
            case 'ArrowRight': screenObject.position.x += step; break;
            case 'w': case 'W': screenObject.position.z -= step; break;
            case 's': case 'S': screenObject.position.z += step; break;
            case 'q': case 'Q': screenObject.rotation.x += rotStep; break;
            case 'e': case 'E': screenObject.rotation.x -= rotStep; break;
            case 'p': case 'P':
                console.log('========== 坐标 ==========');
                console.log(`position.set(${screenObject.position.x.toFixed(2)}, ${screenObject.position.y.toFixed(2)}, ${screenObject.position.z.toFixed(2)})`);
                console.log(`rotation.set(${screenObject.rotation.x.toFixed(2)}, ${screenObject.rotation.y.toFixed(2)}, ${screenObject.rotation.z.toFixed(2)})`);
                console.log('==========================');
                break;
        }
    });
}

// ============================================
// 第 10 步:渲染循环
// ============================================

function animate() {
    requestAnimationFrame(animate);
    controls.update();
    
    // 更新信息板动画
    updatePanelAnimation();
    
    renderer.render(scene, camera);
    cssRenderer.render(cssScene, camera);
}

animate();

// ============================================
// 第 11 步:响应窗口大小变化
// ============================================

window.addEventListener('resize', () => {
    const newWidth = window.innerWidth;
    const newHeight = window.innerHeight;
    camera.aspect = newWidth / newHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(newWidth, newHeight);
    cssRenderer.setSize(newWidth, newHeight);
});

console.log('🖥️ 3D 电脑初始化完成');
console.log('⌨️ 按 R 键开始映射');

image


八、完整文件清单

my-3d-computer/
├── index.html          ← 第 2 步创建
├── js/
│   └── main.js         ← 第 3 步创建
└── assets/
    ├── models/
    │   └── laptop.glb  ← 从 Sketchfab 下载
    └── textures/
        └── laptop.jpg  ← 贴图(可选)

九、总结流程图

开始
  │
  ▼
创建文件夹结构
  │
  ▼
创建 index.html(基础网页)
  │
  ▼
创建 main.js(核心代码)
  │
  ▼
下载 3D 模型放入 assets/models/
  │
  ▼
用 Live Server 运行
  │
  ▼
看到 3D 电脑?
  ├── 否 → 检查路径/控制台错误 → 返回
  ▼
是
  │
  ▼
按键盘调试屏幕位置
  │
  ▼
按 P 打印坐标
  │
  ▼
修改代码固定坐标
  │
  ▼
完成!

最终版

index.html(v2)

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>3D 可旋转电脑</title>
    <!-- 引入 Three.js -->
    <script type="importmap">
    {
        "imports": {
            "three": "https://unpkg.com/three@0.160.0/build/three.module.js",
            "three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
        }
    }
    </script>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        
        body {
            overflow: hidden;
            background: #1a1a1a;
        }
        
        #canvas-container {
            width: 100vw;
            height: 100vh;
            position: relative;
        }
        
        /* 7.1 加载动画样式 */
        #loading {
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            color: white;
            font-size: 20px;
            font-family: Arial, sans-serif;
            text-align: center;
            z-index: 10;
        }
        
        #progress {
            margin-top: 10px;
            font-size: 16px;
            color: #aaa;
        }
    </style>
</head>
<body>
    <div id="canvas-container">
        <!-- 7.1 添加加载动画 -->
        <div id="loading">
            <div>加载中...</div>
            <div id="progress">0%</div>
        </div>
    </div>
    
    <script type="module" src="./js/main.js"></script>
</body>
</html>

main.js(v5)

// ============================================
// 3D 可旋转电脑 - 完整代码(R键触发+前上方60度+音效+文字提示: 按下U恢复电脑居中位置)
// ============================================
 
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { CSS3DRenderer, CSS3DObject } from 'three/addons/renderers/CSS3DRenderer.js';
 
// ============================================
// 全局状态管理(新增)
// ============================================
const state = {
    isPanelVisible: false,
    isAnimating: false,
    panelTargetOpacity: 0,
    panelCurrentOpacity: 0,
    panelTargetScale: 0.1,
    panelCurrentScale: 0.1
};
 
// 存储需要动画的对象(新增)
let backPanel = null;
let backPanelMaterial = null;
let connectionLines = [];
let panelLight = null;
let rainTextSprite = null;  // "雨从未迟到"文字精灵

// 与电脑屏幕面保持一致的旋转(用于让贴图“与电脑面平行”)
// 注:createScreen 里屏幕 iframe 也使用同一角度 
// ============================================
// 第 1 步:获取容器
// ============================================
const container = document.getElementById('canvas-container');
 
// ============================================
// 第 2 步:创建场景(Scene)
// ============================================
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x222222);
 
const cssScene = new THREE.Scene();
 
// ============================================
// 第 3 步:创建相机(Camera)
// ============================================
const width = window.innerWidth;
const height = window.innerHeight;
 
const camera = new THREE.PerspectiveCamera(
    35,
    width / height,
    10,
    100000
);
camera.position.set(0, 0, 5000);
 
// ============================================
// 第 4 步:创建两个渲染器
// ============================================
 
// 4.1 WebGL 渲染器
const renderer = new THREE.WebGLRenderer({
    antialias: true,
    alpha: true
});
renderer.setSize(width, height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.domElement.style.position = 'absolute';
renderer.domElement.style.top = '0';
renderer.domElement.style.left = '0';
renderer.domElement.style.zIndex = '1';
container.appendChild(renderer.domElement);
 
// 4.2 CSS3D 渲染器
const cssRenderer = new CSS3DRenderer();
cssRenderer.setSize(width, height);
cssRenderer.domElement.style.position = 'absolute';
cssRenderer.domElement.style.top = '0';
cssRenderer.domElement.style.left = '0';
cssRenderer.domElement.style.zIndex = '2';
container.appendChild(cssRenderer.domElement);
 
// ============================================
// 第 4.5 步:添加灯光
// ============================================
const ambientLight = new THREE.AmbientLight(0xffffff, 0.8);
scene.add(ambientLight);
 
const dirLight = new THREE.DirectionalLight(0xffffff, 1.5);
dirLight.position.set(10, 20, 10);
scene.add(dirLight);
 
const fillLight = new THREE.DirectionalLight(0xffffff, 0.5);
fillLight.position.set(-10, 10, -10);
scene.add(fillLight);
 
const bottomLight = new THREE.DirectionalLight(0xffffff, 0.3);
bottomLight.position.set(0, -10, 0);
scene.add(bottomLight);
 
// ============================================
// 第 5 步:创建轨道控制器
// ============================================
const controls = new OrbitControls(camera, cssRenderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
 
// ============================================
// 第 6 步:加载 3D 模型
// ============================================
const loader = new GLTFLoader();
const modelPath = './assets/models/laptop.glb';
 
// 存储电脑屏幕的四个角位置(用于连线)
let laptopScreenCorners = null;
let laptopFrontUpperPosition = null;  // 修改为前上方位置
 
loader.load(
    modelPath,
    function (gltf) {
        const model = gltf.scene;
        
        // 修复材质
        model.traverse((child) => {
            if (child.isMesh) {
                const oldMaterial = child.material;
                const newMaterial = new THREE.MeshStandardMaterial({
                    color: 0xffffff,
                    roughness: 0.5,
                    metalness: 0.1,
                });
                
                if (oldMaterial && oldMaterial.map) {
                    newMaterial.map = oldMaterial.map;
                    newMaterial.map.flipY = false;
                    newMaterial.map.colorSpace = THREE.SRGBColorSpace;
                }
                
                child.material = newMaterial;
            }
        });
        
        autoFitCamera(model);
        scene.add(model);
        
        // ========================================
        // 新增:计算电脑屏幕四角位置和前上方位置
        // ========================================
        calculateLaptopGeometry(model);
        
        // 创建前上方信息板(初始隐藏)
        createFrontUpperPanel();
        
        // 创建连接线(初始隐藏)
        createConnectionLines();
        
        // 创建"雨从未迟到"文字
        createRainText();
        
        // 创建屏幕网页
        createScreen();
        
        // ========================================
        // 新增:设置R键监听
        // ========================================
        setupRKeyListener();
        
        document.getElementById('loading').style.display = 'none';
    },
    function (progress) {
        const percent = (progress.loaded / progress.total * 100).toFixed(1);
        const mb = (progress.loaded / 1024 / 1024).toFixed(1);
        const totalMb = (progress.total / 1024 / 1024).toFixed(1);
        
        const loading = document.getElementById('loading');
        if (loading) {
            loading.innerHTML = `加载模型中...<br>${percent}%<br>(${mb}MB / ${totalMb}MB)<br>按 R 键触发映射`;
        }
    },
    function (error) {
        console.error('模型加载失败:', error);
        alert('模型加载失败,请检查路径是否正确');
    }
);
 
// ============================================
// 第 6.5 步:计算电脑几何信息(修改:前上方60度)
// ============================================
 
function calculateLaptopGeometry(model) {
    // 创建包围盒获取电脑尺寸
    const box = new THREE.Box3().setFromObject(model);
    const size = new THREE.Vector3();
    const center = new THREE.Vector3();
    box.getSize(size);
    box.getCenter(center);
    
    console.log('电脑尺寸:', size);
    console.log('电脑中心:', center);
    
    // 估算屏幕位置
    const screenWidth = size.x * 0.9;
    const screenHeight = size.y * 0.6;
    
    // 屏幕中心位置
    const screenCenter = new THREE.Vector3(
        center.x,
        center.y + size.y * 0.1,
        center.z - size.z * 0.1
    );
    
    // 计算屏幕四角位置
    const halfWidth = screenWidth / 2;
    const halfHeight = screenHeight / 2;
    
    laptopScreenCorners = {
        frontTopLeft: new THREE.Vector3(
            screenCenter.x - halfWidth,
            screenCenter.y + halfHeight,
            screenCenter.z
        ),
        frontTopRight: new THREE.Vector3(
            screenCenter.x + halfWidth,
            screenCenter.y + halfHeight,
            screenCenter.z
        ),
        frontBottomLeft: new THREE.Vector3(
            screenCenter.x - halfWidth,
            screenCenter.y - halfHeight,
            screenCenter.z
        ),
        frontBottomRight: new THREE.Vector3(
            screenCenter.x + halfWidth,
            screenCenter.y - halfHeight,
            screenCenter.z
        ),
        center: screenCenter
    };
    
    // ========================================
    // 修改:电脑“背面的上前方”位置(60度角,距离400)
    // 约定:Z 轴正方向为“前方”,因此背面方向为 -Z
    // ========================================
    const distance = 400;
    const angle = Math.PI / 3; // 60度
    
    laptopFrontUpperPosition = new THREE.Vector3(
        screenCenter.x,
        screenCenter.y + distance * Math.sin(angle),  // 上方
        screenCenter.z - distance * Math.cos(angle)   // 背面(-Z)
    );
    
    console.log('屏幕四角:', laptopScreenCorners);
    console.log('前上方位置(60度):', laptopFrontUpperPosition);
}
 
// ============================================
// 第 6.6 步:创建前上方信息板(修改:BasicMaterial避免偏色)
// ============================================
 
function createFrontUpperPanel() {
    if (!laptopFrontUpperPosition) return;
    
    // 加载贴图
    const textureLoader = new THREE.TextureLoader();
    const panelTexture = textureLoader.load('./assets/textures/laptop.jpg');
    
    // 设置颜色空间避免偏色
    panelTexture.colorSpace = THREE.SRGBColorSpace;
    
    const panelWidth = 400;
    const panelHeight = 300;
    
    const geometry = new THREE.PlaneGeometry(panelWidth, panelHeight);
    
    // ========================================
    // 修改:使用BasicMaterial避免灯光影响颜色
    // ========================================
    backPanelMaterial = new THREE.MeshBasicMaterial({
        map: panelTexture,
        side: THREE.DoubleSide,
        transparent: true,
        opacity: 0  // 初始完全透明
    });
    
    backPanel = new THREE.Mesh(geometry, backPanelMaterial);
    backPanel.position.copy(laptopFrontUpperPosition);

    // 让贴图与电脑面平行:不再 lookAt 电脑,而是使用与屏幕相同的倾角
    // 并保持正面朝向镜头侧(PlaneGeometry 默认正面朝 +Z)
    backPanel.rotation.set(SCREEN_ROT_X, 0, 0);
    
    // 初始缩放为0.1
    backPanel.scale.set(0.1, 0.1, 0.1);
    
    scene.add(backPanel);
    
    // 发光边框(初始隐藏)
    const edges = new THREE.EdgesGeometry(geometry);
    const lineMaterial = new THREE.LineBasicMaterial({ 
        color: 0x00ffff,
        transparent: true,
        opacity: 0
    });
    const wireframe = new THREE.LineSegments(edges, lineMaterial);
    wireframe.name = 'wireframe';
    backPanel.add(wireframe);
    
    // 点光源(初始关闭)
    panelLight = new THREE.PointLight(0x00ffff, 0, 800);
    panelLight.position.set(0, 0, 50);
    backPanel.add(panelLight);
    
    console.log('前上方信息板已创建(按R键显示)');
}
 
// ============================================
// 新增:创建"雨从未迟到"文字精灵
// ============================================

function createRainText() {
    // 创建画布
    const canvas = document.createElement('canvas');
    canvas.width = 512;
    canvas.height = 128;
    const ctx = canvas.getContext('2d');
    
    // 清除画布
    ctx.fillStyle = 'rgba(0, 0, 0, 0)';
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    
    // 设置文字样式
    ctx.font = 'bold 48px "Microsoft YaHei", "SimHei", sans-serif';
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    
    // 发光效果
    ctx.shadowColor = '#00ffff';
    ctx.shadowBlur = 20;
    ctx.fillStyle = '#ffffff';
    ctx.fillText('雨从未迟到', canvas.width / 2, canvas.height / 2);
    
    // 创建纹理
    const texture = new THREE.CanvasTexture(canvas);
    texture.needsUpdate = true;
    
    // 创建精灵材质
    const spriteMaterial = new THREE.SpriteMaterial({
        map: texture,
        transparent: true,
        opacity: 0  // 初始隐藏
    });
    
    // 创建精灵
    rainTextSprite = new THREE.Sprite(spriteMaterial);
    rainTextSprite.scale.set(200, 50, 1);
    
    // 位置在信息板上方
    if (laptopFrontUpperPosition) {
        rainTextSprite.position.copy(laptopFrontUpperPosition);
        rainTextSprite.position.y += 180;  // 信息板上方
    }
    
    scene.add(rainTextSprite);
    
    console.log('"雨从未迟到"文字已创建');
}
 
// ============================================
// 第 6.7 步:创建连接线(修改:连接到前上方)
// ============================================
 
function createConnectionLines() {
    if (!laptopScreenCorners || !laptopFrontUpperPosition) return;
    
    const corners = laptopScreenCorners;
    const frontPos = laptopFrontUpperPosition;
    
    const panelWidth = 400;
    const panelHeight = 300;
    const halfW = panelWidth / 2;
    const halfH = panelHeight / 2;
    
    // 计算信息板四角(考虑信息板旋转;忽略缩放,保持与原效果一致)
    const panelQuat = new THREE.Quaternion().setFromEuler(
        new THREE.Euler(SCREEN_ROT_X, 0, 0)
    );
    const localCorners = [
        new THREE.Vector3(-halfW, +halfH, 0),
        new THREE.Vector3(+halfW, +halfH, 0),
        new THREE.Vector3(-halfW, -halfH, 0),
        new THREE.Vector3(+halfW, -halfH, 0)
    ];
    const frontCorners = localCorners.map((v) => v.clone().applyQuaternion(panelQuat).add(frontPos));
    
    const screenCorners = [
        corners.frontTopLeft,
        corners.frontTopRight,
        corners.frontBottomLeft,
        corners.frontBottomRight
    ];
    
    // 创建四条连接线(初始隐藏)
    const lineMaterial = new THREE.LineBasicMaterial({
        color: 0x00ffff,
        transparent: true,
        opacity: 0
    });
    
    const tubeMaterial = new THREE.MeshBasicMaterial({
        color: 0x00ffff,
        transparent: true,
        opacity: 0
    });
    
    for (let i = 0; i < 4; i++) {
        const from = screenCorners[i];
        const to = frontCorners[i];
        
        // 创建上升曲线
        const midPoint = new THREE.Vector3().addVectors(from, to).multiplyScalar(0.5);
        midPoint.y += 80;  // 向上弯曲
        
        const curve = new THREE.QuadraticBezierCurve3(from, midPoint, to);
        const points = curve.getPoints(50);
        const geometry = new THREE.BufferGeometry().setFromPoints(points);
        
        const line = new THREE.Line(geometry, lineMaterial.clone());
        line.name = `connectionLine_${i}`;
        scene.add(line);
        connectionLines.push(line);
        
        const tubeGeometry = new THREE.TubeGeometry(curve, 20, 2, 8, false);
        const tube = new THREE.Mesh(tubeGeometry, tubeMaterial.clone());
        tube.name = `connectionTube_${i}`;
        scene.add(tube);
        connectionLines.push(tube);
    }
    
    console.log('连接线已创建');
}

// ============================================
// 新增:R键监听
// ============================================

function setupRKeyListener() {
    console.log('⌨️ R键监听已启用');
    
    window.addEventListener('keydown', (e) => {
        if (e.key === 'r' || e.key === 'R') {
            console.log('🎯 R键被按下');
            
            if (!state.isPanelVisible && !state.isAnimating) {
                showPanel();
            } else if (state.isPanelVisible && !state.isAnimating) {
                hidePanel();
            }
        }
    });
}

// 显示信息板
function showPanel() {
    console.log('>>> 显示信息板');
    state.isAnimating = true;
    state.isPanelVisible = true;
    state.panelTargetOpacity = 1;
    state.panelTargetScale = 1;
    
    // 播放音效
    playRainSound();
}

// 隐藏信息板
function hidePanel() {
    console.log('>>> 隐藏信息板');
    state.isAnimating = true;
    state.isPanelVisible = false;
    state.panelTargetOpacity = 0;
    state.panelTargetScale = 0.1;
}

// ============================================
// 新增:音效(雨声+科技音)
// ============================================

function playRainSound() {
    try {
        const AudioContext = window.AudioContext || window.webkitAudioContext;
        const ctx = new AudioContext();
        
        // 创建噪声(雨声)
        const bufferSize = ctx.sampleRate * 2; // 2秒
        const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate);
        const data = buffer.getChannelData(0);
        
        for (let i = 0; i < bufferSize; i++) {
            data[i] = (Math.random() * 2 - 1) * 0.1; // 雨噪声
        }
        
        const noise = ctx.createBufferSource();
        noise.buffer = buffer;
        
        // 噪声滤波(更像雨声)
        const noiseFilter = ctx.createBiquadFilter();
        noiseFilter.type = 'lowpass';
        noiseFilter.frequency.value = 800;
        
        const noiseGain = ctx.createGain();
        noiseGain.gain.setValueAtTime(0.05, ctx.currentTime);
        noiseGain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 1.5);
        
        noise.connect(noiseFilter);
        noiseFilter.connect(noiseGain);
        noiseGain.connect(ctx.destination);
        noise.start();
        
        // 科技音效(和弦)
        const frequencies = [440, 554, 659]; // A大调和弦
        frequencies.forEach((freq, i) => {
            const osc = ctx.createOscillator();
            const gain = ctx.createGain();
            
            osc.type = 'sine';
            osc.frequency.value = freq;
            
            gain.gain.setValueAtTime(0, ctx.currentTime);
            gain.gain.linearRampToValueAtTime(0.03, ctx.currentTime + 0.1 + i * 0.05);
            gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 1.5);
            
            osc.connect(gain);
            gain.connect(ctx.destination);
            osc.start(ctx.currentTime);
            osc.stop(ctx.currentTime + 1.5);
        });
        
    } catch (e) {
        console.log('音效播放失败', e);
    }
}

// ============================================
// 新增:动画更新
// ============================================

function updatePanelAnimation() {
    if (!backPanel || !backPanelMaterial) return;
    
    const dt = 0.016;
    const speed = 2.0;
    
    // 透明度渐变
    const opacityDiff = state.panelTargetOpacity - state.panelCurrentOpacity;
    if (Math.abs(opacityDiff) > 0.001) {
        state.panelCurrentOpacity += opacityDiff * speed * dt;
        backPanelMaterial.opacity = Math.max(0, Math.min(1, state.panelCurrentOpacity));
        
        // 文字同步淡入淡出
        if (rainTextSprite) {
            rainTextSprite.material.opacity = state.panelCurrentOpacity;
        }
    }
    
    // 缩放动画(弹性效果)
    const scaleDiff = state.panelTargetScale - state.panelCurrentScale;
    if (Math.abs(scaleDiff) > 0.001) {
        state.panelCurrentScale += scaleDiff * speed * dt;
        const scale = Math.max(0.1, state.panelCurrentScale);
        backPanel.scale.set(scale, scale, scale);
    }
    
    // 边框发光
    const wireframe = backPanel.getObjectByName('wireframe');
    if (wireframe && wireframe.material) {
        wireframe.material.opacity = state.panelCurrentOpacity * 0.8;
    }
    
    // 连接线延迟显示
    connectionLines.forEach((line, index) => {
        if (line.material) {
            const delay = index * 0.03;
            let effectiveOpacity;
            if (state.isPanelVisible) {
                effectiveOpacity = Math.max(0, state.panelCurrentOpacity - delay);
            } else {
                effectiveOpacity = state.panelCurrentOpacity;
            }
            line.material.opacity = effectiveOpacity * 0.6;
        }
    });
    
    // 点光源
    if (panelLight) {
        const targetIntensity = state.isPanelVisible ? 1.5 : 0;
        panelLight.intensity += (targetIntensity - panelLight.intensity) * speed * dt;
    }
    
    // 悬浮动画(显示后)
    if (state.isPanelVisible && state.panelCurrentOpacity > 0.95) {
        const time = Date.now() * 0.001;
        backPanel.position.y = laptopFrontUpperPosition.y + Math.sin(time) * 5;
        backPanel.rotation.z = Math.sin(time * 0.5) * 0.02;
        
        // 文字跟随浮动
        if (rainTextSprite) {
            rainTextSprite.position.y = laptopFrontUpperPosition.y + 180 + Math.sin(time) * 5;
        }
    }
    
    // 动画结束
    if (state.isAnimating) {
        const opacityDone = Math.abs(state.panelCurrentOpacity - state.panelTargetOpacity) < 0.01;
        const scaleDone = Math.abs(state.panelCurrentScale - state.panelTargetScale) < 0.01;
        
        if (opacityDone && scaleDone) {
            state.isAnimating = false;
            console.log(state.isPanelVisible ? '>>> 显示完成' : '>>> 隐藏完成');
        }
    }
}
 
// ============================================
// 第 7 步:自动对焦函数
// ============================================
function autoFitCamera(object) {
    const box = new THREE.Box3().setFromObject(object);
    const size = new THREE.Vector3();
    const center = new THREE.Vector3();
    box.getSize(size);
    box.getCenter(center);
    
    const maxDim = Math.max(size.x, size.y, size.z);
    const fov = camera.fov * (Math.PI / 180);
    const cameraDistance = Math.abs(maxDim / 2 / Math.tan(fov / 2)) * 2.5;
    
    camera.position.set(
        center.x + maxDim * 0.5,
        center.y + maxDim * 0.3,
        center.z + cameraDistance
    );
    
    controls.target.copy(center);
    controls.update();
}
 
// ============================================
// 第 7.2 步:添加光影效果
// ============================================
const ambientLightExtra = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLightExtra);
 
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(10, 10, 10);
scene.add(directionalLight);
 
// ============================================
// 第 7.3 步:添加辅助线(保留!)
// ============================================
const gridHelper = new THREE.GridHelper(10000, 100);
scene.add(gridHelper);
 
const axesHelper = new THREE.AxesHelper(5000);
scene.add(axesHelper);
 
// ============================================
// 第 8 步:创建电脑屏幕(原有)
// ============================================
function createScreen() {
    const iframe = document.createElement('iframe');
    iframe.src = 'https://rockosdev.github.io/';
    iframe.style.width = '1480px';
    iframe.style.height = '1100px';
    iframe.style.border = 'none';
    iframe.style.backgroundColor = 'white';
    
    const screenObject = new CSS3DObject(iframe);
    screenObject.position.set(0.00, 52.00, -8.00);
    screenObject.rotation.x = SCREEN_ROT_X;
    screenObject.scale.set(0.125, 0.112, 0.20);
    screenObject.element.style.backfaceVisibility = 'hidden';
    
    cssScene.add(screenObject);
    
    console.log('屏幕已创建');
    setupDebugControls(screenObject);
}
 
// ============================================
// 第 9 步:键盘调试功能(保留原有+添加R键说明)
// ============================================
function setupDebugControls(screenObject) {
    console.log('=== 调试模式已开启 ===');
    console.log('R     : 触发/隐藏信息板映射');
    console.log('方向键 ↑↓←→ : 移动屏幕位置');
    console.log('W/S : 前后移动');
    console.log('Q/E : 旋转 X 轴');
    console.log('A/D : 旋转 Y 轴');
    console.log('Z/X : 旋转 Z 轴');
    console.log('+/- : 缩放');
    console.log('P : 打印当前坐标');
    
    const step = 2;
    const rotStep = 0.01;
    
    window.addEventListener('keydown', (e) => {
        // R键已在setupRKeyListener中处理,这里跳过
        if (e.key === 'r' || e.key === 'R') return;
        
        switch(e.key) {
            case 'ArrowUp': screenObject.position.y += step; break;
            case 'ArrowDown': screenObject.position.y -= step; break;
            case 'ArrowLeft': screenObject.position.x -= step; break;
            case 'ArrowRight': screenObject.position.x += step; break;
            case 'w': case 'W': screenObject.position.z -= step; break;
            case 's': case 'S': screenObject.position.z += step; break;
            case 'q': case 'Q': screenObject.rotation.x += rotStep; break;
            case 'e': case 'E': screenObject.rotation.x -= rotStep; break;
            case 'a': case 'A': screenObject.rotation.y += rotStep; break;
            case 'd': case 'D': screenObject.rotation.y -= rotStep; break;
            case 'z': case 'Z': screenObject.rotation.z += rotStep; break;
            case 'x': case 'X': screenObject.rotation.z -= rotStep; break;
            case '+': case '=': screenObject.scale.multiplyScalar(1.1); break;
            case '-': screenObject.scale.multiplyScalar(0.9); break;
            case 'p': case 'P':
                console.log('========== 当前坐标 ==========');
                console.log(`position.set(${screenObject.position.x.toFixed(2)}, ${screenObject.position.y.toFixed(2)}, ${screenObject.position.z.toFixed(2)})`);
                console.log(`rotation.set(${screenObject.rotation.x.toFixed(2)}, ${screenObject.rotation.y.toFixed(2)}, ${screenObject.rotation.z.toFixed(2)})`);
                console.log(`scale.set(${screenObject.scale.x.toFixed(2)}, ${screenObject.scale.y.toFixed(2)}, ${screenObject.scale.z.toFixed(2)})`);
                console.log('==============================');
                break;
        }
    });
}
 
// ============================================
// 第 10 步:渲染循环(添加动画更新)
// ============================================
function animate() {
    requestAnimationFrame(animate);
    controls.update();
    
    // 新增:更新面板动画
    updatePanelAnimation();
    
    renderer.render(scene, camera);
    cssRenderer.render(cssScene, camera);
}
 
animate();
 
// ============================================
// 第 11 步:响应窗口大小变化
// ============================================
window.addEventListener('resize', () => {
    const newWidth = window.innerWidth;
    const newHeight = window.innerHeight;
    camera.aspect = newWidth / newHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(newWidth, newHeight);
    cssRenderer.setSize(newWidth, newHeight);
});
 
console.log('3D 电脑初始化完成');
console.log('按 R 键触发映射');

// ============================================
// 追加功能:按 U 键恢复“刚进入电脑时”的视角位置
// 约束:只增加代码,不修改原有逻辑/效果。
// 还原内容:camera.position、controls.target、camera.zoom。
// ============================================

const __initialViewState = {
    saved: false,
    cameraPosition: new THREE.Vector3(),
    controlsTarget: new THREE.Vector3(),
    cameraZoom: 1
};

function __saveInitialViewStateOnce() {
    if (__initialViewState.saved) return;
    __initialViewState.cameraPosition.copy(camera.position);
    __initialViewState.controlsTarget.copy(controls.target);
    __initialViewState.cameraZoom = camera.zoom;
    __initialViewState.saved = true;
    console.log('✅ 已记录初始视角(按 U 可恢复)');
}

function __restoreInitialViewState() {
    if (!__initialViewState.saved) {
        console.warn('⚠️ 初始视角尚未记录,无法恢复');
        return;
    }
    camera.position.copy(__initialViewState.cameraPosition);
    controls.target.copy(__initialViewState.controlsTarget);
    camera.zoom = __initialViewState.cameraZoom;
    camera.updateProjectionMatrix();
    controls.update();
}

// 模型加载并 autoFitCamera 生效后,再记录“刚进入电脑时”的视角
(function __captureInitialViewWhenReady() {
    const loadingEl = document.getElementById('loading');
    const isLoaded = !loadingEl || loadingEl.style.display === 'none';
    if (isLoaded) {
        requestAnimationFrame(() => {
            __saveInitialViewStateOnce();
        });
        return;
    }
    requestAnimationFrame(__captureInitialViewWhenReady);
})();

// 监听 U 键
window.addEventListener('keydown', (e) => {
    if (e.key === 'u' || e.key === 'U') {
        __restoreInitialViewState();
    }
});

image

posted @ 2026-03-17 11:14  游翔  阅读(0)  评论(0)    收藏  举报