news 2026/10/24 7:35:02

TensorFlow.js与Three.js实战:摄像头驱动的实时3D交互与AI人体识别

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
TensorFlow.js与Three.js实战:摄像头驱动的实时3D交互与AI人体识别

简介:面向前端开发者与Web端AI爱好者的摄像头交互创意项目,完整演示了TensorFlow.js与Three.js的工程化结合思路。项目在浏览器中实时运行PoseNet人体姿态识别、FaceMesh面部特征点检测与BodyPix人体分割三类模型,将关键点、网格与分割结果交由Three.js渲染引擎驱动三维场景,可实现人体姿态跟随、面部表情映射到虚拟角色、手势与场景物体碰撞等交互效果。源码共147个文件,压缩包约432KB,核心构成为39个JavaScript脚本、29个Vue组件,以及21个片元着色器(fs)与16个顶点着色器(vs),另含obj/mtl格式三维模型、多张PNG/JPG纹理贴图和一份附赠资源文档。目录按功能模块划分,便于定位模型加载、着色器渲染与交互逻辑代码。已有65人学习下载,适合希望系统掌握TensorFlow.js模型部署、WebGL着色器编写及Three.js场景整合的中高级前端开发者,可直接复跑并作为二次开发参考。

1. 摄像头一开,TensorFlow.js 与 Three.js 就把你变成了 3D 舞台的主角:PoseNet、FaceMesh、BodyPix 各管一段

摄像头里的画面不再只是一帧帧像素,而是一组组坐标。这个基于 TensorFlow.js 和 Three.js 的网页交互项目,做的就是这件事:浏览器打开页面,摄像头捕捉你的动作,PoseNet 识别人体 17 个关键点,FaceMesh 抓出面部 468 个特征点,BodyPix 把人物和背景逐像素分离,Three.js 拿到这些数据后驱动一个 3D 角色、一群粒子或一块会变形的网格。整个过程零安装、纯网页端推理,视频流不出本地。想快速验证一个交互创意、做数字艺术装置或互动营销页的人,这套技术栈是目前成本最低的落地路径。下面的内容按我的实际项目经验拆解,从环境搭建到参数调优,最后落在性能瓶颈和创意方向上。

2. 技术选型:TensorFlow.js 推理 + Three.js 渲染,数据怎么从摄像头流到屏幕

2.1 为什么是浏览器原生方案,而不是 Python + Unity

很多熟悉 OpenCV 和 MediaPipe 的人会先想到 Python 后端,再把识别的坐标用 WebSocket 推给前端 3D 渲染。这个方案不是不能做,但每路摄像头都要占一个后端进程,视频流要转码、推流,延迟至少多出几十毫秒,部署时还要处理并发和鉴权。对单人或少数人同时访问的创意演示项目来说,完全是杀鸡用牛刀。

TensorFlow.js 的优势在于模型推理直接发生在浏览器里的 WebGL 上下文中,视频帧不需要离开设备。Three.js 跑在同一个页面里,AI 输出的归一化坐标可以直接喂给 Object3D,省掉了中间网络传输和序列化。两者共享同一块 GPU,虽然是抢资源,但数据通路短,交互是实时的。

另外浏览器天然解决了摄像头权限问题,一行navigator.mediaDevices.getUserMedia就能拿到流,这在 Electron 或原生 App 里反而更麻烦。对有 Web 基础的人来说,这套栈的学习曲线相当平滑。看过的 threejs 菜鸟教程大多停在旋转立方体,真正到 threejs 真实案例这一步,卡住的往往不是渲染 API,而是怎么把 AI 模型的输出对齐到 3D 坐标系。这就是下面要展开的部分。

2.2 三个模型的内部逻辑与分工

这三个模型都属于轻量级 CNN,运行在 TensorFlow.js 的 WebGL 后端,但输出形态完全不同。PoseNet 输出 17 个关键点,每个点是一个{x, y, score}对象,坐标是相对输入图像尺寸的归一化值(0 到 1),没有真正的深度信息。适合用来驱动角色的四肢和躯干,做动作镜像或姿态识别。它的可选参数里有个multiplier,控制 MobileNet 的宽度系数,0.75 和 1.0 我都会在真实项目里试,后面详细说。

