feat(spine): build Spine in as a 2D skeletal animation solution (4.2 backend)#3050
feat(spine): build Spine in as a 2D skeletal animation solution (4.2 backend)#3050cptbtptpbcptdtptp wants to merge 5 commits into
Conversation
Move engine-spine into the engine monorepo following the physics provider pattern: a version-agnostic facade plus a pluggable version backend. Public API matches the engine-spine 4.2 branch. - @galacean/engine-spine: user-facing SpineAnimationRenderer + loaders, owns the Galacean-side buffers/material, with zero spine-core runtime dependency. ISpineRuntime and ISpineRenderTarget are the seam; the backend self-registers on import, mirroring IPhysics with physics-physx / physics-lite. - @galacean/engine-spine-core-4.2: bundles @esotericsoftware/spine-core 4.2, the mesh generator and Spine42Runtime. - 2D/Spine ShaderLab shader, precompiled and registered in ShaderPool. - e2e: spineboy case. The 3.8 backend (@galacean/engine-spine-core-3.8) follows in a separate PR. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
This pull request is abnormally large and would use a significant amount of tokens to review. If you still wish to review it, comment "augment review" and we will review it. |
|
Important Review skippedDraft detected. 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:
WalkthroughThis PR introduces two new packages — ChangesSpine Animation Integration
Sequence Diagram(s)sequenceDiagram
participant App
participant SpineLoader
participant SpineAtlasLoader
participant LoaderUtils
participant ISpineRuntime as Spine42Runtime
participant SpineResource
participant SpineAnimationRenderer
App->>SpineLoader: load({ url: "spineboy.json" })
SpineLoader->>SpineAtlasLoader: load({ url: "spineboy.atlas" })
SpineAtlasLoader->>LoaderUtils: loadTexturesByPaths(imagePaths)
LoaderUtils-->>SpineAtlasLoader: Texture2D[]
SpineAtlasLoader->>ISpineRuntime: createTextureAtlas(atlasText, textures)
ISpineRuntime-->>SpineAtlasLoader: TextureAtlas
SpineLoader->>ISpineRuntime: createSkeletonData(rawData, atlas, scale)
ISpineRuntime-->>SpineLoader: SkeletonData
SpineLoader->>SpineResource: new SpineResource(engine, skeletonData)
SpineResource->>SpineAnimationRenderer: attach renderer with Skeleton + AnimationState
Note over SpineAnimationRenderer,ISpineRuntime: Per-frame rendering loop
SpineAnimationRenderer->>ISpineRuntime: updateState(skeleton, state, delta)
SpineAnimationRenderer->>ISpineRuntime: buildPrimitive(skeleton, renderer)
ISpineRuntime->>SpineAnimationRenderer: _addSubPrimitive / _getMaterial
SpineAnimationRenderer-->>App: render elements pushed to camera pipeline
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ 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 |
There was a problem hiding this comment.
Actionable comments posted: 8
🧹 Nitpick comments (3)
packages/spine/src/renderer/SpineAnimationRenderer.ts (1)
178-179: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueDead guard.
_subPrimitivesis initialized to[]and only ever reset/refilled, so!_subPrimitivesis never true. The loop already handles the empty case. Safe to drop, or guard on.lengthif early-out is intended.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/spine/src/renderer/SpineAnimationRenderer.ts` around lines 178 - 179, The guard in SpineAnimationRenderer’s update path is unreachable because _subPrimitives is always initialized and only emptied/refilled, so !this._subPrimitives will never fire. Remove the dead early return in the method that destructures _primitive, _subPrimitives, and _materials, or change it to check _subPrimitives.length if you want to skip work when there are no sub-primitives.packages/spine/src/loader/SpineLoader.ts (1)
116-139: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueHardcoded skeleton scale (
0.01).
createSkeletonData(skeletonRawData, textureAtlas, 0.01)bakes in a magic scale. If this is intentional for engine units, consider extracting a named constant; if it should be configurable, surface it viaSpineLoaderParams. Non-blocking.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/spine/src/loader/SpineLoader.ts` around lines 116 - 139, The Spine resource creation path hardcodes the skeleton scale in _loadAndCreateSpineResource via LoaderUtils.createSkeletonData, which makes the unit conversion an opaque magic value. Update SpineLoader to replace the inline 0.01 with a named constant if it is a fixed engine-wide convention, or thread the scale through SpineLoaderParams and use it in _loadAndCreateSpineResource so the behavior is explicit and configurable.packages/spine-core-4.2/src/index.ts (1)
15-15: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueUnconditional
console.logon import.This logs to every consumer's console as an import side effect, including production builds. Consider gating it behind a dev/debug check or removing it.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/spine-core-4.2/src/index.ts` at line 15, The top-level console.log in the index module is running on every import as a side effect. Remove that unconditional logging or guard it behind a dev/debug condition so the module initialization in spine-core-4.2 does not emit to consumer consoles in production; update the index.ts entrypoint where version is logged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/spine-core-4.2/package.json`:
- Around line 31-40: Promote `@esotericsoftware/spine-core` out of devDependencies
in package.json because src/index.ts re-exports it and the generated
types/index.d.ts exposes that module specifier. Move it to dependencies or
peerDependencies alongside the existing `@galacean/engine` entries so consumers
installing the package get the runtime module they need.
In `@packages/spine-core-4.2/src/SpineGenerator.ts`:
- Around line 108-144: Wrap each non-fallthrough switch branch in
SpineGenerator’s attachment type switch with its own block so the case-local
bindings are scoped correctly. In the RegionAttachment, MeshAttachment, and
ClippingAttachment cases, enclose the declarations for regionAttachment,
meshAttachment, and clip in { } and keep the existing logic inside those blocks,
while leaving the default branch unchanged.
In `@packages/spine-core-4.2/src/SpineTexture.ts`:
- Around line 18-26: The setFilters method in SpineTexture is checking the wrong
parameter for the mipmap/trilinear path, which makes that branch unreachable.
Update the condition that sets TextureFilterMode.Trilinear to use minFilter
instead of magFilter, keeping the existing TextureFilter.Nearest handling and
Bilinear fallback intact. This should align with the mipmap logic already driven
by generateMipmaps() in the SpineTexture constructor.
In `@packages/spine/src/loader/LoaderUtils.ts`:
- Around line 46-49: The rejection handling in LoaderUtils.loadTexturesByPaths
is swallowing the original failure by calling reject(error) and then resolving
with an empty array, which lets callers continue with bad state. Update the
catch path in loadTexturesByPaths to rethrow or return a rejected promise
instead of returning [], so the Promise.all chain in SpineAtlasLoader.load
short-circuits immediately and preserves the original error from texture
loading.
In `@packages/spine/src/loader/SpineAtlasLoader.ts`:
- Around line 14-39: The Spine atlas asset path collection is missing image
extension tracking, so KTX/KTX2 files are never recognized correctly. Update
SpineAtlasLoader._groupAssetsByExtension and
SpineAtlasLoader._assignAssetPathsFromUrl to populate
SpineAtlasAsset.imageExtensions alongside imagePaths, preserving the extension
for each collected image URL/virtual path. Make sure the extension is pushed
whenever atlas dependencies are expanded so LoaderUtils.loadTexturesByPaths can
read the correct per-image format; also remove or otherwise address the unused
resourceManager parameter in _groupAssetsByExtension if it is no longer needed.
- Around line 68-78: The atlas loading path in SpineAtlasLoader should use the
validated resolved atlas path instead of shadowing it with item.url, which can
be undefined when item.urls is used. Update the loadTextureAtlas call in the
imagePaths.length === 0 branch to use spineAtlasAsset.atlasPath (the same
resolved value already computed earlier in the loader) so the texture atlas is
loaded from the correct path.
In `@packages/spine/src/renderer/SpineAnimationRenderer.ts`:
- Around line 317-323: In SpineAnimationRenderer’s _onDestroy cache cleanup,
guard the material.shaderData.getTexture("material_SpineTexture") result before
using texture.instanceId, since it can be null and currently can throw during
teardown. Also make the cache key computation match _getMaterial by using the
same premultiplied-alpha source as the material key logic instead of
this.premultipliedAlpha, so materialCache.delete targets the exact entry and
does not leave stale cache items behind.
- Around line 299-311: `SpineAnimationRenderer._getMaterial` is reading
`_materialCacheMap` with bracket/property access instead of `Map.get`, so the
cache lookup always misses and new `SpineMaterial` instances are created
repeatedly. Update the lookup to use the map API consistently with the existing
`.set()` and `.delete()` calls, and keep the rest of the material reuse logic
unchanged so cached materials are actually returned.
---
Nitpick comments:
In `@packages/spine-core-4.2/src/index.ts`:
- Line 15: The top-level console.log in the index module is running on every
import as a side effect. Remove that unconditional logging or guard it behind a
dev/debug condition so the module initialization in spine-core-4.2 does not emit
to consumer consoles in production; update the index.ts entrypoint where version
is logged.
In `@packages/spine/src/loader/SpineLoader.ts`:
- Around line 116-139: The Spine resource creation path hardcodes the skeleton
scale in _loadAndCreateSpineResource via LoaderUtils.createSkeletonData, which
makes the unit conversion an opaque magic value. Update SpineLoader to replace
the inline 0.01 with a named constant if it is a fixed engine-wide convention,
or thread the scale through SpineLoaderParams and use it in
_loadAndCreateSpineResource so the behavior is explicit and configurable.
In `@packages/spine/src/renderer/SpineAnimationRenderer.ts`:
- Around line 178-179: The guard in SpineAnimationRenderer’s update path is
unreachable because _subPrimitives is always initialized and only
emptied/refilled, so !this._subPrimitives will never fire. Remove the dead early
return in the method that destructures _primitive, _subPrimitives, and
_materials, or change it to check _subPrimitives.length if you want to skip work
when there are no sub-primitives.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: a123da16-c16c-49dc-b710-3bb01ae304da
⛔ Files ignored due to path filters (3)
e2e/.dev/public/spineboy.pngis excluded by!**/*.pngpackages/shader/src/Shaders/2D/Spine.shaderis excluded by!**/*.shaderpnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (33)
e2e/.dev/public/spineboy.atlase2e/.dev/public/spineboy.jsone2e/case/spine-spineboy.tse2e/package.jsonpackages/galacean/src/ShaderPool.tspackages/shader/compiledShaders/2D/Spine.shadercpackages/shader/compiledShaders/index.tspackages/shader/src/Shaders/index.tspackages/spine-core-4.2/package.jsonpackages/spine-core-4.2/src/Spine42Runtime.tspackages/spine-core-4.2/src/SpineGenerator.tspackages/spine-core-4.2/src/SpineTexture.tspackages/spine-core-4.2/src/index.tspackages/spine-core-4.2/src/util/ClearablePool.tspackages/spine-core-4.2/src/util/ReturnablePool.tspackages/spine-core-4.2/tsconfig.jsonpackages/spine/package.jsonpackages/spine/src/SpineConstant.tspackages/spine/src/enums/SpineBlendMode.tspackages/spine/src/index.tspackages/spine/src/loader/LoaderUtils.tspackages/spine/src/loader/SpineAtlasLoader.tspackages/spine/src/loader/SpineLoader.tspackages/spine/src/loader/SpineResource.tspackages/spine/src/loader/index.tspackages/spine/src/renderer/SpineAnimationRenderer.tspackages/spine/src/renderer/SpineMaterial.tspackages/spine/src/renderer/index.tspackages/spine/src/runtime/ISpineRenderTarget.tspackages/spine/src/runtime/ISpineRuntime.tspackages/spine/src/runtime/SpineRuntimeRegistry.tspackages/spine/tsconfig.jsonrollup.config.js
| "dependencies": { | ||
| "@galacean/engine-spine": "workspace:*" | ||
| }, | ||
| "devDependencies": { | ||
| "@esotericsoftware/spine-core": "~4.2.66", | ||
| "@galacean/engine": "workspace:*" | ||
| }, | ||
| "peerDependencies": { | ||
| "@galacean/engine": "workspace:*" | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Inspect spine-core-4.2 entry to confirm public re-export surface and check sibling package dep classification.
fd -t f 'index.ts' packages/spine-core-4.2/src --exec cat -n {}
echo "---- package.json deps ----"
fd -t f 'package.json' packages/spine-core-4.2 packages/spine --exec sh -c 'echo "== $1 =="; jq "{dependencies,devDependencies,peerDependencies}" "$1"' _ {}Repository: galacean/engine
Length of output: 1484
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== packages/spine-core-4.2/package.json =="
cat -n packages/spine-core-4.2/package.json
echo "== packages/spine-core-4.2/tsconfig.json =="
cat -n packages/spine-core-4.2/tsconfig.json
echo "== packages/spine-core-4.2 source tree =="
fd -t f . packages/spine-core-4.2 | sed 's#^\./##'
echo "== package metadata refs =="
rg -n '"(name|main|module|types|exports|files|publishConfig|bundleDependencies|dependencies|devDependencies|peerDependencies)"' packages/spine-core-4.2 -g 'package.json' -g 'tsconfig*.json'Repository: galacean/engine
Length of output: 3297
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== packages/spine-core-4.2/package.json =="
cat -n packages/spine-core-4.2/package.json
echo "== packages/spine-core-4.2/tsconfig.json =="
cat -n packages/spine-core-4.2/tsconfig.json
echo "== packages/spine-core-4.2 source tree =="
fd -t f . packages/spine-core-4.2 | sed 's#^\./##'
echo "== package metadata refs =="
rg -n '"(name|main|module|types|exports|files|publishConfig|bundleDependencies|dependencies|devDependencies|peerDependencies)"' packages/spine-core-4.2 -g 'package.json' -g 'tsconfig*.json'Repository: galacean/engine
Length of output: 3297
Promote @esotericsoftware/spine-core out of devDependencies src/index.ts re-exports it, so the published types/index.d.ts exposes that module specifier. Consumers installing this package won’t get @esotericsoftware/spine-core from devDependencies, so it should be a runtime dependency or peer dependency.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/spine-core-4.2/package.json` around lines 31 - 40, Promote
`@esotericsoftware/spine-core` out of devDependencies in package.json because
src/index.ts re-exports it and the generated types/index.d.ts exposes that
module specifier. Move it to dependencies or peerDependencies alongside the
existing `@galacean/engine` entries so consumers installing the package get the
runtime module they need.
| switch (attachment.constructor) { | ||
| case RegionAttachment: | ||
| const regionAttachment = <RegionAttachment>attachment; | ||
| attachmentColor = regionAttachment.color; | ||
| numFloats = clippedVertexSize << 2; | ||
| regionAttachment.computeWorldVertices(slot, tempVerts, 0, clippedVertexSize); | ||
| triangles = SpineGenerator.QUAD_TRIANGLES; | ||
| uvs = regionAttachment.uvs; | ||
| texture = regionAttachment.region.texture; | ||
| break; | ||
| case MeshAttachment: | ||
| const meshAttachment = <MeshAttachment>attachment; | ||
| attachmentColor = meshAttachment.color; | ||
| numFloats = (meshAttachment.worldVerticesLength >> 1) * clippedVertexSize; | ||
| if (numFloats > _vertices.length) { | ||
| SpineGenerator.tempVerts = new Array(numFloats); | ||
| } | ||
| meshAttachment.computeWorldVertices( | ||
| slot, | ||
| 0, | ||
| meshAttachment.worldVerticesLength, | ||
| tempVerts, | ||
| 0, | ||
| clippedVertexSize | ||
| ); | ||
| triangles = meshAttachment.triangles; | ||
| uvs = meshAttachment.uvs; | ||
| texture = meshAttachment.region.texture; | ||
| break; | ||
| case ClippingAttachment: | ||
| let clip = <ClippingAttachment>attachment; | ||
| _clipper.clipStart(slot, clip); | ||
| continue; | ||
| default: | ||
| _clipper.clipEndWithSlot(slot); | ||
| continue; | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Wrap switch case bodies in blocks to scope the declarations.
regionAttachment (Line 110), meshAttachment (Line 119), and clip (Line 138) are declared directly inside case clauses, so their bindings are visible to sibling cases and can hit a temporal dead zone. Enclose each case in { }.
🔧 Proposed fix
switch (attachment.constructor) {
- case RegionAttachment:
+ case RegionAttachment: {
const regionAttachment = <RegionAttachment>attachment;
attachmentColor = regionAttachment.color;
numFloats = clippedVertexSize << 2;
regionAttachment.computeWorldVertices(slot, tempVerts, 0, clippedVertexSize);
triangles = SpineGenerator.QUAD_TRIANGLES;
uvs = regionAttachment.uvs;
texture = regionAttachment.region.texture;
break;
- case MeshAttachment:
+ }
+ case MeshAttachment: {
const meshAttachment = <MeshAttachment>attachment;
attachmentColor = meshAttachment.color;
numFloats = (meshAttachment.worldVerticesLength >> 1) * clippedVertexSize;
if (numFloats > _vertices.length) {
SpineGenerator.tempVerts = new Array(numFloats);
}
meshAttachment.computeWorldVertices(
slot,
0,
meshAttachment.worldVerticesLength,
tempVerts,
0,
clippedVertexSize
);
triangles = meshAttachment.triangles;
uvs = meshAttachment.uvs;
texture = meshAttachment.region.texture;
break;
- case ClippingAttachment:
+ }
+ case ClippingAttachment: {
let clip = <ClippingAttachment>attachment;
_clipper.clipStart(slot, clip);
continue;
+ }
default:
_clipper.clipEndWithSlot(slot);
continue;
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| switch (attachment.constructor) { | |
| case RegionAttachment: | |
| const regionAttachment = <RegionAttachment>attachment; | |
| attachmentColor = regionAttachment.color; | |
| numFloats = clippedVertexSize << 2; | |
| regionAttachment.computeWorldVertices(slot, tempVerts, 0, clippedVertexSize); | |
| triangles = SpineGenerator.QUAD_TRIANGLES; | |
| uvs = regionAttachment.uvs; | |
| texture = regionAttachment.region.texture; | |
| break; | |
| case MeshAttachment: | |
| const meshAttachment = <MeshAttachment>attachment; | |
| attachmentColor = meshAttachment.color; | |
| numFloats = (meshAttachment.worldVerticesLength >> 1) * clippedVertexSize; | |
| if (numFloats > _vertices.length) { | |
| SpineGenerator.tempVerts = new Array(numFloats); | |
| } | |
| meshAttachment.computeWorldVertices( | |
| slot, | |
| 0, | |
| meshAttachment.worldVerticesLength, | |
| tempVerts, | |
| 0, | |
| clippedVertexSize | |
| ); | |
| triangles = meshAttachment.triangles; | |
| uvs = meshAttachment.uvs; | |
| texture = meshAttachment.region.texture; | |
| break; | |
| case ClippingAttachment: | |
| let clip = <ClippingAttachment>attachment; | |
| _clipper.clipStart(slot, clip); | |
| continue; | |
| default: | |
| _clipper.clipEndWithSlot(slot); | |
| continue; | |
| } | |
| switch (attachment.constructor) { | |
| case RegionAttachment: { | |
| const regionAttachment = <RegionAttachment>attachment; | |
| attachmentColor = regionAttachment.color; | |
| numFloats = clippedVertexSize << 2; | |
| regionAttachment.computeWorldVertices(slot, tempVerts, 0, clippedVertexSize); | |
| triangles = SpineGenerator.QUAD_TRIANGLES; | |
| uvs = regionAttachment.uvs; | |
| texture = regionAttachment.region.texture; | |
| break; | |
| } | |
| case MeshAttachment: { | |
| const meshAttachment = <MeshAttachment>attachment; | |
| attachmentColor = meshAttachment.color; | |
| numFloats = (meshAttachment.worldVerticesLength >> 1) * clippedVertexSize; | |
| if (numFloats > _vertices.length) { | |
| SpineGenerator.tempVerts = new Array(numFloats); | |
| } | |
| meshAttachment.computeWorldVertices( | |
| slot, | |
| 0, | |
| meshAttachment.worldVerticesLength, | |
| tempVerts, | |
| 0, | |
| clippedVertexSize | |
| ); | |
| triangles = meshAttachment.triangles; | |
| uvs = meshAttachment.uvs; | |
| texture = meshAttachment.region.texture; | |
| break; | |
| } | |
| case ClippingAttachment: { | |
| let clip = <ClippingAttachment>attachment; | |
| _clipper.clipStart(slot, clip); | |
| continue; | |
| } | |
| default: | |
| _clipper.clipEndWithSlot(slot); | |
| continue; | |
| } |
🧰 Tools
🪛 Biome (2.5.0)
[error] 110-110: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.
(lint/correctness/noSwitchDeclarations)
[error] 119-119: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.
(lint/correctness/noSwitchDeclarations)
[error] 138-138: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.
(lint/correctness/noSwitchDeclarations)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/spine-core-4.2/src/SpineGenerator.ts` around lines 108 - 144, Wrap
each non-fallthrough switch branch in SpineGenerator’s attachment type switch
with its own block so the case-local bindings are scoped correctly. In the
RegionAttachment, MeshAttachment, and ClippingAttachment cases, enclose the
declarations for regionAttachment, meshAttachment, and clip in { } and keep the
existing logic inside those blocks, while leaving the default branch unchanged.
Source: Linters/SAST tools
| setFilters(minFilter: TextureFilter, magFilter: TextureFilter) { | ||
| if (minFilter === TextureFilter.Nearest) { | ||
| this._image.filterMode = TextureFilterMode.Point; | ||
| } else if (magFilter === TextureFilter.MipMapLinearLinear) { | ||
| this._image.filterMode = TextureFilterMode.Trilinear; | ||
| } else { | ||
| this._image.filterMode = TextureFilterMode.Bilinear; | ||
| } | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate the target file and related texture/filter code
git ls-files | rg '^packages/spine-core-4\.2/src/.*(SpineTexture|Texture|TextureFilter).*(ts|js)$|^packages/spine-core-4\.2/src/SpineTexture\.ts$|^packages/spine-core-4\.2/src/Texture.*\.ts$' || true
echo '--- SpineTexture.ts outline ---'
ast-grep outline packages/spine-core-4.2/src/SpineTexture.ts --view expanded || true
echo '--- Search for setFilters and mipmap filter usage ---'
rg -n "setFilters|MipMapLinearLinear|Trilinear|generateMipmaps|TextureFilterMode" packages/spine-core-4.2/src -S || trueRepository: galacean/engine
Length of output: 1414
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Read the relevant file with line numbers in a small slice
sed -n '1,120p' packages/spine-core-4.2/src/SpineTexture.ts | cat -n
echo '--- Search for original engine-spine implementation in repo ---'
rg -n "class SpineTexture|setFilters\\(|MipMapLinearLinear|TextureFilterMode" . -S || trueRepository: galacean/engine
Length of output: 10397
🌐 Web query:
spine runtime TextureFilter setFilters magFilter MipMapLinearLinear minFilter trilinear source
💡 Result:
In the Spine runtime, the TextureFilter enumeration defines options for texture sampling, including MipMapLinearLinear [1][2]. When calling setFilters(minFilter, magFilter), these values are used to configure the GPU's texture sampling behavior [1][3]. Important considerations regarding these filters include: 1. Magnification Filter Constraints: Standard graphics APIs (like WebGL) generally only support Nearest or Linear for the magnification filter (magFilter) [4]. Attempting to use a MipMap-based filter (such as MipMapLinearLinear) as a magnification filter will typically cause errors or be invalid [4]. To handle this, implementations (such as the WebGL backend) often include validation logic to automatically fall back to Linear if an invalid MipMap filter is provided for magnification [3]. 2. Minification and MipMaps: MipMapLinearLinear (and other MipMap filters) are valid for the minification filter (minFilter) [4]. When a MipMap-based minification filter is selected, the runtime typically flags the texture to use MipMaps and may trigger a call to generate them (e.g., gl.generateMipmap in WebGL) [3]. 3. Atlas Configuration: The filter setting in a.atlas file defines these values for texture pages [5]. When loading, the runtime interprets the filter string to set the minFilter and magFilter for the corresponding texture page [6][5]. If you are manually configuring textures in code, ensure that only Linear or Nearest are passed as the magFilter to avoid runtime API errors [4][3]. In summary, while MipMapLinearLinear exists as a valid enumeration member for Spine textures [1][2], it should only be applied to the minification filter [4][3]. The runtime's setFilters method is responsible for applying these states to the underlying graphics API, and developers should ensure valid combinations are provided to satisfy GPU constraints [7][3].
Citations:
- 1: https://github.com/EsotericSoftware/spine-runtimes/blob/393a6c2b/spine-ts/spine-core/src/Texture.ts
- 2: https://docs.cocos.com/creator/3.8/api/en/enumeration/sp.spine.TextureFilter
- 3: https://github.com/EsotericSoftware/spine-runtimes/blob/393a6c2b/spine-ts/spine-webgl/src/GLTexture.ts
- 4: [spine-ts] webgl should allow
LINEARandNEARESTonly in texture magnification filter EsotericSoftware/spine-runtimes#1508 - 5: https://en.esotericsoftware.com/spine-atlas-format
- 6: https://github.com/EsotericSoftware/spine-runtimes/blob/393a6c2b/spine-c/spine-c/include/spine/Atlas.h
- 7: https://docs.cocos.com/creator/3.8/api/en/class/sp.spine.Texture
Use minFilter for the mipmap branch. TextureFilter.MipMapLinearLinear is a minification value, so checking magFilter makes the trilinear path unreachable. Switch the condition to minFilter; generateMipmaps() in the constructor already matches that case.
🔧 Proposed fix
setFilters(minFilter: TextureFilter, magFilter: TextureFilter) {
if (minFilter === TextureFilter.Nearest) {
this._image.filterMode = TextureFilterMode.Point;
- } else if (magFilter === TextureFilter.MipMapLinearLinear) {
+ } else if (minFilter === TextureFilter.MipMapLinearLinear) {
this._image.filterMode = TextureFilterMode.Trilinear;
} else {
this._image.filterMode = TextureFilterMode.Bilinear;
}
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| setFilters(minFilter: TextureFilter, magFilter: TextureFilter) { | |
| if (minFilter === TextureFilter.Nearest) { | |
| this._image.filterMode = TextureFilterMode.Point; | |
| } else if (magFilter === TextureFilter.MipMapLinearLinear) { | |
| this._image.filterMode = TextureFilterMode.Trilinear; | |
| } else { | |
| this._image.filterMode = TextureFilterMode.Bilinear; | |
| } | |
| } | |
| setFilters(minFilter: TextureFilter, magFilter: TextureFilter) { | |
| if (minFilter === TextureFilter.Nearest) { | |
| this._image.filterMode = TextureFilterMode.Point; | |
| } else if (minFilter === TextureFilter.MipMapLinearLinear) { | |
| this._image.filterMode = TextureFilterMode.Trilinear; | |
| } else { | |
| this._image.filterMode = TextureFilterMode.Bilinear; | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/spine-core-4.2/src/SpineTexture.ts` around lines 18 - 26, The
setFilters method in SpineTexture is checking the wrong parameter for the
mipmap/trilinear path, which makes that branch unreachable. Update the condition
that sets TextureFilterMode.Trilinear to use minFilter instead of magFilter,
keeping the existing TextureFilter.Nearest handling and Bilinear fallback
intact. This should align with the mipmap logic already driven by
generateMipmaps() in the SpineTexture constructor.
Source: Linters/SAST tools
| return Promise.all(texturePromises).catch((error) => { | ||
| reject(error); | ||
| return []; | ||
| }); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Swallowed rejection lets callers continue with empty textures.
On failure this calls reject(error) but then resolves the returned promise with []. Callers like SpineAtlasLoader.load (Promise.all([...text, LoaderUtils.loadTexturesByPaths(...)])) will still proceed into createTextureAtlas(atlasText, []), which can throw a second, less meaningful error before the original reject settles the outer AssetPromise. Prefer rethrowing so the promise chain short-circuits cleanly.
🛡️ Proposed fix
- return Promise.all(texturePromises).catch((error) => {
- reject(error);
- return [];
- });
+ return Promise.all(texturePromises).catch((error) => {
+ reject(error);
+ throw error;
+ });📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| return Promise.all(texturePromises).catch((error) => { | |
| reject(error); | |
| return []; | |
| }); | |
| return Promise.all(texturePromises).catch((error) => { | |
| reject(error); | |
| throw error; | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/spine/src/loader/LoaderUtils.ts` around lines 46 - 49, The rejection
handling in LoaderUtils.loadTexturesByPaths is swallowing the original failure
by calling reject(error) and then resolving with an empty array, which lets
callers continue with bad state. Update the catch path in loadTexturesByPaths to
rethrow or return a rejected promise instead of returning [], so the Promise.all
chain in SpineAtlasLoader.load short-circuits immediately and preserves the
original error from texture loading.
| private static _groupAssetsByExtension(url: string, assetPath: SpineAtlasAsset, resourceManager: ResourceManager) { | ||
| let ext = SpineLoader._getUrlExtension(url); | ||
| if (!ext) return; | ||
|
|
||
| if (ext === "atlas") { | ||
| assetPath.atlasPath = url; | ||
| } | ||
| if (["png", "jpg", "webp", "jpeg", "ktx", "ktx2"].includes(ext)) { | ||
| assetPath.imagePaths.push(url); | ||
| } | ||
| } | ||
|
|
||
| private static _assignAssetPathsFromUrl(url: string, assetPath: SpineAtlasAsset, resourceManager: ResourceManager) { | ||
| const ext = SpineLoader._getUrlExtension(url); | ||
| if (ext === "atlas") { | ||
| assetPath.atlasPath = url; | ||
| // @ts-ignore | ||
| const atlasDependency = resourceManager?._virtualPathResourceMap?.[url]?.dependentAssetMap; | ||
| if (atlasDependency) { | ||
| for (let key in atlasDependency) { | ||
| const imageVirtualPath = atlasDependency[key]; | ||
| assetPath.imagePaths.push(imageVirtualPath); | ||
| } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
imageExtensions is never populated, breaking KTX/KTX2 detection.
Both _groupAssetsByExtension and _assignAssetPathsFromUrl compute ext/image paths but never push into spineAtlasAsset.imageExtensions. Downstream, LoaderUtils.loadTexturesByPaths(imagePaths, imageExtensions, ...) (Line 86) reads imageExtensions[index], which is always undefined, so the ktx/ktx2 branches never fire and those textures are loaded as AssetType.Texture. The resourceManager parameter in _groupAssetsByExtension is also unused.
🐛 Proposed fix
- private static _groupAssetsByExtension(url: string, assetPath: SpineAtlasAsset, resourceManager: ResourceManager) {
+ private static _groupAssetsByExtension(url: string, assetPath: SpineAtlasAsset) {
let ext = SpineLoader._getUrlExtension(url);
if (!ext) return;
if (ext === "atlas") {
assetPath.atlasPath = url;
}
if (["png", "jpg", "webp", "jpeg", "ktx", "ktx2"].includes(ext)) {
assetPath.imagePaths.push(url);
+ assetPath.imageExtensions.push(ext);
}
}Apply the same imageExtensions.push(...) in _assignAssetPathsFromUrl where image virtual paths are collected.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/spine/src/loader/SpineAtlasLoader.ts` around lines 14 - 39, The
Spine atlas asset path collection is missing image extension tracking, so
KTX/KTX2 files are never recognized correctly. Update
SpineAtlasLoader._groupAssetsByExtension and
SpineAtlasLoader._assignAssetPathsFromUrl to populate
SpineAtlasAsset.imageExtensions alongside imagePaths, preserving the extension
for each collected image URL/virtual path. Make sure the extension is pushed
whenever atlas dependencies are expanded so LoaderUtils.loadTexturesByPaths can
read the correct per-image format; also remove or otherwise address the unused
resourceManager parameter in _groupAssetsByExtension if it is no longer needed.
| const imagePaths = spineAtlasAsset.imagePaths; | ||
| if (imagePaths.length === 0) { | ||
| const atlasPath = item.url; | ||
| LoaderUtils.loadTextureAtlas(atlasPath, engine, reject) | ||
| .then((textureAtlas) => { | ||
| resolve(textureAtlas); | ||
| }) | ||
| .catch((err) => { | ||
| reject(err); | ||
| }); | ||
| } else { |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Use the resolved atlasPath, not item.url.
When item.urls is provided (no separate images), item.url may be undefined, yet Line 70 shadows the outer atlasPath with item.url and passes it to loadTextureAtlas. Use the already-validated spineAtlasAsset.atlasPath instead.
🐛 Proposed fix
const imagePaths = spineAtlasAsset.imagePaths;
if (imagePaths.length === 0) {
- const atlasPath = item.url;
LoaderUtils.loadTextureAtlas(atlasPath, engine, reject)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const imagePaths = spineAtlasAsset.imagePaths; | |
| if (imagePaths.length === 0) { | |
| const atlasPath = item.url; | |
| LoaderUtils.loadTextureAtlas(atlasPath, engine, reject) | |
| .then((textureAtlas) => { | |
| resolve(textureAtlas); | |
| }) | |
| .catch((err) => { | |
| reject(err); | |
| }); | |
| } else { | |
| const imagePaths = spineAtlasAsset.imagePaths; | |
| if (imagePaths.length === 0) { | |
| LoaderUtils.loadTextureAtlas(atlasPath, engine, reject) | |
| .then((textureAtlas) => { | |
| resolve(textureAtlas); | |
| }) | |
| .catch((err) => { | |
| reject(err); | |
| }); | |
| } else { |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/spine/src/loader/SpineAtlasLoader.ts` around lines 68 - 78, The
atlas loading path in SpineAtlasLoader should use the validated resolved atlas
path instead of shadowing it with item.url, which can be undefined when
item.urls is used. Update the loadTextureAtlas call in the imagePaths.length ===
0 branch to use spineAtlasAsset.atlasPath (the same resolved value already
computed earlier in the loader) so the texture atlas is loaded from the correct
path.
| const key = `${texture.instanceId}_${blendMode}_${premultipliedAlpha ? 1 : 0}`; | ||
| let cached = SpineAnimationRenderer._materialCacheMap[key] as SpineMaterial; | ||
| if (!cached) { | ||
| cached = new SpineMaterial(engine); | ||
| cached.isGCIgnored = true; | ||
| SpineAnimationRenderer._materialCacheMap.set(key, cached); | ||
| } | ||
| cached._setBlendMode(blendMode, premultipliedAlpha); | ||
| cached._setTexture(texture); | ||
| cached._setTintBlack(tintBlack); | ||
| cached._setPremultipliedAlpha(premultipliedAlpha); | ||
| return cached; | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win
Cache read uses Map property access — always misses, leaking materials.
_materialCacheMap is a Map, but the lookup on Line 300 uses property/bracket access (_materialCacheMap[key]) while writes use .set() (Line 304) and deletes use .delete() (Line 322). Property access on a Map never returns values stored via .set(), so cached is always undefined. A new SpineMaterial is created on every _getMaterial call, and since each is marked isGCIgnored = true, they are never reclaimed — a per-frame memory/GPU leak and wasted allocation.
🐛 Proposed fix
- let cached = SpineAnimationRenderer._materialCacheMap[key] as SpineMaterial;
+ let cached = SpineAnimationRenderer._materialCacheMap.get(key);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const key = `${texture.instanceId}_${blendMode}_${premultipliedAlpha ? 1 : 0}`; | |
| let cached = SpineAnimationRenderer._materialCacheMap[key] as SpineMaterial; | |
| if (!cached) { | |
| cached = new SpineMaterial(engine); | |
| cached.isGCIgnored = true; | |
| SpineAnimationRenderer._materialCacheMap.set(key, cached); | |
| } | |
| cached._setBlendMode(blendMode, premultipliedAlpha); | |
| cached._setTexture(texture); | |
| cached._setTintBlack(tintBlack); | |
| cached._setPremultipliedAlpha(premultipliedAlpha); | |
| return cached; | |
| } | |
| const key = `${texture.instanceId}_${blendMode}_${premultipliedAlpha ? 1 : 0}`; | |
| let cached = SpineAnimationRenderer._materialCacheMap.get(key); | |
| if (!cached) { | |
| cached = new SpineMaterial(engine); | |
| cached.isGCIgnored = true; | |
| SpineAnimationRenderer._materialCacheMap.set(key, cached); | |
| } | |
| cached._setBlendMode(blendMode, premultipliedAlpha); | |
| cached._setTexture(texture); | |
| cached._setTintBlack(tintBlack); | |
| cached._setPremultipliedAlpha(premultipliedAlpha); | |
| return cached; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/spine/src/renderer/SpineAnimationRenderer.ts` around lines 299 -
311, `SpineAnimationRenderer._getMaterial` is reading `_materialCacheMap` with
bracket/property access instead of `Map.get`, so the cache lookup always misses
and new `SpineMaterial` instances are created repeatedly. Update the lookup to
use the map API consistently with the existing `.set()` and `.delete()` calls,
and keep the rest of the material reuse logic unchanged so cached materials are
actually returned.
| for (let i = 0, len = materials.length; i < len; i += 1) { | ||
| const material = materials[i] as SpineMaterial; | ||
| const texture = material.shaderData.getTexture("material_SpineTexture"); | ||
| const blendMode = material._getBlendMode(); | ||
| const key = `${texture.instanceId}_${blendMode}_${premultipliedAlpha ? 1 : 0}`; | ||
| materialCache.delete(key); | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Guard against a null texture during cache cleanup.
shaderData.getTexture(...) can return null; the subsequent texture.instanceId would then throw during _onDestroy. Since the key here is also computed using this.premultipliedAlpha rather than each material's own value, a mismatch with the key built in _getMaterial can additionally leave stale entries.
🛡️ Proposed guard
const material = materials[i] as SpineMaterial;
const texture = material.shaderData.getTexture("material_SpineTexture");
+ if (!texture) continue;
const blendMode = material._getBlendMode();📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| for (let i = 0, len = materials.length; i < len; i += 1) { | |
| const material = materials[i] as SpineMaterial; | |
| const texture = material.shaderData.getTexture("material_SpineTexture"); | |
| const blendMode = material._getBlendMode(); | |
| const key = `${texture.instanceId}_${blendMode}_${premultipliedAlpha ? 1 : 0}`; | |
| materialCache.delete(key); | |
| } | |
| for (let i = 0, len = materials.length; i < len; i += 1) { | |
| const material = materials[i] as SpineMaterial; | |
| const texture = material.shaderData.getTexture("material_SpineTexture"); | |
| if (!texture) continue; | |
| const blendMode = material._getBlendMode(); | |
| const key = `${texture.instanceId}_${blendMode}_${premultipliedAlpha ? 1 : 0}`; | |
| materialCache.delete(key); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/spine/src/renderer/SpineAnimationRenderer.ts` around lines 317 -
323, In SpineAnimationRenderer’s _onDestroy cache cleanup, guard the
material.shaderData.getTexture("material_SpineTexture") result before using
texture.instanceId, since it can be null and currently can throw during
teardown. Also make the cache key computation match _getMaterial by using the
same premultiplied-alpha source as the material key logic instead of
this.premultipliedAlpha, so materialCache.delete targets the exact entry and
does not leave stale cache items behind.
e2e: spineboy basic render, plus a tint-black two-color case using the real tank-pro asset sampled at the muzzle-smoke frame where tintBlack on/off differs ~7% (registered in config.ts with 0.05 tolerance). unit (tests/src/spine): pool reuse, runtime registry, vertex stride and blend ordinal contracts, loader url parsing, SpineMaterial blend factors, SpineAnimationRenderer tintBlack setter and material cache. fix: SpineAnimationRenderer._getMaterial read its cache via Map[key] (always undefined) instead of .get(key), so every sub-mesh allocated a fresh SpineMaterial every frame. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
# Conflicts: # packages/shader/compiledShaders/index.ts
The packages/spine-core-3.8 directory was never committed, but a leftover lockfile importer entry for it remained, which pnpm install prunes.
The merge's pre-commit hook auto-fixed Object -> object in this file per the newly-merged ESLint config, which made the implementation signature's AssetPromise.all(promises) return type no longer assignable (T is unconstrained here since it must satisfy both the single-item and collection overloads).
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## dev/2.0 #3050 +/- ##
===========================================
+ Coverage 79.37% 79.42% +0.05%
===========================================
Files 903 925 +22
Lines 100632 101995 +1363
Branches 11260 11323 +63
===========================================
+ Hits 79879 81014 +1135
- Misses 20569 20794 +225
- Partials 184 187 +3
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
Superseded by #3057 — same branch, now pushed directly to galacean/engine instead of a fork. |
RFC: Spine 内置化与多版本内核拆分
origin/dev/2.0(97d8d610c, v2.0.0-alpha.34)feat/builtin-spinepackages/spine、packages/spine-core-4.2(已落地);packages/spine-core-3.8(待做)1. 摘要
把 Spine 从独立仓库收进 engine,并按引擎物理包的 provider 模式把版本内核拆成可替换后端:共享门面
@galacean/engine-spine(版本无关)+engine-spine-core-4.2/-3.8两个内核(对标physics-physx/physics-lite)。对外 API 与 engine-spine 4.2 分支一致。Phase 1/2 已落地、tsc通过;3.8 后端与真机渲染冒烟待做。2. 架构
三个包,职责对标物理:
@galacean/engine-spineCollider+IPhysics@galacean/engine-spine-core-4.2physics-physx@galacean/engine-spine-core-3.8physics-lite接缝(
IPhysics的对应物):ISpineRuntime— 解析、实例化、步进(updateState)、生成(buildPrimitive)。后端实现它。ISpineRenderTarget— 门面SpineAnimationRenderer暴露给后端生成器写入的窄契约(缓冲/材质/包围盒),不含 spine-core 类型。注入走副作用自注册:后端
import时registerSpineRuntime(new Spine42Runtime()),门面经getSpineRuntime()取。不进 engine config——Spine 是卫星包,不污染 engine core。3. 关键决策
D1 — facade 切在"渲染数据 + 高频控制",不抽象整个 spine 对象模型。 版本相关的解析/步进/生成进后端,Galacean 侧缓冲/材质/绘制留门面;深层 spine-core 对象直接暴露原生类型,不重造中立模型。
D2 — spine-core 类的 import 路径落到内核包(physics-faithful)。门面
export *不再含 spine-core,由内核包 re-export。代价:手动构造 spine-core 对象的高级用法,import 从engine-spine改到engine-spine-core-4.2(常规用法不变)。D3 — 着色器内置进
packages/shader。 engine 2.0 移除了运行时 GLSL 字符串 shader,故重写为内置 ShaderLab2D/Spine(随引擎预编译、ShaderPool注册,同 UI 的2D/UIDefault);SpineMaterial用Shader.find,per-material blend 走 shaderData 绑定。全工程唯一非直译处,公开行为不变。D4 — 命名
engine-spine-core-4.2/-3.8。core前缀避免裸版本号与包自身 semver 撞车。D5 — 单 runtime 注册表。
SpineRuntimeRegistry后注册覆盖,一个 app 一个内核。D6 — 默认安装期单版本(A);运行时多版本并存(B)为可选。
update随活跃骨骼数线性O(n),与版本数无关。D7 — es5 目标下的两处构建适配(Phase 4 冒烟才暴露,tsc 看不到)。
SpineTexture extends Texture(spine-core):monorepo 编 es5,外部 spine-core 是原生 es6 类,es5 子类super()调 es6 基类崩。解:后端 bundle spine-core(spine-core 降为 devDependency)+ rollup swcexclude放行@esotericsoftware一并转 es5。_render:原版 4.2 用 engine 1.x 的_subRenderElementPool(2.0 已移除)。改为 2.0 模型——每个 sub-mesh 一个engine._renderElementPool.get()+renderElement.set(...)+pushRenderElement(对标MeshRenderer)。4. 决议(原未决问题)
AnimationStateData→ 共享(N:1) ✅ 已实现。_cloneTo复用源的AnimationStateData,resource上配的 mix 时长随之对所有实例生效;只有 per-instance 的Skeleton/AnimationState是新建的。5. 对象模型:资产 / 组件 / 数据
SpineResource("Spine")TextureAtlas("SpineAtlas")、Texture2DSpineMaterialSpineAnimationRendererSkeletonData、AnimationStateDataSkeleton、AnimationState基数:
1 SpineResource──instantiate 1:N──>N SpineAnimationRenderer1 SpineAnimationRenderer→1 Skeleton+1 AnimationStateN Skeleton──N:1共享──>1 SkeletonDataN skeleton : 1 runtime(共享的是代码不是工作)6. 版本差异
SpineGeneratorMeshGeneratorupdateWorldTransform(Physics.update)updateWorldTransform()差异被各自
Runtime.updateState()吸收,门面与ISpineRuntime不感知——即抽象的收益。7. 用法
8. 动机与非目标
动机:engine-spine 现为独立仓库、绑定单一 spine-core 版本,用户无法自由选版本,集成也游离于引擎外。
非目标:不复刻 3.8 的
SpineRenderer/SpineAnimation(它们extends Script,与 4.2extends Renderer是两套 API);统一用 4.2 形状门面驱动 3.8 资产。默认不做运行时多版本并存。9. 实施状态
packages/spine= 4.2 迁到 engine 2.0(12 处漂移修复);2D/Spine.shader预编译+注册;闭包tsc零错误。engine-spine-core-4.2+Spine42Runtime;门面运行时零 spine-core(全import type);闭包tsc零错误。Spine38Runtime+ 适配生成器(读 3.8 attachment API,写共享顶点格式)。含 tint black:generator 读slot.darkColor填DARK_COLOR,让 3.8 资产也渲染两色染色(原版 3.8 galacean 没做;数据层与共享 shader 都现成)。10. 参考
IPhysics(packages/design/src/physics/IPhysics.ts)Summary by CodeRabbit
New Features
Bug Fixes