- 游戏开发
- 图形学
【免费下载链接】melonJS
a modern & lightweight HTML5 game engine
本指南围绕 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: "gltf"(JSON + 外部 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):
| 选项 | 默认值 | 作用 |
|---|---|---|
scale | 1 | 每 glTF 单位的像素数,作用于整个场景。Blender 以米为单位导出的模型通常需要 20–100。 |
container | game.world | 节点被加入的目标容器 |
onLoaded | app.onLevelLoaded | 加载完成回调,传入的是关卡 id而非场景对象 |
async | false | 返回一个“场景进入 world 后即 resolve”的 Promise,而不是布尔值 |
rightHanded | true | 见下文“右手系转换”一节 |
lights | true | 把资产内创作的KHR_lights_punctual灯光实例化为Light3dworld 子节点 |
lightIntensityScale | — | 保留资产创作的强度比值,而不是把所有灯光归一化到 1 |
castGroundShadow | 继承应用设置(默认开启) | 为场景网格开启 blob 地面阴影 |
shadowGroundY | 各对象自身基座 | blob 阴影落地处的 world Y |
container、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被刻意排除在外。带color+duration的触发器会自己设置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 分量换算,实现的就是“Y+Z 双取反的旋转”而非“只取反 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设置中的textureFilter("nearest"/"linear"/"auto")驱动采样;声明了自有 sampler 的 glTF 材质会原样携带它。像素风模型请用"nearest"。
baseColorFactor是 LINEAR,tint是 sRGB
loader 会替你处理这一转换。它只在一种情况下咬人:你从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.05 | 13 | 63 |
| 0.20 | 51 | 124 |
| 0.42 | 107 | 173 |
| 0.72 | 184 | 221 |
| 1.00 | 255 | 255 |
Color.setLinear替你完成编码——它是setFloat的线性空间孪生兄弟;后者接收同样的0..1范围但将其视作已经是 sRGB:
mesh.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 太阳落在 1,500-lux 补光落在 0.5 level.load("diorama", { scale: 50, lightIntensityScale: 0.001 });实现上,intensityScale为数字时按light.intensity * intensityScale计算,否则强制为 1(GLTFScene.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.js);count: 0是创作时清空的散落,必须绘制零个副本而不是一个孤零零的原型。
地面阴影
level.load("diorama", { scale: 50, castGroundShadow: true, shadowGroundY: 0 });castGroundShadow是三态的:显式设置则在两个方向上覆盖该场景的应用设置;省略则继承——而应用默认是true,所以阴影默认已开启。作为场景级开关,它会跳过没有垂直跨度的节点——地面平面正是如此,给它自己投阴影会把整个地板糊成一片 blob。blob 阴影需要 GPU 后端和Camera3d;2D 相机路径与 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(自发光)、Ks+Ns(镜面高光)、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,用 slot(Blender 4.4+):
shared = 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[].fcurves,channelbag 以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)的盒子,会留下一个带非均匀缩放的节点、几何再被它除回去。它渲染在正确位置,但会歪掉平直着色的法线——合并后应用变换(rotation=True, scale=True)。注意一个相关陷阱:对尺寸已经应用过的网格设置ob.scale.x是相乘而不是替换。
模型只需要在动画驱动处保持拆分
每个 glTF 图元是一次 draw call。把所有静态部分合并进一个网格,只为剪辑真正驱动的部件保留独立节点——一艘载着兔子和船桨的船,用这个方法从 28 个图元降到 5 个,外观无可见变化。
一条调色板条带胜过每色一个材质
InstancedMesh是一个几何、一个材质,所以想要五种颜色的道具不能用五个材质。给整个模型一个单一材质,其图片是一张N×1 条带,每色一像素,把每个面的 UV 指向一个单元中心(u = (cell + 0.5) / N,v = 0.5),sampler 设为 nearest。整个场景的调色板于是只需一张微小纹理,合并后的网格可以二十种颜色,单元之间零渗色。
程序化生成这条条带时的色彩空间陷阱:Blender 的image.pixels是场景线性浮点,但这条导出路径原样写出它们——所以预先做 sRGB→线性转换会让每个颜色变暗。白色是线索——它是两个空间中唯一相同的值;如果白色幸存而其他都变暗,就是这个 bug。验证方法:把 PNG 从 GLB 中解码回来,与源调色板逐字节对比。
把创作好的网格喂进InstancedMesh
EXT_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或setFloat |
getGLTF(name).nodes[0]几何落在错误位置 | nodes按图元分列、各带world;合并为单图元并在原点导出 |
| 道具不投可见阴影 | 宽而平底——blob 在它身下;shadowGroundY只会给它套光环 |
| 阴影只出现在相机附近的投掷者身上 | shadowGroundY设错了方向——Y-down 意味着地板是更大的y,pos.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 URL
- melonjs-plugins — Spine 插件,用于骨骼角色
仓库中的相关测试可进一步验证这些行为:tiledMapLoader与gltf系列测试覆盖场景加载与解析契约,sprite3d_webgl.spec.js、mesh系列测试覆盖网格与rightHanded桥接,测试清单见 packages/melonjs/tests。
- 游戏开发
- 图形学
【免费下载链接】melonJS
a modern & lightweight HTML5 game engine
相关推荐
Three.js Loaders 资产加载实战:从 GLTF 纹理管线到 OpenMontage 生产级 3D 场景
Three.js Loaders 资产加载实战:从 GLTF 纹理管线到 OpenMontage 生产级 3D 场景 本指南以 OpenMontage 仓库中的
人工智能AI Agent音视频媒体生成工作流自动化为什么Point Transformers是点云处理的未来?三大创新架构解析
为什么Point Transformers是点云处理的未来?三大创新架构解析 点云数据作为三维空间信息的重要载体,正广泛应用于自动驾驶、机器人视觉和3D建模等领
终极指南:如何让小爱音箱变身全能音乐播放器
终极指南:如何让小爱音箱变身全能音乐播放器 你是否曾经对着小爱音箱说"播放周杰伦的歌",却被告知"抱歉,我没有找到相关资源"?是否厌倦了每月为音乐会员付费,却依
后端智能硬件音视频
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考