金三银四,一个面试官连连夸赞的个人网页技术分享

不知道大家有没有同样的感觉:

今年的 "金三银四",似乎没有想象中那么热闹。 尤其是 前端岗位,不少公司都在收缩,机会明显少了很多。

我最近也参加了几场大厂面试(我面的是 AI 全栈开发岗)。 有一件事让我挺意外的。

某度的面试官在面试完后加微信聊天的时候说:

"刚看到你简历上的个人网站时,就明显感觉到你是一个有技术追求的同学,还让 hr 赶快联系一下面试。"

然后就把这个网站发到分享到小红书上,b站上去了(之前看过类似的效果,特地复刻了一版),反响很不错,所以特此非常以下里面的技术细节。

效果如下:

点击跳转在线地址

同时,欢迎大家关注我的微信公众号:ai超级个人,会有更多的炫酷网页分享

这篇文章会稍微偏技术一些。

如果你:

  • 对网页动效感兴趣
  • 或者几乎没有 Three.js 基础
  • 甚至不太懂 3D

这些思路依然很有价值。

尤其是在 你让 AI 帮你调试代码、改造项目的时候,理解这些结构会非常有帮助。

如果你想系统学习 Three.js,我只推荐一套教程:

Three.js Journey(点击跳转 B 站)

这是我心中目前全球范围内,从 0 到 1 最完美的 Three.js 课程。说实话,市面上很多所谓的基础教程,且不说是否存在"割韭菜"的行为,单是乏味的教学逻辑就在浪费你的学习生命。

好了,回到正文。今天我们要深度拆解上面网站效果,以及解决如下三个核心问题:

1. 空间定位:如何在网页中调试模型位置?

在 3D 世界里,任何模型都有它的坐标。以下面这个电脑模型为例:

模型默认被放置在原点 ( 0 , 0 , 0 ) (0, 0, 0) (0,0,0),这通常没问题。但真正的痛点在于:摄像机应该架在哪?

打个比方,这就好比现实中的人像摄影:

  • 被拍的人 → 相当于 3D 模型
  • 摄影师 → 相当于 Camera(相机)

模型在那不动,但摄影师的位置(Position)和对焦的方向(LookAt)决定了最终的画面。

而且我们能不能像在 Blender(知名的 3D 图形软件)里一样,在网页端也能直观地旋转、调整远近,从而找到那个最完美的视觉角度?如下图是 blender 的界面:

2. 跨次元融合:如何将真实网页嵌入 3D 场景?

请看下图:

在这台 3D 电脑模型的屏幕中央,其实嵌套了一个真实的网页(前端术语叫 iframe)。

所以问题来了,在 three.js 中如何嵌套一个别的网站的网页呢?

3. 精准对位:如何调试 iframe 的 3D 坐标?

这是上一个问题的延伸。iframe(网页) 作为一个平面,在 3D 空间中同样拥有自己的坐标和旋转参数。但问题是:

你很难凭直觉盲猜出电脑模型那块屏幕的精确数值。

因此,我们需要一套可视化的调试界面 。然后配合我们第一步提到的工具,让我们可以在页面上手动微调 iframe 的位置,直到它与模型屏幕完美贴合。

最后,直接将调试好的坐标参数"写死"在代码里就能保证初始化电脑模型和网页都在合适的坐标上。

接下来,我们一步一步,从 0 到 1 实现这个过程:

网页中调整相机位置小技巧

初始的时候,我们假设有如下代码(精简后的 demo )。代码主要做的是将电脑的模型贴图加载进来。

如果你有不了解的代码块可以借助 ai 了解详细信息,因为已经是最基础的 three.js 代码,如果缺乏必要的基础,建议学习上面的教程。

注:代码使用了 react 框架,你也可以让 ai 改造为你的熟悉的技术栈,例如 vue 或者 html:

javascript 复制代码
import * as THREE from "three";
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";

export interface Computer3DConfig {
  container: HTMLElement;
  modelPath: string;
  texturePath: string;
  modelScale?: number;
}

