京程一灯宣传官网面试手册
# 京程一灯宣传官网面试手册
# 使用方法
面向 3D 前端岗位,先讲清 项目介绍,再掌握 技术图标粒子场、FFT 海洋与船体随浪运动、海天线与 DOM 坐标对齐 和 加载与运行性能。面试前用 重点问题速查 回顾,数学与图形基础按正文链接补充。
# 项目范围
官网包含五条页面路径、中英日三语、实时海洋、技术图标粒子场、交互地球与视觉动效。React 页面以 JavaScript / JSX 为主,海洋计算源码使用 TypeScript;首页实际加载的是 public/ocean-demo 中的预构建页面。FFT Ocean 来自 OceanDemo (opens new window),项目工作集中在站点集成、场景改造、交互、加载体验与性能控制。
工业三维场景扩展 补充模型查看、选择和测量等岗位问题的设计思路,供后续练习。
# 技术栈与项目分工
版本取自项目依赖声明,用于识别技术代际;主站与海洋运行时分开维护。
| 技术 | 项目职责 |
|---|---|
| React (opens new window) 19 | 五个页面、共享组件、语言状态、加载提示与交互控件 |
| TypeScript (opens new window) 5 | 海洋与着色器工具源码;项目允许 JS,未开启 strict |
| Vite (opens new window) | 主站构建、WGSL 源码导入与 Cesium 静态资源复制 |
| Babylon.js (opens new window) | 主站 9.x 实现粒子场;海洋使用独立的 5.x 运行时 |
| WebGL (opens new window) | 主站三维场景的图形渲染接口,由引擎封装使用 |
| WebGPU (opens new window) / WGSL (opens new window) | 海洋计算着色器与渲染;WGSL(WebGPU Shading Language)是 WebGPU 的着色语言 |
| Cesium (opens new window) | 椭球地球、测地线航线、相机与目的地交互 |
| GSAP (opens new window) / Motion (opens new window) | 滚动入场、文字及局部交互动效 |
| React PageFlip (opens new window) | AI 发展史翻页书 |
| Canvas 2D (opens new window)、CSS (opens new window) 与 SVG (opens new window) | Loader、动态纹理、折叠图片、玻璃与光照效果 |
| Playwright (opens new window) | 多语言、路由、移动端及异常路径浏览器测试 |
| Cloudflare Pages (opens new window) | 静态发布配置、SPA 路由回退与产物检查 |
# 第一部分 项目介绍与架构
# 项目介绍
这是京程一灯的品牌官网,展示课程、公开课和招生信息,支持中英日三语和多端访问。我负责前端开发与视觉模块集成:用 React 和 Vite 组织页面,使用 Babylon.js 实现技术图标粒子场,接入并改造开源 WebGPU 海洋,用 Cesium 展示全球航线。重点处理了场景初始化、交互控制、海天线与页面对齐、资源加载和兼容降级。
# 你主要负责什么?
建议回答:我负责页面与组件开发、三语状态、三维场景集成和交互,以及性能与浏览器测试。海洋底层复用开源 FFT Ocean,我主要做运行时隔离、加载就绪判断、静态预览、船模替换和构图调整。粒子场重点是图标分布、相机控制和屏幕尺寸适配,地球模块重点是航线生成、交互与渲染调度。
# 架构总览
React 页面与共享组件
├─ 页面导航、三语状态、加载提示与交互控件
├─ Babylon.js 9.x:技术图标粒子场
├─ Cesium:地球、航线与目的地切换
├─ GSAP / Motion / CSS / SVG:页面动效
└─ 同源 iframe:Babylon.js 5.x OceanDemo
├─ WebGPU 计算波面
├─ 海面几何与材质渲染
└─ 主站接入:相机、船模、就绪判断与海天线同步
Vite 构建
├─ 主站 JS、CSS、图片与字体
├─ 复制 Cesium 运行时资源
└─ 保留 public/ocean-demo 预构建资产
↓
dist 静态产物 → 发布检查 → 静态托管
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
React 管理界面状态和场景生命周期,三维引擎负责每帧更新与绘制。 场景中每个粒子的坐标不放进 React state;按钮的暂停状态、加载结果和选中目的地才需要驱动界面更新。
相关代码:src/Router.jsx、src/App.jsx、src/AdmissionsParticles.jsx、src/OceanScene.jsx、src/CareerGlobe.jsx。
# 页面结构
| 路径 | 主要内容 |
|---|---|
/ | 品牌首屏、海洋背景、课程价值与咨询入口 |
/curriculum | 技术图标粒子场、课程图谱与三语海报 |
/open-class | 公开课、翻页书、折叠图片与交互地球 |
/admissions | 招生信息、学习方向与报名咨询 |
/about | 品牌介绍、里程碑与卡片动效 |
# 第二部分 三维场景与交互实现
# Babylon.js、WebGL 与 WebGPU
Babylon.js (opens new window) 封装相机、网格、材质、灯光、资源加载和渲染循环。项目通过它操作三维对象,再由引擎把绘制任务提交给 WebGL 或 WebGPU。
| 对象 | 作用 |
|---|---|
| Engine | 连接 Canvas 与 GPU,管理渲染后端和帧循环 |
| Scene | 组织相机、网格、灯光、材质与场景更新 |
| Camera | 决定从哪里看、看向哪里,以及怎样投影 |
| Mesh | 保存几何及其位置、旋转、缩放,配合材质参与绘制 |
WebGL 主要提供图形绘制管线;WebGPU 同时提供渲染管线和原生计算管线。计算着色器可以并行处理波谱、位移等数据,不必先把任务包装成画三角形。项目粒子场使用 Babylon Engine,海洋使用 WebGPUEngine 和计算着色器。
相关代码:src/AdmissionsParticles.jsx、vendor/OceanDemo/src/index.ts。
为什么选择 Babylon.js,而不是 Three.js?
建议回答:项目需要接入已有的 Babylon WebGPU 海洋,继续使用 Babylon 可以复用其场景、材质和计算着色器能力,也减少集成成本。主站粒子场也使用同一套引擎概念。Three.js 同样能完成很多展示需求,但换引擎需要迁移和验证,当前没有足够收益。地球单独用 Cesium,是为了复用椭球坐标和测地线能力。
一个三维模型是怎样画到屏幕上的?
建议回答:CPU 准备顶点、索引、材质和绘制命令,GPU 的顶点着色器把顶点变换到裁剪空间。经过裁剪、透视除法和视口映射后,三角形被光栅化成片元,再由片元着色器计算颜色,并结合深度测试、混合等步骤写入目标缓冲区。Babylon 封装了这些流程;海洋额外在绘制前用计算着色器生成波面数据。
通用原理见 GPU 与渲染管线:WebGL 和 深度检测和深度缓冲区。
# 技术图标粒子场
# 图标怎样分布在三维空间中
图标使用斐波那契球面采样的思路:沿高度均匀分层,每层用黄金角错开方位,避免简单地等间隔排列经纬度时在两极挤成一团。之后分别缩放三个轴,形成横向展开的椭球分布。
下面是项目初始位置计算的独立示例。count 是图标数量,index 是从 0 开始的图标序号;返回值是场景中的初始坐标。
function getIconPosition(index, count) {
// 高度均匀分层,并避开两极端点。
const y = 1 - ((index + 0.5) / count) * 2;
const radius = Math.sqrt(1 - y * y);
const angle = index * Math.PI * (3 - Math.sqrt(5));
// 三轴缩放把单位球面拉成适合页面构图的椭球。
return {
x: Math.cos(angle) * radius * 10.6,
y: y * 6.1,
z: Math.sin(angle) * radius * 7.5,
};
}
console.log(getIconPosition(0, 60));
2
3
4
5
6
7
8
9
10
11
12
13
14
15
球面点是近似均匀分布;经过不等比拉伸后,不能再说椭球表面仍然严格等密度。
# 图标、拖拽和环绕动画分别怎样实现
每个图标使用一个容量为 1 的 ParticleSystem,SVG 图标先绘制到 DynamicTexture。每个图标旁还有三个小光点,背景另外有星点、尘埃和星云粒子系统。
拖拽由 ArcRotateCamera (opens new window) 接管:用户改变相机绕中心的角度,滚轮调整相机半径;代码关闭了平移并限制俯仰与缩放范围。它实现的是观察视角变化,没有把某个图标拖到新位置。
自动运动则在场景每帧回调中完成:图标围绕 Y 轴旋转,叠加轻微上下漂移;小光点沿相机的 right/up 方向绕图标运动,因此始终呈现朝向观察者的环绕效果。React 控件通过 ref 把速度、范围与暂停状态传给每帧回调。
相关代码:src/AdmissionsParticles.jsx 的初始坐标、onBeforeRenderObservable 和 settingsRef。
# 相机深度怎样限制图标大小
透视相机有近大远小的效果。项目保留这种层次,但限制靠近镜头的图标,避免一个图标突然占满画布。
先用相机到图标的向量与相机前向单位向量做点积,得到沿视线方向的深度,用于计算图标在屏幕上的大小。点积的几何含义见 向量的点乘。
在当前垂直视野角模式下:
每个 CSS 像素对应的世界长度 = 2 × 深度 × tan(垂直视野角 / 2) ÷ Canvas 的 CSS 高度。
图标世界尺寸 = min(基础尺寸,目标像素上限 × 每像素对应的世界长度)。
项目基础尺寸为 0.68;像素上限取桌面 110px / 窄屏 70px 与画布高度 14% 中的较小值。图标越靠近相机,允许的世界尺寸越小;远处仍按基础尺寸绘制,自然显得更小。这里的上限针对整个粒子面片,图标纹理自身还留有透明边距。
例如画布高 600 CSS px、垂直视野角 60°、深度 2、像素上限 70px,每像素约对应 0.00385 世界单位,图标尺寸会被限制到约 0.269,而不是继续使用 0.68。接近镜头时还会逐渐降低透明度,避免突然穿过画面。
相关代码:src/AdmissionsParticles.jsx 的 viewDepth、worldUnitsPerPixel、particle.size 与 nearFade。
如何让三维图标既有远近感,又不因缩放变得过大?
建议回答:我保留透视相机的近大远小,只限制最大屏幕尺寸。每帧先算图标沿视线的深度,再根据视野角和画布 CSS 高度,把像素上限换算成世界长度,与基础尺寸取较小值。这样远处图标仍然小,靠近时也不会无限放大;同时在近处做透明度渐隐。
用了粒子系统,就意味着图标只需要一次 Draw Call 吗?
建议回答:不能这样判断。当前每个图标都有独立纹理和粒子系统,光点也有独立系统,不能把全部图标当作一次绘制。数量继续增大时,我会先测绘制提交开销,再考虑纹理图集和批量绘制;如果改成共享几何、材质的网格,也可以评估实例化。透明重叠和排序同样需要检查。
# FFT 海洋与船体随浪运动
# 从波谱到海面画面
FFT(Fast Fourier Transform,快速傅里叶变换)是高效计算离散傅里叶变换的算法,IFFT(Inverse Fast Fourier Transform)用于计算逆变换。可以把海洋理解为许多方向、波长和振幅不同的波叠加:先在频域描述这些波如何变化,再通过二维 IFFT 求出空间中各处的位移。
噪声、风向、风速等参数
↓
初始波谱 → 随时间演化的频域数据
↓
计算着色器执行二维 IFFT
↓
位移、导数与泡沫相关纹理
├─ 顶点阶段:改变海面网格位置
├─ 材质阶段:计算法线、反射、折射与泡沫效果
└─ 位移数据读回:供浮力模块采样
2
3
4
5
6
7
8
9
10
计算着色器用 WGSL (opens new window) 编写,JavaScript / TypeScript 负责绑定资源并提交计算任务。工作组是一组一起调度的 GPU 调用;项目按纹理尺寸安排多组任务,让大量纹理位置并行计算。
二维 IFFT 先按行、再按列处理,N×N 数据的计算量约为 O(N² log N)。每个阶段把输出交给下一阶段,使用两张纹理交替读写,称为 ping-pong;计算结果继续供后续绘制使用,通常不需要整张传回 JavaScript。
相关代码:vendor/OceanDemo/src/scenes/ 下的 initialSpectrum.ts、wavesCascade.ts、fft.ts、wavesGenerator.ts;着色器位于 vendor/OceanDemo/src/assets/ocean/。首页运行入口是 public/ocean-demo/index.html,主站构建直接使用这份预构建页面。
为什么需要 ping-pong 纹理?
建议回答:每轮 FFT 计算都要读取上一轮结果。如果一边读取、一边覆盖同一张纹理,可能破坏其他并行任务还需要的数据。项目让输入和输出在两张纹理间交换,分阶段提交计算,既保留正确的数据依赖,也避免为每一轮重新分配纹理。
# 船体怎样随浪运动
上游浮力模块读取波面位移数据,通过多个采样点求高度和倾斜姿态。主站隐藏原浮标的可见网格,加载船模型并挂到新的父节点,逐帧复制浮标的位置,再叠加缓慢前进与水线高度修正。
旋转先组合浮标姿态与船头方向,再使用 四元数 的球面线性插值 Slerp,在基础朝向和随浪姿态之间取 0.62 的权重,减轻摇晃。这个系数是姿态混合权重,并非精确地把每个欧拉角都乘以 62%。
当前船有额外前进位移,却仍复用原浮标的采样结果。 它是用于背景展示的近似效果,不能保证船移动到任何位置后都与当地浪面严格贴合。若需要更准确的船体运动,应在船当前世界位置重新设置多个采样点,求出对应高度和姿态。
相关代码:src/OceanScene.jsx 的 replaceBuoyWithBoat;上游 vendor/OceanDemo/src/scenes/buoyancy.ts、wavesGenerator.ts。
为什么用四元数调整船体姿态?
建议回答:船同时有转向、左右摇摆和前后俯仰,直接对欧拉角做插值容易受到角度跨界和旋转顺序影响。单位四元数适合组合旋转和做平滑姿态插值。这里用 Slerp 混合基础朝向与浮标姿态,降低视觉晃动;它并没有加入真实船体的质量、惯性和受力计算。
海洋模块中,你具体改造了哪些部分?
建议回答:波谱和 FFT 计算复用开源 OceanDemo。我负责接入官网、隔离引擎版本、调整相机与光照、替换船模并复用浮力结果,还增加了海面就绪判断、静态预览、失败重试以及与 DOM 的海天线对齐。
# 海天线与 DOM 坐标对齐
海洋在 iframe 的 Canvas 里绘制,页面文字和暖色渐变在父页面的 DOM 中。如果渐变分界只写一个固定百分比,窗口比例或相机变化时就容易与海面脱节。
# 世界坐标怎样变成屏幕坐标
通用变换顺序如下:
局部坐标 → 模型变换 → 世界坐标 → 视图变换 → 相机观察空间
→ 投影变换 → 裁剪空间 → 透视除法 → 标准化设备坐标
→ 视口映射 → 屏幕坐标
2
3
模型矩阵放置物体;视图矩阵把世界转换到相机视角;投影矩阵生成裁剪坐标。透视除法把 x、y、z 分别除以 w,形成标准化设备坐标,再映射到视口。使用列向量记法时可以写成 clip = P × V × M × position;引擎 API 的矩阵乘法顺序要遵循自己的约定,不能直接照抄公式顺序。
项目取相机水平前向,在海平面上选一个约 10000 世界单位外的点,近似表示远处海天线。点已经在世界空间,因此模型变换使用单位矩阵;Vector3.Project 使用主相机的视图、投影矩阵和视口得到屏幕 y。
通用说明见 相机和视图矩阵、正投影和透视投影;完整坐标过程可参考 WebGL model view projection (opens new window)。
# 为什么转成百分比再传给 CSS
海天线位置百分比 = 投影后的 y ÷ 引擎渲染高度 × 100%。
例如内部渲染高度为 1200px、投影 y 为 348px,结果为 29%;父容器 CSS 高度是 600px 时,对应位置就是 174 CSS px。分子分母使用同一套渲染像素单位,比例不受 DPR 或内部渲染缩放直接影响。这个换算依赖 Canvas 与父层使用一致的显示区域;若存在裁切或额外偏移,还要补上对应变换。
项目将结果限制在 8%~80%,过滤 NaN、Infinity 和无效高度,只有与上次结果相差超过 0.05 个百分点时才更新 CSS 变量。这是范围限制和更新阈值,没有做逐帧插值式的平滑滤波。
相关代码:src/OceanScene.jsx 的 syncHorizonPosition、src/oceanHorizon.js。
为什么不用 Scene 当前保存的矩阵计算海天线?
建议回答:场景可能还进行反射等其他视角的渲染,当前矩阵未必属于首页主相机。我明确获取主相机的视图与投影矩阵,把海平面远点投影到屏幕,再转为百分比同步给 CSS。这样跟随的是实际展示镜头,而不是其他渲染过程留下的状态。
# Cesium 地球与动态航线
# 为什么选 Cesium
Cesium (opens new window) 提供 WGS84 椭球、经纬度转换、测地线、地球相机和资源管理。项目需要的是从北京到多个目的地的地理连线,使用这些能力比自行处理球面与地理坐标更直接。
# 航线怎样生成和运动
- 用
EllipsoidGeodesic计算北京到目的地的椭球测地线,得到沿地面的路径。 - 沿路线插值生成 193 个点,再加入高度:
55000 + sin(πt) × 弧线高度,其中t从 0 到 1。两端在距椭球面约 55km 的展示高度,中部隆起。 - 用
Cartesian3.fromRadians把经纬度和高度转为三维位置,绘制主体线、光晕、头部点和尾迹。 - 根据累计时间推进头部,在相邻采样点之间插值;尾迹保留最近一段点列。
- 点击国家按钮时高亮对应航线,并将相机飞向该路线附近;页面读数同步显示目的地。
测地线决定地面方向,高度函数决定展示弧度。它展示地区连接关系,不是现实飞机的飞行轨迹。当前目的地选择由 DOM 按钮触发;场景中的射线拾取用于检查地球表面是否可见,不能把它说成已实现点击模型部件。
相关代码:src/CareerGlobe.jsx 的 routes、controlsRef 和 render。
为什么不能直接对两个城市的三维坐标做直线插值?
建议回答:直接连两个地表点得到的是穿过地球内部的弦,不是沿地表的路线。项目先用椭球测地线插值经纬度,再叠加正弦高度形成可视弧线,最后转换为三维坐标。这样路线方向符合地理关系,弧线也不会从地球内部穿过。
地球怎样控制渲染成本?
建议回答:地球接近视口时才加载运行时和创建 Viewer,关闭默认渲染循环,由自己的 RAF 回调调度。离开观察区域或标签页隐藏时跳过更新和绘制;可见时用约 22.2ms 的间隔阈值限流,按实际时间差推进动画。这个写法不保证稳定 45 FPS,在 60Hz 屏幕上可能接近 30 FPS。持续航线动画会持续请求绘制,因此也不能把 requestRenderMode 理解成整个模块静止不渲染。
# 怎样判断地球可以显示
创建 Viewer 后先保留静态预览。代码等待至少三次 postRender、globe.tilesLoaded 为真,并从画布中心发射射线,确认命中地球表面,再显示交互地球。这些信号共同检查初始场景,不把 Viewer 创建成功当成画面已经完整。
相关代码:src/CareerGlobe.jsx 的 readyCleanup、IntersectionObserver 和 destroyViewer。
# 页面动效与键盘交互
页面动效按表现形式选择实现,不全部放进三维引擎:
| 效果 | 实现与交互重点 |
|---|---|
| 滚动入场 | GSAP / ScrollTrigger;布局变化后刷新位置,卸载时撤销 |
| 翻页书 | React PageFlip;页面状态与移动端操作 |
| 折叠图片 | CSS 3D 变换;点击和键盘触发,避免 hover 意外展开 |
| 玻璃与聚光 | SVG filter、CSS 与指针位置;控制模糊范围和移动幅度 |
| 文字动画 | GSAP / Motion 等;保证文字仍可读取,减少动态效果时降级 |
SiteMotion 在支持 hover 的精细指针设备上启用按钮磁吸;键盘焦点进入尚未出现的元素时,主动完成入场动画,避免焦点落在不可见控件上。prefers-reduced-motion 表示用户偏好减少动态效果,GSAP、地球和 Loader 各自有处理,但粒子场与海洋尚未形成统一的动态效果关闭策略。
相关代码:src/reactbits/SiteMotion.jsx、src/AIEvolutionBook.jsx、src/OpenClassPage.jsx、src/CoursePoster.jsx。
# 第三部分 React 架构与工程化
# React 与引擎生命周期
组件挂载后,在 Effect 中创建场景;需要跨渲染访问的实例和控制参数使用 ref;React state 保存加载、错误、选中项等界面状态。每帧坐标更新直接操作引擎对象。
清理责任按模块区分:粒子场释放系统、纹理、Scene 和 Engine;地球取消 RAF、断开 Observer 并销毁 Viewer;海洋外壳清理轮询、计时器和渲染观察者,iframe 随组件卸载。异步加载还要检查组件是否已销毁,避免加载完成后继续操作旧场景。
相关代码:src/AdmissionsParticles.jsx、src/CareerGlobe.jsx、src/OceanScene.jsx。
为什么不把三维场景的每帧数据放进 React state?
建议回答:粒子位置、相机矩阵等由引擎每帧使用,不需要通过 JSX 更新。每帧 setState 会把 React 渲染带入高频路径。我让引擎管理画面,用 ref 传递最新控制参数,只有加载结果、按钮状态和选中目的地变化时更新 React state。
动画回调为什么可能读到旧状态?
建议回答:只在初次挂载时创建的回调,可能捕获当时的 state。之后按钮更新了 state,旧回调仍读原来的值。粒子场用 settingsRef 保存最新速度、范围和暂停状态,每帧从 ref 读取;这样不会为了改一个参数重新创建整个场景。
为什么切换页面后还要显式释放三维资源?
建议回答:移除 Canvas 后,动画回调、事件监听和 GPU 资源仍可能存活。需要停止调度、解除监听,再释放对应纹理、场景和引擎。开发环境的 StrictMode 会额外执行 Effect 的设置与清理,正好能暴露重复场景和清理不对称的问题;验证时还应反复进出页面,观察资源是否持续增长。
# 页面导航与三语状态
路由只处理五条固定路径。文档级 click 监听接管合法站内导航,保留下载、新窗口、修饰键和外链的默认行为。pushState 修改 URL 后,项目主动派发 popstate 通知页面更新;浏览器前进后退则触发原生历史事件。旧 Hash 地址通过 replaceState 迁移。
语言按“URL 参数 → localStorage 偏好 → 中文默认值”初始化。useSyncExternalStore 订阅模块级语言状态,切换时同步 URL 和存储,站内链接继续携带 lang。localStorage 读写有异常保护,存储不可用时仍能切换语言。
海报、下载文件与页面 metadata 也随语言更新。中文句子作为翻译 Key 的代价是改文案会影响查找,后续可迁移到稳定语义 Key。
相关代码:src/Router.jsx、src/navigation.js、src/useLocale.js、src/i18n.js、src/CoursePoster.jsx。
为什么语言状态要同时考虑 URL 和 localStorage?
建议回答:URL 用于分享明确的页面语言,localStorage 用于记住设备偏好,所以首次访问优先听 URL。运行时由一个共享 Store 提供语言快照,页面和海报都订阅它。存储失败不会阻断页面,刷新、导航和历史返回则通过 URL 与状态同步维持一致性。
手写路由适合这个项目吗?
建议回答:当前只有五条固定路径,没有嵌套路由和数据加载流程,简单映射可以覆盖需求。不过要处理链接边界、刷新回退和历史导航。现有页面仍是静态 import,尚未路由拆包;如果增加动态参数、权限和复杂数据加载,我会使用成熟路由库,不继续扩展这套简单映射。
# Vite、TypeScript 与资源管线
主站由 Vite 编译 JSX 并输出静态资产。自定义插件把 .wgsl 转成源码字符串,供计算着色器使用;.exr、.hdr、.glb 作为资源 URL 交给加载器处理。Cesium 的主脚本、Workers、Assets、Widgets 和 ThirdParty 复制到发布目录,运行时通过 CESIUM_BASE_URL 定位。
源码导入与 public 资产是两条链路。 首页 iframe 加载预构建 OceanDemo,主站构建不会自动把 vendor 中修改的算法重新打入这个海洋文件。修改底层算法时,需要在对应海洋构建流程中重新生成并同步运行资产。
TypeScript 配置为 allowJs: true、strict: false,页面主要是 JSX,海洋源码主要是 TypeScript。Vite 构建主要完成转换与打包,完整类型检查需要单独执行;不能用“build 成功”代替类型验证。
相关代码:vite.config.js、tsconfig.json、src/CareerGlobe.jsx、public/ocean-demo/index.html。
为什么加载了 Cesium.js,还可能出现资源 404?
建议回答:Cesium 运行时还会加载 Worker、贴图、控件样式等资源。主脚本成功不代表这些相对路径正确。项目把运行时目录整体复制到 dist/cesium,并统一设置 CESIUM_BASE_URL;部署时需要同时检查入口脚本和子资源路径。
# 第四部分 加载、性能与兼容降级
# 海洋初始化与首帧就绪
检测 WebGPU 入口
├─ 不支持 → 解码静态预览 → 显示预览
└─ 支持 → 加载 iframe → 等待场景并检查计算能力
↓
配置主相机与材质
↓
当前活动海面网格完成绘制、场景与网格就绪
↓ 连续三帧满足
等待引擎帧结束
↓
显示 iframe,再经两次 RAF 通知 Loader
2
3
4
5
6
7
8
9
10
11
waitForOceanSurface 只统计海面材质对应的网格;只画天空、海面材质尚未编译好、没有活动海面时都不能通过。中间一帧不满足条件,连续帧计数归零。引擎帧结束是比 Scene 渲染回调更靠后的信号;之后的两次 requestAnimationFrame 给页面显示和合成留出机会,但不能作为 GPU 完成或像素已呈现的严格保证。
启动超过 30 秒时先显示静态预览,保留后台初始化;明确不支持或失败时回退,并在可重试时提供按钮。船模异步加载失败只记录错误,当前海面就绪条件不要求船一定加载完成。
相关代码:src/OceanScene.jsx、src/oceanReadiness.js、src/OceanLoader.jsx。
为什么 iframe 的 load 事件不能代表海洋已经显示?
建议回答:页面加载结束后,设备初始化、Shader 编译、模型和纹理准备仍可能继续。我判断的是海面本身:活动海面网格连续三帧完成绘制且资源就绪,再等引擎帧结束后显示 iframe。这样能避免只有天空时就撤掉 Loader。
为什么慢加载和初始化失败要分开处理?
建议回答:慢设备可能仍在正常编译 Shader,超时就销毁会浪费已完成的工作。项目在 30 秒时先显示预览,后台继续准备,成功后切换实时海洋;能力缺失或明确错误才进入回退。后续还可以增加主动关闭实时场景和统一处理运行时设备丢失的能力。
# 加载与运行性能
# 当前各模块实际做了什么
| 模块 | 已有措施 | 仍需注意 |
|---|---|---|
| 海洋 | 静态预览、失败重试;预构建页面降低内部渲染分辨率 | 没有由主站统一控制的离屏暂停;计算和持续渲染仍有开销 |
| 粒子场 | 图标尺寸限制、近处渐隐、窄屏相机调整、卸载释放 | 暂停按钮只停止时间推进;固定粒子数量,未按移动端减少图标数 |
| Cesium | 进入观察区域再初始化、隐藏时跳过更新与绘制、限流、销毁 | 动态航线仍持续请求帧;RAF 调度回调本身仍存在 |
| Loader | Canvas 使用 DPR 1,初始化或 resize 时安排绘制 | Loader 展示时间和背景场景就绪是两件事 |
| 主站资源 | 本地模型与纹理、Cesium 延迟加载 | 页面仍静态 import;保留两套引擎和海洋资产,需检查主包和资源重复 |
相关代码:public/ocean-demo/index.html、src/AdmissionsParticles.jsx、src/CareerGlobe.jsx、src/OceanLoader.jsx、src/Router.jsx。
# 怎样定位卡顿,再决定优化
先固定设备、窗口大小、场景和操作路径,分别观察加载和交互阶段。逐项改变一个因素,避免同时降低全部效果后无法判断根因。
| 方向 | 观察与验证方法 |
|---|---|
| 网络与解码 | 检查资源瀑布、下载大小和模型/图片解码耗时,区分首次与缓存访问 |
| CPU | 用 Performance 查看长任务、每帧 JS、布局与对象分配;减少对象更新后对比帧时间 |
| GPU | 在可用时查看 GPU 时间,分别降低分辨率、阴影、材质和透明效果,观察敏感项 |
| 绘制提交 | 查看 Draw Call、材质切换与对象数量;共享材质、图集或实例化后比较 |
| 资源增长 | 重复进入、离开页面,检查场景/纹理数量和内存趋势,不只看一次 FPS |
Draw Call 是一次绘制提交。大量小对象可能增加 CPU 提交成本;大面积透明叠加和复杂着色器则可能主要消耗 GPU。实例化减少重复几何的提交,不会自动消除像素填充和透明混合成本。
Canvas 内部像素数约为 CSS 宽度 × CSS 高度 × DPR²。 DPR 从 1 增至 2 会带来约四倍像素,但总帧耗时不一定四倍,取决于瓶颈。纹理也要区分下载与显存:未压缩 RGBA8 的 2048×2048 纹理基础层约占 16 MiB,完整 mipmap 通常还增加约三分之一;压缩下载文件不代表 GPU 中仍占相同大小。
三维页面掉帧,你会怎么排查?
建议回答:先看是资源没加载完,还是交互中持续掉帧。运行时同时看主线程、绘制提交和 GPU 成本:降分辨率明显改善,优先查像素填充与后处理;减少对象数明显改善,继续查每帧 JS 和 Draw Call。每次只调整一个因素,在相同设备与操作下比较帧时间分布,再确定优化方向。
暂停动画,就能停止 GPU 消耗吗?
建议回答:要看暂停的是时间推进还是渲染循环。当前粒子场按钮只冻结动画时间,场景仍然每帧 render。若要降低负载,需要在不可见时跳过更新和绘制,静止时按交互或状态变化请求重绘;恢复时重置计时基准,避免出现很大的时间差。地球已有可见性控制,但还没有统一到所有三维模块。
# 下一步优化顺序
先建立主包、实时场景就绪时间、交互帧时间和重复进出页面的资源基线;再按证据处理最大的瓶颈:
- 用动态 import 做页面与重模块拆包,避免未访问页面的代码占用首屏。
- 统一场景的启动、暂停、恢复与销毁,补齐粒子场和海洋的可见性控制。
- 检查重复资产;按视觉质量验证模型简化、纹理压缩与设备分档。
- 给能力检测、初始化、失败回退与运行时错误增加阶段化记录。
# 响应式与无障碍
响应式除了改列数,还要调整相机距离、标题折行、触摸目标、安全区和短横屏布局。代码中的 CSS 断点与场景内部相机参数需要一起检查,不能只在桌面缩窄窗口后看截图。
保留真实 button、link 和 dialog 语义;Canvas 配可读标签,目的地用 DOM 按钮操作。键盘焦点应可见、弹窗可退出。减少动态效果时,应停掉不必要的自动运动而保留信息和操作;现有模块处理程度不同,尚需统一覆盖。
相关代码:src/Responsive.css、src/reactbits/SiteMotion.jsx、src/CoursePoster.jsx、src/CareerGlobe.jsx。
# 第五部分 测试与静态部署
# 测试覆盖与验证方法
| 层次 | 当前覆盖 |
|---|---|
| 逻辑测试 | Node test 检查海天线非法值与边界、连续海面帧、材质未就绪和卸载取消 |
| 页面回归 | Playwright:五路径 × 三语言 × 两宽度共 30 个用例,另有四个交互/异常用例 |
| 海洋专项 | 独立浏览器脚本检查加载和 WebGPU 场景,不属于上面的 34 个页面用例 |
| 视觉与设备 | 响应式及视觉 QA;真实 GPU、浏览器与触摸体验仍需对应设备验证 |
| 发布产物 | 检查 dist 文件数量、单文件限制、海洋与 Cesium 等关键资产 |
页面回归检查语言、标题、溢出、导航、刷新、历史返回、海报和存储异常。配置使用 Chrome channel,并设为减少动态效果,因此通过这组用例不能证明实时海洋在所有设备都正常。
海洋首帧的逻辑测试用替身模拟场景事件,能验证判断流程;真实 Shader、GPU 提交和屏幕效果仍由浏览器专项及设备检查验证。
相关代码:tests/oceanHorizon.test.mjs、tests/oceanReadiness.test.mjs、tests/browser/i18n.spec.js、tests/ocean-loading.browser.mjs、tests/ocean-webgpu.browser.mjs、playwright.config.js。
怎样测试一个三维展示模块?
建议回答:把可独立计算的逻辑与真实渲染分开。海天线边界、就绪帧计数和清理用逻辑测试;加载、重试、导航和控件用浏览器测试;真实画面、GPU 兼容与性能在固定设备和视口下检查。还要反复挂载卸载,检查旧回调和资源是否残留,不能只断言 Canvas 存在。
# Cloudflare Pages 与发布检查
主站输出 dist 静态文件。public/_redirects 中的 /* /index.html 200 让页面路径回到 SPA 入口,解决直接访问或刷新子页面的问题。scripts/check-pages.mjs 检查项目设置的 20000 文件和 25 MiB 单文件阈值,以及关键资产完整性;平台当前配额另见 Limits (opens new window)。
发布前重新构建并检查输出,发布后验证五条路径、三语、下载和三维子资源。
相关代码:vite.config.js、public/_redirects、scripts/check-pages.mjs、DEPLOYMENT.md。
静态站升级后,怎样避免旧资源缓存导致页面异常?
建议回答:带内容 Hash 的资产适合长缓存,入口 HTML 要能及时获取新版本;public 中固定文件名的海洋页面和依赖,需要版本化或合适的重新验证策略。发布后除了首页,还要检查 iframe、Cesium Worker 和纹理路径,避免入口已更新、子资源仍是旧版本。
# 第六部分 技术难点与解决方案
# 难点一 异步初始化与首帧就绪
问题:iframe 加载或场景创建成功时,海面可能仍在编译,只显示天空。
处理:分开判断能力、场景和海面绘制,连续三帧满足条件后等待引擎帧结束,再显示场景;慢加载先显示预览,明确失败提供回退与重试。
验证:逻辑测试覆盖仅天空、材质未就绪、计数中断和卸载取消;实际可见效果还要检查真实 GPU。实现见 海洋初始化与首帧就绪。
# 难点二 三维场景与页面坐标对齐
问题:固定百分比无法适应所有相机和窗口比例,反射渲染留下的矩阵也可能干扰定位。
处理:使用主相机矩阵投影海平面远点,转为比例写入 CSS;过滤无效值并设置更新阈值。
验证:检查多种宽高比、初始无效投影和恢复后的 CSS 位置。实现见 海天线与 DOM 坐标对齐。
# 难点三 多场景的加载与运行成本
问题:海洋计算、粒子场和地球分别消耗网络、主线程和 GPU,优化首屏与降低持续负载需要不同措施。
处理:地球接近视口才启动,隐藏时跳过更新和绘制;海洋保留预览并降低内部渲染分辨率;场景卸载时释放资源。
验证:分别比较首次加载、持续交互和重复进入页面,不能用“加载更快”代替帧率或内存结果。现有粒子暂停与海洋离屏控制仍有改进空间,见 加载与运行性能。
# 难点四 不同版本引擎的集成
问题:旧海洋基于 Babylon 5.x,主站粒子使用 9.x,直接迁移需要重新验证计算、材质和资源加载。
处理:保留同源 iframe,让它有独立 Window、全局对象和运行时。父页面通过 contentWindow.scene 配置场景并监听状态,降低一次性升级范围。
代价:两套依赖增加体积,直接访问内部对象依赖同源和对象名称;iframe 也不保证拥有独立线程或独立 GPU。后续可先用消息协议封装状态和命令,再评估统一版本,不把隔离本身当作性能优化。
相关代码:src/OceanScene.jsx、public/ocean-demo/index.html、vendor/OceanDemo/package.json。
# 项目复盘
如果继续做,我会先统一三维模块的启动、暂停、恢复和销毁约定,补齐资源与性能基线;然后拆分重模块、整理重复资产,再根据实际收益决定是否统一海洋引擎版本。国际化和视觉调整继续留在各自组件与内容模块,避免每次页面改动都影响底层渲染。
# 第七部分 重点问题速查
先掌握简历中已经出现的实现,再准备岗位扩展。
| 主题 | 问题与答案 |
|---|---|
| 项目职责 | 你主要负责什么? |
| 引擎选型 | 为什么选择 Babylon.js,而不是 Three.js? |
| 渲染基础 | 一个三维模型是怎样画到屏幕上的? |
| 球面分布 | 图标怎样分布在三维空间中 |
| 图标尺寸 | 如何让三维图标既有远近感,又不因缩放变得过大? |
| 粒子性能 | 用了粒子系统,就意味着图标只需要一次 Draw Call 吗? |
| 海洋计算 | 从波谱到海面画面 |
| 船体姿态 | 为什么用四元数调整船体姿态? |
| 坐标对齐 | 海天线与 DOM 坐标对齐 |
| 地球航线 | 航线怎样生成和运动 |
| 加载就绪 | 为什么 iframe 的 load 事件不能代表海洋已经显示? |
| 场景清理 | 为什么切换页面后还要显式释放三维资源? |
| 性能排查 | 三维页面掉帧,你会怎么排查? |
| 暂停策略 | 暂停动画,就能停止 GPU 消耗吗? |
| 质量验证 | 怎样测试一个三维展示模块? |
| 工业场景 | 工业三维场景扩展 |
# 附录 术语、扩展与代码索引
# 技术术语速查
| 术语 | 含义与阅读入口 |
|---|---|
| Mesh | 网格:顶点、索引等组成的可绘制几何;见 Babylon.js、WebGL 与 WebGPU |
| Shader | GPU 执行的程序,负责顶点、片元或计算任务;见 一个三维模型是怎样画到屏幕上的? |
| FFT / IFFT | 快速傅里叶变换及逆变换;见 从波谱到海面画面 |
| FOV | Field of View,视野角,影响透视投影范围;见 相机深度怎样限制图标大小 |
| DPR | Device Pixel Ratio,设备像素比;内部像素密度影响清晰度与绘制成本,见 加载与运行性能 |
| Slerp | 球面线性插值,用于混合单位四元数姿态;见 船体怎样随浪运动 |
| Draw Call | 一次绘制提交;见 用了粒子系统,就意味着图标只需要一次 Draw Call 吗? |
| PBR | Physically Based Rendering,基于物理的材质模型,用粗糙度、金属度等参数描述光与表面作用 |
| GLB | glTF 的二进制封装,常用于传输模型、材质和动画;浏览器渲染网格不自动具备 CAD 精确曲面语义 |
| RAF | requestAnimationFrame,浏览器动画帧回调;见 地球怎样控制渲染成本? |
| LOD | Level of Detail,细节层次,按距离或屏幕占比使用不同复杂度的模型 |
| BVH | Bounding Volume Hierarchy,包围体层次结构,用层级包围盒缩小射线等查询的候选范围 |
# 工业三维场景扩展
这一节是面向工业软件岗位的设计准备。官网已有的相机、坐标转换、交互状态和性能经验可以复用,但部件编辑、测量和剖切需要另外实现。
怎样实现点击模型部件并高亮?
建议回答:先把指针位置换成 Canvas 内的相对坐标,再按引擎拾取接口的坐标约定,从相机生成射线,找到最近的有效命中。网格关联稳定的业务部件 ID,选中 ID 同时驱动高亮和结构树;取消时恢复材质或移除高亮效果。复杂模型先用包围盒或 BVH 缩小候选,再做精细相交测试,避免每次遍历所有三角形。
怎样实现两点距离测量和拖拽?
建议回答:测量时把两个命中点统一到世界坐标,计算距离,再按模型约定换算毫米或米;若要保存到部件上,需要转换回对应局部坐标。拖拽不能简单把鼠标移动像素加到三维坐标,要先确定拖拽平面或轴约束,用射线与平面求交,再把结果转换到对象父节点的局部空间。工业测量还需要确认单位、精度和几何数据来源。
工业模型对象很多时,怎样保持交互流畅?
建议回答:先确定瓶颈。重复零件可以共享几何和材质、使用实例化;看不到的对象做视锥裁剪,远处用 LOD;纹理和模型按需加载,并控制显存。拾取用空间索引缩小候选,业务属性按稳定 ID 保存,避免与渲染对象强绑定。合并网格时要考虑部件选择和编辑,不能为了减少 Draw Call 丢掉业务结构。
剖切模型只是把一半隐藏掉吗?
建议回答:简单展示可以用裁剪平面,在着色阶段丢弃平面一侧的片元,但这不改变原始模型,也不会自动生成封闭截面。若要显示切口、测量截面积或导出几何,还需要封口、相交轮廓或更精确的几何计算。要先确认需求是视觉查看,还是带工程精度的建模操作。
可用 Babylon.js 做一个独立练习:加载有层级的 GLB → 点击高亮并联动结构树 → 隐藏/聚焦部件 → 两点测量 → 记录性能与卸载资源。它用于验证学习成果,不计入官网既有功能。
# 相关代码索引
以下路径相对 yd-website 项目根目录。
| 主题 | 主要文件 |
|---|---|
| 导航与语言 | src/Router.jsx、src/navigation.js、src/useLocale.js、src/i18n.js |
| 粒子与相机 | src/AdmissionsParticles.jsx |
| 海洋接入 | src/OceanScene.jsx、public/ocean-demo/index.html |
| 海面就绪 | src/oceanReadiness.js、tests/oceanReadiness.test.mjs |
| 海天线 | src/oceanHorizon.js、tests/oceanHorizon.test.mjs |
| 波谱与 FFT | vendor/OceanDemo/src/scenes/initialSpectrum.ts、wavesCascade.ts、fft.ts |
| 浮力采样 | vendor/OceanDemo/src/scenes/buoyancy.ts、wavesGenerator.ts |
| 地球航线 | src/CareerGlobe.jsx |
| 加载画面 | src/OceanLoader.jsx |
| 页面动效 | src/reactbits/SiteMotion.jsx、src/AIEvolutionBook.jsx、src/OpenClassPage.jsx |
| 三语海报 | src/CoursePoster.jsx、src/content/messages.json |
| 构建与发布 | vite.config.js、scripts/check-pages.mjs、public/_redirects |
| 浏览器测试 | tests/browser/i18n.spec.js、tests/ocean-loading.browser.mjs、tests/ocean-webgpu.browser.mjs |