LPAI 独立网页游戏 AI 社区指南

页面与交互

Three.js 三维 Canvas

在同一 Canvas 渲染脚本中使用 Three.js、模型、动画和事件绑定。

# Three.js 三维 Canvas

场景 Canvas 渲染器选择 3D / Three.js 后,现有渲染脚本、资源定义、源码模块和动作绑定继续可用。缺省类型为 2d,旧 Pixi 工程不需要迁移。只有明确向编辑器 AI 要求“3D”“三维”或“Three.js”时,它才应读取独立的 canvas_3d_rendering 技能;普通 Canvas 请求沿用 2D。

Three 是渲染引擎。物理、导航、ECS、复杂游戏规则仍需要相应库和游戏逻辑;属性、对象、存档的权威状态继续由 Worker 管理。WebGL2 是 3D 的运行前提,不支持 WebGPU/TSL 宿主。

# 挂载和脚本

在场景 canvasrenderers 新建 { id: 'world', type: '3d' },渲染布局中放置:

<canvas lp-canvas="world" lp-key="world-canvas" class="block h-dvh w-full"></canvas>

脚本路径仍为 scene/<sceneId>/canvas/<rendererId>/render。宿主必须有 CSS 宽高,不要同时绑定 lp-control / lp-events。默认把 HUD 和操作按钮放在上层 HTML 布局。

const canvas = tools.canvas
let box
return {
  mount() {
    box = canvas.draw.mesh('box', { color: 0x22c55e }).object
  },
  frame(ctx) {
    box.rotation.y += ctx.deltaMs / 1000
  },
  dispose() {
    canvas.draw.clear()
  }
}

mount/update/frame/bindingResult/dispose 都必须同步返回。ctx.deltaMselapsedMs 是毫秒,delta 上限 100ms;位置尺寸为世界单位,旋转为弧度,右手坐标、Y 向上。隐藏/零尺寸暂停,恢复后重新测量,DPR 上限 2。配置改变会重建 Session 和实际 canvas,普通状态快照更新保持宿主稳定。

# 核心和门面

canvas.three 暴露固定版本 [email protected] 的完整核心命名空间。canvas.app.renderer / scene / camera 是真实 Three 对象。特别注意,canvas.scenectx.scene 只是 { id,width,height,dpr } 元数据,不能向它们添加 Mesh。

默认透视相机位于 (0,2,5)、朝向原点,视角 50 度,near/far 为 0.1/1000;提供半球光和方向光、sRGB 输出、ACES 色调映射、曝光 1、透明背景。原始对象允许作者自行调整。开启阴影还需设置 app.renderer.shadowMap.enabled 并配置灯光和网格。

接口 作用
draw.group/mesh/model/light(key, options) 稳定 key 的组、网格、GLB 实例、灯光
draw.rect/sprite/text(key, options) XY 平面、面向相机的精灵/文字
draw.remove/clear/bounds 清理托管节点、读取世界 AABB
animation.play(key,clip,options) / stop / state 模型实例独立动画,时间以秒表示
camera.project({x,y,z}) / ray(x,y) 世界坐标与 canvas CSS 像素间投影/射线
pick(x,y) 最近可见托管节点的序列化命中
collision.intersects(a,b) / contains(key,x,y,z) / clamp(key,bounds) 世界 AABB 查询和约束,不是物理引擎
app.setCamera(camera,{autoAspect}) 设置透视/正交相机,默认自动适配宽高
app.setRenderCallback(callback或null) 同步接管最终绘制或恢复默认 render
resources.track(value) / release(value) 登记作者拥有的可 dispose 资源,按身份去重

draw 返回 {key,object,bounds}object 为真实 Object3D。bounds 为 {x,y,z,width,height,depth} 或 undefined。通用字段为 x/y/z、rotationX/Y/Z、scale/scaleX/Y/Z、parent、visible、castShadow、receiveShadow、bindings、objectId、payload。mesh 支持 box/sphere/plane/cylinder,也可传入作者管理的原始 geometry/material。

