Pārlūkot izejas kodu

[phaser] WebGL and Canvas rendering, fix mixins

# Conflicts:
#	spine-ts/package-lock.json
#	spine-ts/package.json
Mario Zechner 2 gadi atpakaļ
vecāks
revīzija
7ae9c490d5

+ 6 - 7
spine-ts/package.json

@@ -17,7 +17,7 @@
     "build:player": "npx copyfiles -f spine-player/css/spine-player.css spine-player/dist/ && npx npx esbuild --bundle spine-player/src/index.ts --tsconfig=spine-player/tsconfig.json  --sourcemap --outfile=spine-player/dist/iife/spine-player.js --format=iife --global-name=spine",
     "build:phaser": "npx esbuild  --bundle spine-phaser/src/index.ts  --tsconfig=spine-phaser/tsconfig.json   --sourcemap --outfile=spine-phaser/dist/iife/spine-phaser.js   --external:Phaser --alias:phaser=Phaser --format=iife --global-name=spine",
     "build:threejs": "npx esbuild --bundle spine-threejs/src/index.ts --tsconfig=spine-threejs/tsconfig.json  --sourcemap --outfile=spine-threejs/dist/iife/spine-threejs.js --external:three --format=iife --global-name=spine",
-    "minify": "npx esbuild --minify spine-core/dist/iife/spine-core.js --outfile=spine-core/dist/iife/spine-core.min.js && npx esbuild --minify spine-canvas/dist/iife/spine-canvas.js --outfile=spine-canvas/dist/iife/spine-canvas.min.js && npx esbuild --minify spine-player/dist/iife/spine-player.js --outfile=spine-player/dist/iife/spine-player.min.js && npx esbuild --minify spine-webgl/dist/iife/spine-webgl.js --outfile=spine-webgl/dist/iife/spine-webgl.min.js && npx esbuild --minify spine-threejs/dist/iife/spine-threejs.js --outfile=spine-threejs/dist/iife/spine-threejs.min.js",
+    "minify": "npx esbuild --minify spine-core/dist/iife/spine-core.js --outfile=spine-core/dist/iife/spine-core.min.js && npx esbuild --minify spine-canvas/dist/iife/spine-canvas.js --outfile=spine-canvas/dist/iife/spine-canvas.min.js && npx esbuild --minify spine-player/dist/iife/spine-player.js --outfile=spine-player/dist/iife/spine-player.min.js && npx esbuild --minify spine-phaser/dist/iife/spine-phaser.js --outfile=spine-phaser/dist/iife/spine-phaser.min.js && npx esbuild --minify spine-webgl/dist/iife/spine-webgl.js --outfile=spine-webgl/dist/iife/spine-webgl.min.js && npx esbuild --minify spine-threejs/dist/iife/spine-threejs.js --outfile=spine-threejs/dist/iife/spine-threejs.min.js",
     "dev": "concurrently \"npx live-server --no-browser\" \"npm run dev:canvas\" \"npm run dev:webgl\" \"npm run dev:phaser\" \"npm run dev:player\" \"npm run dev:threejs\"",
     "dev:modules": "npm run build:modules -- --watch",
     "dev:canvas": "npm run build:canvas -- --watch",
@@ -54,13 +54,12 @@
     "spine-webgl"
   ],
   "devDependencies": {
-    "concurrently": "^6.2.1",
+    "@types/offscreencanvas": "^2019.6.4",
+    "concurrently": "^7.6.0",
     "copyfiles": "^2.4.1",
-    "esbuild": "^0.16.3",
-    "live-server": "^1.2.1",
-    "npx": "^10.2.2",
+    "esbuild": "^0.16.4",
+    "live-server": "^1.2.2",
     "rimraf": "^3.0.2",
-    "typescript": "^4.3.5",
-    "@types/offscreencanvas": "^2019.6.4"
+    "typescript": "^4.9.4"
   }
 }

+ 23 - 5
spine-ts/spine-phaser/example/index.js

