feat(spine): support rendering spine inside UICanvas#3068
feat(spine): support rendering spine inside UICanvas#3068cptbtptpbcptdtptp wants to merge 1 commit into
Conversation
Add @galacean/engine-spine-ui with UISpineAnimationRenderer, the UI-space host for spine: collected/ordered/culled by its root canvas (world-space, screen-space camera and overlay), faded by UIGroup alpha, clipped by RectMask2D and hit-tested against the UITransform rect. - Extract the world renderer's primitive/buffer/material-cache/skeleton bookkeeping into shared SpineRendererUtils; SpineAnimationRenderer keeps its public API and behavior, both hosts share one SpineMaterialCache - Add ISpineRenderTarget.globalAlpha; both version generators fold it into the final vertex alpha before premultiplication so PMA light/dark colors follow automatically - Spine shader: RENDERER_UI_RECT_CLIP macro variant derives the rect-clip world position from renderer_ModelMat (spine vertices are entity-local, unlike pre-transformed UI chunk vertices) and fades PMA rgb explicitly; overlay pass sRGB output correction for parity with the UI shader - e2e: serve spine packages from source in the case dev server — their ES5 dist subclasses of native @esotericsoftware/spine-core classes throw "Class constructor cannot be invoked without 'new'", so spine cases could not run at all; existing spineboy/tintBlack baselines pass unchanged - tests: 17-case UISpineAnimationRenderer suite; e2e: spine-ui-canvas and spine-ui-rect-mask with baselines Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. 🗂️ Base branches to auto review (1)
Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
GuoLei1990
left a comment
There was a problem hiding this comment.
审查 @ 75c9348db(首轮)— feat(spine): support rendering spine inside UICanvas
总结
新增 @galacean/engine-spine-ui 包,UISpineAnimationRenderer extends UIRenderer,让 spine 能像 Image/Text 一样被 UICanvas 采集/排序/裁剪。同时把世界渲染器的 primitive/buffer/material-cache/skeleton 簿记抽进共享的 SpineRendererUtils + SpineMaterialCache(SpineAnimationRenderer 净 +21/-107,是真去重不是加壳)。方向完全正确、抽象健康:共享核心恰好 2 个真实消费者(world + UI),新包只承载 UI 专属 host;世界路径逐字对齐、shader 与 uiDefault.fs.glsl 的 rect-clip 逻辑同源。逐链核对无 P0/P1,仅两条环境类 [P2](均不阻塞代码正确性)+ 两条 [P3]。是 stacked 在 fix/shaderlab 上的干净 PR(同 #3063 base)。
逐链核对(非信 PR 描述 / commit message,均对 fix/shaderlab tree 实读)
- UIRenderer/UICanvas/Engine 契约全部坐实:
_getGlobalAlpha()(UIRenderer.ts:230=this._getGroup()?._getGlobalAlpha()??1)、_rectMasks(:90)、@dependentComponents(UITransform,AutoAdd)(:32)、_getRootCanvas/engine._renderElementPool/canvas._renderElements/_realRenderMode/_sortDistance/sortOrder均存在且与Image._render(advanced/Image.ts:266-328) 同构使用——overlay 分支renderElement.subShader = material.shader.subShaders[0]逐字一致(overlay pass 不走 pipeline 的pushRenderElement故 host 自赋 subShader)。_render与Image._render不能共用 helper(Image 走 subChunk/assembler,spine 走 subPrimitives 多 draw),是忠实适配非可去重的重复。 _updateTransformShaderData覆写有据且必需:UIRenderer基类覆写走_updateWorldSpaceTransformShaderData(视顶点已在世界空间、model=identity),但 spine 顶点是 entity-local,故本类跳过父覆写直调Renderer.prototype._updateTransformShaderData.call(this,context,false)写真renderer_ModelMat,并强制onlyMVP=false让 overlay pass 也刷新renderer_ModelMat(rect-clip varyingv_worldPosition = renderer_ModelMat*POSITION依赖它)。多写 3 个矩阵/帧但 spine 不合批、量小可忽略。测试:1542反向钉住(overlayonlyMVP=true时 model 仍须写)。- shader rect-clip 是
uiDefault.fs.glsl的忠实移植 + PMA 增补:四个renderer_UIRectClip*uniform 均为 UI 系统真实注册的 ShaderProperty(UIRenderer.ts:47-53),getUIRectClipAlpha()与uiDefault.fs.glsl:12逐字相同;差异仅:① spine 用RENDERER_UI_RECT_CLIP宏门控(world spine 永不 enable=零开销,UI spine 构造期enableMacro),UIDefault 无条件计算——这是对的(world spine 无 UI 概念不该带这段码,宏变种恰当);② 新增gl_FragColor.rgb *= mix(1.0, rectClipAlpha, renderer_PremultipliedAlpha)因 spine 用 PMA 而 UIDefault 不用。renderer_PremultipliedAlpha由SpineMaterial._setPremultipliedAlpha(:45) 恒设、outputSRGBCorrection在common.glsl:55(spine#include <common>在 scope)、ENGINE_SHOULD_SRGB_CORRECT是#ifdef门控(未定义即跳过,无编译风险)。 globalAlpha只影响顶点色 alpha、不动 bounds:两 generator(3.8/4.2 同一两行改动)finalAlpha = ...*globalAlpha只喂finalColor.a+ PMA 的 rgb 缩放;_localBounds经_expandBounds(x,y,z)只吃位置。故改 group alpha 不动几何包围盒(测试:1559"computes bounds from the skeleton, not the UITransform rect" 钉住)。globalAlpha=0→finalAlpha=0→ rgb=0/alpha=0,无除法无 NaN,安全。_render里globalAlpha<=0早返(测试:1505钉 group alpha=0 时 push 0 个 renderElement)。世界渲染器readonly globalAlpha=1定值,与接口ISpineRenderTarget.globalAlpha:readonly一致。- 共享 material cache 对齐:
SpineAnimationRenderer._materialCacheMap = SpineMaterialCache._cacheMap(alias 保 public API),world/UI 都经SpineMaterialCache.getMaterial同 key(含 tintBlack)→ 跨 host 共享(测试:1525钉 world 与 UI 拿同一 material)。destroySpinePrimitiverefCount 逐链归零(primitive._addReferCount(-1)级联到 binding buffer)与 #3063 二轮核过的一致。 - clone 正确:
_cloneTo空 skeleton 守卫 + 共享不可变SkeletonData/AnimationStateData只新建 per-instance skeleton/state,defaultConfig@deepClone(同世界渲染器),SpineAnimationDefaultConfig现从 spine 包 index 导出复用。测试:1600/:1621双向覆盖(含未绑 resource 时 clone 不抛)。
问题
[P2] CI 白名单:本 PR target=fix/shaderlab 不在 ci.yml 的 pull_request: branches(main/dev/*)内 → 仅 labeler 跑,build×3 / e2e×4 / lint / codecov 全静默不跑。 gh pr checks 3068 现只有 labeler 通过。PR 里真正被重写/新增的部分(共享核心抽取、RENDERER_UI_RECT_CLIP shader 变种、两个新 e2e case)恰是渲染/结构改动,却零 CI 覆盖,"4 个 spine case 本地全绿"无 CI 实证。同 #3063 的判例——建议合并前在 base 本地跑 e2e 四 case 对图,或在 fix/shaderlab→dev/2.0(#2983)合并时由完整 CI 补验。不阻塞(受 shaderlab 线合并粒度约束)。
[P2] e2e ES5-dist 别名 hack 揭示的更深问题(已在 PR 描述标注,记录不阻塞): vite.config.js 把 4 个 spine 包别名到 src(scoped 到 spine,其它 case 仍走 dist),修的是"ES5 dist 子类 extends 外部 @esotericsoftware/spine-core native class 抛 Class constructor cannot be invoked without 'new'"这个 pre-existing 破坏。PR 已诚实指出发布产物 @galacean/engine-spine-core-* 同样受影响、"bundle spine-core vs 抬 build target"值得单独决策。方向正确、scope 收敛,仅提示这个 debt 别忘。
[P3] UISpineAnimationRenderer.ts:485 _canBatch(): boolean 覆写丢了基类的两个入参(基类 Renderer._canBatch(preElement, curElement) / UIRenderer._canBatch(preElement, curElement))。TS 合法(收窄参数)、运行时正确(恒 false),测试 _canBatch(null, null) 也能跑;纯签名一致性,与 _batch(): void {} 同。可保留基类签名以对齐,锦上添花。
[P3] UISpineAnimationRenderer.ts:467 @remarks 续行缺 * 前缀(\n If this option is enabled, the Spine editor must export... 没有 * 前导),是从世界渲染器 SpineAnimationRenderer.ts:52 逐字带进的既有格式瑕疵——但本新文件是新出现,顺手补上 * 对齐 JSDoc 块即可。
简化建议
代码已相当干净,没有可去的冗余。共享核心抽取(SpineRendererUtils + SpineMaterialCache)是本 PR 最有价值的部分:把 world 渲染器里的 primitive 构造 / buffer 绑定 / material cache / skeleton-state 构造收口成纯函数 + 单一 cache 类,两个 host 共享一份实现,SpineAnimationRenderer 反而瘦了 86 行。这正是"大道至简"——新增能力却减少了重复真相源。
🤖 Generated with Claude Code
Summary
Spine can now be hosted by the UI system as well as world space. New package
@galacean/engine-spine-uiexposesUISpineAnimationRenderer extends UIRenderer:Image/Text(WorldSpace, ScreenSpaceCamera, and the overlay pass — overlay assignssubShaderitself, matchingImage)UIGroupalpha folds into skeleton vertex colors (rebuilt every frame; PMA-aware)RectMask2Dclips it; rect hit-testing viaUITransform(sizedefines the raycast area only — the skeleton renders at the entity transform scale, and the inheritedcolorproperty intentionally does not tint the skeleton:skeleton.coloris the spine-native way)_canBatch→ false), so the constructor-gated batcher passes its elements through untouchedShared render core
The world renderer's primitive/buffer/material-cache/skeleton bookkeeping moved into shared
SpineRendererUtils+SpineMaterialCache(SpineAnimationRendererkeeps its public API and behavior;_materialCacheMapremains as an alias). Both hosts and both version backends share one implementation:ISpineRenderTarget.globalAlpha(default 1): the 3.8/4.2 generators multiply it into the final vertex alpha before premultiplication, so PMA light/dark colors follow automatically — a 2-line change per generator, no other backend changesRENDERER_UI_RECT_CLIPmacro variant derives the rect-clip world position fromrenderer_ModelMat(spine vertices are entity-local, unlike pre-transformed UI chunk vertices; the UI host always writes the full transform set so the model matrix stays fresh in the overlay pass) and fades PMA rgb explicitly — hard clip discards on the rect factor, not final alpha, since additive slots legitimately emit zero-alpha fragments; plusENGINE_SHOULD_SRGB_CORRECToutput correction for overlay parity withUIDefaultWhy a separate package
@galacean/engine-spinemust stay free of a@galacean/engine-uidependency (world-only users, UMD script-tag globals). Same layering as the spine version-backend packages.e2e harness fix (pre-existing breakage)
Spine e2e cases could not run at all on this branch: package dist is downleveled to ES5, and
SpineTexture(ES5 class inspine-core-4.2dist) extends the external@esotericsoftware/spine-corenative class →Class constructor Texture cannot be invoked without 'new'. The case dev server now aliases the spine packages to source (scoped to spine only; every other case still runs against dist). The existingspineboy/tintBlackbaselines pass unchanged under this fix — which also serves as pixel-level regression evidence for the world-path refactor.Tests
tests/src/spine-ui/): canvas collection (world-space + overlay incl.subShader), group alpha vertex assertions (straight + PMA), alpha=0 element skip, material-cache sharing across hosts, full-transform injection, skeleton-derived bounds, rect hit-test, RectMask wiring, clone (fresh skeleton/state, shared data), destroy (cache eviction + primitive release), batching opt-outspine-ui-canvas— world renderer vs UI renderer render identically side by side, third one faded byUIGroup;spine-ui-rect-mask— masked vs unmasked. All 4 Spine cases green locallyPre-existing failures on a clean
fix/shaderlabcheckout, unrelated to this PR (verified via stash baseline runs):Text > get bounds(tests/src/ui/Text.test.ts); local-only vitest browser-mode infra issues in thecore(websocketWS_ERR_UNSUPPORTED_MESSAGE_LENGTH) andshader-lab(collection error) suites.Out of scope
Maskinteraction with spine is untested (RectMask2D is the supported clipping path)SpineResource.instantiate()still instantiates the world-space template; UI usage isaddComponent(UISpineAnimationRenderer)+ theresourcesetter🤖 Generated with Claude Code