01|先看结果:为什么“模型更多”不等于“更真实”
这间咖啡店最初由立方体、圆柱体和球体拼成。它能漫游,但像积木;后来精修近景模型、加入 PBR 材质,并用 Blender Cycles 烘焙光照和 AO,才开始像一个有空间氛围的室内场景。
同一场景、同一漫游入口的前后交付截图。变化不只来自模型,还来自材质响应、接触阴影、间接光、曝光和构图。
核心观点:真实感的优先级通常是 光影与遮挡 → 材质响应 → 轮廓细节 → 后期效果。先堆高面数,往往只是得到“更精致的积木”。
02|基础 3D 模型:你看到的物体是怎样组成的
一个可渲染的 3D 物体,不只是一团“形状”。最少要理解下面这些概念:
顶点、边、面与三角形
顶点定义空间位置,边连接顶点,面围出表面。GPU 最终主要绘制三角形;四边面、多边形也会在导出或渲染时三角化。
法线 Normal
描述表面朝向,用来计算光线与表面的夹角。硬边、平滑法线和倒角会显著改变物体的观感。
UV 坐标
把二维图片贴到三维表面的坐标。可以把 UV 想成“包装盒展开图”:模型负责立体形状,UV 负责图片贴在哪里。
材质 Material
描述表面怎样响应光:颜色、粗糙度、金属度、透明度等。材质不是模型本身,也不是一张颜色图就结束。
变换 Transform
位置、旋转、缩放决定物体在空间中的状态。单位、坐标轴和原点不规范,会在导出后变成“模型飞了”。
场景 Scene
模型只是演员,还需要相机、灯光、环境和渲染器。没有相机,就没有可见画面;没有光,材质也无从表现。
倒角为什么比“多加几千条边”更值得
现实里的桌板、金属机身和柜门几乎没有绝对锋利的边。一个小倒角能产生高光边,让物体立刻更像制造出来的东西。倒角改善轮廓与高光,法线贴图改善细小凹凸,两者不能互相替代。
常见误区:法线贴图不会改变真实轮廓,也不会让杯子真正长出杯口厚度。近景轮廓交给模型,木纹、织物和细微凹凸交给贴图。
03|WebGL 在做什么:从数据到屏幕像素
WebGL 是浏览器提供的图形接口,让 JavaScript 能调用 GPU 绘制 2D/3D 图形。它本身不是建模软件,也不会自动提供灯光、材质或碰撞系统。
写入缓冲区
变换空间位置
三角形变像素
计算颜色
显示到 Canvas
顶点着色器:决定“画在哪里”
初版代码使用模型、视图、投影三个矩阵,把局部坐标一步步变换到屏幕:
vec4 world = uModel * vec4(aPosition, 1.0);
gl_Position = uProjection * uView * world;- 模型矩阵 Model:物体自身的位置、旋转、缩放。
- 视图矩阵 View:相机从哪里、朝哪里看。
- 投影矩阵 Projection:透视、视野角、近远裁剪面。
片元着色器:决定“这个像素是什么颜色”
最基础的漫反射可以写成:表面越朝向光源越亮,背光面越暗。
float diffuse = max(dot(normalize(vNormal), lightDir), 0.0);
vec3 color = uBaseColor * (ambient + diffuse);初版还叠加了点光、窗光近似和距离雾,但没有真实遮挡。因此,一个面即使被柜台挡住,仍可能按同一套公式得到亮度。
那 Three.js 又是什么
Three.js 是建立在 WebGL 之上的 3D 引擎。它把场景、相机、网格、材质、灯光、加载器和渲染器组织起来,减少手写缓冲区与着色器管理的成本。它不是 WebGL 的替代品,而是更高层的封装。
本项目的选择:初版用手写 WebGL 学习底层;进阶版用 Three.js 承载 glTF、PBR、烘焙贴图和色调映射。漫游、碰撞和界面逻辑则继续复用。
04|真实感从哪里来:模型、材质、烘焙各管一件事
| 组成 | 主要负责 | 典型内容 | 不能替代什么 |
|---|---|---|---|
| 模型 | 轮廓和结构 | 杯口厚度、椅背、倒角、按钮 | 不能自动产生真实光照 |
| 材质 | 表面如何响应光 | 颜色、粗糙度、金属度、法线 | 不能改变大轮廓 |
| 烘焙 | 预先计算静态结果 | AO、间接光、静态阴影 | 不适合频繁变化的光和物体 |
| 实时光 | 处理动态变化 | 移动物体、可切换灯光、实时阴影 | 需要持续消耗 GPU |
PBR 材质里最常用的几张图
- Base Color:表面本身的颜色,不应该把明显光影重复画进去。
- Roughness:粗糙度。陶瓷、金属、木材和布料的高光差异,很大程度来自它。
- Normal:用切线空间法线描述细小凹凸,让表面在光下产生细节。
- AO:环境遮蔽,表现缝隙、接触面和角落不容易被环境光照到的程度。
- Light Map:预计算的光照结果,可包含静态直接与间接光。
AO、光照贴图、实时阴影不要混为一谈
实时阴影则是渲染时根据当前灯和物体重新计算。静态咖啡店适合把大量结果烘焙;如果灯要开关、桌椅要拖动,就需要实时光影补充。
相机会移动,不代表不能烘焙。只要物体和灯光保持固定,烘焙结果依然有效。第一人称在店内走动,只是换相机观察已经算好的光。
05|ABCD 递进关系:每一档到底在解决什么
这不是“越往后越正确”,而是从可运行 Demo 到商业展示的投入递增。B 与 C 也不是互相排斥:最实用的路线往往是 B 的静态基础 + C 的少量动态能力。
轻量润色:先摆脱积木感
做什么:保留手写 WebGL,加倒角、简单纹理、接触阴影,调整色温和雾。
解决:低成本提升观感。边界:光影仍是近似,材质系统有限。
模型 + PBR + 烘焙混合 本项目已实施
做什么:Blender 精修近景,Three.js 加载 glTF,使用法线与粗糙度,Cycles 烘焙静态光照和 AO。
解决:固定室内漫游中,真实感与性能最平衡。边界:需要 UV、资产制作和烘焙流程。
实时光影:让动态变化也成立
做什么:动态软阴影、环境反射、SSAO、更多实时灯;可与 B 的烘焙叠加。
解决:灯光切换、物体移动、时间变化。边界:填充率、阴影和后期会增加手机压力。
展示级精修:画质与交付体系一起升级
做什么:完整室内资产、扫描材质、反射探针、烘焙与实时阴影结合、桌面/移动双档画质。
解决:商业空间展示与长期维护。边界:制作、压缩、加载和测试成本都明显增加。
06|B 方案实战:这间咖啡店具体怎样落地
1. 只精修视线附近的东西
吧台、咖啡机、桌椅、杯子和沙发是玩家会靠近观察的对象,因此有独立结构、杯口厚度、把手、圆角软包和金属倒角。墙体、天花和室外背景保持简单。
2. 材质使用法线与粗糙度,不盲目提高面数
木材、织物、石材和墙面使用本地生成的颜色与法线贴图,再分别设置粗糙度。金属咖啡机依赖环境反射,陶瓷杯子使用较低粗糙度,沙发则保留较高粗糙度。
3. Blender 负责静态空间与 Cycles 烘焙
场景合并为静态网格,保留材质分组;第一套 UV 用于颜色和法线,第二套 UV 用于光照与 AO。最终导出 glTF/GLB,H5 加载后把烘焙图绑定到第二套 UV。
// 本项目:glTF 的第二套 UV 对应 Three.js 的 channel 1
lightMap.flipY = false;
lightMap.channel = 1;
aoMap.flipY = false;
aoMap.channel = 1;
material.lightMap = lightMap;
material.aoMap = aoMap;这里的 flipY = false 是配合本项目 glTF UV 的处理,不是任何图片都必须照抄的规则。
4. 实时光只做补充,避免重复照明
如果烘焙已经包含很亮的阳光,再把同强度实时太阳叠上去,就会过曝。进阶版降低实时平行光、窗光和吊灯强度,并使用 ACES 色调映射控制高光。
5. 漫游与渲染解耦
观景点、键盘、鼠标、摇杆和碰撞继续使用原来的逻辑。更换渲染器不等于重做产品交互,这也是 A → B 能复用成果的关键。
07|HTML 怎样加载模型与资源,并把交互接起来
一个 3D 页面不是“把 GLB 放进场景”就结束。HTML 负责画布与状态层,JavaScript 负责创建渲染器、取得资源、组装场景,最后才开放漫游。加载失败、资源尚未就绪和首帧尚未出现时,都应该有明确界面。
Canvas + 加载层
GLB + 贴图
网格 / 材质 / 图片
纹理与着色器
开放交互
1. HTML 先提供能独立显示的加载层
加载页不依赖 3D 初始化也应可见。画布、加载状态、错误信息和重试按钮放在同一容器;漫游按钮先禁用,避免用户进入半初始化场景。
<main id="app">
<canvas id="scene" aria-label="3D 咖啡店"></canvas>
<section id="loading" role="status" aria-live="polite">
<p id="loadingText">正在准备咖啡店…</p>
<progress id="progress" max="100" value="0"></progress>
</section>
<section id="error" hidden role="alert">
<p id="errorText">场景暂时无法打开</p>
<button id="retry">重新加载</button>
</section>
<button id="enter" disabled>进入咖啡店</button>
</main>状态文字用 aria-live 让辅助技术能读到变化;错误层用 role="alert"。重试按钮应是真正的 <button>,不能只画一个可点击的 <div>。
2. 用统一加载管理器串起模型与贴图
下面是正式拆资源版本的简化骨架。它表达关键顺序,不是要求原样替换当前单文件项目:GLB 用 GLTFLoader,独立烘焙图用 TextureLoader,两者共用 LoadingManager;全部就绪后组装场景、编译并绘制首帧。
const ui = {
loading: document.querySelector('#loading'),
text: document.querySelector('#loadingText'),
bar: document.querySelector('#progress'),
error: document.querySelector('#error'),
enter: document.querySelector('#enter')
};
const manager = new THREE.LoadingManager();
manager.onProgress = (url, loaded, total) => {
ui.bar.value = loaded / total * 100; // 按资源数,不是字节进度
ui.text.textContent = `已准备 ${loaded} / ${total} 项资源`;
};
async function boot() {
try {
const renderer = new THREE.WebGLRenderer({
canvas: document.querySelector('#scene'), antialias: true
});
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(66, 1, .05, 50);
const loader = new GLTFLoader(manager);
const textures = new THREE.TextureLoader(manager);
ui.text.textContent = '正在下载模型与光照…';
const [gltf, lightMap, aoMap] = await Promise.all([
loader.loadAsync('./assets/cafe.glb'),
textures.loadAsync('./assets/lightmap.webp'),
textures.loadAsync('./assets/ao.webp')
]);
ui.text.textContent = '正在组装材质与光照…';
for (const texture of [lightMap, aoMap]) {
texture.flipY = false;
texture.channel = 1;
}
scene.add(gltf.scene);
// 在这里把 lightMap、aoMap 绑定到对应材质。
ui.text.textContent = '正在准备第一帧…';
await renderer.compileAsync(scene, camera);
renderer.render(scene, camera); // 真首帧,不是假进度
ui.loading.hidden = true;
ui.enter.disabled = false; // 此时才允许进入
} catch (error) {
console.error(error);
ui.loading.hidden = true;
ui.error.hidden = false;
}
}
document.querySelector('#retry').addEventListener('click', () => location.reload());
boot();示例有意省略了尺寸处理和材质绑定细节,用来突出加载状态机。实际项目还应处理相机机位、窗口尺寸、第二套 UV、纹理色彩空间,这里的小场景用整页刷新重试,避免遗留旧渲染器和动画循环。
3. “下载完成”不等于“可以交互”
| 阶段 | 界面应显示什么 | 常见问题 |
|---|---|---|
| 网络下载 | 资源数量或真实字节进度 | 弱网、404、跨域、服务器未返回 Content-Length |
| 解析与解码 | “正在解析模型 / 解码纹理” | 主线程阻塞、Draco 解码耗时、图片解码峰值内存 |
| 场景组装 | “正在布置空间” | 材质尚未绑定、UV / 色彩空间错误 |
| GPU 准备 | “正在准备第一帧” | 首次纹理上传、着色器编译导致长帧 |
| 可交互 | 隐藏加载层并启用按钮 | 只等 Promise,未确认首帧就提前放行 |
LoadingManager 的默认进度是已加载资源个数,不能宣称是精确下载百分比。若需要字节进度,应自行 fetch() 读取响应流,并按各文件字节数加权;服务器没有 Content-Length 时,就改用阶段提示,不要伪造 99%。
4. 失败、重试与后台切换也是加载交互的一部分
- 失败要可理解:区分浏览器不支持 WebGL、网络失败、模型解析失败和纹理失败;控制台保留原始错误,界面给可行动提示。
- 重试要可恢复:小场景可整页刷新重试;复杂场景要取消旧请求、释放旧 GPU 资源,防止重复场景和重复动画循环。
- 页面隐藏时暂停:监听
visibilitychange,后台停止漫游帧循环,回前台重置上一帧时间,避免突然位移。 - 处理上下文丢失:监听画布的
webglcontextlost,显示“图形资源正在恢复”,恢复后重新确认首帧,必要时提供重新进入。 - 不要反复自动重试:弱网下无限重试只会增加流量与发热;让用户决定重试,并提供轻量模式或静态封面。
当前项目的真实加载方式:交付版把 GLB、Light Map 和 AO 以 Base64 内嵌进 HTML,再用 atob()、GLTFLoader.parse() 和 TextureLoader 读取。因此它没有运行时外部资源请求,也没有真实下载进度;当前全屏加载层只表示“正在初始化”,不是联网下载百分比。
08|手机端加载页面:优化方向与可执行细节
手机加载体验的目标不是“转圈更漂亮”,而是尽快出现可信反馈、降低首屏成本、避免假死,并在性能不足时仍能离开或降级。需要同时优化网络、CPU 解码、GPU 首帧和界面反馈。
1. 先让加载页画出来,再做重活
如果脚本一执行就同步解析大模型、逐像素生成法线,浏览器可能来不及绘制加载文字。应让 HTML 和 CSS 先完成首屏,再在下一帧启动重型初始化;长任务按阶段拆分,在每阶段更新提示。
// 给浏览器一次实际绘制加载页的机会,而不是固定猜一个延时。
requestAnimationFrame(() => requestAnimationFrame(boot));
// 每个重阶段结束后更新文字;必要时让出主线程再继续。
ui.text.textContent = '正在生成材质…';
await new Promise(resolve => setTimeout(resolve, 0));
prepareMaterials();让出主线程只能改善阶段之间的反馈,不能把一次几百毫秒的同步循环变成流畅任务。真正昂贵的法线生成、几何压缩和纹理转换,优先在制作阶段离线完成。
2. 当前咖啡店最值得改的不是颜色,而是初始化顺序
现有版本启动时会生成多组 Canvas 纹理,再通过 CPU 计算 normalFromTexture(),随后复制 Base64、解析 GLB、生成 PMREM 环境并编译材质。这些都可能发生在第一次可交互画面之前。可优先做:
- 把法线贴图离线生成:不要在每台手机启动时重复计算同一份像素;随资源直接交付 PNG/WebP,正式版进一步考虑 KTX2。
- 首屏只准备入口需要的资源:先显示房间、入口家具和低分辨率烘焙图,沙发或吧台近景细节在进入后再补。
- 先低清首帧,再渐进增强:限制初始 DPR、先关闭高成本后期,首帧稳定后再在目标设备允许时提升。
- 避免同一资源多份常驻副本:解析完成后释放不再需要的 Base64、临时数组和图像引用;纹理与几何销毁时调用对应的
dispose()。
3. 单文件适合交付,发布版要换一种资源策略
| 策略 | 当前离线单文件 | 正式上线建议 |
|---|---|---|
| HTML | 引擎、GLB、贴图全部内嵌 | 保持轻量,让加载页和静态封面先到达 |
| GLB | Base64 后解码,增加体积与临时内存 | 独立文件,使用 Brotli/Gzip、长期缓存和版本号 |
| 纹理 | 随 HTML 一次取得,之后解码 | 合理尺寸,按区域/机位分批;评估 KTX2 GPU 压缩 |
| 再次访问 | 整个 HTML 重新读取,资源难独立更新 | 未变资源命中缓存,只更新变化的模型或贴图 |
| 首屏 | 等待整份 HTML 与初始化 | HTML 封面先显示,再并行拉取必要资源 |
Base64 通常会比原始二进制多约三分之一体积;它不是“压缩”。当前单文件的优势是离线双击与完整打包,正式 H5 则更应利用独立请求、缓存和内容压缩。
4. 进度、文案和操作应该怎样设计
- 第一阶段就给反馈:先显示品牌、静态场景封面和“正在准备”,不要白屏,也不要只放无限旋转动画。
- 使用阶段进度:下载、解析、光照准备、首帧各有状态。资源数比例应明确是“已准备 2/3 项”,不要冒充真实字节百分比。
- 长等待给下一步:明显超过正常耗时后显示“网络较慢,可重试或查看轻量版”;具体秒数根据真机数据设定,不凭空写死行业标准。
- 首帧后短暂停顿:等构图稳定再淡出加载层,避免材质逐个弹出;动画只负责过渡,不掩盖仍在加载的事实。
- 控制安全区:重试、进入和退出按钮考虑
env(safe-area-inset-*),触控目标保持足够尺寸,不贴近刘海和系统手势区。 - 尊重减少动态效果:通过
prefers-reduced-motion减少旋转、闪烁和大幅过渡,加载信息仍需清晰。
5. 弱网与低端机要有出口
轻量模式不是一句“建议换手机”,而是一组可执行降级:更低分辨率烘焙图、较少近景模型、限制 DPR、关闭实时阴影和 SSAO,最后才是静态封面 + 可观景点切换的图片模式。保留明确退出入口,避免 WebGL 初始化失败后用户被困在全屏遮罩里。
合理资源预算应从目标设备测出来。例如“首屏必要资源多少 MB、低端机首帧多少秒、允许多少峰值内存”都应成为项目指标,但不要在没有测试数据时把某个通用数字当作所有手机的硬标准。
6. 加载优化怎样验收
| 测量点 | 记录内容 | 要回答的问题 |
|---|---|---|
| 加载页出现 | 首个文字/封面的时间 | 用户是否很快知道页面没有死? |
| 资源下载 | 各类资源字节数、缓存命中、失败请求 | 慢在网络,还是慢在设备? |
| 解析与初始化 | Long Task、各阶段耗时、JS 堆趋势 | 哪段同步代码让加载页不动? |
| 首个真实帧 | 从 boot 到第一次有效 render 的时间 | 进度结束后是否真的看得见场景? |
| 持续使用 | 帧时间、发热后表现、后台恢复、上下文丢失 | 能打开是否也等于能持续漫游? |
至少在 Wi-Fi 与受限网络、冷缓存与热缓存、一台目标低端真机上分别测试。桌面 DevTools 的 CPU/网络节流适合复现瓶颈,不能替代手机 GPU、内存和热降频测试。
09|清晰度实战:为什么更真实反而更模糊
咖啡店升级到模型、PBR 和烘焙后,实际观感却可能比纯色初版更“糊”。这不等于精细模型失败,也不能直接推出“贴图不够高清”。清晰度由实际渲染像素、物体在画面中的大小、纹理过滤和明暗对比共同决定。
两张图均使用 430×800 CSS 视口、设备 DPR 3 和同一入口机位。模型、材质、烘焙与光照不变,只调整渲染像素比、视野和纹理过滤。
1. 先看 Canvas 真正绘制了多少像素
CSS 里的 430×800 只是画布布局尺寸,不等于实际渲染分辨率。在同样的 DPR 3 测试环境下,第一版纯色场景的实际像素比上限为 2;第二版最初为了性能把手机限制到 1.5:
| 参数 | 清晰化前 | 清晰版 | 实际影响 |
|---|---|---|---|
| 设备 DPR / 渲染像素比 | 3 / 1.5 | 3 / 2 | 浏览器缩放低像素画面的程度不同 |
| Canvas 缓冲区 | 645×1200 | 860×1600 | 总像素提高约 78% |
| 垂直视野 FOV | 66° | 60° | 同一机位下,近景物体占屏更大 |
| 各向异性过滤 | 4× | 8× | 斜视地板和木纹保留更多细节 |
| 模型与烘焙 | 原有资产 | 完全不变 | 证明本轮不是重新制作模型 |
const ratio = Math.min(window.devicePixelRatio, 2);
renderer.setPixelRatio(ratio);
renderer.setSize(width, height, false);
camera.fov = 60;
camera.updateProjectionMatrix();清晰化前,第二版的总像素比第一版少约 44%。这时再精细的模型也要经过低分辨率采样,边缘自然不如高像素比的纯色硬边利落。
DPR 不是越高越好。像素比从 1.5 提到 2,像素工作量约增加 78%;若直接跟随高端手机的 DPR 3,负载还会继续上升。应根据目标真机帧时间设上限,而不是无上限追求物理分辨率。
2. FOV 改变的是“细节占多少屏幕像素”
视野越宽,同一台咖啡机在画面里越小。第二版从约 56° 的初版改为 66° 后,虽然能看到更多空间,但近景物件获得的屏幕像素更少,菜单和杯子自然更难看清。收窄到 60° 是调整构图与细节分配,不会增加资产本身的精度。
3. 斜向表面要检查各向异性过滤
木地板以很斜的角度伸向远处时,一个屏幕像素会覆盖拉长区域内的许多纹素。普通三线性过滤容易把这些细节平均掉;各向异性过滤沿不同方向采样,通常能明显改善地板、路面和长桌面的斜视清晰度。
const anisotropy = Math.min(
8, renderer.capabilities.getMaxAnisotropy()
);
// 项目实际遍历所有材质中的纹理,而不是只处理木纹。
if (texture.isTexture) texture.anisotropy = anisotropy;本项目设备报告最高支持 16×,清晰版选择 8× 作为画质与成本的折中。还要确保菜单、装饰画等后续创建的纹理也被统一遍历,不能只修最初注册的那一批。
各向异性不是万能锐化。它主要帮助斜视纹理;如果源贴图本身分辨率不足、UV 分配过小,或文字正对着相机仍模糊,就要继续检查纹素密度,而不是把过滤倍数无限调高。
4. “更柔和”不一定都是技术故障
纯色初版的颜色边界和明暗对比更硬;写实版增加法线、AO、间接光和 ACES 色调映射后,明暗过渡会更连续。这是风格差异,不是分辨率问题。排查时先修可量化的分辨率和过滤,再讨论曝光与风格,不要第一步就加锐化滤镜。
5. 清晰化怎样验收
- 固定测试条件:使用相同机位、CSS 视口和设备 DPR,保留前后截图。
- 读取实际缓冲区:检查
canvas.width / canvas.height和renderer.getPixelRatio(),不要只看 CSS。 - 裁切原始像素:直接检查菜单、杯沿和金属边的 1:1 像素,不凭缩略图判断源贴图清晰度。
- 重新检查四个机位:收窄 FOV 后,吧台、窗边和沙发可能变得过近,构图要一起回归。
- 补测持续性能:模型面数和 Draw Calls 没变,不代表 GPU 负载没变;像素比提高后仍要测手机帧时间与发热。
本轮最终保持 75,646 个三角面、24 次绘制调用,没有换模型或重烘焙。漫游、碰撞和离线回归均通过;真实手机持续帧率仍需单独验收。
10|后续优化:下一阶段不只看“还能不能更细”
视觉质量
- 轮廓优先:继续精修近景高频物件,而不是平均增加所有物体面数。
- 材质统一:控制贴图尺度、色彩空间和粗糙度范围,避免木纹像地板贴上去、金属像塑料。
- 动态层:确实需要灯光切换或可移动物件时,再增加 C 方案的实时阴影与 SSAO。
- 克制后期:Bloom、暗角和景深不能修复错误光照,过量反而会掩盖材质。
渲染性能
性能不是只看三角面数。还要看绘制调用、纹理显存、屏幕像素、透明叠加、阴影和着色器复杂度。
| 瓶颈 | 观察指标 | 常见优化 |
|---|---|---|
| CPU / 调用开销 | Draw Calls、对象数量 | 合批、实例化、共享材质 |
| 顶点处理 | 三角面、骨骼与变形 | LOD、减少不可见细节 |
| 填充率 | 分辨率、DPR、透明层 | 限制像素比、减少过度绘制 |
| 阴影与后期 | 阴影贴图、SSAO 成本 | 降低分辨率、减少投影灯 |
| 显存与带宽 | 纹理尺寸与格式 | KTX2/Basis、合理尺寸与 Mipmap |
60 FPS 的单帧预算约为 16.7 ms,30 FPS 约为 33.3 ms。应记录帧时间与长帧,而不是只报一个平均 FPS;手机还要观察发热后的持续表现。
文件大小不等于显存。一张 2048×2048 的 RGBA 纹理,连 Mipmap 粗略估算就占约 22 MB 显存。磁盘上的 PNG/JPEG 再小,解码后仍可能很大,因此发布版应考虑 GPU 压缩纹理。
模型与加载
- glTF / GLB:适合 Web 交付的标准格式,可携带网格、材质、UV 和场景结构。
- Draco / Meshopt:压缩几何体,降低下载体积;同时要计入解码器与解码时间。
- KTX2 / Basis:压缩纹理并减少显存占用,通常比单纯压 PNG 更接近渲染性能问题。
- 按需加载:先显示入口,再加载近景细节;大场景可分区分批加载。
当前单文件版本的取舍
目前约 12.4 MB 的 HTML 把引擎、模型和贴图全部内嵌,适合离线演示和直接交付,运行时没有外部请求。但它不是发布优化的终点:正式上线可拆成 HTML + GLB + 纹理,利用缓存、压缩和按需加载。
测试与可维护性
- 固定入口、吧台、窗边、沙发四个机位,持续做前后截图回归。
- 检查 UV 通道、贴图方向、色彩空间、曝光和烘焙叠加,防止“技术接上了但画面错了”。
- 漫游碰撞与渲染分离,避免换模型后交互逻辑全部重写。
- 至少覆盖桌面与手机真机;桌面无报错不代表手机帧率合格。
11|推荐学习顺序:每一步都做一个能看的结果
| 阶段 | 练习 | 要理解的问题 |
|---|---|---|
| 1. 几何与相机 | 画一个立方体并绕它旋转 | 顶点、三角形、法线、MVP 矩阵 |
| 2. 光照与材质 | 给立方体加颜色和漫反射 | 点积、明暗、粗糙度、金属度 |
| 3. UV 与贴图 | 制作木纹桌面和陶瓷杯 | UV、Base Color、Normal、色彩空间 |
| 4. Blender 与烘焙 | 做一间只有桌椅的小房间 | 倒角、第二套 UV、AO、Light Map |
| 5. Web 集成 | 导出 glTF 并用 Three.js 漫游 | 加载器、坐标、贴图通道、交互解耦 |
| 6. 加载交互 | 给 GLB 与贴图做加载、失败和重试界面 | 资源管理、首帧、状态反馈与交互解锁 |
| 7. 清晰度诊断 | 固定机位做不同 DPR、FOV 和过滤对比 | 实际像素、纹素密度与主观锐利感 |
| 8. 性能与发布 | 做桌面/移动两档画质 | 帧时间、Draw Calls、DPR、压缩与缓存 |
最有效的练习方法:每次只改变一个变量。先只加倒角,再只加法线,再只加 AO,最后加烘焙。你会真正知道每一项技术贡献了什么。
12|一张总结表:遇到问题时先找哪一层
| 你看到的问题 | 优先检查 | 不要先做什么 |
|---|---|---|
| 物体像积木、边缘假 | 轮廓、倒角、平滑法线 | 先加 Bloom |
| 木头、金属、布料没区别 | 粗糙度、金属度、环境反射 | 先增加面数 |
| 家具像漂在地上 | 接触阴影、AO、光照方向 | 先换更高清颜色图 |
| 画面过曝或灰平 | 色彩空间、曝光、烘焙与实时光叠加 | 先盲目提高灯强度 |
| 贴图错位或颠倒 | UV 通道、flipY、导出坐标 | 先重新建模 |
| 写实版比纯色版更糊 | 实际画布像素、FOV、各向异性过滤和色彩对比 | 先重做模型或盲目加锐化 |
| 手机卡顿 | 帧时间、DPR、阴影、纹理、Draw Calls | 只盯三角面数 |
| 下载慢但运行也卡 | 几何压缩、纹理压缩、显存与加载策略 | 只把 PNG 再压一遍 |
| 进度满格但还不能进入 | 图片解码、GPU 上传、着色器编译和首帧 | 用固定定时器假装加载完成 |
| 手机加载页卡死 | 同步长任务、Base64 临时内存、纹理生成与首屏资源量 | 只给加载动画换样式 |
最后记住一句话:模型负责形状,材质负责表面,光照负责空间,优化负责让这一切稳定运行。