From 0a0c4a7291a72b5f512355789c99831cd0eeec58 Mon Sep 17 00:00:00 2001 From: cptbtptpbcptdtptp Date: Thu, 23 Apr 2026 18:08:29 +0800 Subject: [PATCH] feat: atlas support rotate --- .../src/2d/assembler/SimpleSpriteAssembler.ts | 25 +++-- .../src/2d/assembler/SlicedSpriteAssembler.ts | 7 +- .../src/2d/assembler/TiledSpriteAssembler.ts | 7 +- packages/core/src/2d/sprite/Sprite.ts | 97 ++++++++++++------- packages/loader/src/SpriteAtlasLoader.ts | 2 +- 5 files changed, 89 insertions(+), 49 deletions(-) diff --git a/packages/core/src/2d/assembler/SimpleSpriteAssembler.ts b/packages/core/src/2d/assembler/SimpleSpriteAssembler.ts index 563f812106..6e0e07c368 100644 --- a/packages/core/src/2d/assembler/SimpleSpriteAssembler.ts +++ b/packages/core/src/2d/assembler/SimpleSpriteAssembler.ts @@ -66,20 +66,25 @@ export class SimpleSpriteAssembler { } static updateUVs(renderer: ISpriteRenderer): void { + // sprite._uvs 16 个网格点 (column-major, index=column*4+row),4 corner: [0]=LB / [3]=LT / [12]=RB / [15]=RT + // atlasRotated 由 sprite._updateUVs 内部处理,此处直接用结果,无需关心朝向 const spriteUVs = renderer.sprite._getUVs(); - const { x: left, y: bottom } = spriteUVs[0]; - const { x: right, y: top } = spriteUVs[3]; const subChunk = renderer._subChunk; const vertices = subChunk.chunk.vertices; const offset = subChunk.vertexArea.start + 3; - vertices[offset] = left; - vertices[offset + 1] = bottom; - vertices[offset + 9] = right; - vertices[offset + 10] = bottom; - vertices[offset + 18] = left; - vertices[offset + 19] = top; - vertices[offset + 27] = right; - vertices[offset + 28] = top; + const uvLB = spriteUVs[0]; + const uvLT = spriteUVs[3]; + const uvRB = spriteUVs[12]; + const uvRT = spriteUVs[15]; + // SimpleAssembler 的 vertex 顺序:0=LB, 1=RB, 2=LT, 3=RT (按 _rectangleTriangles 的拓扑) + vertices[offset] = uvLB.x; + vertices[offset + 1] = uvLB.y; + vertices[offset + 9] = uvRB.x; + vertices[offset + 10] = uvRB.y; + vertices[offset + 18] = uvLT.x; + vertices[offset + 19] = uvLT.y; + vertices[offset + 27] = uvRT.x; + vertices[offset + 28] = uvRT.y; } static updateColor(renderer: ISpriteRenderer, alpha: number): void { diff --git a/packages/core/src/2d/assembler/SlicedSpriteAssembler.ts b/packages/core/src/2d/assembler/SlicedSpriteAssembler.ts index 31d19d149c..09abce5be3 100644 --- a/packages/core/src/2d/assembler/SlicedSpriteAssembler.ts +++ b/packages/core/src/2d/assembler/SlicedSpriteAssembler.ts @@ -126,14 +126,15 @@ export class SlicedSpriteAssembler { } static updateUVs(renderer: ISpriteRenderer): void { + // 16 UV 网格 (column-major: index = i*4+j, i=column, j=row),与 vertex 索引一一对应 const subChunk = renderer._subChunk; const vertices = subChunk.chunk.vertices; const spriteUVs = renderer.sprite._getUVs(); for (let i = 0, o = subChunk.vertexArea.start + 3; i < 4; i++) { - const rowU = spriteUVs[i].x; for (let j = 0; j < 4; j++, o += 9) { - vertices[o] = rowU; - vertices[o + 1] = spriteUVs[j].y; + const uv = spriteUVs[i * 4 + j]; + vertices[o] = uv.x; + vertices[o + 1] = uv.y; } } } diff --git a/packages/core/src/2d/assembler/TiledSpriteAssembler.ts b/packages/core/src/2d/assembler/TiledSpriteAssembler.ts index bc765c45f9..448d0543a4 100644 --- a/packages/core/src/2d/assembler/TiledSpriteAssembler.ts +++ b/packages/core/src/2d/assembler/TiledSpriteAssembler.ts @@ -188,7 +188,12 @@ export class TiledSpriteAssembler { const spritePositions = sprite._getPositions(); const { x: left, y: bottom } = spritePositions[0]; const { x: right, y: top } = spritePositions[3]; - const [spriteUV0, spriteUV1, spriteUV2, spriteUV3] = sprite._getUVs(); + // 16 UV column-major: [0]=LB(left,bottom), [5]=border-LB(bLeft,bBottom), [10]=border-RT(bRight,bTop), [15]=RT(right,top) + const allUVs = sprite._getUVs(); + const spriteUV0 = allUVs[0]; + const spriteUV1 = allUVs[5]; + const spriteUV2 = allUVs[10]; + const spriteUV3 = allUVs[15]; const expectWidth = sprite.width * referenceResolutionPerUnit; const expectHeight = sprite.height * referenceResolutionPerUnit; const fixedL = expectWidth * border.x; diff --git a/packages/core/src/2d/sprite/Sprite.ts b/packages/core/src/2d/sprite/Sprite.ts index 5232001c9a..b7f01f342e 100644 --- a/packages/core/src/2d/sprite/Sprite.ts +++ b/packages/core/src/2d/sprite/Sprite.ts @@ -20,7 +20,16 @@ export class Sprite extends ReferResource { private _customHeight: number = undefined; private _positions: Vector2[] = [new Vector2(), new Vector2(), new Vector2(), new Vector2()]; - private _uvs: Vector2[] = [new Vector2(), new Vector2(), new Vector2(), new Vector2()]; + // 16 UV 顶点构成 4×4 网格(含 9-slice 内边界)。column-major:index = i*4 + j,i=column(0=left..3=right),j=row(0=bottom..3=top)。 + // 与 SlicedAssembler/TiledAssembler 的 vertex 索引图一致:[0]=LB, [3]=LT, [12]=RB, [15]=RT。 + // SimpleAssembler 取 [0]/[3]/[12]/[15] 4 个 corner;Sliced/Tiled 用全 16。 + // prettier-ignore + private _uvs: Vector2[] = [ + new Vector2(), new Vector2(), new Vector2(), new Vector2(), + new Vector2(), new Vector2(), new Vector2(), new Vector2(), + new Vector2(), new Vector2(), new Vector2(), new Vector2(), + new Vector2(), new Vector2(), new Vector2(), new Vector2(), + ]; private _bounds: BoundingBox = new BoundingBox(); private _texture: Texture2D = null; @@ -287,16 +296,18 @@ export class Sprite extends ReferResource { private _calDefaultSize(): void { if (this._texture) { - const { _texture, _atlasRegion, _atlasRegionOffset, _region } = this; - const pixelsPerUnitReciprocal = 1.0 / Engine._pixelsPerUnit; + const { _texture, _atlasRegion, _atlasRegionOffset, _region, _atlasRotated } = this; + const ppuReciprocal = 1.0 / Engine._pixelsPerUnit; + // 先算 atlas 中绝对像素(texture 不一定是方形,必须各自乘对应维度) + const atlasPxW = _texture.width * _atlasRegion.width; + const atlasPxH = _texture.height * _atlasRegion.height; + // atlas 顺时针 pack 90°:原图 W×H 在 atlas 中占 H×W 区域,仅交换 atlasPx 的 W/H + const originWidth = _atlasRotated ? atlasPxH : atlasPxW; + const originHeight = _atlasRotated ? atlasPxW : atlasPxH; this._automaticWidth = - ((_texture.width * _atlasRegion.width) / (1 - _atlasRegionOffset.x - _atlasRegionOffset.z)) * - _region.width * - pixelsPerUnitReciprocal; + (originWidth / (1 - _atlasRegionOffset.x - _atlasRegionOffset.z)) * _region.width * ppuReciprocal; this._automaticHeight = - ((_texture.height * _atlasRegion.height) / (1 - _atlasRegionOffset.y - _atlasRegionOffset.w)) * - _region.height * - pixelsPerUnitReciprocal; + (originHeight / (1 - _atlasRegionOffset.y - _atlasRegionOffset.w)) * _region.height * ppuReciprocal; } else { this._automaticWidth = this._automaticHeight = 0; } @@ -332,34 +343,52 @@ export class Sprite extends ReferResource { } private _updateUVs(): void { - const { _uvs: uv, _atlasRegionOffset: atlasRegionOffset } = this; - const { x: regionX, y: regionY, width: regionW, height: regionH } = this._region; - const regionRight = 1 - regionX - regionW; - const regionBottom = 1 - regionY - regionH; + const { _uvs: uvs, _atlasRotated: atlasRotated, _border: border } = this; + const { x: regionLeft, y: regionTop, width: regionW, height: regionH } = this._region; + const regionRight = 1 - regionLeft - regionW; + const regionBottom = 1 - regionTop - regionH; const { x: atlasRegionX, y: atlasRegionY, width: atlasRegionW, height: atlasRegionH } = this._atlasRegion; - const { x: offsetLeft, y: offsetTop, z: offsetRight, w: offsetBottom } = atlasRegionOffset; + const { x: offsetLeft, y: offsetTop, z: offsetRight, w: offsetBottom } = this._atlasRegionOffset; const realWidth = atlasRegionW / (1 - offsetLeft - offsetRight); const realHeight = atlasRegionH / (1 - offsetTop - offsetBottom); - // Coordinates of the four boundaries. - const left = Math.max(regionX - offsetLeft, 0) * realWidth + atlasRegionX; - const top = Math.max(regionBottom - offsetTop, 0) * realHeight + atlasRegionY; - const right = atlasRegionW + atlasRegionX - Math.max(regionRight - offsetRight, 0) * realWidth; - const bottom = atlasRegionH + atlasRegionY - Math.max(regionY - offsetBottom, 0) * realHeight; - const { x: borderLeft, y: borderBottom, z: borderRight, w: borderTop } = this._border; - // Left-Bottom - uv[0].set(left, bottom); - // Border ( Left-Bottom ) - uv[1].set( - (regionX - offsetLeft + borderLeft * regionW) * realWidth + atlasRegionX, - atlasRegionH + atlasRegionY - (regionY - offsetBottom + borderBottom * regionH) * realHeight - ); - // Border ( Right-Top ) - uv[2].set( - atlasRegionW + atlasRegionX - (regionRight - offsetRight + borderRight * regionW) * realWidth, - (regionBottom - offsetTop + borderTop * regionH) * realHeight + atlasRegionY - ); - // Right-Top - uv[3].set(right, top); + // 4 个外边界 + 4 个 9-slice 内边界 + let left: number, top: number, right: number, bottom: number; + let bLeft: number, bTop: number, bRight: number, bBottom: number; + if (atlasRotated) { + // 原图 region/offset (left/top/right/bottom) 在 atlas 中映射为 (bottom/left/top/right) + left = Math.max(regionBottom - offsetLeft, 0) * realWidth + atlasRegionX; + top = Math.max(regionLeft - offsetTop, 0) * realHeight + atlasRegionY; + right = atlasRegionW + atlasRegionX - Math.max(regionTop - offsetRight, 0) * realWidth; + bottom = atlasRegionH + atlasRegionY - Math.max(regionRight - offsetBottom, 0) * realHeight; + bLeft = (regionBottom - offsetLeft + border.y * regionH) * realWidth + atlasRegionX; + bTop = (regionLeft - offsetTop + border.x * regionW) * realHeight + atlasRegionY; + bRight = atlasRegionW + atlasRegionX - (regionTop - offsetRight + border.w * regionH) * realWidth; + bBottom = atlasRegionH + atlasRegionY - (regionRight - offsetBottom + border.z * regionW) * realHeight; + } else { + left = Math.max(regionLeft - offsetLeft, 0) * realWidth + atlasRegionX; + top = Math.max(regionBottom - offsetTop, 0) * realHeight + atlasRegionY; + right = atlasRegionW + atlasRegionX - Math.max(regionRight - offsetRight, 0) * realWidth; + bottom = atlasRegionH + atlasRegionY - Math.max(regionTop - offsetBottom, 0) * realHeight; + bLeft = (regionLeft - offsetLeft + border.x * regionW) * realWidth + atlasRegionX; + bTop = (regionBottom - offsetTop + border.w * regionH) * realHeight + atlasRegionY; + bRight = atlasRegionW + atlasRegionX - (regionRight - offsetRight + border.z * regionW) * realWidth; + bBottom = atlasRegionH + atlasRegionY - (regionTop - offsetBottom + border.y * regionH) * realHeight; + } + + // 16 UV 网格填充(column-major:index=i*4+j,i=column[0=left..3=right],j=row[0=bottom..3=top]) + // - 非 rotated:column 决定 atlas X (left/bLeft/bRight/right),row 决定 atlas Y (bottom/bBottom/bTop/top) + // - rotated 90° 顺时针 packed:display column 对应 atlas Y 的反向(顶→底),display row 对应 atlas X + if (atlasRotated) { + uvs[0].set(left, top), uvs[1].set(bLeft, top), uvs[2].set(bRight, top), uvs[3].set(right, top); + uvs[4].set(left, bTop), uvs[5].set(bLeft, bTop), uvs[6].set(bRight, bTop), uvs[7].set(right, bTop); + uvs[8].set(left, bBottom), uvs[9].set(bLeft, bBottom), uvs[10].set(bRight, bBottom), uvs[11].set(right, bBottom); + uvs[12].set(left, bottom), uvs[13].set(bLeft, bottom), uvs[14].set(bRight, bottom), uvs[15].set(right, bottom); + } else { + uvs[0].set(left, bottom), uvs[1].set(left, bBottom), uvs[2].set(left, bTop), uvs[3].set(left, top); + uvs[4].set(bLeft, bottom), uvs[5].set(bLeft, bBottom), uvs[6].set(bLeft, bTop), uvs[7].set(bLeft, top); + uvs[8].set(bRight, bottom), uvs[9].set(bRight, bBottom), uvs[10].set(bRight, bTop), uvs[11].set(bRight, top); + uvs[12].set(right, bottom), uvs[13].set(right, bBottom), uvs[14].set(right, bTop), uvs[15].set(right, top); + } this._dirtyUpdateFlag &= ~SpriteUpdateFlags.uvs; } diff --git a/packages/loader/src/SpriteAtlasLoader.ts b/packages/loader/src/SpriteAtlasLoader.ts index 0d0fe7550f..b9616398d7 100644 --- a/packages/loader/src/SpriteAtlasLoader.ts +++ b/packages/loader/src/SpriteAtlasLoader.ts @@ -102,7 +102,7 @@ class SpriteAtlasLoader extends Loader { const { x: offsetLeft, y: offsetTop, z: offsetRight, w: offsetBottom } = atlasRegionOffset; sprite.atlasRegionOffset.set(offsetLeft * invW, offsetTop * invH, offsetRight * invW, offsetBottom * invH); } - config.atlasRotated && (sprite.atlasRotated = true); + sprite.atlasRotated = config.atlasRotated ?? false; } width === undefined || (sprite.width = width); height === undefined || (sprite.height = height);