FaceMesh 的输出是 468 个 3D 特征点,这里需要注意:它返回的并不是像素坐标,而是一个相对坐标系,x、y 范围约在 -1 到 1,z 是相对深度。嘴巴、眼睛的关键点索引是固定且有规律的,比如嘴角附近是 61 和 291 号点,左右眼角是 33 和 263 号点,这个映射关系在做表情驱动时非常有用。

BodyPix 和前两者完全不同,它做的是逐像素语义分割。segmentPerson返回一个 PersonSegmentation 对象,里面有mask和allPixels,mask 是一个二值化的 ImageData,标记哪些像素属于人。它还有 23 个部位级别的分割能力,不过实际项目中二值图就够用了。表格里我总结了三个模型的核心差异,方便按需选型。

模型输出内容坐标系典型应用
PoseNet17 个关键点 + 置信度归一化 [0, 1] 像素比例动作驱动、骨骼绑定
FaceMesh468 个 3D 点相对坐标 [-1, 1] + z 深度面部表情、面具、虚拟形象
BodyPix像素级 mask / 部位标签图像像素坐标背景替换、抠像特效

性能上,在普通笔记本的 720p 输入下,三个模型全部推理一次大约要 40 到 80 毫秒,其中 BodyPix 最重,PoseNet 次之,FaceMesh 最轻。这个数据只做参考,实际取决于 GPU 和当前 Three.js 渲染的负载。

2.3 数据流:一帧画面怎么变成 3D 动画

整个管线可以拆成五步,理解了这条链路,后面调参会有的放矢。第一步,摄像头视频流进入 HTML 的<video>元素。第二步,把当前帧用canvas 2d画到一个较小的画布上,这个缩小后的画布是 AI 模型的输入,通常控制在 256 到 512 像素宽,直接拿 720p 视频帧做推理是新手最常见的性能坑。第三步,tf.browser.fromPixels把画布变成 Tensor,交给三个模型推理。第四步,模型输出被转成 JavaScript 数组或 TypedArray,映射到 [-1, 1] 或 [0, 1] 的坐标范围。第五步,Three.js 场景里的骨骼、网格或粒子系统根据这些坐标更新位置、旋转和顶点。

这里有个经常被忽略的细节:网页里摄像头画面是镜像的,AI 模型并不知道这一点,如果不做水平翻转,你会看到 3D 角色抬左手,屏幕上抬的是右手。后文代码会在初始化时用flipHorizontal: true,即模型内部直接做镜像推理,避免在 Three.js 侧再翻转三维坐标。另外,AI 推理是异步的,而 Three.js 的requestAnimationFrame是同步的 60Hz 循环,两条线必须显式做同步,否则会出现角色抽搐或姿势滞后半秒的问题。

3. 最小可运行工程:搭建摄像头、三模型与 Three.js 场景

3.1 环境准备:目录结构与本地服务器

用 Vite 初始化一个空项目是最快的路径。不要直接双击 HTML 文件打开,ES Module 语法会被浏览器拦截,而且getUserMedia在非安全上下文里直接失效。本地开发时用npm run dev起一个 localhost 服务,生产部署时再考虑 HTTPS 或内网穿透。

项目里我一般把模型文件和静态资源分开,TensorFlow.js 的模型文件默认从 CDN 加载,如果需要离线内网使用,要把模型权重下载到public/models目录下,然后用load方法的modelUrl参数指向本地地址。目录结构大致是这样:

project-root/ ├── index.html ├── src/ │ ├── main.js # 入口,负责初始化和主循环 │ ├── camera.js # 摄像头封装 │ ├── models.js # 三个模型加载与推理 │ └── scene.js # Three.js 场景构建 ├── public/ │ └── models/ # 离线模型权重(可选) └── package.json

依赖我一般固定在@tensorflow/tfjs、@tensorflow-models/posenet、@tensorflow-models/facemesh、@tensorflow-models/body-pix、three这五个包上,版本号每次都是重新安装时最新的,但这几个模型库的版本兼容性问题并不少,如果发现某个模型加载报错,先把所有相关包按同一天发布的版本固定住,再逐个升级。