# 模型和纹理资源

资源统一在资源管理 → 3D 模型 / 3D 贴图上传,布局与 SVG、图片、音乐资源库一致,分个人与公开,支持项目筛选、缩略图、放大预览、复制链接、替换、公开/私有切换和批量删除。模型预览可旋转、缩放及播放动画,贴图预览可调整曝光。Three.js 及解码器仅在需要预览时加载。

场景 Canvas 面板只填写资源 source 链接或 <|资源字符串ID|>,并设置 kind、色彩空间和解码器,不在场景上传。模型类型 model 接受自包含 GLB,最大 20 MiB;texture3d 接受 PNG/JPEG/WebP/HDR/EXR/KTX2,最大 8 MiB。不会经过普通图片压缩或 SVG 清洗。

模型/3D纹理每次上传获得独立版本 key 和 URL。同名替换后请把资源动态字符串更新为新返回的 URL,使新内容不命中旧缓存;普通图片/音频继续沿用原有稳定 URL 行为。

AI 使用 list_project_assets({assettype:'model'})texture3d 查询,再通过原有 Canvas assets 工具维护。可复用资源优先由不可编辑动态字符串返回真实项目 URL,source 保留 <|资源字符串ID|>

{
  "actor": { "source": "<|模型URL字符串ID|>", "kind": "model", "decoders": ["draco"] },
  "floor": { "source": "<|纹理URL字符串ID|>", "kind": "texture", "colorSpace": "srgb" },
  "environment": { "source": "<|HDR字符串ID|>", "kind": "hdr", "colorSpace": "linear" }
}

kind 支持 texture/model/hdr/exr/ktx2。decoders 按 GLB 实际需要声明 draco/meshopt/ktx2;独立 KTX2 自动选择转码。GLB 外链 buffer/image 会被拒绝,请导出内嵌资源。原始 GLTFLoader 仍可处理完整外链资源树,由作者负责 URL 和清理。frames/animations 是 2D 图集字段,不适用于 3D。

资源 API 行为
assets.resolve/state(id, objectId?) 解析 source 和 idle/loading/ready/error 状态
assets.preload(id, objectId?) Promise,成功保持预加载租约;不保证 GPU 上传/着色器预编译
assets.get(id, objectId?) 借用已就绪 Texture 或完整 GLTF,未就绪返回 undefined,不触发加载
assets.release(id, objectId?) 释放此 ID/对象上下文历次 source 的预加载租约,节点保留独立引用

模型实例使用独立骨架、材质和 AnimationMixer,几何体/纹理引用共享。用 animation.state(key).clips 读取真实 clip 名称后播放,不要猜动画名。

const canvas = tools.canvas
let started = false
return {
  mount() {
    canvas.draw.model('a', { asset:'actor', x:-1, bindings:{click:'inspect'}, payload:{slot:'a'} })
    canvas.draw.model('b', { asset:'actor', x:1, bindings:{click:'inspect'}, payload:{slot:'b'} })
  },
  frame() {
    const a=canvas.animation.state('a'), b=canvas.animation.state('b')
    if (!started && a.ready && b.ready && a.clips.length) {
      canvas.animation.play('a',a.clips[0],{timeScale:1})
      canvas.animation.play('b',b.clips[0],{timeScale:0.5})
      started=true
    }
  },
  dispose() { canvas.draw.clear() }
}

# 交互与源码模块

先配置 actionBindings.inspect = {controlId:'真实控件ID', event:'js_g'}。按下、释放、点击、移动沿用现有绑定协议;Worker 在 tools.kwargs.pointer.hit 读取 {key,objectId,point,normal?,uv?,distance,instanceId?},业务参数仍在 tools.kwargs.payload,不会把 Three 对象传过 Worker。

手动 canvas.emitBinding(name,payload,objectId) 返回 requestId;bindingResult 只表示执行链是否接受,业务结果由权威状态确认,不保证在紧接的下一次 update 出现。相机控制和每帧动画不会自动向 Worker 提交动作。