export class Computer3D {
  private scene: THREE.Scene = new THREE.Scene();
  private camera: THREE.PerspectiveCamera;
  private renderer: THREE.WebGLRenderer;
  private container: HTMLElement;
  private config: Computer3DConfig;

  constructor(config: Computer3DConfig) {
    this.config = { ...config };
    this.container = config.container;

    // 1. 初始化相机
    const aspect = window.innerWidth / window.innerHeight;
    this.camera = new THREE.PerspectiveCamera(35, aspect, 10, 100000);

    // 2. 初始化渲染器 (WebGL)
    this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
    this.renderer.setSize(window.innerWidth, window.innerHeight);
    this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
    this.renderer.domElement.style.position = "absolute";
    this.renderer.domElement.style.top = "0";
    this.renderer.domElement.style.zIndex = "1";
    this.container.appendChild(this.renderer.domElement);

    this.render();
  }

  // 加载模型与贴图
  public async load(): Promise<void> {
    const loader = new GLTFLoader();
    const textureLoader = new THREE.TextureLoader();

    // 并行加载模型和贴图
    const [gltf, texture] = await Promise.all([
      loader.loadAsync(this.config.modelPath),
      textureLoader.loadAsync(this.config.texturePath),
    ]);

    // 贴图配置
    texture.flipY = false;
    texture.colorSpace = THREE.SRGBColorSpace;
    const material = new THREE.MeshBasicMaterial({ map: texture });

    // 遍历模型应用材质
    gltf.scene.traverse((child: any) => {
      if (child instanceof THREE.Mesh) {
        child.material = material;
      }
    });

    this.scene.add(gltf.scene);
  }

  // 渲染循环
  private render(): void {
    requestAnimationFrame(this.render.bind(this));
    this.renderer.render(this.scene, this.camera);
  }

  // 处理窗口大小调整(建议添加)
  public onWindowResize(): void {
    const width = window.innerWidth;
    const height = window.innerHeight;
    this.camera.aspect = width / height;
    this.camera.updateProjectionMatrix();
    this.renderer.setSize(width, height);
  }
}

export default Computer3D;

避坑指南:为什么初始化你的 3D 世界是一团黑?

很多同学在加载代码后,满怀期待地打开页面,结果发现是一片漆黑。例如我们上面的代码。

别担心,并不是模型消失了,只是你正"站在模型肚子里"!(模型内部)默认情况下,相机的初始坐标在 (0, 0, 0)。而模型加载进来也通常在原点。

这种"合二为一"的状态让你什么也看不见。为了解决这个问题,我们需要像真正的摄影师一样,完成以下三个层层递进的步骤。

首先就是让相机能够完整的看到电脑模型:

第一步:开启"自动对焦",给模型一个完美的全身照

首先模型的大小是不可控的,有的只有几厘米,有的却有几百米。

所以我们需要一个通用的"自动对焦"函数,让相机自动根据模型的大小调整距离,起码能看清楚模型的全身,然后再后续微调相机位置。

核心逻辑: 用一个隐形的方框把模型包起来(Box3),测量它的尺寸,然后把相机推到足够远的地方。

JAVASCRIPT 复制代码
/**
 * ✅ 自动对焦:不仅要移相机,还要移控制器的目标点
 */
private autoFitCamera(object: THREE.Object3D): void {
  // 1. 计算模型的包围盒
  const box = new THREE.Box3().setFromObject(object);
  const size = box.getSize(new THREE.Vector3());   // 获取模型长宽高
  const center = box.getCenter(new THREE.Vector3()); // 获取模型的中心点

  // 2. 根据模型大小计算相机距离
  const maxDim = Math.max(size.x, size.y, size.z);
  const fov = this.camera.fov * (Math.PI / 180); // 视角转弧度
  // 数学公式:距离 = 对边 / tan(角度)
  const cameraDistance = Math.abs(maxDim / 2 / Math.tan(fov / 2)) * 1.5;

  // 3. 移动相机位置:稍微偏一点,让画面有立体感
  this.camera.position.set(
    center.x + maxDim * 0.2,
    center.y + maxDim * 0.3,
    center.z + cameraDistance,
  );

  // 4. 🔥 关键:让相机不仅"站得远",还要"盯着看"
  // 在使用控制器时,必须更新 target 才能真正改变视线方向
  this.controls.target.copy(center);
  this.controls.update();
}