3.2 摄像头接入与三个模型初始化:必调的参数

摄像头初始化代码没什么悬念,关键是facingMode: 'user'用前置摄像头、宽高用 640x480。分辨率不需要太高,AI 推理输入还要再缩小一轮,原始流太大会白白增加内存和功耗。

// src/camera.js export async function initCamera(videoElement) { const stream = await navigator.mediaDevices.getUserMedia({ video: { width: 640, height: 480, facingMode: 'user' // 前置摄像头,和镜像逻辑对应 }, audio: false }); videoElement.srcObject = stream; await videoElement.play(); return stream; }

这段代码的逻辑是:getUserMedia返回一个 MediaStream,直接赋给<video>元素的srcObject属性。video.play()必须等待用户设备授权完成,所以用了await。常见的失败场景是页面没有 HTTPS,浏览器直接拒绝授予摄像头权限,这个后面避坑章节再展开。

模型初始化放在另一个文件里,负责任务分离。加载参数是核心,我先给出完整代码再拆开解释:

// src/models.js import * as posenet from '@tensorflow-models/posenet'; import * as facemesh from '@tensorflow-models/facemesh'; import * as bodyPix from '@tensorflow-models/body-pix'; export async function loadModels() { const poseNet = await posenet.load({ multiplier: 1.0, // 1.0 精度高,0.75 速度快 quantBytes: 4 // 权重量化字节数,4 为最准 }); const faceMesh = await facemesh.load({ maxFaces: 1 // 同时检测的最大人脸数,1 就够 }); const bodyPixModel = await bodyPix.load({ architecture: 'MobileNetV1', multiplier: 0.75, // 宽度系数,越小越快 stride: 16, // 输出步长,8 精度高但慢 quantBytes: 4 }); return { poseNet, faceMesh, bodyPixModel }; }

注意这几个参数会直接影响帧率。PoseNet 的multiplier从 1.0 降到 0.5,推理时间能省一半,但关键点抖动会明显增加。我的习惯是先用 1.0 验证交互逻辑,确认没问题时再调小,而不是一开始就追求快。FaceMesh 的maxFaces默认参数在依赖库内部已经在变,强制写成 1 是为了行为可预期。

BodyPix 的stride是个容易误解的参数。它表示输出特征图的步长,16 表示输出分辨率是输入分辨率除以 16,这直接影响分割边缘的精细度。做背景替换时用stride: 16够用,做像素级特效时改成 8 边缘会好很多,但帧率会掉。

3.3 把视频帧喂给模型:推理主循环的最小代码

三个模型虽然输出形态不同,但输入的预处理是一样的。先把视频帧缩小绘制到 2D 画布,再转成 Tensor。2D 画布建议设置willReadFrequently: true,告诉浏览器这个画布会被频繁读取像素,避免每次都走慢速路径。

// src/main.js 主循环核心片段 async function inferenceLoop() { const inputWidth = 256; const inputHeight = 192; aiCanvas.width = inputWidth; aiCanvas.height = inputHeight; aiCtx.drawImage(video, 0, 0, inputWidth, inputHeight); const input = tf.browser.fromPixels(aiCanvas); const resized = tf.image.resizeBilinear(input, [224, 224]); const img = resized.expandDims(0); const poseResult = await poseNet.estimateSinglePose(img, { flipHorizontal: true, // 摄像头是镜像的,关键点也要镜像 decodingMethod: 'single-person' }); const faceResult = await faceMesh.estimateFaces({ input }); const bodyResult = await bodyPixModel.segmentPerson(img); tf.dispose([input, resized, img]); // 把结果交给 Three.js 场景更新 updateScene(poseResult, faceResult, bodyResult); requestAnimationFrame(inferenceLoop); }

这段代码里有几个值得展开的点。

