二(1)、3D 可旋转电脑网页实现教程
启发来自B站Up主:ai超级个人的视频。
一、准备工作
1.1 你需要准备什么
| 物品 | 用途 | 获取方式 |
|---|---|---|
| 电脑 | 写代码 | 你的电脑 |
| 代码编辑器 | 写代码 | 下载 VS Code(免费) |
| 浏览器 | 看效果 | Chrome 或 Edge |
| 3D 电脑模型 | 显示在网页上 | Sketchfab 免费下载 |
| 模型贴图 | 给模型上色 | 和模型一起下载 |
1.2 下载 3D 模型
步骤:
- 打开网站:https://sketchfab.com
- 搜索关键词:laptop 或 notebook
- 筛选免费模型(Price → Free)
- 下载格式选择 GLB 或 GLTF
- 解压后你会得到:
- laptop.glb(模型文件)
- texture.jpg(贴图文件,可能没有)
提示:如果没有贴图,模型可能是纯色的,也能用。
搜索框搜索laptop,我选择的是Berk Gedik'sCyberPunk Laptop Concept Design

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

会得到一个名为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

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
浏览器会自动打开


方式二:使用 Python 临时服务器
# 打开终端,进入项目文件夹
cd my-3d-computer
# Python 3
python -m http.server 8000
# 然后浏览器访问 http://localhost:8000
4.2 你应该看到什么
| 阶段 | 现象 |
|---|---|
| 加载中 | 黑屏,显示"加载中..." |
| 加载完成 | 出现 3D 电脑模型 |
| 鼠标操作 | 可以旋转、缩放、平移 |
| 屏幕位置 | 可能不对,需要调试 |
五、调试屏幕位置(关键步骤!)
5.1 调试方法
因为每个 3D 模型的屏幕位置和大小都不一样,你需要手动调整。
操作步骤:
- 打开浏览器控制台(按 F12)
- 查看按键说明(控制台会打印)
- 使用键盘调整:
| 按键 | 功能 |
|---|---|
| ↑ ↓ ← → | 上下左右移动屏幕 |
| W / S | 前后移动 |
| Q / E | 绕 X 轴旋转 |
| A / D | 绕 Y 轴旋转 |
| Z / X | 绕 Z 轴旋转 |
| + / - | 放大 / 缩小 |
| P | 打印当前坐标 |
调整之后按下P打印当前坐标

然后在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-)可以缩小漂浮的网页

6.2 网页显示在模型后面(无法点击)
原因:层级不对或控制器绑定错误
解决:
- 确保 cssRenderer.domElement.style.zIndex = '2'
- 确保 renderer.domElement.style.zIndex = '1'
- 确保 OrbitControls 绑定到 cssRenderer.domElement
6.3 屏幕位置调不准
原因:需要耐心调试
解决:
- 使用小步长(把 step 改成 1)
- 先调位置,再调旋转,最后调缩放
- 一次只调一个轴
6.4 模型加载慢
原因:模型文件太大
解决:
- 使用 Blender 简化模型面数
- 压缩贴图图片
- 添加加载进度条
完工,但是和原来的相比,总怪还是差点东西,好像比较暗淡。我决定优化一下。

七、进阶优化(可选)
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 电脑初始化完成');
这一版本我很满意:就它了。

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 电脑初始化完成');

| 功能 | 实现 |
|---|---|
| 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 键开始映射');

八、完整文件清单
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();
}
});


浙公网安备 33010602011771号