第二步:引入"上帝视角",让场景动起来

有了对焦还不够,如果你想 360 度观察模型,就需要 OrbitControls(轨道控制器)。它能让你的鼠标变成相机的"推进器"和"转盘"。

也就是有了 OrbitControls,我们就可以动态调整相机的位置,让相机上下左右移动,并且旋转相机视角。

代码集成方案:

javascript 复制代码
// 1. 引入辅助组件
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";

// 2. 在类里面定义控制器属性
private controls: OrbitControls;

// 3. 在 constructor (构造函数) 中初始化
// 这里的第二个参数 renderer.domElement 很重要,它决定了鼠标在哪里滑有效
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
this.controls.enableDamping = true; // 开启阻尼(手感更丝滑,不会突然停住)
this.controls.dampingFactor = 0.05;

// 4. 重点:在 render 循环中每一帧都要更新它
private render(): void {
  requestAnimationFrame(this.render.bind(this));
  
  // 只有更新了 controls,你的拖拽和阻尼效果才会生效
  this.controls.update(); 
  this.renderer.render(this.scene, this.camera);
}

第三步:搭建"导演工作站",用键盘寻找黄金视角

上面 OrbitControls 虽然能动态调整相机位置,但是毕竟鼠标不好控制,我们还需要增加一些精细的微调方法。

我们采取的是通过键盘微调相机位置,找到感觉最好的那一刻,记录下坐标。也就是键盘一些快捷键能小范围的移动相机视角。

功能说明:

  • 方向键和 W/S:可以在 3D 空间的前后左右上下移动。

  • P 键:在控制台打印当前相机的"黄金坐标"。

javascript 复制代码
private setupDebugControls(): void {
  // 辅助线:添加网格和坐标轴,防止在 3D 空间迷失方向
  const gridHelper = new THREE.GridHelper(10000, 100);
  const axesHelper = new THREE.AxesHelper(5000);
  this.scene.add(gridHelper, axesHelper);

  window.addEventListener("keydown", (e) => {
    const step = 100; // 每次按键移动的距离
    const moveMap: Record<string, [number, number, number]> = {
      ArrowUp: [0, step, 0],    // 向上
      ArrowDown: [0, -step, 0], // 向下
      ArrowLeft: [-step, 0, 0], // 向左
      ArrowRight: [step, 0, 0], // 向右
      w: [0, 0, -step],         // 向前
      s: [0, 0, step],          // 向后
    };

    if (moveMap[e.key]) {
      const [x, y, z] = moveMap[e.key];
      this.camera.position.addScaledVector(new THREE.Vector3(x, y, z), 1);
    }

    // ✅ 导出视角:当你调到满意的角度时,按 P 打印参数
    if (e.key === "p") {
      console.log(`--- 记录当前黄金视角 ---`);
      console.log(`相机位置: .set(${this.camera.position.x}, ${this.camera.position.y}, ${this.camera.position.z})`);
      console.log(`盯着看的目标点: .set(${this.controls.target.x}, ${this.controls.target.y}, ${this.controls.target.z})`);
    }
  });
}

综上所述,大概就能模拟一个 3D 模型软件的 Camera 视角了。

将网页加入到 three.js 中

这一章节我们进入最酷的部分:让你的 3D 电脑真正"联网"(嵌入网页)。

在普通的 3D 场景中,物体通常只是死板的几何体。但 Three.js 提供了一个强大的"传送门" ------ CSS3DObject。

它能把真实的 HTML 元素(如 div、iframe、video)直接塞进 3D 空间,让网页像贴纸一样贴在模型的屏幕上,且依然保持可点击、可交互。

要实现这个效果,我们需要同时运行两个渲染器:

  • WebGLRenderer (底层):负责渲染 3D 模型(电脑外壳)。

  • CSS3DRenderer (顶层):负责将 HTML 元素通过 CSS3 矩阵变换,投影到 3D 空间中。

