
游戏开发图形学【免费下载链接】melonJSa modern lightweight HTML5 game engine项目地址https://gitcode.com/gh_mirrors/me/melonJS点击查看免费下载本指南围绕 melonJS 的 3D 资产生态展开glTF/GLB 是 3D 内容的主路径它经由**关卡导演level director**而非直接操作 world 加载——level.load(scene)与 Tiled 地图使用同一个调用OBJ/MTL 则是加载单个静态模型的更简单路径。你将掌握level.load的全部 glTF 选项含async标志、rightHanded转换、灯光、地面阴影与 GPU 实例化、GLTFModel动画驱动、OBJ/MTL 材质解析、Blender 出资产的最佳实践以及从症状反推原因的排障表。文中所有实现细节均可在仓库源码中验证。加载一个 glTF/GLB 场景3D 内容在 melonJS 中的标准入口是level.load()——同样的调用也服务于 Tiled 地图因此 3D 场景可以像 2D 关卡一样注册、切换和回收。加载前先通过loader.preload把资源放进清单await loader.preload([ { name: diorama, type: glb, src: data/scenes/diorama.glb }, ]); level.load(diorama, { scale: 50 });type: gltfJSON 外部 buffer和type: glb单一二进制文件都是合法的 loader 类型。自包含的 GLB 是更优的发布格式——一次请求、没有相对路径解析问题。场景会加载到当前舞台stage已有的相机之下请用Camera3d来观察它否则场景会以平面方式渲染2D 相机路径是 CPU 投影且无光照Canvas 上配Camera3d甚至会得到黑屏。相机约定与Camera3d的使用详见 melonjs-3d。在源码中level.add会按格式注册关卡对象gltf与glb都走GLTFScene见 level.js也就是说二者共用同一套解析与实例化管线区别只在于传输封装。level.load的 glTF/GLB 选项下表完整列出level.load为 glTF/GLB 场景提供的选项定义于 level.js 的LevelLoadOptions选项默认值作用scale1每 glTF 单位的像素数作用于整个场景。Blender 以米为单位导出的模型通常需要 20–100。containergame.world节点被加入的目标容器onLoadedapp.onLevelLoaded加载完成回调传入的是关卡 id而非场景对象asyncfalse返回一个“场景进入 world 后即 resolve”的 Promise而不是布尔值rightHandedtrue见下文“右手系转换”一节lightstrue把资产内创作的KHR_lights_punctual灯光实例化为Light3dworld 子节点lightIntensityScale—保留资产创作的强度比值而不是把所有灯光归一化到 1castGroundShadow继承应用设置默认开启为场景网格开启 blob 地面阴影shadowGroundY各对象自身基座blob 阴影落地处的 world Ycontainer、onLoaded、async、flatten、setViewportBounds由关卡导演自身处理其余六项 glTF 专属选项由GLTFScene.loadOptions声明GLTFScene.js两者合并为LEVEL_LOAD_OPTIONS单一事实来源level.js。这意味着给某个关卡格式新增一个选项只需在读取它的代码旁命名一次所有转发器自动获得它——这修复了此前 glTF 六个选项被Trigger静默丢弃三个版本的问题#1649。onLoaded与async别被“立即返回”骗了onLoaded收到的是关卡 id——它是一个“完成”信号而不是场景句柄。在游戏循环运行时level.load会先停止循环、把实际加载推迟到定时器里所以默认情况下它在任何内容进入 world之前就已返回。如果你需要拿到已加载的场景二者选其一await level.load(diorama, { scale: 50, async: true }); // 到这里场景已经在 world 中了注意不加标志直接await level.load(diorama)并不会等待加载——调用返回的是布尔值await true会立即 resolve。要拿到加载结果可以把场景加载进你自己持有的容器或者按资产创作的节点名去查找level.load(diorama, { scale: 50, onLoaded: () { const sun app.world.getChildByName(Sun)[0]; // 注意返回的是数组 sun.intensity 2; }});源码对这两种形态有明确注解async: true返回 Promise、resolve 为true且“场景已在 world 中”onLoaded两种形态下都会触发二者可混用level.js。另外未知的levelId会同步抛出而不是 reject——那是拼写错误而非加载失败不需要await来暴露。从 TiledTrigger传递同样的选项在 Tiled 中创作的Trigger会把下列设置转发给level.load()因此一个“玩家走入即切换到 glTF 关卡”的触发器可以通过 Tiled 属性携带场景的全部设置world.addChild(new Trigger(x, y, { shapes: [new Rect(0, 0, 100, 100)], to: diorama, // 关卡 id —— 是 to不是 level scale: 50, rightHanded: false, castGroundShadow: false, }));scale、rightHanded、lights、lightIntensityScale、castGroundShadow、shadowGroundY都会随触发器传递container、onLoaded、flatten、setViewportBounds也一样。这份清单在Trigger中是硬编码过滤的实际由LEVEL_LOAD_OPTIONS派生见 trigger.js——超出清单的level.load选项会被无警告丢弃。async被刻意排除在外。带colorduration的触发器会自己设置async: true因为它需要这个 Promise 来编排淡出与显现的时序不带过渡的触发器则 fire-and-forget、永不设置。无论哪种情况资产中创作的async属性都会被忽略而不是被尊重。rightHanded——两个 API 之间不同的默认值glTF 资产是Y-up、右手系的melonJS 是Y-down、Z 向前的。两者之间有两条桥接路径且不等价旋转——保持手性chirality与创作工具一致镜像仅对 Y 取反——把场景左右翻转。level.load默认rightHanded: true所以经关卡导演加载的场景会正确进入。Mesh构造函数默认是false见 mesh.js因此你自己喂给Mesh的 glTF 或 OBJ 几何除非显式传rightHanded: true否则会以镜像方式进入。一个“看起来微妙地不对”的模型——文字反了、角色的装备在错误的一侧——就是这个问题没有别的。在GLTFScene.addTo中zSign rightHanded ? -1 : 1统一作用于所有节点的 Z 分量换算实现的就是“YZ 双取反的旋转”而非“只取反 Y 的镜像”GLTFScene.js。不要在桥接之上再自己加翻转——两次纠正会抵消回错误状态。材质——实际到达引擎的是什么导入器覆盖的是一个经过取舍的子集pbrMetallicRoughness的base colour 纹理与因子metallic/roughness因子换算到引擎的 specular 模型emissiveFactor含KHR_materials_emissive_strengthKHR_materials_unlit——予以支持服务于烘焙/风格化工作流alphaMode: MASK——应用镂空阈值doubleSided不导入法线贴图、metallic/roughness纹理、occlusion 贴图、morph targets、KHR_texture_transform。这些不会报错——材质只是“没有它们地到达”表现为“比创作工具预览更平”。需要的话请把内容烘焙进 base colour。glTF 解析器的 Tier 1 范围在 gltf.js 中明确列出静态节点图 mesh 图元POSITION / TEXCOORD_0 / indices baseColorTexture/baseColorFactor 节点 TRS 动画skin 权重、morph、完整 PBR 贴图、KHR 扩展、Draco 压缩均在范围外。Draco 压缩是唯一会报错的例外。它的几何放在没有 buffer view 的 accessor 里解析器直接拒绝于是整个资产加载失败。请导出未压缩版本。Application设置中的textureFilternearest/linear/auto驱动采样声明了自有 sampler 的 glTF 材质会原样携带它。像素风模型请用nearest。baseColorFactor是 LINEARtint是 sRGBloader 会替你处理这一转换。它只在一种情况下咬人你从loader.getGLTF(name).nodes自己构建网格这是获得逐节点控制的唯一途径因为没有公开的逐节点Mesh工厂然后本能地写* 255// 错误——所有无纹理材质都会渲染得过暗 mesh.tint.setColor(f[0] * 255, f[1] * 255, f[2] * 255, 1);glTF 规定pbrMetallicRoughness.baseColorFactor在线性空间中规范 3.9.2而 melonJS 的tint是 8 位sRGB——与 CSS 颜色或 PNG 出来的 texel 同一空间。二者不是同一个数而差距最大的地方恰好是美术常驻的区域线性值* 255正确值0.0513630.20511240.421071730.721842211.00255255Color.setLinear替你完成编码——它是setFloat的线性空间孪生兄弟后者接收同样的0..1范围但将其视作已经是 sRGBmesh.tint.setLinear(...node.baseColorFactor); // 编码这才是你想要的 mesh.tint.setFloat(...node.baseColorFactor); // 不编码过暗setLinear会做钳制所以某些导出器发出的略微越界因子不会变成NaN它还把编码值保留为浮点数而不是经 8 位取整。场景加载路径正是这样处理 baseColorFactor 的——mesh.tint.setLinear(f[0], f[1], f[2])GLTFScene.js。实测在一个每个材质都走了* 255的场景上平均帧亮度 85.5编码后为 147.8每个通道低了 60–73 个计数。尽早修复它。它作为 bug 几乎不可见——场景看起来仍然自洽只是“阴郁”于是光照与 bloom 阈值都对着错误的值调。事后纠正编码会让画面瞬间过曝、逼着你重调灯光那比一开始编码大得多。这条只适用于来自 glTF 的因子。你自己为屏幕创作的颜色——HUD 色调、发光效果——已经是 sRGB* 255对它们是正确的。灯光资产内创作的KHR_lights_punctual灯光——太阳、点光、聚光——会成为Light3dworld 子节点外加一个柔和的环境补光且各自保留创作的名称。携带任一此类灯光的场景会自动走光照lit渲染路径。glTF 存储物理单位——太阳用 lux、灯具用 candela——而 Blender 的日光太阳是 1000会冲垮引擎的风格化着色。因此默认情况下每盏灯都被归一化到强度 1。要保留美术设定的比值用缩放而非归一化// 1000-lux 太阳落在 1500-lux 补光落在 0.5 level.load(diorama, { scale: 50, lightIntensityScale: 0.001 });实现上intensityScale为数字时按light.intensity * intensityScale计算否则强制为 1GLTFScene.js。传lights: false则完全自己管理光照。动画节点 TRS 动画是受支持的平移、旋转、缩放通道——也就是 blocky/低多边形角色包所用的刚性层级动画。带动画的资产实例化为单个GLTFModel保留完整节点层级API 刻意镜像Sprite。模型携带资产名加载后正是靠它找到模型const model app.world.getChildByName(hero)[0]; // 即 GLTFModel model.getAnimationNames(); // [idle, walk, ...] model.setCurrentAnimation(walk, { loop: true, speed: 1, onComplete }); model.setCurrentAnimation(jump, { next: idle }); // 链式 model.isCurrentAnimation(walk); model.play(); model.pause();speed是播放倍率1 资产原始速度不是帧延迟。只有显式loop: false才停止循环。setCurrentAnimation在传入资产未定义的剪辑名时会抛出——先查getAnimationNames()。该 API 在 GLTFModel.js 中有完整注解setCurrentAnimation(name, { loop, speed, onComplete, next })统一使用 options 形态animationspeed是播放倍率setCurrentAnimation在切换相同剪辑时是幂等的。摆放与移动一个模型GLTFModel像其他任何 renderable 一样摆放——pos、depth、rotate、scale移动整个骨架并与当前剪辑叠加所以行走循环可以在角色所在任意位置播放const boat new me.GLTFModel(me.loader.getGLTF(boat), { scale: 40, lit: false }); boat.setCurrentAnimation(paddle, { loop: true }); app.world.addChild(boat); boat.pos.set(steerX, waterLevel); boat.depth travelled; boat.rotate(lean - lastLean, AXIS_Z); // rotate 是 RELATIVE —— 喂给它增量 boat.animationspeed 0.85 0.9 * pace; // 一个创作节奏多种奔跑速度直接从loader.getGLTF(name)构造是合法的也是模型作为游戏对象而非场景时的正确做法——level.load用于把整个场景载入 world。顶点蒙皮不在范围内——JOINTS_0/WEIGHTS_0不会被读取morph targets 亦然。一个平滑变形的角色不会变形。可选方案改为层级绑定把分离的网格挂进节点树、用Sprite3d公告牌或对 2D 骨骼工作使用 Spine 插件见 melonjs-plugins。实例化使用EXT_mesh_gpu_instancing的节点会加载为InstancedMesh——每个副本一次 draw call这就是“一百棵树”与“十万棵树”的差别。在你的工具支持的地方启用实例化再导出。实例化属性上的稀疏 accessor 会被明确报错拒绝而不是悄悄错放每个实例。在GLTFScene.addTo中带instances的节点自动选择InstancedMesh而非Mesh并调用fillInstances填充副本GLTFScene.jscount: 0是创作时清空的散落必须绘制零个副本而不是一个孤零零的原型。地面阴影level.load(diorama, { scale: 50, castGroundShadow: true, shadowGroundY: 0 });castGroundShadow是三态的显式设置则在两个方向上覆盖该场景的应用设置省略则继承——而应用默认是true所以阴影默认已开启。作为场景级开关它会跳过没有垂直跨度的节点——地面平面正是如此给它自己投阴影会把整个地板糊成一片 blob。blob 阴影需要 GPU 后端和Camera3d2D 相机路径与 Canvas 路径上什么都不画。省略shadowGroundY时每个 blob 以满强度坐在对象自身基座上——对已经落地的道具是对的。当物体跳跃或飞行时设置它让阴影留在地板上并随高度缩小。blob 以其投掷者caster的 x/z 为中心永不按光源方向偏移所以宽而平底的道具会把自己的阴影藏在自己身下。调高shadowGroundY把它强行显出来效果是 blob 浮到物体上方形成暗环而不是滑到一旁——这个设置是给“离开地面”的东西用的不是可见性旋钮。底层实现见 mesh.js 的优先级逻辑网格自身的castGroundShadow优先于应用设置shadowGroundY未设置时 blob 落在对象自身基座。OBJ/MTL这是完全不同的形态OBJ 产出原始几何你要把它交给Mesh而不是场景。预加载两个文件并在Mesh设置里命名它们——model与material名字相互独立两者同名只是惯例await loader.preload([ { name: fox, type: obj, src: models/fox.obj }, { name: fox, type: mtl, src: models/fox.mtl }, ]); const mesh new Mesh(400, 300, { model: fox, material: fox, // 纹理从材质 map_Kd 解析 width: 200, height: 200, rightHanded: true, // Mesh 默认是 false —— 见上文 }); world.addChild(mesh);MTL 加载器会自己去抓取它引用的纹理——map_Kd、map_d、map_bump相对.mtl解析并替你预加载所以上面的清单里没有图片条目。显式传texture:会覆盖这一点把一个绑定钉在整个模型上——对多材质模型来说这会压制逐材质纹理拆分除非你确实想这样否则别传。MTL 贡献Kd漫反射色调、d/Tr不透明度、Ke自发光、KsNs镜面高光、map_Kd漫反射纹理、map_d逐 texel 不透明度、map_bump/bump/norm切线空间法线贴图——需要lit: true切线框架逐片元推导模型无需切线属性。三种法线贴图拼写都被当作法线贴图读取——尽管格式把其中两种规定为高度图但每个导出器实际写的都是法线。镜面贴图map_Ks与 PBR 扩展的map_Pr/map_Pm不受支持且会警告——Pr/Pm标量被读取并近似到镜面项贴图则需要真正的 PBR 模型。Ka环境光与illum光照模型会被读取到材质上、可从loader.getMTL(name)读取但不参与着色。这是刻意的而非待办Ka早于场景级环境光会与Stage.ambientLightingColor打架illum枚举的是固定管线行为本渲染器没有对应物。特别是illum为 0 或 1 名义上意味着“无镜面”引擎不会遵守——Ns仍是唯一闸门所以今天渲染正确的模型不会被导出器写得不一致的字段重新造型。loader.getOBJ(name)与loader.getMTL(name)返回解析后的数据想要可直接取用。OBJ 没有场景图、没有灯光、没有动画——超出单个静态模型的场景请用 glTF。为引擎创作资产以下是针对本加载器制作道具与绑定角色时的笔记。API 细节是 Blender 专属的陷阱是通用的。一个剪辑 一个动作glTF 按动画来源的 action命名动画所以三个各自携带 action 的对象会导出为三个独立剪辑——而游戏同一时刻只能播放一个剪辑另外两个永远不会动。把所有带动画的对象放进一个 action用 slotBlender 4.4shared bpy.data.actions.new(paddle) for ob in (pivot, shoulder_l, shoulder_r, body): ad ob.animation_data_create() ad.action shared ad.action_slot shared.slots.new(OBJECT, ob.name)用“scene/whole-timeline”模式导出不会把各对象的 action 合并成一个剪辑——它仍然按 action 逐个发射。Slot 才是修复。在 Blender 5.x 中action.fcurves不再存在曲线位于action.layers[].strips[].channelbags[].fcurveschannelbag 以cb.slot.identifier标识其 slot没有slot_identifier。绝不要对处于姿态中的骨架重新挂载或合并父级化与合并都会烘焙对象的当前世界变换。在父对象处于动画姿态时给子对象挂父级会把该姿态的逆矩阵写进matrix_parent_inverse从而悄悄抵消动画——部件保持刚性渲染而曲线依然存在。先摘掉 action、把骨架归零、做完手术再挂回saved [(o, o.animation_data.action_slot) for o in animated] for ob, _ in saved: ob.animation_data.action None ob.rotation_euler (0, 0, 0) # ...在这里重新挂父级 / 合并... for ob, slot in saved: ob.animation_data.action act ob.animation_data.action_slot slot合并后的网格还会继承活动对象的缩放合并进一个被缩放到(0.8, 2.0, 0.1)的盒子会留下一个带非均匀缩放的节点、几何再被它除回去。它渲染在正确位置但会歪掉平直着色的法线——合并后应用变换rotationTrue, scaleTrue。注意一个相关陷阱对尺寸已经应用过的网格设置ob.scale.x是相乘而不是替换。模型只需要在动画驱动处保持拆分每个 glTF 图元是一次 draw call。把所有静态部分合并进一个网格只为剪辑真正驱动的部件保留独立节点——一艘载着兔子和船桨的船用这个方法从 28 个图元降到 5 个外观无可见变化。一条调色板条带胜过每色一个材质InstancedMesh是一个几何、一个材质所以想要五种颜色的道具不能用五个材质。给整个模型一个单一材质其图片是一张N×1 条带每色一像素把每个面的 UV 指向一个单元中心u (cell 0.5) / Nv 0.5sampler 设为 nearest。整个场景的调色板于是只需一张微小纹理合并后的网格可以二十种颜色单元之间零渗色。程序化生成这条条带时的色彩空间陷阱Blender 的image.pixels是场景线性浮点但这条导出路径原样写出它们——所以预先做 sRGB→线性转换会让每个颜色变暗。白色是线索——它是两个空间中唯一相同的值如果白色幸存而其他都变暗就是这个 bug。验证方法把 PNG 从 GLB 中解码回来与源调色板逐字节对比。把创作好的网格喂进InstancedMeshEXT_mesh_gpu_instancing覆盖的是文件中创作的实例。当游戏自己摆放它们时改为从解析后的描述符取几何const node me.loader.getGLTF(palm).nodes[0]; const trees new me.InstancedMesh(0, groundY, { vertices: node.vertices, uvs: node.uvs, normals: node.normals, indices: node.indices, texture: palette, textureFilter: nearest, scale: 40, instanceCount: count, });nodes是每个图元一条目各自位于自己的局部空间、带独立的world矩阵——所以只有当资产是单个合并图元、且以单位节点变换导出时才干净。在原点建模、底座贴地、导出前应用变换。一个几何被盖几百次读起来像同一个对象被复制几百次所以要在变换中让每个实例有所变化。先平移后旋转否则实例会绕组原点摆动而不是原地转身placement.identity().translate(x / s, -y / s, z / s) .rotate(math.randomFloat(0, Math.PI * 2), AXIS_Y); const j math.randomFloat(0.82, 1.18); placement.scale(j, j, j);用一个缩放常量给所有道具定尺寸而不是各给各的让“看起来不对”始终是建模问题而非缩放问题。打包自包含 GLB 是单个文件、无任何解析负担——最安全的发布物。外部引用在两种格式下仍可用.binbuffer 与图片uri都相对资产自身 URL解析带外部纹理的 GLB 可按原样加载。但外部文件 404 会拒绝整个解析——整个资产加载失败而不是部分到达、无纹理兜底。注意loader.setBaseURL(*, …)会刻意跳过gltf、glb、obj、mtl类型正是出于这个原因需要前缀时请为这些类型单独设置。症状 → 原因症状原因模型镜像——文字反了、装备在错误的一侧Mesh默认rightHanded为false传true模型上下颠倒在 loader 转换之上又手动翻转了轴场景巨大或小得看不见scale对米制导出仍停留在 1资产完全加载失败外部 buffer/图片uri404或 Draco 压缩几何一切过曝发白资产创作了 lux/candela 强度用lightIntensityScale材质比创作工具预览更平法线与 metallic/roughness纹理不会被导入角色不变形顶点蒙皮超出范围层级绑定或公告牌动画名返回为空资产没有节点 TRS 通道纯蒙皮骨架一百个副本拖垮帧率导出时未启用EXT_mesh_gpu_instancing整片散落像同一个对象的重复每个实例共享组变换——逐实例改变偏航/缩放先平移后旋转创作了一个剪辑却得到三个每个对象一个 action改用带 slot 的单一 action骨架某部件刚性渲染而曲线仍在它在中途姿态下被重新挂载逆矩阵被烘焙进matrix_parent_inverse合并后的网格沿某轴光照错误合并时继承了活动对象的非均匀缩放——应用变换创作调色板整体偏暗但白色正确线性值写入条带并被原样保存每个无纹理 glTF 材质都过暗白色不受影响baseColorFactor是线性、tint是 sRGB——用tint.setLinear(...)不要* 255或setFloatgetGLTF(name).nodes[0]几何落在错误位置nodes按图元分列、各带world合并为单图元并在原点导出道具不投可见阴影宽而平底——blob 在它身下shadowGroundY只会给它套光环阴影只出现在相机附近的投掷者身上shadowGroundY设错了方向——Y-down 意味着地板是更大的ypos.y - lift会把 blob 放进投掷者内部深度测试只剩一圈发丝环阴影糊满整个地板地面平面给自己投了 blob——用场景级开关onLoaded收到字符串而非场景它收到的是关卡 id改为加载进自己的容器场景平面渲染且无光照没有Camera3d——2D 相机路径是 CPU 投影且无光照Canvas 上配Camera3d则得到黑屏相关技能文档melonjs-3d — 约定、Camera3d、网格、Light3d、实例化以及camera.setFog户外场景几乎总需要它它设在相机上所以加载的场景无需逐节点处理melonjs-lighting — 导入后的Light3d行为melonjs-loading-assets — loader、资产类型与 base URLmelonjs-plugins — Spine 插件用于骨骼角色仓库中的相关测试可进一步验证这些行为tiledMapLoader与gltf系列测试覆盖场景加载与解析契约sprite3d_webgl.spec.js、mesh系列测试覆盖网格与rightHanded桥接测试清单见 packages/melonjs/tests。赞分享游戏开发图形学【免费下载链接】melonJSa modern lightweight HTML5 game engine项目地址https://gitcode.com/gh_mirrors/me/melonJS点击查看免费下载相关推荐Three.js Loaders 资产加载实战从 GLTF 纹理管线到 OpenMontage 生产级 3D 场景Three.js Loaders 资产加载实战从 GLTF 纹理管线到 OpenMontage 生产级 3D 场景 本指南以 OpenMontage 仓库中的人工智能AI Agent音视频媒体生成工作流自动化为什么Point Transformers是点云处理的未来三大创新架构解析为什么Point Transformers是点云处理的未来三大创新架构解析 点云数据作为三维空间信息的重要载体正广泛应用于自动驾驶、机器人视觉和3D建模等领终极指南如何让小爱音箱变身全能音乐播放器终极指南如何让小爱音箱变身全能音乐播放器 你是否曾经对着小爱音箱说播放周杰伦的歌却被告知抱歉我没有找到相关资源是否厌倦了每月为音乐会员付费却依后端智能硬件音视频上一篇解决Home Manager中Mako通知服务启动失败的完整指南下一篇从静态脚本到动态伙伴Hermes Agent如何重构AI代理的协作范式创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考