canvas.modules.load(key,source) 继续加载作者只读动态字符串或静态表格中的同步源码。addons 是引擎模块,modules 是作者源码,两者分别缓存。复杂游戏按移动、实体、战斗、相机等职责组织模块,禁止把玩家输入、联网内容或 AI 输出作为代码执行。

# 输入边界

ctx.input 为冻结的 {pointer,keys,pressedKeys,releasedKeys}。pointer 完整字段为 x/y/down/pressed/released/pointerType/button/buttons/movementX/movementY/wheelX/wheelYcanvas.input 额外提供按键和按钮查询方法。x/y 是 Canvas 逻辑像素,down 表示任意按钮,buttons 是 DOM 位掩码(左1/右2/中4),button 为按钮编号(左0/中1/右2)。

movement 和 wheel 从上一帧末开始累加,当前帧结束后归零,update 或读取字段不会清零;movement 未缩放,不要再乘 deltaMs。指针边沿只保留聚合状态,快速按下再松开可能只剩 released,不能据此统计全部点击。键盘 code 的 pressed/released 集合也在帧末清理。

普通 HTML 按钮聚焦会先清理 Canvas 旧按键,下一个非编辑按键可回到最后使用的可见 Canvas。文本输入、textarea、select、contenteditable、Tab、输入法组合或已阻止默认行为的事件不会被抢走。Canvas/窗口失焦、后台、零尺寸、WebGL context lost 清理本地持续输入,不向 Worker 合成释放事件;pointercancel 会给原按下节点发 release,但不发 click。

输入门面只维护一个聚合指针。手机左右手必须使用宿主 app.renderer.domElement 的原生 Pointer Events,以 pointerId 分配独立角色并设置捕获,处理 up/cancel/lostpointercapture,失焦/后台/销毁时清空。不要同时让这些手势触发自动节点绑定。

第一人称锁定使用原生 requestPointerLock 或已加载的官方控制器,必须在真实用户事件中请求;iframe sandbox 要有 allow-pointer-lock。Canvas 位于 ShadowRoot,锁定目标应检查 domElement.getRootNode().pointerLockElement;当前官方 PointerLockControls 的 document 检查可能不适用。右键菜单默认在 Canvas 真正聚焦时禁止。所有自行添加的宿主/文档输入监听器都必须在 dispose 中解绑。独立 3D skill 提供完整桌面第一人称和手机双手脚本。

# 权威状态与回执

properties 是客户端当前同步的整张属性值映射,不按资源引用筛选;dynamicStrings 仅玩家原始存档覆写,显示值用 player.getString,未覆写的字符串通常不在快照里。objects 是当前同步的按类对象数据,不是全部定义。

mount 收到初始化期间的最新快照,后续才调用 update;不保证初始版本再补调一次 update,因此 mount/update 应共用 applyState。状态批次可合并,revision 是布局本地递增版本,不是 Worker 事务号或动作编号。消息按通道顺序投递,但异步脚本完成可交错;回执与 update 无先后保证。

可靠交互流程:锁住本地重复提交 → emitBinding 保存 requestId → Worker 校验并修改权威状态及业务结果 → update 与 bindingResult 都尝试核对结果 → 成功或业务拒绝后解除等待。5秒回执超时并不撤销动作,异常也不能证明从未扣费;结果不确定时停止转圈、提示待核对,不自动重付。一次性建造在 Worker 检查地块已建状态;重复购买则需要持久化 operationId 与幂等结果记录。3D skill 中提供完整的单地块建造 Worker/Canvas 配对示例。

# 模块导出与 Session

模块不是 ESM,用同步 return 导出函数或对象;顶层有只读 player、objects、tools.canvas 和 JavaScript 标准全局,没有隐式 THREE/ctx/config,也不能访问调用方局部变量。通过工厂参数传入依赖:

// 作者只读动态字符串中的完整模块源码
return {
  create({ THREE, speed }) {
    const position = new THREE.Vector3()
    return { step(dt) { position.x += speed * dt; return position } }
  }
}
// renderer 主脚本
const canvas = tools.canvas
const movement = canvas.modules.load('movement', player.getString('移动源码ID'))
const actor = movement.create({ THREE: canvas.three, speed: 2 })
return {
  mount() { canvas.draw.mesh('actor', { color: 0x06b6d4 }) },
  frame(ctx) { canvas.draw.mesh('actor', { x: actor.step(ctx.deltaMs / 1000).x, color: 0x06b6d4 }) },
  dispose() { canvas.draw.clear() }
}

Session 是一个实际 Canvas 元素的一次挂载。相同 Session 内相同 key/source 共享导出与模块闭包,工厂每次 create 创建独立状态;不同 Canvas、重挂载或配置修改后的 Session 不共享,隐藏暂停不重建。持久状态留在 Worker。

宿主自动调用顶层导出的 dispose(ctx),不会递归清理工厂返回对象;模块应跟踪自己创建的实例。销毁顺序是 renderer.dispose、模块按登记逆序 dispose、后端资源清理。换源码成功后才清理旧导出,调用者需接住新返回值;旧引用不会自动更新。模块 dispose 必须同步,不要由 renderer 再重复调用。

# 官方扩展与清理

canvas.addons.list() 返回 248 个注册的官方公共入口,load(path) 按需加载并返回模块全部导出。常用入口包括 Orbit/Map/TransformControls、GLTF/DRACO/KTX2/HDR/EXRLoader、SkeletonUtils、BufferGeometryUtils、EffectComposer、RenderPass、OutputPass、UnrealBloomPass、常用几何体、导出器和 shaders。相同请求去重,失败可重试,销毁后未完成的 Session 请求拒绝。

const canvas=tools.canvas
let alive=true, controls
return {
  mount() {
    canvas.draw.mesh('box',{color:0x06b6d4})
    canvas.addons.load('controls/OrbitControls.js').then(({OrbitControls})=>{
      if (!alive) return
      controls=canvas.resources.track(new OrbitControls(canvas.app.camera,canvas.app.renderer.domElement))
      controls.enableDamping=true
    }).catch(error=>{ if(alive) console.warn(error) })
  },
  frame(ctx) { controls?.update(ctx.deltaMs/1000) },
  dispose() { alive=false; if(controls) canvas.resources.release(controls) }
}

准确排除项及原因可查 addons.exclusions:WebGPU/TSL、WebXR 会话、外部物理运行时、未部署的 Rhino/IFC/Lottie/LDraw/编码器资源、依赖公共 CDN 的 TTFLoader 等不在支持范围。不得宣传所有 addons、所有 npm 依赖都可用。Draco/Basis 解码文件与引擎同版本本地部署,Meshopt 为独立动态模块。

EffectComposer 用 app.setRenderCallback(ctx => composer.render(ctx.deltaMs/1000)) 接管渲染,随 ctx.scene 的尺寸/DPR 调整 composer。Composer 和各 pass 分别登记 resources;清理时先 setRenderCallback(null)。HDR/EXR 可借用 Texture,经 PMREMGenerator 得到 RenderTarget,生成器和 RenderTarget 都要登记,退出时先解除 scene.environment。

作者创建的 geometry/material/texture/controls/composer/RenderTarget 必须明确释放。scene.remove() 不释放 GPU,material.dispose() 不自动销毁贴图,assets.get() 的借用资源不能自行 dispose/track。宿主会清理托管资源和已登记的作者资源,但不能自动发现所有原始分配。禁止抢占宿主渲染循环或销毁宿主 renderer。

每 Session 默认 12 个并发顶层资源请求、10000 个托管节点、256 MiB geometry/texture 估算预算。原始 Three 分配需要作者自控。WebGL context 恢复会重建标准 GPU 对象,自定义 RenderTarget 旧像素需要重新绘制。

继续阅读:渲染布局与 Canvastools API