我们将这两个渲染器的画布重叠在一起,并让它们共享同一个相机(Camera)。这样当你旋转镜头时,网页和模型就会同步运动,看起来就像网页长在模型上一样。

最小实现 Demo 代码: 以下是整合了模型加载与 iframe 屏幕渲染的完整代码:

javascript 复制代码
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";

export interface Computer3DConfig {
  container: HTMLElement;
  modelPath: string;
  texturePath: string;
  screenUrl: string; // 注入 iframe 的网址
  modelScale?: number;
}

export class Computer3D {
  private scene: THREE.Scene = new THREE.Scene(); // WebGL 场景
  private cssScene: THREE.Scene = new THREE.Scene(); // CSS3D 专用场景
  private camera: THREE.PerspectiveCamera;
  private renderer: THREE.WebGLRenderer;
  private cssRenderer: CSS3DRenderer;
  private controls: OrbitControls;
  private container: HTMLElement;
  private config: Computer3DConfig;

  constructor(config: Computer3DConfig) {
    this.config = { modelScale: 4200, ...config };
    this.container = config.container;

    const width = window.innerWidth;
    const height = window.innerHeight;

    // 1. 初始化相机
    this.camera = new THREE.PerspectiveCamera(35, width / height, 10, 100000);
    this.camera.position.set(930, 600, 4500);

    // 2. 初始化 WebGL 渲染器 (渲染底层模型)
    this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
    this.renderer.setSize(width, height);
    this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
    this.renderer.domElement.style.position = "absolute";
    this.renderer.domElement.style.top = "0";
    this.renderer.domElement.style.zIndex = "1"; // 确保在底层
    this.container.appendChild(this.renderer.domElement);

    // 3. 初始化 CSS3D 渲染器 (渲染顶层网页)
    this.cssRenderer = new CSS3DRenderer();
    this.cssRenderer.setSize(width, height);
    this.cssRenderer.domElement.style.position = "absolute";
    this.cssRenderer.domElement.style.top = "0";
    this.cssRenderer.domElement.style.zIndex = "2"; // 盖在模型上面
    this.container.appendChild(this.cssRenderer.domElement);

    // 4. 初始化轨道控制器
    // 💡 注意:事件监听要绑定在最上层的 cssRenderer 元素上,否则会被遮挡
    this.controls = new OrbitControls(this.camera, this.cssRenderer.domElement);
    this.controls.enableDamping = true;
    this.controls.target.set(925, 310, -300);

    this.render();
    window.addEventListener("resize", () => this.onWindowResize());
  }

  // 加载模型与贴图
  public async load(): Promise<void> {
    const loader = new GLTFLoader();
    const textureLoader = new THREE.TextureLoader();

    const [gltf, texture] = await Promise.all([
      loader.loadAsync(this.config.modelPath),
      textureLoader.loadAsync(this.config.texturePath),
    ]);

    texture.flipY = false;
    texture.colorSpace = THREE.SRGBColorSpace;
    const material = new THREE.MeshBasicMaterial({ map: texture });

    gltf.scene.traverse((child: any) => {
      if (child instanceof THREE.Mesh) {
        child.scale.setScalar(this.config.modelScale!);
        child.material = material;
      }
    });

    this.scene.add(gltf.scene);
    
    // 🔥 模型加载完后,开始在 3D 空间"插"一个网页
    this.initIframeScreen();
  }

  private initIframeScreen(): void {
    // 设定网页的逻辑分辨率(相当于显示器的分辨率)
    const SCREEN_W = 1480;
    const SCREEN_H = 1100;

    // 创建原生 iframe
    const iframe = document.createElement("iframe");
    iframe.src = this.config.screenUrl;
    iframe.style.width = `${SCREEN_W}px`;
    iframe.style.height = `${SCREEN_H}px`;
    iframe.style.border = "none";
    iframe.style.backgroundColor = "#000";

    // 包装成 3D 对象
    const cssObject = new CSS3DObject(iframe);
    
    /**
     * 🛠 坐标微调:
     * 这是最关键也最耗时的一步。你需要根据模型屏幕的具体位置,
     * 反复调整 position 和 rotation,直到 iframe 严丝合缝地贴在模型框里。
     * 下一小节会有微调的方法
     */
    cssObject.position.set(900, 458, 765); 
    cssObject.rotation.x = -1; // 配合模型显示器的后仰角度

    this.cssScene.add(cssObject);
  }

