在前面的智慧农业和智慧高速实践里,我已经把“调研、出图、拆素材、建模、网页化”这条链路走了一遍。这次,我把场景换到了企业展厅。
室内场景给我的第一个感受,是细节更藏不住了。园区可以先看建筑体量和道路关系,展厅里的镜头一靠近,墙体边缘、玻璃厚度、电脑支架、展柜接缝和地面反射都会进入视线。把一块屏幕摆到墙上很容易,让它像一台真实设备,并且能直接播放内容、接收操作,就要把模型和前端一起设计。
这篇按实际制作链路展开:先把行业调研写成需求,再生成完整设计稿,拆出资源与实现边界,通过 Blender-MCP 制作模型,最后接入网页、屏内内容和相机导览。期间的返工会放在对应技术环节里,解释为什么改、改在哪一层。
项目业务读数使用固定样例,模型属于可视化级概念场景。文中代码从实际项目整理、裁剪,省略了部分上下文与异常处理;其中参数用于解释本次实现,不能直接当成所有展厅的通用配置。
视频预览效果:
一、先调研业务,再让 Codex 写出可以执行的 Brief
我给这个项目确定了两个使用入口:企业运营总览和展陈导览。
运营人员打开页面,要知道今天来了多少人、人集中在哪个展区、哪些设备或环境测点需要查看。参观者进入导览,则要能找到展区、靠近展项、阅读介绍并操作屏幕。两种使用方式共享一个空间,但注意力和交互深度不同。
因此,调研首先要回答三个具体问题。
- 客流怎样统计? 累计入馆人次、离馆人次和当前在馆人数分别是什么含义,统计周期如何划分?
- 设备怎样判断状态? 心跳有效、内容播放中、播放错误、数据过期分别由什么信息支持?
- 内容怎样与空间关联? 选择一个展区以后,页面和相机应该把用户带到哪里?
AXIS 的客流文档区分通行计数与区域人数估算;BrightSign 的诊断功能分别检查连接、网络、显示和日志;ArtLens 则把展品信息、地图与导览关联起来。我用这些资料确定指标和操作关系,再回到企业展厅的具体场景做取舍。AXIS 客流文档、BrightSign 诊断文档、ArtLens 导览介绍
调研结束后,我让 Codex 把内容整理成 Brief 和数据合同。Brief 写清使用者、屏幕尺寸、阅读顺序、展区和交互;数据合同则约定字段、单位、统计范围、时间戳和缺失状态。
这次按 1920×1080、鼠标操作设计,设置序厅、企业历程、核心技术、产品体验、应用案例、未来愿景六个概念展区。固定样例中,累计入馆 720 人次、离馆 600 人次、当前在馆 120 人,六区人数也合计为 120。这样后面做图、写图表和联动时,都有同一份数据依据。
这里有两个容易忽略的细节:设备心跳正常,不足以单独证明内容正常播放;某个区域没有历史曲线,就应该显示“暂无记录”。同时,49 条业务设备记录与 29 块三维物理屏面属于不同的演示对象,完整设备映射需要真实台账。
这一阶段的产物,是可以交给设计和开发继续使用的需求、数据与交互约定。 如果这些约定没有写清,后续每一层都可能各自理解“客流”和“当前区域”。
二、先出完整设计稿,把整屏关系确定下来
有了 Brief,才进入设计生成。
我让 Codex 沿三个方向组织完整画面:A 侧重完整展厅与周围运营信息;B 采用明亮银白的建筑轴线,底部形成连续数据带;C 强调产品体验近景,同时保留全厅定位。三稿比较的是信息组织和观察尺度。
生成任务里,我把要求写成几组具体约束:业务字段与固定读数、各阅读组的位置、展厅观察角度、墙面与玻璃等材质分工,以及选中状态的对应关系。例如人数柱图明确为六个区域、共同 0–40 人刻度,空间标签和详情都指向产品体验区。生成后的检查也逐项对照这些约束,避免画面好看,却多出一根柱子、漏掉一个单位或出现互相矛盾的数字。
最终我选择了 B“银白展轴”。浅色墙体和银灰设备形成主体,深色文字保证阅读,蓝色承载业务图表,紫色用于选中和导览提示。中央留给展厅,左侧看客流,右侧看区域详情,底部看趋势、分布与设备记录。
图:选定的 B“银白展轴”设计稿。它是后续构图与材质方向的参考,不是浏览器运行截图。
这一步需要认真看整屏,而不是只盯着中央模型。标题与导航是否挤占空间,图表绘图区是否够大,文字能不能读,左右面板会不会挡住关键展项,都要一起判断。
选稿后,我把它冻结成唯一设计基准。本次冻结图为 1664×936,映射到 1920×1080 时采用 15/13 的等比坐标换算,同时保存原始生成图、裁边记录和文件摘要。没有把放大的图片重新当成更高分辨率的原稿。
这样做的意义是,后面讨论“模型偏了”“卡片位置不对”时,能够回到同一份参照。设计图可以提供可见轮廓、色彩和光照线索,但单张图片不能直接给出墙后结构、物体深度与真实尺寸,这些需要在建模阶段另行建立。
三、拆资源之前,先给每一个元素确定实现归属
设计图出来后,最容易做错的一步,是把整块面板连文字、数字、图表一起裁成图片。短时间内很像设计稿,接入数据和交互时却会到处受限。
我先让 Codex 建立元素清单,记录每个元素的原图坐标、实现归属、数据来源和最终去向。
| 设计中的元素 | 实现方式 | 拆分时要保留什么 |
|---|---|---|
| 图标、边框、装饰表面 | PNG 与界面组件 | 原始区域、透明边缘、复用关系 |
| 标题、数值、单位、表格 | React 与真实字体 | 文本、格式、状态和数据绑定 |
| 趋势与分布图表 | ECharts | 数据、刻度、图例和交互 |
| 墙、地面、玻璃、展台、设备 | Blender 与 Three.js | 尺度、几何、材质和对象标识 |
| 空间卡片、点位、连线、选区 | 运行时 DOM、SVG 与三维效果 | 世界锚点、选择状态、遮挡关系 |
| 展屏中的内容 | Canvas、视频纹理或 CSS3D | 屏面位置、方向、尺寸和内容类型 |
实际提取时,每个原生区域都要留下对应记录。下面是环境面板资源的简化示例:
{ "id": "surface.environment", "dimensions": [449, 499], "source_rect_native": [1213, 77, 449, 499], "method": "BACKGROUND_SEPARATION_DERIVED", "owner": "SCAFFOLD"}
这里的坐标指向冻结设计稿;method 说明它经过背景分离。它不是“从源图剪下来就保持全部原始像素”的资源。
带字的底板,要先分离文字与表面。 本项目在指定区域内做同图邻近像素修复,并保留修复蒙版;图标和柔边则保留 Alpha 与排除区域。经过修复的资源明确标为派生,不能声称恢复了被文字遮住的原始背景。
透明边缘尤其值得检查。截图里一个像素的颜色,是前景、背景和覆盖率混合后的结果。直接把浅色背景删掉,边缘常会留下白边;估计 Alpha 时又可能误删图标内部本来就存在的白色。我们把素材分别放到深色、浅色和棋盘背景上查看,再回贴原位比较。
最后生成资源注册表和重组预览:面板、图标用提取资源,文字与图表重新绘制,按坐标重新组装整屏。这样才能检查遗漏、重复、对齐和边缘问题。直接拿原图当底,再盖几个切片,很难发现真正的缺失。
资源清单还区分可复用母资源与画面中的出现实例:同一个图标可以出现多次,多处使用不能当成多张独立素材。后期空间卡片和 Pin 又按交互需要改成 DOM 样式,设计资源也要跟着实际实现更新。
完成这一步,前端拿到的是可组合的资源包,建模拿到的是建筑轮廓、材质和局部构造参考。地面在设计稿中的裁片只能帮助判断外观,不能直接当成无缝石材贴图铺满场景。
四、通过 Blender-MCP 建模,先把尺度、结构和机位搭起来
到了三维阶段,我先做了序厅、产品体验区及相邻地面的代表切片,包含入口、展墙、展台和植物,用来暴露比例、材质与导出问题,再逐步扩展全厅。
本项目的调用链是:Codex 编写项目 Python 脚本,通过脚本桥接已安装的 Blender-MCP 插件,在 Blender 中执行 bpy 操作,保存可编辑场景,再导出网页资产。平台保存阶段材料与执行记录,便于回到某一轮修改继续工作。
建模前,我先让 Codex 梳理三个基础条件。
第一,统一空间尺度。 建筑、展柜、屏幕与观察机位必须使用同一单位。建模中的一个单位按一米理解时,墙厚、台面高度和相机高度才有可比较的关系。本项目后续把完整空间层高调整到概念设定的 4.2 米;它用于建立比例,最终仍需要现场资料校准。
第二,建立对象层级。 展厅根节点下面区分建筑与各展区,再放置展墙、展柜、设备、植物。可拆卸屋顶单独管理,方便切换完整建筑与剖切总览。每个展区保留稳定的 zoneId,屏面则单独记录位置、旋转和尺寸。
第三,先对齐构图。 先看入口到后排的轴线、展墙体量和主要设备落点,再调整相机。相机距离、视场角和目标点不同,同一个模型也会显得过扁、过高或拥挤。构图尚未对齐时,继续加高光通常解决不了差异。
复用已有构件时,还要处理对象变换。项目脚本先把源对象顶点变换到世界坐标,计算中心和尺寸,再按统一比例放入新展区。若保留了源资产的父级缩放,又在新父节点上重复缩放,设备很容易变形或错位。移动和导出后,要检查最终世界坐标,而不只看属性面板里的局部数值。
这时我会要求 Codex 输出几类检查结果:整体包围盒、主要对象尺寸、对象层级、总览与近景机位,以及导出的内容范围。Blender 工作文件与运行 GLB 分开保存,预览用的辅助对象也不应混进正式场景。
这一阶段先建立可信的空间骨架。 屏幕里的文字和业务数字仍留给运行时,避免把以后会变化的内容做死在模型里。
五、从灰模到写实:几何、法线和 PBR 要一起处理
用户最常见的反馈是“材质不对”“边缘不真实”。这类问题可能出在材质,也可能来自没有厚度的几何、错误的法线或过度圆滑的轮廓。我把深化顺序固定为:结构 → 边缘 → 法线 → 材质 → 灯光。
1. 给墙体和设备补真实的构造关系。
墙面需要厚度、转角、顶面与侧面;展柜需要台面、柜身、踢脚和门缝;显示器需要外壳、边框、凹入的显示面和支撑结构。电脑也要区分上盖、转轴、键盘、触控板和机身厚度。远看像一个矩形,近看则要能解释它是怎样组成的。
同时,所有子件要跟随正确的父对象移动。后来盆栽与柜体穿插时,我们移动的是包含盆、土壤、枝干和叶片的整组对象,避免出现盆移动了、植物还留在原处的情况。
2. 用倒角产生边缘高光,再用法线稳定大平面。
完全锐利的数学边缘很难形成真实的高光带;倒角过大,又会让墙像软塑料。倒角半径应与物体尺寸、制造方式和观察距离一起考虑。
下面是墙体深化脚本的节选。执行前,对象已按局部尺寸重建,缩放归一,dims 是尺寸向量:
bevel = o.modifiers.new("ManufacturedEdge", "BEVEL")bevel.width = min(0.028, float(min(dims)) * 0.11)bevel.segments = 5bevel.affect = "EDGES"bevel.harden_normals = Truebpy.context.view_layer.objects.active = obpy.ops.object.modifier_apply(modifier=bevel.name)normal = o.modifiers.new("PlanarNormals", "WEIGHTED_NORMAL")normal.keep_sharp = Truenormal.weight = 65bpy.ops.object.modifier_apply(modifier=normal.name)
半径由最薄方向约束,并设置上限;它不是给所有物体统一加 28 毫米圆角。墙面、侧封边和顶面还分别分配材质,让大平面与边缘在光下产生差异。
法线决定表面怎样响应光。加权法线可以帮助大平面保持平整,同时让倒角过渡自然;但法线不能修补错误轮廓,也不能代替真正的厚度。不同 Blender 版本对法线操作的入口可能不同,脚本需要按实际版本核对。Blender 加权法线说明:docs.blender.org/manual/en/3…
3. 把颜色、粗糙度和微法线分开设计。
PBR 中,Base Color 描述基色,Metallic 区分金属与非金属响应,Roughness 控制微表面散射,Normal 改变局部着色方向,Transmission 与 IOR 则参与玻璃透射和折射。它们各有职责,不能用一个“亮度”参数包办。Blender Principled BSDF:docs.blender.org/manual/en/4…
这次石材深化分别生成颜色、粗糙度和法线三张 512×512 贴图。颜色只保留低幅云纹,粗糙度加入细微变化,法线控制很弱的表面起伏。UV 按世界尺度铺设,避免同一材质在大地面与小物体上出现完全不同的纹理比例。
这里有一个建模知识很实用:法线贴图改变光照响应,不改变外轮廓。 石材细小颗粒可以放进法线;墙体厚度、电脑边缘和叶片弯曲,仍然需要由几何承担。
颜色贴图与数据贴图也不能混用色彩空间。颜色通常使用 sRGB,粗糙度、法线和 AO 按非颜色数据处理。若把粗糙度图也进行颜色变换,调好的材质响应会发生变化。Three.js 色彩管理:threejs.org/manual/page…
4. 玻璃要同时处理透射、厚度与环境。
把 opacity 调低,只是在做透明混合,并没有完整描述玻璃的光学效果。本项目一轮深化中使用了以下玻璃参数:
bs = glass.node_tree.nodes["Principled BSDF"]bs.inputs["Base Color"].default_value = (.985, .995, 1, 1)bs.inputs["Roughness"].default_value = .018bs.inputs["Metallic"].default_value = 0bs.inputs["Transmission Weight"].default_value = .985bs.inputs["IOR"].default_value = 1.47# glTF Material Output 节点已在上下文中创建settings.inputs["Thickness"].default_value = .02
场景还配置了体积吸收,并在网页侧核对 thickness、attenuationDistance 与 attenuationColor。上述数值是视觉调校参数;真实项目应结合玻璃种类与厚度重新设定。
玻璃后面需要有可见空间,前面需要有可反射的环境。边框、接缝、侧边与厚度共同帮助人判断它的存在。只有一个完全透明的薄平面,即使参数看起来齐全,也可能缺少质感。
5. 小型设备与植物,优先修轮廓和接地关系。
显示器的薄厚比例、底座与台面的关系,比盲目增加面数更容易被看出来。盆栽则要看枝干层级、叶片朝向、大小变化与自然的不对称。近景需要保留叶片轮廓和曲率,远处细小枝干可以适度简化。
我们后来清除了盆栽与邻柜的相交,并重新检查落地高度、接触阴影和导出位置。这个例子说明,物体的位置和组合关系,也属于真实感的一部分。
六、补齐光照、AO 和导出链路,让材质真正进入网页
模型在 Blender 中看着不错,导入网页后却发白、发灰,常常涉及两套渲染环境的差异。
我把这一步拆成三个层次:环境反射提供整体光照线索,灯光塑造空间,接触 AO 加强局部遮蔽。 三者分别检查。
1. 先为金属和玻璃提供可反射的环境。
网页中使用 RoomEnvironment 生成环境,再通过 PMREM 预过滤,供不同粗糙度的材质采样。核心配置如下:
const room = new RoomEnvironment();const pmrem = new THREE.PMREMGenerator(renderer);const environment = pmrem.fromScene(room, 0.05);scene.environment = environment.texture;scene.environmentIntensity = 0.9;renderer.toneMapping = THREE.ACESFilmicToneMapping;renderer.toneMappingExposure = 1.04;
环境、渲染目标与生成器需要在卸载时释放。调整曝光前,还要先确认纹理和输出的色彩空间正确,避免用更强的灯光补偿错误的颜色转换。
灯带也分成两个部分:可见的发光几何与实际照明。常规实时渲染中,一个 emissive 表面不会自动提供完整的间接照明。本项目另外使用矩形区域灯塑造顶面照明,并配合局部灯光和选中效果。
2. 用接触 AO 让展柜、墙角和设备落在空间里。
AO 描述附近几何造成的遮蔽。它能帮助表现柜底、接缝与墙角,但不包含完整的直接光照,也不能代替镜面反射。
本项目在导出副本上建立独立的 ContactAO_UV,保留石材原来的表面 UV,使用一个 2048×2048 图集统一烘焙。节点临时接成“AO → Emission → 输出”,再烘焙 EMIT,避免把灯光和材质颜色一起混入遮蔽图:
ao = nodes.new("ShaderNodeAmbientOcclusion")ao.inputs["Distance"].default_value = 0.55ao.samples = 24emission = nodes.new("ShaderNodeEmission")links.new(ao.outputs["AO"], emission.inputs["Color"])links.new(emission.outputs[0], output.inputs["Surface"])# 已创建非重叠 UV、绑定目标图集并选定烘焙对象scene.render.bake.margin = 5bpy.ops.object.bake(type="EMIT", uv_layer="ContactAO_UV")
这里省略了材质副本、目标 Image Texture 激活与对象筛选代码。完整流程在烘焙后恢复原 BSDF,再把图集接到 glTF Material Output 的 Occlusion 输入。网页读取的是导出的遮蔽纹理;不能把临时 Emission 材质直接留在最终模型里。Blender glTF 遮蔽贴图导出说明:docs.blender.org/manual/en/5…
透明玻璃、植物和发光部件并没有一律套用同一套 AO 烘焙。对象移动后,对应遮蔽也要重新计算。最终 UV 通道可能经过清理,读取时应以导出的 texCoord 为准,不能假定所有材质永远使用第二套 UV。
3. 地面倒影单独处理,避免把石材做成镜子。
网页使用 Reflector 获取平面反射,再根据石材的粗糙度和微法线,对采样进行扰动与柔化。片元着色器中还根据视线与地面的夹角调整反射强度,简化节选如下:
float facing = clamp( dot(normalize(cameraPosition - vWorld), vec3(0.0, 1.0, 0.0)), 0.0, 1.0);float fresnel = pow(1.0 - facing, 3.0);float reflectance = mix(0.13, 0.37, fresnel) * (1.0 - rough * 0.22);
这是本项目的实时视觉近似,不是完整的物理光传输求解。它让斜视时的反射更明显,同时保留粗糙度带来的变化。实际代码还对反射图做多点采样,减弱过于锐利的倒影。
到导出阶段,我们只导出独立物理场景,保存几何、法线、UV 和必要材质,排除预览媒体及辅助相机。程序生成的材质细节转换成可携带的贴图,并核对 glTF 材质扩展是否进入文件。
同时生成模型清单,记录运行 URL、内容摘要、展区锚点和屏面变换。可编辑 Blend、运行 GLB、贴图和清单,必须是彼此对应的一组版本。
七、接入 Scaffold,把模型、图表和选择状态连起来
模型与资源准备好以后,进入网页实现。
页面继续使用我之前使用的 @lius1314/visual-dashboard-scaffold,通过 App、注册区块和 JSON 布局管理整屏;React 组织内容与状态,ECharts 绘制外围业务图表,React Three Fiber 和 Three.js 承载场景。
前面拆好的图片进入资源配置,标题、数字、表格继续用真实字体绘制。加载占位图只在加载或错误状态下出现;模型就绪后,由交互场景接管中央区域。
我先让 Codex 建立共享状态,而不是让每张卡片各自维护一套“当前展区”。选择逻辑的核心很短:
const selectZone = (id: string) => { setZone(id); setDevice(null);};const selectDevice = (id: string) => { const device = data.devices.find(item => item.deviceId === id); if (!device) return; setZone(device.zoneId); setDevice(id); setTab('devices');};
空间标签、右侧详情、设备列表和相机读取同一状态。用户从图表选中某区,场景能找到同一个 zoneId;从设备列表进入,则先定位所属展区。全馆总数保留全馆口径,区域详情再展示当前选择。
同样需要集中处理的还有页面模式、模型是否就绪、动效暂停和返回总览。项目把动效许可统一为“未暂停、系统没有要求减少动态效果、页面处于可见状态”,各动画轨道再读取这个许可。
场景加载也与业务选择分离。切换展区只更新状态、标记和相机,不重新请求同一个模型。真正换模型或重试时,再根据模型 URL 发起加载;旧请求晚返回时,应丢弃并释放它,避免覆盖当前场景。
完成这一阶段,数据面板与三维空间才开始成为同一个应用,而不是碰巧放在同一张页面上。
八、让展厅里的屏幕承担内容,而不只是贴一张图片
展厅网页跑起来后,我继续提出了一个要求:
“我的意思是直接在3D展厅里面播放。不是点击弹出。”
这个要求决定了屏幕内容的技术选型。我们把内容分成三类处理。
1. 影片:VideoTexture 映射到物理屏面。
视频由浏览器解码,再作为纹理传给 Three.js。屏面的位置、旋转与尺寸来自模型清单,播放由用户点击触发:
const video = document.createElement('video');video.src = movieUrl;video.muted = true;video.playsInline = true;video.loop = true;const texture = new THREE.VideoTexture(video);texture.colorSpace = THREE.SRGBColorSpace;const material = createDisplayMaterial(texture);// 使用屏面记录设置 mesh 的 position、quaternion 与尺寸// 用户点击后调用 video.play(),处理返回的 Promise
影片随相机透视变化,按原始宽高比适配屏幕。退出时暂停视频并清除播放意图,切换或卸载时释放纹理、材质和事件监听,防止画面已退出、后台仍在播放。
2. 企业历程与数据大屏:Canvas 绘制内容,再生成纹理。
企业历程拆成创立起步、技术积累、产品成形、行业应用、协同生态、未来展望六个章节,每块展板对应不同内容。正式年份和企业成果需要企业资料,原型先用可审核的章节文案建立结构。
数据大屏则从同一业务快照绘制运营、客流和设备环境页面。CanvasTexture 只在内容变化后标记 needsUpdate;当前阅读的动态大屏最多约每秒更新 12 次,背景海报保持静态。这个频率只约束内容绘制,不是限制整个场景只能运行在 12 FPS。
阅读内容还遇到过一个问题:反射足够明显,文字却被冲淡。我们把文字、图表与物理外壳分开,让内容使用独立阅读材质:
const readingMaterial = new THREE.MeshBasicMaterial({ map: canvasTexture, color: '#ffffff', toneMapped: false, depthTest: true, depthWrite: true,});
这样,阅读像素保持稳定,金属边框与建筑玻璃继续使用物理材质;内容仍参与深度判断,不会隔着墙显示。字号、行距和字重则按屏幕投影后的大小检查,不能只看 Canvas 的原始分辨率。
3. 可操作电脑:用 CSS3D 放置真实 DOM。
远景中,电脑使用静态桌面预览。进入电脑后,CSS3DRenderer 把 React DOM 放到屏幕平面上,让窗口、按钮和输入框接收鼠标与键盘操作。
其中最重要的转换,是从鼠标屏幕坐标找到电脑桌面的局部坐标:
raycaster.setFromCamera( new THREE.Vector2( (clientX - rect.left) / rect.width * 2 - 1, 1 - (clientY - rect.top) / rect.height * 2, ), camera,);const hit = raycaster.ray.intersectPlane(screenPlane, point);if (!hit) return null;hit.applyMatrix4(inverseScreenMatrix);return { x: hit.x + desktopWidth / 2, y: desktopHeight / 2 - hit.y };
screenPlane 由屏面位置与朝向建立;inverseScreenMatrix 是已经按屏幕尺寸缩放的 CSS3D 对象世界矩阵的逆矩阵,因此局部坐标能对应到桌面像素。进入操作状态时,输入交给桌面,场景相机停止抢占;退出后恢复。
CSS3D 与 WebGL 不共享深度缓冲,我们另外检查朝向和遮挡,决定桌面是否可见。这里实现的是 macOS 风格网页桌面,提供原型内的操作,不是真正运行 macOS。
这几类内容共用物理屏面的定位信息,分别选择适合的呈现方式。以后接入外部看板或流媒体,还需要继续处理认证、跨域与更新机制。
九、补上空间卡片与相机导览,让点击之后有明确去向
内容能显示以后,还要让用户看得到、看得清。
我把交互拆成两件事:空间卡片准确指向对象;点击后,相机能够进入合适的观察位置。
1. 卡片先投影,再布局,最后画连接线。
三维锚点先通过相机投影到标准化坐标,再换算为当前视口的 CSS 像素。卡片和 SVG 连线使用同一坐标系,避免设备像素比造成偏移:
const p = worldAnchor.clone().project(camera);const pinX = (p.x + 1) * viewportWidth / 2;const pinY = (1 - p.y) * viewportHeight / 2;
得到锚点后,才安排卡片位置与避让,并从卡片实际边缘连接到锚点中心。卡片尺寸尽量稳定,选中主要改变颜色;工具栏也要参与避让。可见点位与点击热区可以分开,既保持画面轻巧,也保证容易点中。
动效围绕状态变化安排:选择时短暂描线、光圈轻微变化、面板柔和进入。锚点本身保持稳定,防止卡片、箭头和真实位置看起来不断脱节。
2. 观察机位根据屏幕尺寸计算,再做遮挡检查。
近景阅读不能只写一个固定的“相机向前移动两米”。屏幕有横竖比例之分,视口也会变化。项目先按屏幕高度、宽度与视口比例估算需要的可视范围:
const normal = new THREE.Vector3(0, 0, 1) .applyQuaternion(screenQuaternion);const span = Math.max( screenHeight / 0.78, screenWidth / (Math.max(0.5, aspect) * 0.88),);const distance = span / ( 2 * Math.tan(THREE.MathUtils.degToRad(38 / 2)));const position = target.clone().addScaledVector(normal, distance);
这里使用透视投影关系:在给定垂直视场角下,可见高度由距离决定;宽度则通过视口宽高比转换。0.78 与 0.88 为构图预留边距,38 度是本项目的首选垂直视场角。
这个结果还不是最终机位。我们从候选位置向屏幕中心与边缘采样点发射射线,检查墙、展柜和植物是否挡住阅读区域,再尝试侧向或竖向调整。狭窄位置需要靠近与更大视场角时,也要留意透视畸变。
进入阅读前保存原机位,退出时恢复;窗口变化、模型重新就绪时重新计算。导览切换的不仅是一个目标点,还包括相机位置、目标、视场角和内容状态。
十、控制资源成本,再按不同层次验证结果
功能越来越多时,性能需要跟着制作流程一起管理。我主要从四个方向检查。
- 几何成本:优先保留设备和叶片轮廓,对细小枝干等低收益细节做简化。重复对象是否适合复用或实例化,需要结合材料、拾取与导出结构决定。
- 纹理成本:按远景与阅读状态安排分辨率;静态内容不持续重绘;共用 AO 图集,减少不必要的独立纹理。
- 渲染成本:控制像素比和反射目标分辨率,减少重复调度。Reflector 仍有额外渲染通道,不能把一次主场景调用理解成整个画面只绘制一次。
- 生命周期:检查切换、退出和重试,释放几何、材质、纹理与事件监听;处理过期异步结果,防止旧资源覆盖新状态。
当前迭代模型约 34.87 万三角面、11.4 MB。我们已经做过植物几何优化和 AO 灰度纹理的无损压缩;专门的几何编码、GPU 纹理压缩、分级细节与渐进加载,属于下一步可以评估的方向。
验收也要分层:素材回贴检查资源与布局,几何检查检查尺寸与相交,模型导出检查检查节点、材质和屏面,网页操作检查选择、播放、输入和返回,性能测试检查首屏、切换、帧时间与长时间内存。
我们已有构建、独立内容渲染、几何与有限机位射线检查记录,但这些不能直接代替目标设备上的浏览器表现。目前局部狭窄机位和遮挡仍有取舍,整体视觉也需要继续对照设计。真实接口、现场尺度与长期运行性能,还要在后续部署条件下验证。
回看这条制作链路,我觉得有几项工作值得在下一个项目里继续坚持。
- 调研要留下可执行的需求。 每个指标都能解释来源、口径和缺失状态,每个交互都有明确目的。
- 设计稿要能拆成责任明确的资源。 图片、文字、图表、模型和空间标记分别由合适的技术承担。
- 真实感从几何和尺度开始。 先修结构、边缘、厚度与接触关系,再调整材质和灯光。
- 建模时就考虑网页使用。 对象标识、屏面变换、可拆屋顶和观察机位,都要服务后续交互。
- 每一轮修改都能追溯。 设计参考、模型源文件、导出资产和网页清单对应起来,才能定位差异。
Codex 在这次实践中,把资料整理、设计生成、资源处理、Blender 脚本和前端代码连接到了同一条工作流程里。我的工作则是不断把目标说具体:这块墙需要什么边缘,这台设备应该怎样支撑,这块屏幕点击后该发生什么。
等这些问题逐个有了实现,展厅才会从一张令人期待的设计图,逐渐变成一个能观察、能进入、能阅读和操作的空间。这也是我继续用 Codex 做三维可视化时,最看重的部分。
我已经把3D智慧展厅的资料和对应源码整理好了,有需要的可以关注后私信我智慧展厅获取