@@ -8,10 +8,11 @@ var config = {
     type: Phaser.AUTO,
     width: 800,
     height: 600,
-    type: Phaser.CANVAS,
+    type: Phaser.WEBGL,
     scene: {
         preload: preload,
         create: create,
+        update: update,
     },
     plugins: {
         scene: [
@@ -20,18 +21,35 @@ var config = {
     }
 };
 
-var game = new Phaser.Game(config);
+let game = new Phaser.Game(config);
+let debug;
 
 function preload () {
     this.load.spineJson("raptor-data", "assets/raptor-pro.json");
     this.load.spineAtlas("raptor-atlas", "assets/raptor-pma.atlas");
     this.load.spineBinary("spineboy-data", "assets/spineboy-pro.skel");
     this.load.spineAtlas("spineboy-atlas", "assets/spineboy-pma.atlas");
+    this.load.image("nyan", "nyan.png");
+    let canvas = document.querySelector("#game-canvas");
 }
 
 function create () {
     let plugin = this.spine;
-    var raptor = this.add.spine(400, 600, 'raptor-data', "raptor-atlas");
-    var spineboy = this.add.spine(400, 600, 'spineboy-data', "spineboy-atlas");
-    this.add.text(10, 10, "Spine", { font: '16px Courier', fill: '#00ff00' });
+    let x = 25;
+    let y = 60;
+    for (let j = 0; j < 10; j++, y+= 600 / 10) {
+        for (let i = 0; i < 20; i++, x += 800 / 20) {
+            let obj = Math.random() > 0.5
+                ? this.add.spine(x, y, 'spineboy-data', "spineboy-atlas")
+                : this.add.spine(x, y, 'raptor-data', "raptor-atlas");
+            obj.animationState.setAnimation(0, "walk", true);
+            obj.scale = 0.1;
+        }
+        x = 25;
+    }
+    debug = this.add.text(0, 600 - 40, "FPS: ");
+}
+
+function update () {
+    debug.setText("draw calls: " + spine.PolygonBatcher.getAndResetGlobalDrawCalls() + "\ndelta: " + game.loop.delta);
 }

BIN
spine-ts/spine-phaser/example/nyan.png


+ 77 - 2
spine-ts/spine-phaser/src/SpineGameObject.ts

@@ -1,7 +1,8 @@
 import { SPINE_GAME_OBJECT_TYPE } from "./keys";
 import { SpinePlugin } from "./SpinePlugin";
 import { ComputedSizeMixin, DepthMixin, FlipMixin, ScrollFactorMixin, TransformMixin, VisibleMixin } from "./mixins";
-import { AnimationState, AnimationStateData, Skeleton } from "@esotericsoftware/spine-core";
+import { AnimationState, AnimationStateData, MathUtils, Skeleton } from "@esotericsoftware/spine-core";
+import { Matrix4, Vector3 } from "@esotericsoftware/spine-webgl";
 
 class BaseSpineGameObject extends Phaser.GameObjects.GameObject {
 	constructor (scene: Phaser.Scene, type: string) {
@@ -17,6 +18,7 @@ export class SpineGameObject extends ComputedSizeMixin(DepthMixin(FlipMixin(Scro
 	blendMode = -1;
 	skeleton: Skeleton | null = null;
 	animationState: AnimationState | null = null;
+	private premultipliedAlpha = false;
 
 	constructor (scene: Phaser.Scene, private plugin: SpinePlugin, x: number, y: number, dataKey: string, atlasKey: string) {
 		super(scene, SPINE_GAME_OBJECT_TYPE);
@@ -26,21 +28,94 @@ export class SpineGameObject extends ComputedSizeMixin(DepthMixin(FlipMixin(Scro
 
 	setSkeleton (dataKey: string, atlasKey: string) {
 		if (dataKey && atlasKey) {
+			this.premultipliedAlpha = this.plugin.isAtlasPremultiplied(atlasKey);
 			this.skeleton = this.plugin.createSkeleton(dataKey, atlasKey);
 			this.animationState = new AnimationState(new AnimationStateData(this.skeleton.data));
 		} else {
 			this.skeleton = null;
+			this.animationState = null;
 		}
 	}
 
 	preUpdate (time: number, delta: number) {
+		if (!this.skeleton || !this.animationState) return;
+
+		this.animationState.update(delta / 1000);
+		this.animationState.apply(this.skeleton);
+		this.skeleton.updateWorldTransform();
+	}
+
+	preDestroy () {
+		this.skeleton = null;
+		this.animationState = null;
+		// FIXME tear down any event emitters
 	}
 
-	renderWebGL (renderer: Phaser.Renderer.WebGL.WebGLRenderer, src: SpineGameObject, camera: Phaser.Cameras.Scene2D.Camera, parentMatrix: Phaser.GameObjects.Components.TransformMatrix, container: SpineContainer) {
+	willRender (camera: Phaser.Cameras.Scene2D.Camera) {
+		// FIXME
+		return true;
+	}
+
+	renderWebGL (renderer: Phaser.Renderer.WebGL.WebGLRenderer, src: SpineGameObject, camera: Phaser.Cameras.Scene2D.Camera, parentMatrix: Phaser.GameObjects.Components.TransformMatrix) {
+		if (!this.skeleton || !this.animationState || !this.plugin.webGLRenderer) return;
+
+		let sceneRenderer = this.plugin.webGLRenderer;
+		if (renderer.newType) {
+			renderer.pipelines.clear();
+			sceneRenderer.begin();
+		}
+
+		camera.addToRenderList(src);
+		let transform = Phaser.GameObjects.GetCalcMatrix(src, camera, parentMatrix).calc;
+		let x = transform.tx;
+		let y = transform.ty;
+		let scaleX = transform.scaleX;
+		let scaleY = transform.scaleY;
+		let rotation = transform.rotationNormalized;
+		let cosRotation = Math.cos(rotation);
+		let sinRotation = Math.sin(rotation);
+
+		sceneRenderer.drawSkeleton(this.skeleton, this.premultipliedAlpha, -1, -1, (vertices, numVertices, stride) => {
+			for (let i = 0; i < numVertices; i += stride) {
+				let vx = vertices[i];
+				let vy = vertices[i + 1];
+				let vxOld = vx * scaleX, vyOld = vy * scaleY;
+				vx = vxOld * cosRotation - vyOld * sinRotation;
+				vy = vxOld * sinRotation + vyOld * cosRotation;
+				vx += x;
+				vy += y;
+				vertices[i] = vx;
+				vertices[i + 1] = vy;
+			}
+		});
 
+		if (!renderer.nextTypeMatch) {
+			sceneRenderer.end();
+			renderer.pipelines.rebind();
+			console.log("Draw calls: " + sceneRenderer.batcher.getDrawCalls());
+		}
 	}
 
 	renderCanvas (renderer: Phaser.Renderer.Canvas.CanvasRenderer, src: SpineGameObject, camera: Phaser.Cameras.Scene2D.Camera, parentMatrix: Phaser.GameObjects.Components.TransformMatrix) {
+		if (!this.skeleton || !this.animationState || !this.plugin.canvasRenderer) return;
+
+		let context = renderer.currentContext;
+		let skeletonRenderer = this.plugin.canvasRenderer;
+		(skeletonRenderer as any).ctx = context;
+
+		camera.addToRenderList(src);
+		let transform = Phaser.GameObjects.GetCalcMatrix(src, camera, parentMatrix).calc;
+		let skeleton = this.skeleton;
+		skeleton.x = transform.tx;
+		skeleton.y = transform.ty;
+		skeleton.scaleX = transform.scaleX;
+		skeleton.scaleY = transform.scaleY;
+		let root = skeleton.getRootBone()!;
+		root.rotation = -MathUtils.radiansToDegrees * transform.rotationNormalized;
+		this.skeleton.updateWorldTransform();
 
+		context.save();
+		skeletonRenderer.draw(skeleton);
+		context.restore();
 	}
 }

+ 33 - 31
spine-ts/spine-phaser/src/SpinePlugin.ts

@@ -29,9 +29,9 @@
 
 import Phaser from "phaser";
 import { SPINE_ATLAS_CACHE_KEY, SPINE_CONTAINER_TYPE, SPINE_GAME_OBJECT_TYPE, SPINE_ATLAS_TEXTURE_CACHE_KEY, SPINE_SKELETON_DATA_FILE_TYPE, SPINE_ATLAS_FILE_TYPE, SPINE_SKELETON_FILE_CACHE_KEY as SPINE_SKELETON_DATA_CACHE_KEY } from "./keys";
-import { AtlasAttachmentLoader, GLTexture, SceneRenderer, Skeleton, SkeletonData, SkeletonDebugRenderer, SkeletonJson, SkeletonRenderer, TextureAtlas } from "@esotericsoftware/spine-webgl"
+import { AtlasAttachmentLoader, Bone, GLTexture, SceneRenderer, Skeleton, SkeletonBinary, SkeletonData, SkeletonJson, TextureAtlas } from "@esotericsoftware/spine-webgl"
 import { SpineGameObject } from "./SpineGameObject";
-import { CanvasTexture } from "@esotericsoftware/spine-canvas";
+import { CanvasTexture, SkeletonRenderer } from "@esotericsoftware/spine-canvas";
 
 export class SpinePlugin extends Phaser.Plugins.ScenePlugin {
 	game: Phaser.Game;
@@ -39,7 +39,8 @@ export class SpinePlugin extends Phaser.Plugins.ScenePlugin {
 	gl: WebGLRenderingContext | null;
 	textureManager: Phaser.Textures.TextureManager;
 	phaserRenderer: Phaser.Renderer.Canvas.CanvasRenderer | Phaser.Renderer.WebGL.WebGLRenderer | null;
-	sceneRenderer: SceneRenderer | null;
+	webGLRenderer: SceneRenderer | null;
+	canvasRenderer: SkeletonRenderer | null;
 	skeletonDataCache: Phaser.Cache.BaseCache;
 	atlasCache: Phaser.Cache.BaseCache;
 
@@ -50,7 +51,8 @@ export class SpinePlugin extends Phaser.Plugins.ScenePlugin {
 		this.gl = this.isWebGL ? (this.game.renderer as Phaser.Renderer.WebGL.WebGLRenderer).gl : null;
 		this.textureManager = this.game.textures;
 		this.phaserRenderer = this.game.renderer;
-		this.sceneRenderer = null;
+		this.webGLRenderer = null;
+		this.canvasRenderer = null;
 		this.skeletonDataCache = this.game.cache.addCustom(SPINE_SKELETON_DATA_CACHE_KEY);
 		this.atlasCache = this.game.cache.addCustom(SPINE_ATLAS_CACHE_KEY);
 
@@ -116,28 +118,16 @@ export class SpinePlugin extends Phaser.Plugins.ScenePlugin {
 	}
 
 	boot () {
+		Skeleton.yDown = true;
 		if (this.isWebGL) {
-			//  Monkeypatch the Spine setBlendMode functions, or batching is destroyed!
-			let setBlendMode = function (this: any, srcBlend: any, dstBlend: any) {
-				if (srcBlend !== this.srcBlend || dstBlend !== this.dstBlend) {
-					let gl = this.context.gl;
-					this.srcBlend = srcBlend;
-					this.dstBlend = dstBlend;
-					if (this.isDrawing) {
-						this.flush();
-						gl.blendFunc(this.srcBlend, this.dstBlend);
-					}
-				}
-			};
-
-			var sceneRenderer = this.sceneRenderer;
-			if (!sceneRenderer) {
-				sceneRenderer = new SceneRenderer((this.phaserRenderer! as Phaser.Renderer.WebGL.WebGLRenderer).canvas, this.gl!, true);
-				sceneRenderer.batcher.setBlendMode = setBlendMode;
-				(sceneRenderer as any).shapes.setBlendMode = setBlendMode;
+			if (!this.webGLRenderer) {
+				this.webGLRenderer = new SceneRenderer((this.phaserRenderer! as Phaser.Renderer.WebGL.WebGLRenderer).canvas, this.gl!, true);
+			}
+			this.game.scale.on(Phaser.Scale.Events.RESIZE, this.onResize, this);
+		} else {
+			if (!this.canvasRenderer) {
+				this.canvasRenderer = new SkeletonRenderer(this.scene.sys.context);
 			}
-
-			this.sceneRenderer = sceneRenderer;
 		}
 
 		var eventEmitter = this.systems.events;
@@ -148,13 +138,15 @@ export class SpinePlugin extends Phaser.Plugins.ScenePlugin {
 
 	onResize () {
 		var phaserRenderer = this.phaserRenderer;
-		var sceneRenderer = this.sceneRenderer;
+		var sceneRenderer = this.webGLRenderer;
 
 		if (phaserRenderer && sceneRenderer) {
 			var viewportWidth = phaserRenderer.width;
 			var viewportHeight = phaserRenderer.height;
 			sceneRenderer.camera.position.x = viewportWidth / 2;
 			sceneRenderer.camera.position.y = viewportHeight / 2;
+			sceneRenderer.camera.up.y = -1;
+			sceneRenderer.camera.direction.z = 1;
 			sceneRenderer.camera.setViewport(viewportWidth, viewportHeight);
 		}
 	}
@@ -173,7 +165,13 @@ export class SpinePlugin extends Phaser.Plugins.ScenePlugin {
 	gameDestroy () {
 		this.pluginManager.removeGameObject(SPINE_GAME_OBJECT_TYPE, true, true);
 		this.pluginManager.removeGameObject(SPINE_CONTAINER_TYPE, true, true);
-		if (this.sceneRenderer) this.sceneRenderer.dispose();
+		if (this.webGLRenderer) this.webGLRenderer.dispose();
+	}
+
+	isAtlasPremultiplied(atlasKey: string) {
+		let atlasFile = this.game.cache.text.get(atlasKey);
+		if (!atlasFile) return false;
+		return atlasFile.premultipliedAlpha;
 	}
 
 	createSkeleton (dataKey: string, atlasKey: string) {
@@ -181,8 +179,8 @@ export class SpinePlugin extends Phaser.Plugins.ScenePlugin {
 		if (this.atlasCache.exists(atlasKey)) {
 			atlas = this.atlasCache.get(atlasKey);
 		} else {
-			let atlasFile = this.game.cache.text.get(atlasKey) as string;
-			atlas = new TextureAtlas(atlasFile);
+			let atlasFile = this.game.cache.text.get(atlasKey) as { data: string, premultipliedAlpha: boolean };
+			atlas = new TextureAtlas(atlasFile.data);
 			if (this.isWebGL) {
 				let gl = this.gl!;
 				gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
@@ -207,8 +205,8 @@ export class SpinePlugin extends Phaser.Plugins.ScenePlugin {
 				skeletonData = json.readSkeletonData(jsonFile);
 			} else {
 				let binaryFile = this.game.cache.binary.get(dataKey) as ArrayBuffer;
-				let binary = new SkeletonJson(new AtlasAttachmentLoader(atlas));
-				skeletonData = binary.readSkeletonData(binaryFile);
+				let binary = new SkeletonBinary(new AtlasAttachmentLoader(atlas));
+				skeletonData = binary.readSkeletonData(new Uint8Array(binaryFile));
 			}
 			this.skeletonDataCache.add(dataKey, skeletonData);
 		}
@@ -254,7 +252,7 @@ export class SpineSkeletonDataFile extends Phaser.Loader.MultiFile {
 }
 
 export class SpineAtlasFile extends Phaser.Loader.MultiFile {
-	constructor (loader: Phaser.Loader.LoaderPlugin, key: string, url: string, public premultipliedAlpha: boolean, xhrSettings: Phaser.Types.Loader.XHRSettingsObject) {
+	constructor (loader: Phaser.Loader.LoaderPlugin, key: string, url: string, public premultipliedAlpha: boolean = true, xhrSettings: Phaser.Types.Loader.XHRSettingsObject) {
 		super(loader, SPINE_ATLAS_FILE_TYPE, key, [
 			new Phaser.Loader.FileTypes.TextFile(loader, {
 				key: key,
@@ -305,6 +303,10 @@ export class SpineAtlasFile extends Phaser.Loader.MultiFile {
 						textureManager.addImage(file.key, file.data);
 					}
 				} else {
+					file.data = {
+						data: file.data,
+						premultipliedAlpha: this.premultipliedAlpha || file.data.indexOf("pma: true") >= 0
+					};
 					file.addToCache();
 				}
 			}

+ 1 - 1
spine-ts/spine-phaser/src/index.ts

@@ -2,6 +2,6 @@ export * from "./require-shim"
 export * from "./SpinePlugin"
 export * from "./mixins"
 export * from "@esotericsoftware/spine-core";
-export * from "@esotericsoftware/spine-canvas";
+export * from "@esotericsoftware/spine-webgl";
 import { SpinePlugin } from "./SpinePlugin";
 (window as any).spine = { SpinePlugin: SpinePlugin };

+ 5 - 18
spine-ts/spine-phaser/src/mixins.ts

@@ -26,7 +26,7 @@ SOFTWARE.
 
 let components = (Phaser.GameObjects.Components as any);
 export const ComputedSize = components.ComputedSize;
-export const Depth = components.ComputedSize;
+export const Depth = components.Depth;
 export const Flip = components.Flip;
 export const ScrollFactor = components.ScrollFactor;
 export const Transform = components.Transform;
@@ -52,24 +52,11 @@ export function createMixin<
 	...component: GameObjectComponent[]
 ): Mixin<GameObjectComponent, GameObjectConstraint> {
 	return (BaseGameObject) => {
-		applyMixins(BaseGameObject, component);
+		(Phaser as any).Class.mixin(BaseGameObject, component);
 		return BaseGameObject as any;
 	};
 }
 
-function applyMixins (derivedCtor: any, constructors: any[]) {
-	constructors.forEach((baseCtor) => {
-		Object.getOwnPropertyNames(baseCtor.prototype || baseCtor).forEach((name) => {
-			Object.defineProperty(
-				derivedCtor.prototype,
-				name,
-				Object.getOwnPropertyDescriptor(baseCtor.prototype || baseCtor, name) ||
-				Object.create(null)
-			);
-		});
-	});
-}
-
 type ComputedSizeMixin = Mixin<Phaser.GameObjects.Components.Transform, Phaser.GameObjects.GameObject>;
 export const ComputedSizeMixin: ComputedSizeMixin = createMixin<Phaser.GameObjects.Components.ComputedSize>(ComputedSize);
 
@@ -77,14 +64,14 @@ type DepthMixin = Mixin<Phaser.GameObjects.Components.Depth, Phaser.GameObjects.
 export const DepthMixin: DepthMixin = createMixin<Phaser.GameObjects.Components.Depth>(Depth);
 
 type FlipMixin = Mixin<Phaser.GameObjects.Components.Flip, Phaser.GameObjects.GameObject>;
-export const FlipMixin: FlipMixin = createMixin<Phaser.GameObjects.Components.Flip>(Depth);
+export const FlipMixin: FlipMixin = createMixin<Phaser.GameObjects.Components.Flip>(Flip);
 
 type ScrollFactorMixin = Mixin<Phaser.GameObjects.Components.ScrollFactor, Phaser.GameObjects.GameObject>;
-export const ScrollFactorMixin: ScrollFactorMixin = createMixin<Phaser.GameObjects.Components.ScrollFactor>(Depth);
+export const ScrollFactorMixin: ScrollFactorMixin = createMixin<Phaser.GameObjects.Components.ScrollFactor>(ScrollFactor);
 
 type TransformMixin = Mixin<Phaser.GameObjects.Components.Transform, Phaser.GameObjects.GameObject>;
 export const TransformMixin: TransformMixin = createMixin<Phaser.GameObjects.Components.Transform>(Transform);
 
 type VisibleMixin = Mixin<Phaser.GameObjects.Components.Visible, Phaser.GameObjects.GameObject>;
-export const VisibleMixin: VisibleMixin = createMixin<Phaser.GameObjects.Components.Visible>(Depth);
+export const VisibleMixin: VisibleMixin = createMixin<Phaser.GameObjects.Components.Visible>(Visible);
 

+ 8 - 0
spine-ts/spine-webgl/src/PolygonBatcher.ts

@@ -36,6 +36,7 @@ import { ManagedWebGLRenderingContext } from "./WebGL";
 export class PolygonBatcher implements Disposable {
 	private context: ManagedWebGLRenderingContext;
 	private drawCalls = 0;
+	private static globalDrawCalls = 0;
 	private isDrawing = false;
 	private mesh: Mesh;
 	private shader: Shader | null = null;
@@ -120,6 +121,7 @@ export class PolygonBatcher implements Disposable {
 		this.mesh.setVerticesLength(0);
 		this.mesh.setIndicesLength(0);
 		this.drawCalls++;
+		PolygonBatcher.globalDrawCalls++;
 	}
 
 	end () {
@@ -138,6 +140,12 @@ export class PolygonBatcher implements Disposable {
 		return this.drawCalls;
 	}
 
+	static getAndResetGlobalDrawCalls () {
+		let result = PolygonBatcher.globalDrawCalls;
+		PolygonBatcher.globalDrawCalls = 0;
+		return result;
+	}
+
 	dispose () {
 		this.mesh.dispose();
 	}

+ 3 - 3
spine-ts/spine-webgl/src/SceneRenderer.ts

@@ -34,7 +34,7 @@ import { PolygonBatcher } from "./PolygonBatcher";
 import { Shader } from "./Shader";
 import { ShapeRenderer } from "./ShapeRenderer";
 import { SkeletonDebugRenderer } from "./SkeletonDebugRenderer";
-import { SkeletonRenderer } from "./SkeletonRenderer";
+import { SkeletonRenderer, VertexTransformer } from "./SkeletonRenderer";
 import { ManagedWebGLRenderingContext } from "./WebGL";
 ;
 
@@ -86,10 +86,10 @@ export class SceneRenderer implements Disposable {
 		this.enableRenderer(this.batcher);
 	}
 
-	drawSkeleton (skeleton: Skeleton, premultipliedAlpha = false, slotRangeStart = -1, slotRangeEnd = -1) {
+	drawSkeleton (skeleton: Skeleton, premultipliedAlpha = false, slotRangeStart = -1, slotRangeEnd = -1, transform: VertexTransformer | null = null) {
 		this.enableRenderer(this.batcher);
 		this.skeletonRenderer.premultipliedAlpha = premultipliedAlpha;
-		this.skeletonRenderer.draw(this.batcher, skeleton, slotRangeStart, slotRangeEnd);
+		this.skeletonRenderer.draw(this.batcher, skeleton, slotRangeStart, slotRangeEnd, transform);
 	}
 
 	drawSkeletonDebug (skeleton: Skeleton, premultipliedAlpha = false, ignoredBones?: Array<string>) {

+ 5 - 1
spine-ts/spine-webgl/src/SkeletonRenderer.ts

@@ -37,6 +37,8 @@ class Renderable {
 	constructor (public vertices: NumberArrayLike, public numVertices: number, public numFloats: number) { }
 };
 
+export type VertexTransformer = (vertices: NumberArrayLike, numVertices: number, stride: number) => void;
+
 export class SkeletonRenderer {
 	static QUAD_TRIANGLES = [0, 1, 2, 2, 3, 0];
 
@@ -60,7 +62,7 @@ export class SkeletonRenderer {
 		this.vertices = Utils.newFloatArray(this.vertexSize * 1024);
 	}
 
-	draw (batcher: PolygonBatcher, skeleton: Skeleton, slotRangeStart: number = -1, slotRangeEnd: number = -1) {
+	draw (batcher: PolygonBatcher, skeleton: Skeleton, slotRangeStart: number = -1, slotRangeEnd: number = -1, transformer: VertexTransformer | null = null) {
 		let clipper = this.clipper;
 		let premultipliedAlpha = this.premultipliedAlpha;
 		let twoColorTint = this.twoColorTint;
@@ -174,6 +176,7 @@ export class SkeletonRenderer {
 					clipper.clipTriangles(renderable.vertices, renderable.numFloats, triangles, triangles.length, uvs, finalColor, darkColor, twoColorTint);
 					let clippedVertices = new Float32Array(clipper.clippedVertices);
 					let clippedTriangles = clipper.clippedTriangles;
+					if (transformer) transformer(renderable.vertices, renderable.numFloats, vertexSize);
 					batcher.draw(texture, clippedVertices, clippedTriangles);
 				} else {
 					let verts = renderable.vertices;
@@ -201,6 +204,7 @@ export class SkeletonRenderer {
 						}
 					}
 					let view = (renderable.vertices as Float32Array).subarray(0, renderable.numFloats);
+					if (transformer) transformer(renderable.vertices, renderable.numFloats, vertexSize);
 					batcher.draw(texture, view, triangles);
 				}
 			}