Three.js 图片 + 数据 三维查看器
何时使用
用户想把图片放进三维空间查看,并且要在图片上面/旁边显示数据(文字标注、热点、
指标)。本技能把 Three.js 常用子系统(渲染器、场景、相机、几何体、材质、纹理、加载器、轨道控制器、
精灵标签)封装成一个极简 API,让用户不需要手写样板代码即可交付一个可交互的 3D 查看页面。
> 本地源码基座:/Users/yiban/Desktop/github/three.js(v0.186.0)。
> 关键参考:src/renderers/WebGLRenderer.js、src/objects/Mesh.js、
> src/geometries/PlaneGeometry.js、src/materials/MeshBasicMaterial.js、
> src/materials/SpriteMaterial.js、src/objects/Sprite.js、
> src/textures/CanvasTexture.js、examples/jsm/controls/OrbitControls.js。
交付形态
- 一个
index.html(含 import map,指向 three 的 build 与 examples/jsm)+main.js(用本技能的
assets/image3d.js辅助模块)。 - 需要图片文件与数据(JSON/JS 对象)。
- 本地静态服务器运行即可(
python3 -m http.server等)。
工作流(按用户需求裁剪)
步骤 0:确认需求
向用户确认:
- 图片来源(本地路径 / URL / dataURL / 是否需要我生成占位图)。
- 数据形态:是"烤"在图片像素上(CanvasTexture,导出 PNG 也带数据),还是"悬浮 3D 标签"
(Sprite,始终朝向相机,适合交互高亮);通常两者结合。
- 单图还是多图画廊。
- 是否需要自动旋转、网格参考、截图导出。
步骤 1:准备文件
- 复制
assets/image3d.js到项目目录(或让 index.html 直接引用本技能内的该文件)。 - 生成/准备示例图片(可用 Python PIL 生成占位图,参考下方"生成示例图")。
- 整理数据为 JS 对象数组(见数据格式)。
步骤 2:写 index.html
<!doctype html>
<html lang="zh">
<head>
<meta charset="utf-8" />
<style>#app{position:fixed;inset:0}body{margin:0;background:#0e1018}</style>
<!-- import map:默认走 CDN;离线可改为本地 build 路径 -->
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.186.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.186.0/examples/jsm/"
}
}
</script>
</head>
<body>
<div id="app"></div>
<script type="module" src="./main.js"></script>
</body>
</html>
> 离线使用:把 three.js/build/three.module.js 连同 three.js/build/three.core.js(module 构建内部会
> import './three.core.js',缺它会 404)以及所需 addons(如 examples/jsm/controls/OrbitControls.js,
> 保持 vendor/jsm/controls/ 目录结构)拷到项目 vendor/,import map 改成
> "three": "./vendor/three.module.js"、"three/addons/": "./vendor/jsm/"。
步骤 3:写 main.js(核心调用)
import { createImage3DViewer, makeTextSprite } from './image3d.js';
const viewer = createImage3DViewer(document.getElementById('app'), {
background: 0x0e1018,
autoRotate: true,
enableGrid: true,
});
// 1) 把数据"烤"到图片上(导出 PNG 也带数据)
const mesh = await viewer.addDataOnImage({
url: './sample.jpg',
data: [
{ text: '峰值 128', x: 0.5, y: 0.8, color: '#ffd54a', fontSize: 36 },
{ text: '● 异常点', x: 0.3, y: 0.4, color: '#ff5a5a', type: 'dot', r: 12 },
],
});
// 2) 把数据以 3D 标签悬浮在图片上(始终朝向相机)
viewer.addDataLabelsOnImage(mesh, [
{ text: '区域A 销量 +23%', u: 0.5, v: 0.8, color: '#7ee787' },
{ text: '区域B 待跟进', u: 0.3, v: 0.4, color: '#ffa657' },
]);
// 多图画廊(可选)
// await viewer.addGallery([{url:'./a.jpg'},{url:'./b.jpg'}]);
步骤 4:运行 & 校验
cd <项目目录> && python3 -m http.server 8080
用浏览器打开 http://localhost:8080,确认:图片显示、可用鼠标拖拽旋转 / 滚轮缩放 / 右键平移、
数据(烤图 + 悬浮标签)显示正确。可用 viewer.snapshot() 导出当前视角 PNG。
扩展:帧序列逐帧对比 + 参数叠加(framecmp.js)
适合"汽车部件逐帧检视/对比"场景:拖动进度条逐帧切换,画面叠加实时参数 HUD,可选右侧"对比基线"副视图。
import { createFrameCompareViewer } from './framecmp.js';
const cmp = createFrameCompareViewer(document.getElementById('app'), {
frames: ['./frames/frame_000.png', /* ...逐帧序列图片 */],
params: [ { angle:0, torque:120, temp:94, pressure:2.4, wear:0, rpm:900, status:'正常' }, /* 与帧对齐 */ ],
width: 3.4,
paramKeys: ['frame','angle','torque','temp','wear','status'], // 指定 HUD 显示顺序
});
cmp.setFrame(18); // 切帧(同时刷新 HUD)
cmp.setCompare(true); // 右侧显示"基线·第0帧"副视图
cmp.play(); // 自动播放
- 内置 UI:左上参数 HUD(实时刷新)、底部控制条(播放/暂停、拖动进度条、帧读数、对比基线开关)。
- 底层复用
createImage3DViewer,故图片仍在三维空间、可拖拽旋转/缩放。 - 若图片数量大、切换卡顿,可改用
VideoTexture或预渲染到离屏 canvas 再CanvasTexture。
数据格式
frames[i]与params[i]一一对应。- 参数键自动配中文标签(angle 角度 / torque 扭矩 / temp 温度 / pressure 压力 / wear 磨损 / rpm 转速 / status 状态),未知键原样显示;单位自动追加(° / N·m / °C / bar / % / r/min)。
扩展:真·3D 实体部件(wheel3d.js)— 需要看到"厚度/宽度"时
关键认知:平面图片(PlaneGeometry 贴图)是有"厚度极限"的——把图片旋转 90° 侧看只能
看到一条边,无论怎么调都做不出轮胎的宽度/厚度。这不是 Three.js 的能力问题,而是"贴图平面"
这种载体的物理极限。Three.js 本身就是完整的 WebGL 3D 实体引擎,要表现厚度必须改用
实体几何建模(Torus / Cylinder / Box 等真实网格)。
assets/wheel3d.js 就是这样的实体建模封装(以汽车轮子为例):
import { createWheelViewer } from './wheel3d.js';
const cmp = createWheelViewer(document.getElementById('app'), {
size: 3.0, // 轮子整体尺寸
spokes: 5, // 辐条数
compare: true, // 提供"对比基线"第二只轮子(停在第0帧)
background: null, // 透明渲染,露出容器 CSS 渐变(影棚感)
});
cmp.setFrame(10); // 帧 = 轮子绕轴转角(0°/10°/.../350°),同时刷新 HUD 参数
cmp.setCompare(true); // 右侧显示基线轮子
cmp.play(); // 自动旋转播放
- 轮胎 =
TorusGeometry(真实截面厚度,任意角度斜看都是立体圆环) - 轮毂面 =
CylinderGeometry(metalness 0.95 金属材质)+ 凸出的BoxGeometry辐条 - 中心毂 / 5 颗螺栓 / 刹车盘 + 24 条通风槽(随帧旋转)/ 固定红色卡钳(不转)
- 光照:Hemisphere + 双 Directional + RoomEnvironment 环境贴图(PMREM IBL,金属反射;
需 vendorexamples/jsm/environments/RoomEnvironment.js,构造无参数,
new THREE.PMREMGenerator(renderer).fromScene(new RoomEnvironment(), 0.04).texture) - 默认取景为 3/4 视角(
frameOn把相机放在斜上方),一打开就能看出立体 - 数据叠加沿用:左上 HTML HUD 实时参数 +
makeTextSprite3D 标签锚定部件旁
> 选型口诀:数据贴图、看内容 → image3d/framecmp(平面);看结构、看厚度、看装配 →
> 实体几何建模(wheel3d 思路)。两者可混用(实体 + Sprite 标签 + HUD)。
数据格式约定
- addDataOnImage.data[]:
{ text, x, y, color, fontSize, type, r } x,y:图片归一化坐标(0..1,左上原点;内部已做 v 翻转)。type:'dot'画圆点(配合r),否则画文字。- addDataLabelsOnImage.hotspots[]:
{ text, u, v, color, fontSize } u,v:图片归一化坐标(0..1),标签定位在图片对应位置并跟随图片变换。
生成示例图(PIL,无需联网)
from PIL import Image, ImageDraw
im = Image.new('RGB',(1024,640),(18,22,34))
d = ImageDraw.Draw(im)
d.rectangle([120,120,520,420], outline=(90,110,160), width=3)
d.ellipse([600,200,900,500], outline=(120,200,160), width=3)
im.save('sample.jpg')
能力边界 / 注意
- 浏览器同源策略:跨域图片需服务器允许 CORS,或把图片放本地同目录。
- 大图建议先压缩(>4096px 边宽部分 GPU 不支持),
CanvasTexture受 canvas 尺寸限制。 - 默认世界尺寸:
addImage/addDataOnImage不传width时,平面最大边默认 4 个世界单位
(等比缩放,不再把图片像素直当世界单位);需要精确尺寸请显式传width。 - 本技能默认 MeshBasicMaterial(不受光照影响,所见即所得);如需立体打光感可改
image3d.js中材质为MeshStandardMaterial并保留enableLights。 image3d.js已封装:渲染器、相机、OrbitControls(阻尼+自动旋转)、自适应尺寸、
addImage / addDataOnImage / addDataLabelsOnImage / addGallery / frameContent / snapshot / dispose。