1.2 工位搭建:环境、脚手架与调试台 本节摘要:开工前把车间接好电:两种模块化引入方式、页面容器与尺寸自适应、像素比与抗锯齿的初始配置,外加一组调试辅助工具。本节承接上一节的首件样板,把它从"能跑的草稿"升级为"抗需求变更的工装底座",第 2 至 8 章的案例都建在这套底座上。 草稿与工装的差别 上一节的样板直接把画布挂到 body 上,尺寸写死为窗口大小——演示够用,交付不行。客户的页面里,3D 视图往往只是布局中的一块区域:左边产品参数、右边画布。画布尺寸要跟着容器走,窗口拉伸要跟手,高分屏上不能发虚。这些需求没有一处涉及 3D 算法,却决定项目第一印象,所以它们属于"工位搭建"这道工序:水电不稳,后面所有工位都跟着晃。 引入方式同样属于水电工程。Three.
本节摘要:开工前把车间接好电:两种模块化引入方式、页面容器与尺寸自适应、像素比与抗锯齿的初始配置,外加一组调试辅助工具。本节承接上一节的首件样板,把它从"能跑的草稿"升级为"抗需求变更的工装底座",第 2 至 8 章的案例都建在这套底座上。
上一节的样板直接把画布挂到 body 上,尺寸写死为窗口大小——演示够用,交付不行。客户的页面里,3D 视图往往只是布局中的一块区域:左边产品参数、右边画布。画布尺寸要跟着容器走,窗口拉伸要跟手,高分屏上不能发虚。这些需求没有一处涉及 3D 算法,却决定项目第一印象,所以它们属于"工位搭建"这道工序:水电不稳,后面所有工位都跟着晃。
引入方式同样属于水电工程。Three.js 新版本只提供 ES 模块发行,全局脚本标签的旧用法已被淘汰。实际项目里两条路线二选一:走构建工具,用包管理器安装依赖、由打包器处理模块解析;或走免构建路线,用浏览器原生的导入映射(import map)直接指定模块地址。原型验证期用后者最快,正式项目建议前者——依赖管理、代码压缩、按需拆包都是构建工具顺手的事。
背景:沙盘原型要嵌进后台页面的左侧栏(一个占满高度、宽度随窗口变化的容器),并且要求高分屏下不发虚、窗口缩放时画布跟手。
操作:先在页面里放一个容器元素,3D 视图的一切尺寸以它为准;再写一段通用的初始化代码,以后每个案例只要换中间的装配部分。
<!-- 页面结构:画布的宿主容器,尺寸交给 CSS 布局 --> <div id="stage" style="width: 100%; height: 80vh;"></div>
import * as THREE from 'three'; import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; // 统一入口:传入宿主元素,返回一套可复用的三件套 function createStage(container) { const scene = new THREE.Scene(); scene.background = new THREE.Color(0x202428); const camera = new THREE.PerspectiveCamera( 60, container.clientWidth / container.clientHeight, 0.1, 2000 ); camera.position.set(3, 2, 5); // 稍微抬高、侧移,比正对原点的机位更有层次 const renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); // 高分屏封顶 2,帧预算友好 renderer.setSize(container.clientWidth, container.clientHeight); container.appendChild(renderer.domElement); // 轨道控制先挂上,调参阶段就能转着看(第 4 章展开) const controls = new OrbitControls(camera, renderer.domElement); controls.update(); // 尺寸自适应:监听容器尺寸,宽高比与画布同步重设 const resize = () => { const w = container.clientWidth, h = container.clientHeight; camera.aspect = w / h; // 宽高比变了必须更新相机,否则画面拉伸 camera.updateProjectionMatrix(); renderer.setSize(w, h); }; window.addEventListener('resize', resize); const clock = new THREE.Clock(); // 统一时钟,动画章节的主力工具 const loop = (fn) => { renderer.setAnimationLoop(() => fn(clock.getDelta(), clock.elapsedTime)); }; return { scene, camera, renderer, controls, clock, loop, resize }; } const stage = createStage(document.getElementById('stage')); stage.loop((dt) => { stage.controls.update(); stage.renderer.render(stage.scene, stage.camera); });
结果:画布精确填满容器,拖动窗口尺寸跟手无变形,高分屏上边缘锐利,鼠标可以拖着场景转着看。把这套底座存成模板,之后每一章的案例只在 stage.scene 上追加工序。
解读:三处细节值得咀嚼。其一,setPixelRatio 用 Math.min 封顶 2:多数高分屏像素比是 2 到 3,画到 3 帧开销翻倍、肉眼几乎无差,这是用行话说的"白送性能"。其二,宽高比取自容器而非窗口,配合 resize 里先改 camera.aspect 再调 updateProjectionMatrix——这两步是一对,只做第一步,画面就会拉伸,这是工位搭建阶段报障率最高的一处。其三,setAnimationLoop 代替了手写的 requestAnimationFrame 递归:它内部帮你处理了节奏,并且在 WebXR 场景下自动切换渲染节奏,第 8 章讲 XR 出货时会再遇到它。
变式:容器侧栏宽度由前端框架动态改变、不触发 window 的 resize 事件怎么办?改用 ResizeObserver 监听容器元素本身,回调里直接调 stage.resize 即可。另一个变式是暗色主题切换:把 scene.background 换成由外部传入的颜色参数,让底座支持"换肤",这类小改动最好在底座阶段预留。
调参阶段没有辅助件,等于蒙眼装配。第一件是坐标轴辅助 AxesHelper,一根红绿蓝三色轴放在原点,红是 x、绿是 y、蓝是 z,方位感立刻建立——尤其是相机转了几圈之后。第二件是网格辅助 GridHelper,在 xz 平面铺一层方格地贴,物件是否贴地、间距是否均匀一目了然。第三件是包围盒辅助 BoxHelper,传入任意物件即可显示其包围盒,排查"模型为什么看起来那么小"这类问题时,包围盒一开就知道是尺寸问题还是机位问题。
// 三件辅助件:调参常驻,出货前删除即可 const axes = new THREE.AxesHelper(2); // 长度 2 的三色坐标轴 const grid = new THREE.GridHelper(10, 20); // 10 单位宽、20 分格的地贴 const box = new THREE.BoxHelper(stage.scene.children.at(-1), 0xffcc00); // 最后一个物件的包围盒 stage.scene.add(axes, grid, box);
辅助件是普通物件,add 进场景就生效。它们不参与导出、不进正式画面,但调试期省下的时间远超三行代码的成本。帧率方面,浏览器自带的性能面板足以看渲染耗时分布,第 8 章质检时再上专业探针。
工位就绪、水电接通,下一节进入下料工位:把 Three.js 自带的几何体谱系盘一遍,顺便建立"参数换面数"的成本账。