第一,输入尺寸没直接用 256x256,而是先drawImage到 256x192,再resizeBilinear到 224x224,这是为了保持摄像头 4:3 的比例,避免画面拉伸变形。resizeBilinear这一步其实可以去掉,直接把画布设为 224x224 再绘制即可,但那样视频会被纵向拉伸,关键点坐标虽然不会错,贴图时会很别扭。

第二,flipHorizontal: true必须和视频画面方向一致。前置摄像头天然是镜像的,如果这里设了 true,但后续用 Three.js 纹理贴视频时又翻转一次,动作方向就会相反。建议这里固定设 true,在 Three.js 侧直接用最终坐标角度,不重复翻转。

第三,tf.dispose是必须的。如果不释放中间 Tensor,几秒钟后 GPU 内存就会被打满,页面变得巨卡甚至黑屏。这是所有 TensorFlow.js 项目最常见的性能杀手之一,后面避坑章节再细说。

第四,PoseNet 的estimateSinglePose只检测一个人,如果画面里有多个目标,会取最大置信度的那一个。多人场景要用estimateMultiPose,但输入分辨率会翻倍,性能开销很大。创意演示项目里可以直接用 single-person,专注做单人交互,别给自己找事。

4. PoseNet、FaceMesh、BodyPix 三个模型与 Three.js 的对接实战

4.1 PoseNet 驱动 3D 角色:坐标系的换算与缩放基准

拿到 PoseNet 的 17 个关键点之后,最直接的做法是把它们当成 3D 空间里的球体或骨骼节点。但这里有个坐标陷阱:position.x和position.y是相对于输入图像的归一化比例(0 到 1),而 Three.js 场景里的坐标范围取决于相机设置,通常是 [-10, 10] 或 [-5, 5]。直接相乘会导致角色飞出屏幕。

我习惯的做法是把关键点映射到 [-1, 1],然后乘一个场景半径常量。这个常量不是随便定的,取决于初始相机距离。

const keypoints = poseResult.keypoints; const leftShoulder = keypoints[5].position; const rightShoulder = keypoints[6].position; const nose = keypoints[0].position; // 以肩宽作为缩放基准,确保人走近走远角色不暴走 const shoulderDist = Math.hypot( leftShoulder.x - rightShoulder.x, leftShoulder.y - rightShoulder.y ); const scaleBase = shoulderDist || 0.001; // 归一化到 [-1, 1],水平方向镜像已由 flipHorizontal 处理 const x = (leftShoulder.x - 0.5) * 2; const y = (0.5 - leftShoulder.y) * 2; // y 轴翻转 // 映射到 Three.js 场景 character.position.x = x * radius; character.position.y = y * radius; character.position.z = (nose.x - 0.5) * 2 * radius * 0.3;

这里最关键的逻辑是y轴要手动翻转。图像坐标的 y 向下,Three.js 的 y 向上,所以0.5 - y。这一步写错,角色会上下颠倒。用肩宽作为缩放基准是我的一个习惯,而不是直接用鼻子的 y 坐标做缩放,因为人在画面里的远近变化会同时影响所有关键点,肩宽变化相对稳定且能反映真实距离。

需要注意,PoseNet 的 z 值在single-person模式下基本不可靠,我一般只用它做很小的深度偏移,而不是真的把角色拉到屏幕外。如果要做跟随人体的相机,更稳妥的方式是用肩宽反推人与摄像头的距离,再用 Three.js 相机位置去插值。

4.2 FaceMesh 做面具:从 468 个点到三角网格的映射

FaceMesh 的输出直接包含三角形索引,这对做 3D 面具非常友好。faceResult是一个数组,其中[0].scaledMesh是 468 个点的坐标。把这些点填进BufferGeometry的 position 属性,再用setIndex设置三角形顺序,Three.js 就能绘制出一张人脸网格。

