From a992607456a8d08bbd99038702c1e375b67b2c36 Mon Sep 17 00:00:00 2001 From: Mat Groves Date: Thu, 12 Sep 2024 10:54:37 +0100 Subject: [PATCH] Add darktint to Spine (#42) Co-authored-by: Zyie <24736175+Zyie@users.noreply.github.com> --- package-lock.json | 11 +- package.json | 4 +- src/BatchableSpineSlot.ts | 146 +++++++++++------------ src/Spine.ts | 47 +++----- src/SpineDebugRenderer.ts | 2 +- src/SpinePipe.ts | 6 +- src/darktint/DarkTintBatchGeometry.ts | 65 +++++++++++ src/darktint/DarkTintBatcher.ts | 159 ++++++++++++++++++++++++++ src/darktint/DarkTintShader.ts | 47 ++++++++ src/darktint/darkTintBit.ts | 49 ++++++++ src/index.ts | 1 + 11 files changed, 413 insertions(+), 124 deletions(-) create mode 100644 src/darktint/DarkTintBatchGeometry.ts create mode 100644 src/darktint/DarkTintBatcher.ts create mode 100644 src/darktint/DarkTintShader.ts create mode 100644 src/darktint/darkTintBit.ts diff --git a/package-lock.json b/package-lock.json index 06bf96f..29f4e6b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ }, "devDependencies": { "@pixi/extension-scripts": "^2.4.1", - "pixi.js": "^8.1.3", + "pixi.js": "8.4.0", "typescript": "^5.4.2" }, "engines": { @@ -21,7 +21,7 @@ "npm": ">=8" }, "peerDependencies": { - "pixi.js": "^8.1.2" + "pixi.js": "^8.4.0" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -12547,10 +12547,11 @@ } }, "node_modules/pixi.js": { - "version": "8.1.3", - "resolved": "https://registry.npmjs.org/pixi.js/-/pixi.js-8.1.3.tgz", - "integrity": "sha512-8m+/1Bam4uGW6Ar+ozyOkMJuLcefT8PmuKqebMxDKIt93YgjWkAMg8xrWRJHYVx7uGl0VyIJSP1ILX2jpTnrmA==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/pixi.js/-/pixi.js-8.4.0.tgz", + "integrity": "sha512-IM0YDv7G9XATtD/sPgbEi6FoLg82+XSqejzeCWg575vEyuQGs4RrdVFSV/K/i2PeXr/sLxiHRJDOGuotBUlldA==", "dev": true, + "license": "MIT", "dependencies": { "@pixi/colord": "^2.9.6", "@types/css-font-loading-module": "^0.0.12", diff --git a/package.json b/package.json index 3bcac39..d21820f 100644 --- a/package.json +++ b/package.json @@ -42,11 +42,11 @@ }, "devDependencies": { "@pixi/extension-scripts": "^2.4.1", - "pixi.js": "^8.1.3", + "pixi.js": "8.4.0", "typescript": "^5.4.2" }, "peerDependencies": { - "pixi.js": "^8.1.2" + "pixi.js": "^8.4.0" }, "engines": { "node": ">=16", diff --git a/src/BatchableSpineSlot.ts b/src/BatchableSpineSlot.ts index fef432a..4b4f4a3 100644 --- a/src/BatchableSpineSlot.ts +++ b/src/BatchableSpineSlot.ts @@ -29,83 +29,46 @@ import { AttachmentCacheData, Spine } from './Spine'; -import type { Batch, BatchableObject, Batcher, BLEND_MODES, IndexBufferArray, Texture } from 'pixi.js'; +import type { Batch, Batcher, BLEND_MODES, DefaultBatchableMeshElement, Matrix, Texture } from 'pixi.js'; -export class BatchableSpineSlot implements BatchableObject +export class BatchableSpineSlot implements DefaultBatchableMeshElement { - indexStart: number; - textureId: number; - texture: Texture; - location: number; - batcher: Batcher; - batch: Batch; + indexOffset = 0; + attributeOffset = 0; + + indexSize: number; + attributeSize: number; + + batcherName = 'darkTint'; + + readonly packAsQuad = false; + renderable: Spine; - vertices: Float32Array; + positions: Float32Array; indices: number[] | Uint16Array; uvs: Float32Array; - indexSize: number; - vertexSize: number; - roundPixels: 0 | 1; data: AttachmentCacheData; blendMode: BLEND_MODES; - setData( - renderable:Spine, - data:AttachmentCacheData, - texture:Texture, - blendMode:BLEND_MODES, - roundPixels: 0 | 1) - { - this.renderable = renderable; - this.data = data; + darkTint: number; - if (data.clipped) - { - const clippedData = data.clippedData; - - this.indexSize = clippedData.indicesCount; - this.vertexSize = clippedData.vertexCount; - this.vertices = clippedData.vertices; - this.indices = clippedData.indices; - this.uvs = clippedData.uvs; - } - else - { - this.indexSize = data.indices.length; - this.vertexSize = data.vertices.length / 2; - this.vertices = data.vertices; - this.indices = data.indices; - this.uvs = data.uvs; - } - - this.texture = texture; - this.roundPixels = roundPixels; - - this.blendMode = blendMode; - } + texture: Texture; - packIndex(indexBuffer: IndexBufferArray, index: number, indicesOffset: number) - { - const indices = this.indices; + transform: Matrix; - for (let i = 0; i < indices.length; i++) - { - indexBuffer[index++] = indices[i] + indicesOffset; - } - } + // used internally by batcher specific.. + // stored for efficient updating.. + _textureId: number; + _attributeStart: number; + _indexStart: number; + _batcher: Batcher; + _batch: Batch; - packAttributes( - float32View: Float32Array, - uint32View: Uint32Array, - index: number, - textureId: number - ) + get color() { - const { uvs, vertices, vertexSize } = this; - const slotColor = this.data.color; const parentColor:number = this.renderable.groupColor; @@ -131,34 +94,53 @@ export class BatchableSpineSlot implements BatchableObject abgr = ((mixedA) << 24) | ((slotColor.b * 255) << 16) | ((slotColor.g * 255) << 8) | (slotColor.r * 255); } - const matrix = this.renderable.groupTransform; + return abgr; + } + + get darkColor() + { + const darkColor = this.data.darkColor; + + return ((darkColor.a) << 24) | ((darkColor.b * 255) << 16) | ((darkColor.g * 255) << 8) | (darkColor.r * 255); + } - const a = matrix.a; - const b = matrix.b; - const c = matrix.c; - const d = matrix.d; - const tx = matrix.tx; - const ty = matrix.ty; + get groupTransform() { return this.renderable.groupTransform; } - const textureIdAndRound = (textureId << 16) | (this.roundPixels & 0xFFFF); + setData( + renderable:Spine, + data:AttachmentCacheData, + texture:Texture, + blendMode:BLEND_MODES, + roundPixels: 0 | 1) + { + this.renderable = renderable; + this.transform = renderable.groupTransform; + this.data = data; - for (let i = 0; i < vertexSize; i++) + if (data.clipped) { - const x = vertices[i * 2]; - const y = vertices[(i * 2) + 1]; + const clippedData = data.clippedData; - float32View[index++] = (a * x) + (c * y) + tx; - float32View[index++] = (b * x) + (d * y) + ty; + this.indexSize = clippedData.indicesCount; + this.attributeSize = clippedData.vertexCount; + this.positions = clippedData.vertices; + this.indices = clippedData.indices; + this.uvs = clippedData.uvs; + } + else + { + this.indexSize = data.indices.length; + this.attributeSize = data.vertices.length / 2; + this.positions = data.vertices; + this.indices = data.indices; + this.uvs = data.uvs; + } - // uv - float32View[index++] = uvs[i * 2]; - float32View[index++] = uvs[(i * 2) + 1]; + this.texture = texture; + this.roundPixels = roundPixels; - // color - uint32View[index++] = abgr; + this.blendMode = blendMode; - // texture id - uint32View[index++] = textureIdAndRound; - } + this.batcherName = data.darkTint ? 'darkTint' : 'default'; } } diff --git a/src/Spine.ts b/src/Spine.ts index aa5b8b5..c8498e6 100644 --- a/src/Spine.ts +++ b/src/Spine.ts @@ -37,7 +37,7 @@ import { DestroyOptions, PointData, Ticker, - View, + ViewContainer, } from 'pixi.js'; import { ISpineDebugRenderer } from './SpineDebugRenderer'; import { @@ -101,6 +101,8 @@ export interface AttachmentCacheData uvs: Float32Array; indices: number[]; color: Color; + darkColor: Color | null; + darkTint: boolean; skipRender: boolean; clippedData?: { vertices: Float32Array; @@ -111,16 +113,13 @@ export interface AttachmentCacheData }; } -export class Spine extends Container implements View +export class Spine extends ViewContainer { // Pixi properties public batched = true; public buildId = 0; public override readonly renderPipeId = 'spine'; public _didSpineUpdate = false; - public _boundsDirty = true; - public _roundPixels: 0 | 1; - private _bounds: Bounds = new Bounds(); public beforeUpdateWorldTransforms: (object: Spine) => void = () => { /** */ }; public afterUpdateWorldTransforms: (object: Spine) => void = () => { /** */ }; @@ -425,6 +424,7 @@ export class Spine extends Container implements View const skeleton = slot.bone.skeleton; const skeletonColor = skeleton.color; const slotColor = slot.color; + const attachmentColor = attachment.color; cacheData.color.set( @@ -434,6 +434,13 @@ export class Spine extends Container implements View skeletonColor.a * slotColor.a * attachmentColor.a, ); + cacheData.darkTint = !!slot.darkColor; + + if (slot.darkColor) + { + cacheData.darkColor.setFromColor(slot.darkColor); + } + cacheData.skipRender = cacheData.clipped = false; if (clipper.isClipping()) @@ -585,6 +592,8 @@ export class Spine extends Container implements View indices: [0, 1, 2, 0, 2, 3], uvs: attachment.uvs as Float32Array, color: new Color(1, 1, 1, 1), + darkColor: new Color(0, 0, 0, 0), + darkTint: false, skipRender: false, }; } @@ -599,6 +608,8 @@ export class Spine extends Container implements View indices: attachment.triangles, uvs: attachment.uvs as Float32Array, color: new Color(1, 1, 1, 1), + darkColor: new Color(0, 0, 0, 0), + darkTint: false, skipRender: false, }; } @@ -761,21 +772,6 @@ export class Spine extends Container implements View bounds.addBounds(this.bounds); } - public containsPoint(point: PointData) - { - const bounds = this.bounds; - - if (point.x >= bounds.minX && point.x <= bounds.maxX) - { - if (point.y >= bounds.minY && point.y <= bounds.maxY) - { - return true; - } - } - - return false; - } - /** * Destroys this sprite renderable and optionally its texture. * @param options - Options parameter. A boolean will act as if all options @@ -797,17 +793,6 @@ export class Spine extends Container implements View this.attachmentCacheData = null as any; } - /** Whether or not to round the x/y position of the sprite. */ - get roundPixels() - { - return !!this._roundPixels; - } - - set roundPixels(value: boolean) - { - this._roundPixels = value ? 1 : 0; - } - /** Converts a point from the skeleton coordinate system to the Pixi world coordinate system. */ public skeletonToPixiWorldCoordinates(point: { x: number; y: number }) { diff --git a/src/SpineDebugRenderer.ts b/src/SpineDebugRenderer.ts index 4aeadc2..3246fc4 100644 --- a/src/SpineDebugRenderer.ts +++ b/src/SpineDebugRenderer.ts @@ -179,7 +179,7 @@ export class SpineDebugRenderer implements ISpineDebugRenderer debugDisplayObjects.parentDebugContainer.addChild(debugDisplayObjects.pathsLine); debugDisplayObjects.parentDebugContainer.addChild(debugDisplayObjects.eventText); - debugDisplayObjects.parentDebugContainer.zIndex = 9999999; + (debugDisplayObjects.parentDebugContainer as any).zIndex = 9999999; // Disable screen reader and mouse input on debug objects. (debugDisplayObjects.parentDebugContainer as any).accessibleChildren = false; diff --git a/src/SpinePipe.ts b/src/SpinePipe.ts index b9c4e6f..a65f83c 100644 --- a/src/SpinePipe.ts +++ b/src/SpinePipe.ts @@ -111,7 +111,7 @@ export class SpinePipe implements RenderPipe if (!cacheData.skipRender) { - batcher.addToBatch(batchableSpineSlot); + batcher.addToBatch(batchableSpineSlot, instructionSet); } } @@ -122,7 +122,7 @@ export class SpinePipe implements RenderPipe const container = containerAttachment.container; container.includeInBuild = true; - collectAllRenderables(container, instructionSet, this.renderer.renderPipes); + collectAllRenderables(container, instructionSet, this.renderer); container.includeInBuild = false; } } @@ -152,7 +152,7 @@ export class SpinePipe implements RenderPipe { const batchableSpineSlot = gpuSpine.slotBatches[spine._getCachedData(slot, attachment).id]; - batchableSpineSlot.batcher?.updateElement(batchableSpineSlot); + batchableSpineSlot._batcher?.updateElement(batchableSpineSlot); } } } diff --git a/src/darktint/DarkTintBatchGeometry.ts b/src/darktint/DarkTintBatchGeometry.ts new file mode 100644 index 0000000..8b9113f --- /dev/null +++ b/src/darktint/DarkTintBatchGeometry.ts @@ -0,0 +1,65 @@ +import { Buffer, BufferUsage, Geometry } from 'pixi.js'; + +const placeHolderBufferData = new Float32Array(1); +const placeHolderIndexData = new Uint32Array(1); + +export class DarkTintBatchGeometry extends Geometry +{ + constructor() + { + const vertexSize = 7; + + const attributeBuffer = new Buffer({ + data: placeHolderBufferData, + label: 'attribute-batch-buffer', + usage: BufferUsage.VERTEX | BufferUsage.COPY_DST, + shrinkToFit: false, + }); + + const indexBuffer = new Buffer({ + data: placeHolderIndexData, + label: 'index-batch-buffer', + usage: BufferUsage.INDEX | BufferUsage.COPY_DST, // | BufferUsage.STATIC, + shrinkToFit: false, + }); + + const stride = vertexSize * 4; + + super({ + attributes: { + aPosition: { + buffer: attributeBuffer, + format: 'float32x2', + stride, + offset: 0, + }, + aUV: { + buffer: attributeBuffer, + format: 'float32x2', + stride, + offset: 2 * 4, + }, + aColor: { + buffer: attributeBuffer, + format: 'unorm8x4', + stride, + offset: 4 * 4, + }, + aDarkColor: { + buffer: attributeBuffer, + format: 'unorm8x4', + stride, + offset: 5 * 4, + }, + aTextureIdAndRound: { + buffer: attributeBuffer, + format: 'uint16x2', + stride, + offset: 6 * 4, + }, + }, + indexBuffer + }); + } +} + diff --git a/src/darktint/DarkTintBatcher.ts b/src/darktint/DarkTintBatcher.ts new file mode 100644 index 0000000..6e66351 --- /dev/null +++ b/src/darktint/DarkTintBatcher.ts @@ -0,0 +1,159 @@ +import { + Batcher, + DefaultBatchableMeshElement, + DefaultBatchableQuadElement, + extensions, + ExtensionType, + Shader +} from 'pixi.js'; +import { DarkTintBatchGeometry } from './DarkTintBatchGeometry'; +import { DarkTintShader } from './DarkTintShader'; + +let defaultShader: Shader = null; + +/** The default batcher is used to batch quads and meshes. */ +export class DarkTintBatcher extends Batcher +{ + /** @ignore */ + public static extension = { + type: [ + ExtensionType.Batcher, + ], + name: 'darkTint', + } as const; + + public geometry = new DarkTintBatchGeometry(); + public shader = defaultShader || (defaultShader = new DarkTintShader(this.maxTextures)); + public name = DarkTintBatcher.extension.name; + + /** The size of one attribute. 1 = 32 bit. x, y, u, v, color, darkColor, textureIdAndRound -> total = 7 */ + public vertexSize = 7; + + public packAttributes( + element: DefaultBatchableMeshElement & { darkColor: number }, + float32View: Float32Array, + uint32View: Uint32Array, + index: number, + textureId: number + ) + { + const textureIdAndRound = (textureId << 16) | (element.roundPixels & 0xFFFF); + + const wt = element.transform; + + const a = wt.a; + const b = wt.b; + const c = wt.c; + const d = wt.d; + const tx = wt.tx; + const ty = wt.ty; + + const { positions, uvs } = element; + + const argb = element.color; + const darkColor = element.darkColor; + + const offset = element.attributeOffset; + const end = offset + element.attributeSize; + + for (let i = offset; i < end; i++) + { + const i2 = i * 2; + + const x = positions[i2]; + const y = positions[(i2) + 1]; + + float32View[index++] = (a * x) + (c * y) + tx; + float32View[index++] = (d * y) + (b * x) + ty; + + float32View[index++] = uvs[i2]; + float32View[index++] = uvs[(i2) + 1]; + + uint32View[index++] = argb; + uint32View[index++] = darkColor; + + uint32View[index++] = textureIdAndRound; + } + } + + public packQuadAttributes( + element: DefaultBatchableQuadElement & { darkColor: number }, + float32View: Float32Array, + uint32View: Uint32Array, + index: number, + textureId: number + ) + { + const texture = element.texture; + + const wt = element.transform; + + const a = wt.a; + const b = wt.b; + const c = wt.c; + const d = wt.d; + const tx = wt.tx; + const ty = wt.ty; + + const bounds = element.bounds; + + const w0 = bounds.maxX; + const w1 = bounds.minX; + const h0 = bounds.maxY; + const h1 = bounds.minY; + + const uvs = texture.uvs; + + // _ _ _ _ + // a b g r + const argb = element.color; + const darkColor = element.darkColor; + + const textureIdAndRound = (textureId << 16) | (element.roundPixels & 0xFFFF); + + float32View[index + 0] = (a * w1) + (c * h1) + tx; + float32View[index + 1] = (d * h1) + (b * w1) + ty; + + float32View[index + 2] = uvs.x0; + float32View[index + 3] = uvs.y0; + + uint32View[index + 4] = argb; + uint32View[index + 5] = darkColor; + uint32View[index + 6] = textureIdAndRound; + + // xy + float32View[index + 7] = (a * w0) + (c * h1) + tx; + float32View[index + 8] = (d * h1) + (b * w0) + ty; + + float32View[index + 9] = uvs.x1; + float32View[index + 10] = uvs.y1; + + uint32View[index + 11] = argb; + uint32View[index + 12] = darkColor; + uint32View[index + 13] = textureIdAndRound; + + // xy + float32View[index + 14] = (a * w0) + (c * h0) + tx; + float32View[index + 15] = (d * h0) + (b * w0) + ty; + + float32View[index + 16] = uvs.x2; + float32View[index + 17] = uvs.y2; + + uint32View[index + 18] = argb; + uint32View[index + 19] = darkColor; + uint32View[index + 20] = textureIdAndRound; + + // xy + float32View[index + 21] = (a * w1) + (c * h0) + tx; + float32View[index + 22] = (d * h0) + (b * w1) + ty; + + float32View[index + 23] = uvs.x3; + float32View[index + 24] = uvs.y3; + + uint32View[index + 25] = argb; + uint32View[index + 26] = darkColor; + uint32View[index + 27] = textureIdAndRound; + } +} + +extensions.add(DarkTintBatcher); diff --git a/src/darktint/DarkTintShader.ts b/src/darktint/DarkTintShader.ts new file mode 100644 index 0000000..3e71fad --- /dev/null +++ b/src/darktint/DarkTintShader.ts @@ -0,0 +1,47 @@ +import { + colorBit, + colorBitGl, + compileHighShaderGlProgram, + compileHighShaderGpuProgram, + generateTextureBatchBit, + generateTextureBatchBitGl, + getBatchSamplersUniformGroup, + roundPixelsBit, + roundPixelsBitGl, + Shader +} from 'pixi.js'; +import { darkTintBit, darkTintBitGl } from './darkTintBit'; + +export class DarkTintShader extends Shader +{ + constructor(maxTextures: number) + { + const glProgram = compileHighShaderGlProgram({ + name: 'dark-tint-batch', + bits: [ + colorBitGl, + darkTintBitGl, + generateTextureBatchBitGl(maxTextures), + roundPixelsBitGl, + ] + }); + + const gpuProgram = compileHighShaderGpuProgram({ + name: 'dark-tint-batch', + bits: [ + colorBit, + darkTintBit, + generateTextureBatchBit(maxTextures), + roundPixelsBit, + ] + }); + + super({ + glProgram, + gpuProgram, + resources: { + batchSamplers: getBatchSamplersUniformGroup(maxTextures), + } + }); + } +} diff --git a/src/darktint/darkTintBit.ts b/src/darktint/darkTintBit.ts new file mode 100644 index 0000000..2056fa0 --- /dev/null +++ b/src/darktint/darkTintBit.ts @@ -0,0 +1,49 @@ +/* eslint-disable max-len */ +export const darkTintBit = { + name: 'color-bit', + vertex: { + header: /* wgsl */` + @in aDarkColor: vec4; + @out vDarkColor: vec4; + `, + main: /* wgsl */` + vDarkColor = aDarkColor; + ` + }, + fragment: { + header: /* wgsl */` + @in vDarkColor: vec4; + `, + end: /* wgsl */` + + let alpha = outColor.a * vColor.a; + let rgb = ((outColor.a - 1.0) * vDarkColor.a + 1.0 - outColor.rgb) * vDarkColor.rgb + outColor.rgb * vColor.rgb; + + finalColor = vec4(rgb, alpha); + + ` + } +}; + +export const darkTintBitGl = { + name: 'color-bit', + vertex: { + header: /* glsl */` + in vec4 aDarkColor; + out vec4 vDarkColor; + `, + main: /* glsl */` + vDarkColor = aDarkColor; + ` + }, + fragment: { + header: /* glsl */` + in vec4 vDarkColor; + `, + end: /* glsl */` + + finalColor.a = outColor.a * vColor.a; + finalColor.rgb = ((outColor.a - 1.0) * vDarkColor.a + 1.0 - outColor.rgb) * vDarkColor.rgb + outColor.rgb * vColor.rgb; + ` + } +}; diff --git a/src/index.ts b/src/index.ts index 4bdec86..85d1174 100644 --- a/src/index.ts +++ b/src/index.ts @@ -30,6 +30,7 @@ import './require-shim.js'; // Side effects add require pixi.js to global scope import './assets/atlasLoader.js'; // Side effects install the loaders into pixi import './assets/skeletonLoader.js'; // Side effects install the loaders into pixi +import './darktint/DarkTintBatcher.js'; // Side effects install the batcher into pixi import './SpinePipe.js'; export * from './assets/atlasLoader.js';