  private onWindowResize(): void {
    const w = window.innerWidth;
    const h = window.innerHeight;
    this.camera.aspect = w / h;
    this.camera.updateProjectionMatrix();
    this.renderer.setSize(w, h);
    this.cssRenderer.setSize(w, h);
  }

  private render(): void {
    requestAnimationFrame(this.render.bind(this));
    this.controls.update();
    
    // 💡 必须同时渲染 WebGL 和 CSS3D 两个场景
    this.renderer.render(this.scene, this.camera);
    this.cssRenderer.render(this.cssScene, this.camera);
  }
}

闭坑指南:

  • 关于 zIndex 的博弈:

我们将 cssRenderer 的 zIndex 设为 2(放在模型上面, 模型的 zIndex 是 1,谁的 zIndex 大,谁就在上面)。所以我们需要我们的网页(用 cssRenderer 渲染)层级更高。

这样你才能直接在 3D 场景里点击网页上的按钮、滑动网页。如果 WebGL 渲染器在上面,网页就只能看不能摸了。

  • "穿模"与遮挡:

现在的实现方式有一个小缺陷:因为 cssRenderer 永远在 WebGL 模型之上,所以即便你把电脑转到背面,网页依然会"穿过"模型显示在最前面。

因为 cssRender 渲染的是 HTML 元素, 又因为 html 支持 css 中的 backfaceVisibility 属性可以隐藏背面不可见。

所以自然给 cssRender 元素增加 backfaceVisibility = true 即可解决穿模问题。

如何让网页严丝合缝地"贴"到模型屏幕上?

接下来就是最后一个问题了,如何把网页贴在电脑模型的屏幕上。介绍一种常见的微调方法。

我们引入调试利器:Tweakpane。

有类似功能的调试库有很多,如常见的有 dat.GUI,lil-gui 等等,我们使用的是 ai 建议的 Tweakpane 库,并且调试代码完全可以交给 ai 来写。

调试的界面如下:

这部分代码 ai 实现很容易,你可以用以下 prompt 生成对应代码即可,如下:

scss 复制代码
"我正在使用 Three.js 和 CSS3DObject。请帮我引入 Tweakpane 库,为我的 cssObject 创建一个调试面板。
需求:

1. 添加 position (x,y,z) 的调试滑块,范围设置在 -2000 到 2000。

2. 添加 rotation (x,y,z) 的调试滑块,范围是 -Math.PI 到 Math.PI。

3. 添加一个 'Export' 按钮,点击后能在控制台直接打印出当前的 position.set(x,y,z) 和 rotation.set(x,y,z) 代码,方便我复制固定死。请给我完整的 TypeScript 代码片段。"

通过上面的学习,我们发现 3D 网页开发不仅仅是写代码,更是一场关于"寻找最佳视角"的艺术。

最后,欢迎大家加群一起讨论全栈 AI 的实践,讨论酷炫动画的实现。我们下期再见!

相关推荐
xiaofeichaichai20 小时前
Webpack
前端·webpack·node.js
问心无愧051320 小时前
ctf show web入门111
android·前端·笔记
唐某人丶20 小时前
模型越来越强,我们还需要 Agent 工程吗?—— 从价值重估到 Harness 实践
前端·agent·ai编程
智码看视界21 小时前
现代Web开发基础:全栈工程师的起航点
前端·后端·c5全栈
JS菌21 小时前
手写一个 AI Agent 全栈项目:从沙箱执行到子智能体的完整实现
前端·人工智能·后端
不懂数据的小白21 小时前
面试题一:【二】异动分析(诊断)
面试
excel1 天前
HLS TS 文件损坏的元凶:Git 提交与拉取
前端
Aphasia3111 天前
https连接传输流程
前端·面试
徐小夕1 天前
万字长文!千万级文档 RAG 知识库系统落地实践
前端·算法·github