const mesh = faceResult[0].scaledMesh; const positions = new Float32Array(mesh.length * 3); const uvs = new Float32Array(mesh.length * 2); for (let i = 0; i < mesh.length; i++) { const [x, y, z] = mesh[i]; const fx = -x * 6; // 水平镜像并放大 const fy = -y * 6; // y 轴翻转 const fz = (z - 100) * 0.5; // z 缩小并平移 positions[i * 3] = fx; positions[i * 3 + 1] = fy; positions[i * 3 + 2] = fz; // UV 用于把摄像头画面贴到面具上 uvs[i * 2] = x * 0.5 + 0.5; uvs[i * 2 + 1] = y * 0.5 + 0.5; } const geometry = new THREE.BufferGeometry(); geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); geometry.setAttribute('uv', new THREE.BufferAttribute(uvs, 2)); geometry.setIndex(facemeshOutput.triangles);

这里的核心逻辑是 FaceMesh 的坐标是相对值,范围不固定,不能直接用。我乘 6 不是拍脑袋,而是先量一下默认人脸在画面里占的像素比例,再换算到 Three.js 的场景半径。fz的平移量做了一点偏移,让脸膜不至于穿进相机近裁剪面。UV 直接使用 x 和 y 映射到 0 到 1 区间,因为 FaceMesh 的 x、y 本来就是人脸在画面里的相对坐标,转成 UV 贴视频画面是顺理成章的事。

这个方案最值得炫耀的地方是表情驱动。468 个点里,嘴唇、眼睛周围的点在表情变化时会移动,而网格本身会跟着变,所以面具能实时呈现眨眼、张嘴等动作。但注意,如果把人脸当成一个平面 Mesh 贴在 Three.js 场景里,背景必须保持透明,否则画面会被面具矩形挡住一半。

4.3 BodyPix 做人体分割:mask 怎么变成视觉特效

BodyPix 的输出是一个 ImageData 格式的 mask,我的做法是把它转成一个透明纹理,作为 Three.js 平面材质的 alphaMap,或者直接在 2D Canvas 上做背景替换后,再作为一张贴图展示。

前者的好处是特效可以叠加在 3D 空间里。比如说你想让“人体轮廓”变成粒子发射器,或者让背景变成旋转的星空,就可以用 mask 来确定哪些区域显示人物,哪些区域显示背景。

async function updateBodyPix(videoElement) { const segmentation = await bodyPixModel.segmentPerson(videoElement); const maskCanvas = document.createElement('canvas'); maskCanvas.width = videoElement.videoWidth; maskCanvas.height = videoElement.videoHeight; const ctx = maskCanvas.getContext('2d'); ctx.clearRect(0, 0, maskCanvas.width, maskCanvas.height); // segmentation.mask.toBinaryMask() 得到 RGB 二值图 ctx.drawImage(segmentation.mask.toBinaryMask(), 0, 0); // 把画布作为 alphaMap 传给材质 if (spriteMaterial.alphaMap) { spriteMaterial.alphaMap.dispose(); // 避免累计 GPU 内存 } spriteMaterial.alphaMap = new THREE.CanvasTexture(maskCanvas); spriteMaterial.transparent = true; }

这段代码里最容易被忽略的是dispose旧纹理。如果不释放,每一次推理都会生成一个新的 CanvasTexture,GPU 内存会被慢慢吃光,几十秒后帧率骤降。我刚开始做的时候没注意到,结果页面运行两分钟就开始卡顿,后来在 Chrome 的chrome://gpu面板里看到纹理数量飙升,才知道内存泄漏。

还有一个实用技巧:mask.toBinaryMask()返回的是一张黑底白人的图像,白色代表人体区域。如果你想让人物周围产生发光特效,可以在 2D 里对这个 mask 做ctx.filter = 'blur(12px)'再叠加一层,视觉冲击力立刻不一样。

BodyPix 不建议逐帧调用。如果场景里没有严重的遮挡变化,我会每三帧推理一次,中间帧用上一次的结果凑合,观感几乎没有差别,但帧率能提高不少。

5. 踩坑与排查:模型加载失败、帧率个位数、关键点抖动

5.1 模型加载永远报 404,页面白屏

现象:打开页面后模型一直转圈,控制台报Failed to fetch model.json或直接 404,三个模型全部加载失败。

原因:最常见的不是代码问题,而是模型默认从 Google 的 CDN 加载,内网或部分网络环境无法访问。项目没有内置模型的兜底方案。

解决:把模型文件下载到本地的public/models目录,然后在load方法里显式指定modelUrl。PoseNet 的 modelUrl 是整个模型文件夹的地址结构,比如/models/posenet/model.json,FaceMesh 和 BodyPix 也有类似参数。另外,有些情况下 404 是因为模型已经下载到一半被浏览器缓存了不完整文件,清一下缓存再试。

5.2 帧率个位数,GPU 却跑不满

现象:页面能跑,但帧率只有 5 到 8 FPS,查看任务管理器 GPU 使用率不到 30%,整个页面明显卡顿。

原因:有两个。第一,AI 推理和 Three.js 渲染都在同一个线程里跑,推理是异步的但需要等同步返回,这个await会阻塞渲染循环。第二,每个推理帧都新建了大量 Tensor 和 Texture,GPU 内存和对象池反复分配,GC 频繁触发停顿,GPU 反而在等待内存。

解决:把推理主循环从渲染循环里拆出来。单独用setInterval控制推理频率,比如每 100 毫秒推理一次,而 Three.js 的requestAnimationFrame只负责读取最新一次推理结果并渲染。这样渲染循环不被阻塞,帧率能回到 30 甚至 60。注意推理结果要用变量缓存,渲染线程只读取,不重新计算。

5.3 关键点左右横跳,像得了帕金森

现象:角色骨骼或面具在静止时快速抖动,幅度不大但非常明显,动作越大抖动越剧烈。

原因:模型输出本身带有随机噪声,输入分辨率越低噪声越明显。另外一个关键因素是estimateSinglePose对每帧独立计算,没有时间上的连续性,所以哪怕画面完全静止,关键点也会有几个像素的随机漂移。

解决:做时间域平滑,最常见的是一阶低通滤波,也叫指数移动平均。新位置 = 旧位置 * 0.7 + 新位置 * 0.3,系数可以根据噪声水平调整。更专业一点用 OneEuroFilter,它能同时解决低频漂移和高频抖动。我在驱动角色骨骼时,对肩、肘、腕这几个大关节用了系数 0.2,对鼻尖这种对精度要求高的部位用 0.6。

5.4 FaceMesh 面具贴图错乱,眼睛闭不上

现象:面具能显示,但嘴巴张合和眼睛动作扭曲,眼角的顶点和嘴巴附近的顶点相互拉扯,贴图位置完全不对。

原因:FaceMesh 的三角形索引是建立在“模型已归一化”基础上的,但我在拼 UV 的时候用了x和y直接映射,导致 UV 和顶点位置对不齐。特别是网格有大量三角面,任何一个顶点的 UV 错位,整片区域都会变形。

解决:UV 映射不要用原始 x 和 y,而要用和 position 一样的变换。例如我把x翻转后作为 position 的fx,那 UV 的 u 轴也应该用同一套变换的归一化结果。正确做法是 UV 的 u 使用(x + 1) / 2,v 使用(y + 1) / 2,而不是直接用原始坐标。这个问题排查起来非常费眼睛,建议写一个简单的调试模式,把 UV 和外框同时显示出来,一眼能看出对齐问题。

5.5 移动端打开黑屏,摄像头有画面但 Three.js 场景不显示

现象:PC 上一切正常,手机浏览器打开后视频正常播放,但 Three.js 场景全黑,没有报错。

原因:移动端 GPU 对 WebGL 纹理的尺寸有上限,通常是 2048x2048 或 4096x4096。摄像头画面的原始videoWidth和videoHeight在部分手机上会非常大,直接作为纹理超出 GPU 限制,纹理创建失败,渲染全黑。

解决:在给 Three.js 创建纹理之前,先强制把摄像头画面绘制到一个最大尺寸不超过 1024 的中间 Canvas 上,再把这个 Canvas 作为 Texture 源。这是最粗暴但最有效的兼容方案,我在 iOS Safari 和部分安卓 Chrome 上都验证过。

6. 进阶方向:隔帧推理、关键点平滑与创意玩法

当三个模型都能正常驱动时,项目会进入另一个阶段的瓶颈:如何让交互更顺滑、更有视觉效果。我的习惯是先做性能预算,再做创意装饰。核心理念是 AI 推理的刷新率不一定等于渲染帧率。把 PoseNet 和 BodyPix 降到每 3 帧推理一次,FaceMesh 保持每帧推理但只更新嘴部关键点,这样视觉上几乎无感,性能却能翻倍。配合上一章提到的指数平滑,能让动作曲线自然不少。

创意玩法的方向其实不少。PoseNet 的关键点可以映射到一组粒子系统,让粒子在肩膀、肘部周围环绕飞行,形成线框骨架,这是很多 threejs 真实案例里的方案。FaceMesh 可以做表情驱动变形,比如把嘴巴张合幅度映射成模型的夸张形变,甚至让模型的脸部拓扑完全由真实表情驱动。BodyPix 最实用的是把人从背景抠出来,在 3D 空间里漂浮一个实时“人体贴纸”,再配合背景粒子或扭曲效果,这个效果用在互动营销里有很强的视觉冲击力。

不过这些玩法的共同前提是基础管线稳定。我在第一个完整项目里,花了太多时间在调参和抠细节上,后来发现真正让项目跑起来才是最大的门槛。把推理和渲染解耦,把平滑算法提前,把纹理泄漏问题处理干净,这套地基打好之后,任何创意都只是往场景里加 Mesh 的问题。

写到这里想起一个习惯:我会在每次改动代码后,把摄像头的流关掉再重开一次,确认内存泄漏问题没有复发。用stream.getTracks().forEach(track => track.stop())可以做到。这不算什么高深技巧,但救过我很多次。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/24 7:35:01

极简日记应用caveman:纯文本存储与git同步的设计实践

每次有人瞄到我电脑里那个叫caveman的文件夹&#xff0c;第一反应基本都是笑&#xff1a;都什么年代了&#xff0c;还拿这么原始的办法记日记&#xff1f;等他们真看清楚里面的东西&#xff0c;又沉默了——三个 Markdown 文件、一个纯文本统计脚本、一套 git 自动提交的备份任…

作者头像 李华
网站建设 2026/10/21 20:11:00

基于机器学习的Web日志异常检测工具:配置、实战与避坑指南

简介&#xff1a;这是一套面向安全运维与日志分析学习者的命令行Web日志审计工具&#xff0c;基于Python实现&#xff0c;将日志统计、终端可视化与机器学习恶意请求识别整合在一起&#xff0c;适合具备Python基础、希望上手日志审计与异常检测实战的开发者。资源包共63个文件&…

作者头像 李华
网站建设 2026/10/14 3:22:08

1982-2024年地级市逐月气温面板:ERA5再分析数据处理与实证研究全流程

简介&#xff1a;这份资源为1982至2024年地级市逐月平均气温数据集&#xff0c;面向气象气候研究、区域经济实证与深度学习建模等场景的科研人员和数据分析者&#xff0c;用于分析城市气候长期变化规律、支撑面板数据回归与时间序列建模。压缩包共13个文件&#xff0c;以xml表格…

作者头像 李华
网站建设 2026/10/24 0:18:24

识别钓鱼源码包:二手平台仿冒页面的伪装与防范

简介&#xff1a;这是一份以二手交易平台为蓝本、仿转转和闲鱼界面的完整站点源码&#xff0c;附带独立后台和个人二维码收款模块&#xff0c;适合Web前端开发者、PHP学习人员以及需要研究移动端商城交互的技术爱好者。压缩包共355个文件&#xff0c;体积约11.85MB&#xff0c;…

作者头像 李华
网站建设 2026/10/23 7:51:53

实测AI编程代理pi:从代码补全到自主执行的真实体验

最近AI编程助手圈子是真热闹&#xff0c;前脚Copilot、Cursor这些还在卷补全和对话&#xff0c;后脚就冒出来一批新的代码代理工具。我这一周重点试了试其中一个叫pi的coding agent——它跟普通AI插件最大的不同&#xff0c;是它真的自己去读项目、改文件、跑测试&#xff0c;不…

作者头像 李华