ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Three.js创建三维人物:程序化建模、GLB导入与点云方案全解析

Three.js创建三维人物:程序化建模、GLB导入与点云方案全解析 有人问我Three.js能“做”一个人出来吗我的回答是能但这个“做”字背后的门道比想象中多。我最近正好用Three.js给自己搭了一个网页端的虚拟形象从纯代码用几何体拼人到Blender里建模再导出GLB加载再到处理人体点云数据三条路线都摸了一遍。整个过程踩了不少坑模型载入后是黑的、尺寸小到看不见、动画切换时动作抽搐、点云数据加载后卡成幻灯片。这篇文章就是这次实战的记录你可以把它当成一份地图——根据你要做的角色画风、交付时间和最终形态找到最省力的一条路。适合刚接触Three.js的新手也适合已经在用模型但想补上动画、点云这些进阶方向的开发者。1. 动手前先做选择题你的角色走哪条生产路线很多人接到需求的第一反应是打开编辑器写代码但我的建议是先花半小时做选型。原因很简单“在Three.js里做一个人物”至少有三种技术路线它们的代码结构、开发周期、最终效果几乎没有交集选错了方向后面越努力越尴尬。路线核心流程质量上限需要掌握的技能适用场景程序化建模用Geometry、材质在运行时组合角色卡通、低多边形风格Three.js基础几何体、材质、动画快速原型、轻量页面、风格化吉祥物GLB模型导入Blender建模导出glbGLTFLoader加载写实或高精度风格Blender建模、UV贴图、材质烘焙正式产品中的完整可复用角色点云数据PLY/LAS解析Points渲染真实扫描质感数据格式解析、点云处理数据可视化、扫描重建、科研展示1.1 三条路线的本质区别GLB导入路线最接近传统游戏生产流程。你在Blender里捏好模型装配骨骼和动画导出一个文件Three.js端的工作量其实很少主要任务是尺寸适配、动画播放和交互逻辑。这条路的问题是模型资产的生产成本高一个带骨骼、带表情、能跑步跳跃的人物从建模到绑定通常要两三天对个人开发者来说学习曲线也很陡。程序化建模则完全反着来。它不依赖任何外部软件所有内容都在浏览器运行时通过代码创建。拿一堆Box、Sphere、Cylinder拼出一个小人改颜色、改体型、改高度都是参数化修改。它的天花板也很明显只能做风格化角色做不了写实人脸和复杂服饰。点云是第三种思路它是数据驱动的。手机深度相机、工业激光扫描、摄影测量都能生成点云引入的是扫描数据和可视化思维。它的问题是点云没有拓扑结构不能直接绑骨骼通常用于展示而非交互角色。1.2 我的选择参考标准如果目标只是让网页里有个能转能跳的人形吉祥物我闭着眼睛选程序化建模半天出活代码量控制在几百行以内。如果要做正式项目里的数字人、虚拟形象需要复用、换装、做动作那就老老实实走Blender加GLB把资产做扎实。如果交付物本身要求体现三维扫描的真实感或者你手里已经有一批点云数据就走第三条。一个容易被忽略的问题是要先确定画风再选路线。Q版、逗趣风格用几何体拼接很自然写实风格用几何体拼出来的东西会特别廉价而一套写实扫描点云放在一个可爱卡通页面里也会很违和。画风决定了技术路线技术路线又决定了后面所有的开发方式。1.3 时间预算与最终效果的权衡我按经验排了一个粗略的时间表供参考。纯程序化建模模型加动画半天到一天Blender加GLB如果只是单人模型不动画一到两天加动画绑定就要两天以上点云加载数据已经准备好当天能出效果从采集设备开始做就要按周算了。这个时间差背后是资产生产链路的不同不是Three.js本身的差距。想明白这些后面的路就好走多了。2. 纯代码起手用基础几何体组一个能动的人程序化建模是很多人的第一站因为门槛最低。打开编辑器引入Three.js就能开始搭人。它特别适合做Q版角色、网页吉祥物、占位演示模型也适合用来理解“模型在三维空间里到底怎么组合”这件事。2.1 先拆解人体结构再决定用哪些几何体我在代码里做人物时会先把人体拆成几个基本形体头是球体躯干是圆柱或方盒四肢是细圆柱。Q版比例可以夸张一点——头大、身体短、四肢圆圆粗粗这样做出来不仅可爱还能掩盖几何体拼接时的很多瑕疵。比形体更关键的是层级结构。要让角色的手臂能自然摆动手臂不能直接挂在角色根节点上而应该挂在一个位于肩关节的“枢轴组”上。旋转这个枢轴组手臂才真正像以肩膀为轴在动而不是整条手臂原地打转。所有动效流畅与否都取决于这一层父子关系有没有建对。2.2 完整代码从头部到脚底的角色构建函数下面这个函数可以直接复制到你的项目里生成一个可以继续加工的基础Q版人物。我把肩部枢轴和髋部枢轴都留好了后面加动画会非常方便。import * as THREE from three; function createCharacter(options {}) { const root new THREE.Group(); const skinColor options.skinColor || 0xffccac; const bodyColor options.bodyColor || 0x4fc3f7; const pantsColor options.pantsColor || 0x34495e; // 头部 const headGeo new THREE.SphereGeometry(0.28, 32, 32); const headMat new THREE.MeshLambertMaterial({ color: skinColor }); const head new THREE.Mesh(headGeo, headMat); head.position.y 1.38; root.add(head); // 躯干 const bodyGeo new THREE.CylinderGeometry(0.32, 0.24, 0.8, 16); const bodyMat new THREE.MeshLambertMaterial({ color: bodyColor }); const body new THREE.Mesh(bodyGeo, bodyMat); body.position.y 0.95; root.add(body); // 肩部枢轴 左手臂 const shoulderMat new THREE.MeshLambertMaterial({ color: bodyColor }); const leftShoulder new THREE.Group(); leftShoulder.position.set(-0.42, 1.28, 0); leftShoulder.name leftShoulder; root.add(leftShoulder); const leftArm new THREE.Mesh( new THREE.CylinderGeometry(0.06, 0.06, 0.6, 12), shoulderMat ); leftArm.position.y -0.3; leftShoulder.add(leftArm); // 右手臂 const rightShoulder new THREE.Group(); rightShoulder.position.set(0.42, 1.28, 0); rightShoulder.name rightShoulder; root.add(rightShoulder); const rightArm new THREE.Mesh( new THREE.CylinderGeometry(0.06, 0.06, 0.6, 12), shoulderMat ); rightArm.position.y -0.3; rightShoulder.add(rightArm); // 髋部枢轴 左腿 const legMat new THREE.MeshLambertMaterial({ color: pantsColor }); const leftHip new THREE.Group(); leftHip.position.set(-0.13, 0.6, 0); leftHip.name leftHip; root.add(leftHip); const leftLeg new THREE.Mesh( new THREE.CylinderGeometry(0.1, 0.08, 0.62, 12), legMat ); leftLeg.position.y -0.31; leftHip.add(leftLeg); // 右腿 const rightHip new THREE.Group(); rightHip.position.set(0.13, 0.6, 0); rightHip.name rightHip; root.add(rightHip); const rightLeg new THREE.Mesh( new THREE.CylinderGeometry(0.1, 0.08, 0.62, 12), legMat ); rightLeg.position.y -0.31; rightHip.add(rightLeg); return root; } const character createCharacter(); scene.add(character);注意几个细节。圆柱几何体的轴心默认在几何中心所以我把手臂Mesh的position.y设为-0.3让手臂的顶部对齐肩枢轴这样旋转shoulder就等价于以肩膀为轴摆动。角色根节点定位在脚底水平面也就是y0后续做落地判断、相机跟随都会省很多事。2.3 不用贴图也能有表情CanvasTexture画五官给角色做表情我最早想的是从外部加载一张脸部的PNG贴图后来发现一个更灵活的做法用Canvas把五官画出来再通过CanvasTexture贴到头部前方的平面上。这样做的好处是改表情只需要改Canvas绘制代码完全不需要碰图片资源还能做成参数化。function createFaceTexture() { const canvas document.createElement(canvas); canvas.width 256; canvas.height 256; const ctx canvas.getContext(2d); // 肤色背景 ctx.fillStyle #ffccac; ctx.fillRect(0, 0, 256, 256); // 两只眼睛 ctx.fillStyle #2c3e50; ctx.beginPath(); ctx.arc(88, 128, 14, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.arc(168, 128, 14, 0, Math.PI * 2); ctx.fill(); // 微笑的嘴 ctx.strokeStyle #c0392b; ctx.lineWidth 6; ctx.beginPath(); ctx.arc(128, 158, 32, 0.15 * Math.PI, 0.85 * Math.PI); ctx.stroke(); return new THREE.CanvasTexture(canvas); } const faceTexture createFaceTexture(); const facePlane new THREE.Mesh( new THREE.PlaneGeometry(0.4, 0.4), new THREE.MeshBasicMaterial({ map: faceTexture, transparent: true }) ); facePlane.position.set(0, 1.46, 0.27); character.add(facePlane);这里有个细节面部平面要和球体表面保持一点距离否则两个面重叠会产生Z-fighting闪烁。我把平面放到z0.27刚好在球体半径0.28附近视觉上像脸部贴图又不会和球面共面。材质用MeshBasicMaterial是因为这张贴图本身已经带了颜色不需要场景灯光的干涉如果你希望脸部受光影响换成MeshLambertMaterial并适当提高emissive也行。2.4 让角色动起来用正弦函数控制四肢摆动程序化建模的角色没有骨骼动画文件但简单的挥手、走路、跳跃完全可以用数学函数模拟。核心思想是用正弦函数sin(t)的周期性来控制旋转角度相位差则制造左右对称的效果。const clock new THREE.Clock(); function animate() { requestAnimationFrame(animate); const delta clock.getDelta(); const t clock.elapsedTime; const leftShoulder character.getObjectByName(leftShoulder); const rightShoulder character.getObjectByName(rightShoulder); const leftHip character.getObjectByName(leftHip); const rightHip character.getObjectByName(rightHip); // 走路摆动左右互为反相 if (leftShoulder) leftShoulder.rotation.x Math.sin(t * 4) * 0.4; if (rightShoulder) rightShoulder.rotation.x Math.sin(t * 4 Math.PI) * 0.4; if (leftHip) leftHip.rotation.x Math.sin(t * 4 Math.PI) * 0.3; if (rightHip) rightHip.rotation.x Math.sin(t * 4) * 0.3; renderer.render(scene, camera); } animate();相位差Math.PI的意思是当左侧肩膀向前摆时右侧肩膀向后摆两条腿也交替迈步。再叠加一个身体上下起伏就能模拟出走路时的颠簸感。我用root.position.y Math.abs(Math.sin(t * 4)) * 0.05给角色加了一点弹跳这样比光摆四肢要生动得多。3. 高保真路线Blender建模导出GLB再交给Three.js程序化建模适合快速原型但真正要做一个能放进正式产品里的角色资产走Blender加GLB才是正道。这个流程虽然前期建模成本高但资产一次产出之后可以反复复用而且质量上限远超代码拼接。3.1 为什么我更推荐GLB而不是GLTF或OBJThree.js支持多种模型格式但人物角色这块我基本只选GLB。把GLB和GLTF、OBJ放在一起对比就清楚了格式文件构成加载方式适用场景GLB单文件二进制模型、材质、贴图全打包GLTFLoader生产环境首选请求少、加载快glTFJSON bin 外部贴图GLTFLoader调试期可以直接看JSON结构OBJ几何信息 独立材质文件OBJLoader老工具链不做骨骼动画FBX二进制含动画绑定FBXLoader或转换工具美术工具间流转Web端不常用GLB最大的优势是单文件。一个GLB把网格、材质、纹理、骨骼、动画全部打包在一起浏览器只需要发一次请求就能拿到整个角色。GLTF虽然结构开放但纹理是外部文件部署时容易漏文件跨域配置也多一层麻烦。OBJ连蒙皮动画都不支持基本不用考虑了。3.2 Blender里的建模与命名规范在Blender里建模时命名规范直接决定了Three.js端的开发效率。我在Blender里会把人物层级整理成Root、Spine、Head、LeftUpperArm这样的结构并保证所有骨骼名称统一前缀。因为在Three.js里要用getObjectByName去查节点名字对不上就找不到还要逐个遍历排错。导出前有三件事必须做。第一选中所有物体按CtrlA应用全部变换否则模型会在世界里带着未应用的旋转缩放导入Three.js后出现奇怪的偏移。第二检查法线方向在编辑模式按ShiftN重新计算外侧法线这个步骤漏了模型进浏览器就是一片黑。第三如果是用于骨骼动画的人物模型最好是T-Pose或A-Pose方便后面绑定。导出时选择File Export glTF 2.0格式选glb其他参数保持默认。我习惯勾选“Y Up”把模型的Y轴向上这样和Three.js的坐标系一致省去很多旋转补偿。3.3 GLTFLoader加载与尺寸归一化GLTFLoader是Three.js官方提供的加载器用法非常直接。但新手最常见的问题不是加载不起而是模型尺寸和坐标不对。Blender里的单位不同导出的模型在Three.js里可能是巨人也可能是蚂蚁。我习惯加载后立刻用Box3做一次尺寸和位置归一化。import { GLTFLoader } from three/examples/jsm/loaders/GLTFLoader.js; const loader new GLTFLoader(); loader.load(/models/me.glb, (gltf) { const model gltf.scene; // 以人物高度1.8米作为基准做等比缩放 const box new THREE.Box3().setFromObject(model); const height box.max.y - box.min.y; model.scale.setScalar(1.8 / height); // 把模型底部放到地平线 y0 const newBox new THREE.Box3().setFromObject(model); model.position.y - newBox.min.y; scene.add(model); });为什么要把高度设成1.8因为场景里的相机、碰撞体、交互区域都按现实尺度设计角色的统一高度可以让后续所有逻辑有一个稳定的参照。用Box3计算而不是手动调节是因为不同模型的包围盒尺寸千差万别程序算一次比肉眼估准得多。3.4 模型黑面、材质丢失、部件错位三个高频雷区模型进浏览器后一片黑十有八九是法线问题。Blender中某些模型面朝向混乱导出后Three.js按单面渲染看到的就是黑面。修复方式有两种在Blender里重新计算法线或者临时在材质上设置side: THREE.DoubleSide。我建议优先改模型DoubleSide只适合应急它会把渲染开销翻倍而且某些光照效果会失真。材质丢失的表现是模型加载出来了但颜色发白或发灰。这种情况通常是色彩空间没设置对。Three.js r152版本之后如果是外部贴图必须设置texture.colorSpace THREE.SRGBColorSpace否则PBR材质会显得过暗或过曝。老版本API是texture.encoding THREE.sRGBEncoding不少人升级版本后直接报错就是环境差的这一步。部件错位的问题出在Blender里的原点。物体原点决定了旋转和缩放的基准如果原点不在预期位置导入Three.js后部件会整体偏移。解决方法是导出前选中物体右键Set Origin Origin to Geometry再应用一次全部变换。别嫌这三步麻烦它们能省掉后期大量的调试成本。4. 骨骼动画让角色走路的姿势不再像“僵尸”几何体拼接做动画简单是简单但我很快就发现了它的天花板正弦函数摆出来的四肢非常机械走路像踩高跷完全没有身体重心转移和膝盖弯曲的细节。要让角色真正“活”起来得用骨骼蒙皮动画。4.1 代码模拟动画的局限性没有骨骼的时候我用代码控制四肢旋转本质上是在做“摆 pose”只是这个pose随时间变化。它能做的事情很有限挥手、点头、左右摇头这类简单动作还行但跑步、跳舞、攻击这种复杂动作几乎无法用正弦函数合成。因为真实的动作变化是千变万化的包含大量的加速度和停顿细节函数曲线很难表达。骨骼动画的底层原理是模型表面顶点绑定到若干根骨骼上每个顶点受一根或多根骨骼的权重影响。动画数据记录的是骨骼在关键帧上的旋转和位移运行时驱动网格顶点跟着骨骼变形。Three.js的SkinnedMesh就是处理这套机制的组件。4.2 Mixamo自动绑定与动画下载链路给模型手动绑骨骼是件痛苦的事几百个顶点的权重逐个设置新手很容易在中途崩溃。我常用的办法是让Mixamo做自动绑定。流程如下在Blender里导出不带动画的T-Pose人物模型格式选FBX。打开Mixamo上传FBX文件系统会自动识别并绑定骨骼。进入动画库选择需要的动作比如Idle、Walk、Run、Dance点击下载。下载时可以选择带骨骼带动画的GLB也可以只下FBX动画文件。Mixamo绑定失败的情况我也遇到过常见原因有三个模型不是标准T-Pose、三角面数量过大、模型内部有未清理的几何体。解决方法是回到Blender把模型清理干净用T-Pose重新导出。注意Mixamo上导出的GLB是同时带模型和动画的Three.js加载后可以直接使用。如果只导出FBX动画那就需要把动画数据通过FBXLoader加载后绑定到已有角色模型上链路会绕一些。我推荐直接下GLB一次到位。4.3 Three.js播放动画AnimationMixer的正确姿势加载带骨骼动画的GLB后需要用AnimationMixer来管理和播放动画剪辑。import { GLTFLoader } from three/examples/jsm/loaders/GLTFLoader.js; const loader new GLTFLoader(); let mixer; let currentAction; loader.load(/models/me_with_anim.glb, (gltf) { const model gltf.scene; const clips gltf.animations; mixer new THREE.AnimationMixer(model); const walkClip clips.find((c) c.name walk) || clips[0]; currentAction mixer.clipAction(walkClip); currentAction.play(); scene.add(model); }); let prevTime performance.now(); function animate() { requestAnimationFrame(animate); const now performance.now(); const delta Math.min((now - prevTime) / 1000, 0.1); prevTime now; if (mixer) mixer.update(delta); renderer.render(scene, camera); } animate();有两个容易踩的坑。第一mixer.update()必须在渲染循环里每帧调用而且参数必须是秒为单位的增量时间delta注意不要用固定值否则动画会忽快忽慢。第二当浏览器切后台再回来时delta会变得非常大动画可能瞬间跳了几十秒。我用Math.min(delta, 0.1)钳制上限避免这种情况。4.4 动画切换的交叉淡入淡出如果角色有多个动画比如走路和待机切换时直接action.play()会瞬间跳变动作从一帧直接蹦到另一帧非常突兀。正确做法是使用交叉淡入淡出让一个动画在0.5秒内慢慢过渡到另一个。function switchAction(name) { const clips mixer.getRoot().getAnimationsByClipName ? [] : []; // 从外部传入的clips里找到目标动画 const nextClip clips.find((c) c.name name); if (!nextClip) return; const nextAction mixer.clipAction(nextClip); nextAction.reset(); nextAction.setEffectiveTimeScale(1); nextAction.setEffectiveWeight(1); nextAction.crossFadeFrom(currentAction, 0.5, true); nextAction.play(); currentAction nextAction; }这里有个关键顺序必须先reset()再crossFadeFrom()。如果不reset动作会保留上一次播放的进度混合时可能出现奇怪的闪帧。0.5秒的过渡时间是常规值过渡太短会很跳太长则有拖泥带水的感觉。如果想更精细可以给不同动作设置不同的过渡时间比如站立到走路用0.3秒站立到跳舞用0.8秒。5. 进阶玩法用Three.js加载点云重建一个自己点云是最近被问得比较多的话题因为它能轻松给人“真实感”。不用建模、不用贴图只需要一组三维坐标点就能在网页里还原一个人体的轮廓。Three.js对点云的支持也很完善。5.1 点云为什么是热门话题以及它和网格模型的区别点云是三维空间中大量离散点的集合每个点携带xyz坐标有些还带有RGB颜色或强度信息。它不像网格模型那样有面也没有顶点索引所以不具备拓扑结构。维度三角网格点云数据结构顶点 索引 材质坐标点集合可含颜色渲染方式MeshPoints资源大小取决于模型精细度取决于采样密度表面连续性连续光滑离散点阵常见来源建模软件深度相机、激光扫描、摄影测量点云的数据来源很广泛。手机激光雷达扫描人体、工业扫描仪扫描雕塑、无人机航测地形输出的都是点云。这类数据天然适合在Three.js里直接展示不需要复杂的重建算法。5.2 Three.js加载点云的完整流程如果点云数据是简单的二进制浮点数组直接用BufferGeometry就能渲染出来。async function loadRawPointCloud(url) { const response await fetch(url); const buffer await response.arrayBuffer(); const positions new Float32Array(buffer); const geometry new THREE.BufferGeometry(); geometry.setAttribute(position, new THREE.BufferAttribute(positions, 3)); const material new THREE.PointsMaterial({ color: 0xffffff, size: 0.02, sizeAttenuation: true }); return new THREE.Points(geometry, material); }更常见的场景是加载PLY文件Three.js官方提供了PLYLoader可以自动解析顶点和颜色。import { PLYLoader } from three/examples/jsm/loaders/PLYLoader.js; const loader new PLYLoader(); loader.load(/models/person.ply, (geometry) { const material new THREE.PointsMaterial({ size: 0.01, sizeAttenuation: true, vertexColors: true }); const cloud new THREE.Points(geometry, material); scene.add(cloud); });点云加载后的第一件事是检查坐标系和尺寸。我记得第一次加载时点云模型出现在了场景几百米外完全找不到用Box3一看才发现坐标单位是厘米。解决办法和GLB模型类似做一个尺寸归一化和居中处理。5.3 点云着色与视觉优化技巧点云本身是纯白色的点看起来像星星完全没有人的轮廓。要提升可读性通常用高度着色或密度着色。高度着色是最常用的方式把点的y坐标映射到渐变颜色比如从底部的深蓝到顶部的暖黄这样一眼就能看出人体的轮廓。大多数PLY文件自带颜色只要设置vertexColors: truePointsMaterial就会使用每个顶点自己的RGB值。如果数据不带颜色可以动态生成颜色数组这个灵活性比网格模型强很多。性能方面PointCloud的渲染效率相当高。几十万个点对Three.js来说很轻松几千个点更是毫无压力。要注意的点是不要用Mesh加BufferGeometry的方式去渲染每个点那会产生几十万个Draw Call浏览器直接崩溃一定要用Points它在底层会把这些点合批渲染Draw Call数极低。5.4 从“点云人物”到可用网格的后续扩展点云只能做展示如果要让这个人物参与骨骼动画、物理碰撞就得把点云重建成网格。这个转换过程叫表面重建常用算法有泊松重建和滚球算法在MeshLab或CloudCompare这类软件里可以一键处理。重建出来的网格往往会有破洞和噪声还需要进Blender做人工修复。到这个阶段基本等于重新做了一遍模型工作量不小所以如果不是对数据真实性有硬性要求直接用点云展示反而更省事。6. 角色上屏前性能体检与五个高频渲染Bug角色能在屏幕上动了并不代表开发结束。真实项目里还要考虑性能优化和各种渲染Bug。这两个问题往往比功能开发更耗时。6.1 三角面数与Draw Call卡顿的根源网页里的3D渲染卡顿根源无非两种顶点数太多或者Draw Call太多。顶点数影响GPU光栅化压力Draw Call影响CPU提交渲染命令的开销。对于一个人物角色而言卡通风格模型控制在5万三角面以内写实模型做到20万也就够了再多就是浪费。Draw Call的影响更大。角色身上每个独立的Mesh都会贡献一个Draw Call如果角色拆成了几十个部件再加上场景里的其他物体几百个Draw Call在手机上很容易卡成PPT。优化思路是能合并的部件全部合并不能合并的部件尽量减少材质切换。6.2 合并几何体与共享材质让模型跑得更流畅静态部件合并动态部件保留单独是角色性能优化最常见的手段。把身体、头部、衣服这些不会单独动的Mesh合并成一个MeshDraw Call能大幅下降。import { mergeGeometries } from three/examples/jsm/utils/BufferGeometryUtils.js; const geometries []; character.traverse((obj) { if (obj.isMesh !obj.userData.keepSeparate) { geometries.push(obj.geometry.clone().applyMatrix4(obj.matrixWorld)); } }); const mergedGeometry mergeGeometries(geometries, false); const mergedMesh new THREE.Mesh(mergedGeometry, sharedMaterial);合并之后原来的部件就无法单独操作了。所以动态部件比如手臂、腿部、头部需要保持独立静态部分合到一起。共享材质也一样多个Mesh使用同一个material实例比每个Mesh单独创建一份材质节省大量内存。6.3 五个我在项目里反复遇到并修复的渲染BugBug现象原因修复方式模型全黑法线方向反了或材质被设为单面渲染在Blender里重算法线或设置材质side为DoubleSide模型闪烁像两个面在跳两个平面重叠Z-fighting用polygonOffset偏移或把其中一个面移动0.01到0.1的距离透明部件之间遮挡错乱半透明材质的depthWrite默认开启设置透明材质的depthWrite为false纹理发白或发灰色彩空间没有正确配置设置texture.colorSpace为THREE.SRGBColorSpace模型整体偏移抓手位置不对Blender中物体原点混乱在Blender中Set Origin后再导出法线黑和Z-fighting是最常遇到的两个。法线问题好在能一眼看出Z-fighting就要考验经验了它通常表现为某个面在视角移动时疯狂闪烁。我在做角色衣服贴花时就被这个问题困扰过最后用polygonOffset解决了。透明排序问题则常见于头发、玻璃这类半透明部件记住一条原则透明物体要关闭深度写入同时按距离排序渲染。还有色彩空间这件事从r152版本开始Three.js对颜色管理做了调整很多老项目的纹理直接变灰变暗。修复方法很简单加载贴图后加一行texture.colorSpace THREE.SRGBColorSpace但没遇到过的开发者一时半会很难想到是这个原因。做完这几轮折腾我最大的感触是Three.js人物开发真正的难点不在于框架API而在于资产生产链路是否顺畅。程序化建模适合快速验证GLB加骨骼动画是正式项目的标准打法点云则是数据可视化里很有潜力的方向。如果你正卡在某一环回头检查一下角色原点、命名规范、法线朝向、色彩空间这几个基础项八成问题就出在那里。后面我打算继续做表情驱动和口型同步等有结论了再写一篇。要是你也在用Three.js折腾虚拟人物欢迎一起交流踩过的坑。
返回列表