Refactored rendering and matrix into specific class defined in c3 lib.

This commit is contained in:
Davide Tantillo 2025-11-24 17:20:49 +01:00
parent 86a0950a5f
commit 683574fb41
11 changed files with 427 additions and 253 deletions

View File

@ -0,0 +1,26 @@
Spine Runtimes License Agreement
Last updated April 5, 2025. Replaces all prior versions.
Copyright (c) 2013-2025, Esoteric Software LLC
Integration of the Spine Runtimes into software or otherwise creating
derivative works of the Spine Runtimes is permitted under the terms and
conditions of Section 2 of the Spine Editor License Agreement:
http://esotericsoftware.com/spine-editor-license
Otherwise, it is permitted to integrate the Spine Runtimes into software
or otherwise create derivative works of the Spine Runtimes (collectively,
"Products"), provided that each user of the Products must obtain their own
Spine Editor license and redistribution of the Products in any form must
include this license and copyright notice.
THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -0,0 +1,3 @@
# spine-ts Construct3
Please see the top-level [README.md](../README.md) for more information.

View File

@ -1,3 +1,3 @@
# spine-ts THREE.JS
# spine-ts Construct3 Lib
Please see the top-level [README.md](../README.md) for more information.
This is just an internal lib that contains spine-core and additional basic functions for C3.

View File

@ -28,7 +28,7 @@
*****************************************************************************/
import { AtlasAttachmentLoader, SkeletonBinary, type SkeletonData, SkeletonJson, TextureAtlas, TextureAtlasPage } from "@esotericsoftware/spine-core";
import { C3Texture, C3TextureEditor } from "./C3Texture";
import { C3TextureEditor, C3TextureRuntime } from "./C3Texture";
interface CacheEntry<T> {
@ -40,7 +40,7 @@ export class AssetLoader {
private static CacheSkeleton = new Map<string, CacheEntry<SkeletonData>>();
private static CacheAtlas = new Map<string, CacheEntry<TextureAtlas>>();
private static CacheTexture = new Map<string, CacheEntry<C3Texture>>();
private static CacheTexture = new Map<string, CacheEntry<C3TextureRuntime>>();
public async loadSkeletonEditor (sid: number, textureAtlas: TextureAtlas, scale = 1, instance: SDK.IWorldInstance) {
const projectFile = instance.GetProject().GetProjectFileBySID(sid);
@ -167,7 +167,7 @@ export class AssetLoader {
const image = await AssetLoader.createImageBitmapFromBlob(content, page.pma);
if (!image) return null;
const spineTexture = new C3Texture(image, renderer, page);
const spineTexture = new C3TextureRuntime(image, renderer, page);
this.addToCache(AssetLoader.CacheTexture, cacheKey, spineTexture);

View File

@ -0,0 +1,83 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, Esoteric Software LLC
*
* Integration of the Spine Runtimes into software or otherwise creating
* derivative works of the Spine Runtimes is permitted under the terms and
* conditions of Section 2 of the Spine Editor License Agreement:
* http://esotericsoftware.com/spine-editor-license
*
* Otherwise, it is permitted to integrate the Spine Runtimes into software
* or otherwise create derivative works of the Spine Runtimes (collectively,
* "Products"), provided that each user of the Products must obtain their own
* Spine Editor license and redistribution of the Products in any form must
* include this license and copyright notice.
*
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
import { type Bone, Vector2 } from "@esotericsoftware/spine-core";
export class C3Matrix {
public a = 0;
public b = 0;
public c = 0;
public d = 0;
public tx = 0;
public ty = 0;
private tempPoint = new Vector2();
public update (x: number, y: number, angle: number) {
const cos = Math.cos(angle);
const sin = Math.sin(angle);
this.a = cos;
this.b = sin;
this.c = -sin;
this.d = cos;
this.tx = x;
this.ty = y;
}
public gameToSkeleton (x: number, y: number) {
const tx = x - this.tx;
const ty = y - this.ty;
const { a, b, c, d, tempPoint } = this;
const delta = a * d - b * c;
tempPoint.x = (d * tx - c * ty) / delta;
tempPoint.y = (a * ty - b * tx) / delta;
return tempPoint;
}
public gameToBone (x: number, y: number, bone: Bone) {
const point = this.gameToSkeleton(x, y);
if (bone.parent)
return bone.parent.applied.worldToLocal(point);
return bone.applied.worldToLocal(point);
}
public skeletonToGame = (skeletonX: number, skeletonY: number) => {
const { a, b, c, d, tempPoint } = this;
tempPoint.x = a * skeletonX + c * skeletonY + this.tx;
tempPoint.y = b * skeletonX + d * skeletonY + this.ty;
return tempPoint;
}
public boneToGame (bone: Bone) {
const { applied } = bone;
return this.skeletonToGame(applied.worldX, applied.worldY);
}
}

View File

@ -0,0 +1,239 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, Esoteric Software LLC
*
* Integration of the Spine Runtimes into software or otherwise creating
* derivative works of the Spine Runtimes is permitted under the terms and
* conditions of Section 2 of the Spine Editor License Agreement:
* http://esotericsoftware.com/spine-editor-license
*
* Otherwise, it is permitted to integrate the Spine Runtimes into software
* or otherwise create derivative works of the Spine Runtimes (collectively,
* "Products"), provided that each user of the Products must obtain their own
* Spine Editor license and redistribution of the Products in any form must
* include this license and copyright notice.
*
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
import { type BlendMode, type Bone, MathUtils, type Skeleton, SkeletonRendererCore, Vector2 } from "@esotericsoftware/spine-core";
import type { C3Matrix } from "./C3Matrix";
import { BlendingModeSpineToC3, type C3TextureEditor, type C3TextureRuntime } from "./C3Texture";
type C3Renderer = IRenderer | SDK.Gfx.IWebGLRenderer;
type C3Texture = C3TextureRuntime | C3TextureEditor;
type C3Quad = DOMQuad | SDK.Quad;
abstract class C3SkeletonRenderer<
Renderer extends C3Renderer,
Texture extends C3Texture,
> extends SkeletonRendererCore {
private tempVertices = new Float32Array(4096);
private tempColors = new Float32Array(4096);
private tempPoint = new Vector2();
private inv255 = 1 / 255;
constructor (
protected renderer: Renderer,
private skeleton: Skeleton,
protected matrix: C3Matrix,
) {
super();
}
draw (opacity = 1) {
const { skeleton, matrix, inv255 } = this;
let command = this.render(skeleton);
while (command) {
const { numVertices, positions, uvs, colors, indices, numIndices, blendMode } = command;
const vertices = this.tempVertices.length < numVertices * 3
? (this.tempVertices = new Float32Array(numVertices * 3))
: this.tempVertices;
const c3colors = this.tempColors.length < numVertices * 4
? (this.tempColors = new Float32Array(numVertices * 4))
: this.tempColors;
for (let i = 0; i < numVertices; i++) {
const srcIndex = i * 2;
const { x, y } = matrix.skeletonToGame(positions[srcIndex], positions[srcIndex + 1]);
const dstIndex = i * 3;
vertices[dstIndex] = x;
vertices[dstIndex + 1] = y;
vertices[dstIndex + 2] = 0;
const color = colors[i];
const colorDst = i * 4;
const alpha = (color >>> 24 & 0xFF) * inv255 * opacity;
const alphaInverse = inv255 * alpha;
c3colors[colorDst] = (color >>> 16 & 0xFF) * alphaInverse;
c3colors[colorDst + 1] = (color >>> 8 & 0xFF) * alphaInverse;
c3colors[colorDst + 2] = (color & 0xFF) * alphaInverse;
c3colors[colorDst + 3] = alpha;
}
this.renderSkeleton(
vertices.subarray(0, numVertices * 3),
uvs.subarray(0, numVertices * 2),
indices.subarray(0, numIndices),
c3colors.subarray(0, numVertices * 4),
command.texture,
blendMode)
command = command.next;
}
}
drawDebug (x: number, y: number, quad: C3Quad) {
const { skeleton, matrix } = this;
const bones = skeleton.bones;
for (let i = 0, n = bones.length; i < n; i++) {
const bone = bones[i];
if (!bone.parent) continue;
const boneApplied = bone.applied;
const { x: x1, y: y1 } = matrix.skeletonToGame(boneApplied.worldX, boneApplied.worldY);
const x2 = bone.data.length * boneApplied.a + x1;
const y2 = bone.data.length * boneApplied.c + y1;
this.setColor(1, 0, 0, 1);
this.setColorFillMode();
const t = this.tempPoint.set(y2 - y1, x1 - x2);
t.normalize();
const width = 1 * 0.5;
const tx = t.x * width;
const ty = t.y * width;
this.poly([
x1 + tx, y1 + ty,
x1 - tx, y1 - ty,
x2 + tx, y2 + ty,
x2 - tx, y2 - ty,
x2 + tx, y2 + ty,
x1 - tx, y1 - ty,
]);
this.setColor(0, 1, 0, 1);
this.poly(this.circle(x1, y1, 2));
}
this.renderGameObjectBounds(x, y, quad);
}
protected abstract setColor (r: number, g: number, b: number, a: number): void;
protected abstract setColorFillMode (): void;
protected abstract poly (points: number[]): void;
protected abstract renderSkeleton (vertices: Float32Array, uvs: Float32Array, indices: Uint16Array, colors: Float32Array, texture: Texture, blendMode: BlendMode): void;
public abstract renderGameObjectBounds (x: number, y: number, quad: DOMQuad | SDK.Quad): void;
protected circle (x: number, y: number, radius: number) {
let segments = Math.max(1, (6 * MathUtils.cbrt(radius)) | 0);
if (segments <= 0) throw new Error("segments must be > 0.");
const angle = 2 * MathUtils.PI / segments;
const cos = Math.cos(angle);
const sin = Math.sin(angle);
let cx = radius, cy = 0;
segments--;
const poly = [];
for (let i = 0; i < segments; i++) {
poly.push(x, y);
poly.push(x + cx, y + cy);
const temp = cx;
cx = cos * cx - sin * cy;
cy = sin * temp + cos * cy;
poly.push(x + cx, y + cy);
}
poly.push(x, y, x + cx, y + cy);
cx = radius;
cy = 0;
poly.push(x + cx, y + cy);
return poly;
}
}
export class C3RendererRuntime extends C3SkeletonRenderer<IRenderer, C3TextureRuntime> {
constructor (renderer: IRenderer, skeleton: Skeleton, matrix: C3Matrix) {
super(renderer, skeleton, matrix);
}
protected setColor (r: number, g: number, b: number, a: number): void {
this.renderer.setColor([r, g, b, a]);
}
protected setColorFillMode (): void {
this.renderer.setColorFillMode();
}
protected poly (points: number[]): void {
this.renderer.convexPoly(points);
}
protected renderSkeleton (vertices: Float32Array, uvs: Float32Array, indices: Uint16Array, colors: Float32Array, texture: C3TextureRuntime, blendMode: BlendMode) {
this.renderer.setTexture(texture.texture);
this.renderer.setBlendMode(BlendingModeSpineToC3[blendMode]);
this.renderer.drawMesh(vertices, uvs, indices, colors);
};
public renderDragHandles (bone: Bone, radius: number) {
const boneApplied = bone.applied;
const { x: x1, y: y1 } = this.matrix.skeletonToGame(boneApplied.worldX, boneApplied.worldY);
this.renderer.setColorFillMode();
this.renderer.setColor([1, 0, 0, .2]);
this.renderer.convexPoly(this.circle(x1, y1, radius));
}
public renderGameObjectBounds (x: number, y: number, quad: DOMQuad) {
const { renderer, matrix } = this;
renderer.setAlphaBlendMode();
renderer.setColorFillMode();
renderer.setColorRgba(0.25, 0, 0, 0.25);
renderer.lineQuad(quad);
renderer.line(x, y, matrix.tx, matrix.ty);
};
}
export class C3RendererEditor extends C3SkeletonRenderer<SDK.Gfx.IWebGLRenderer, C3TextureEditor> {
protected setColor (r: number, g: number, b: number, a: number): void {
this.renderer.SetColorRgba(r, g, b, a);
}
protected setColorFillMode (): void {
this.renderer.SetColorFillMode();
}
protected poly (points: number[]): void {
this.renderer.ConvexPoly(points);
}
protected renderSkeleton (vertices: Float32Array, uvs: Float32Array, indices: Uint16Array, colors: Float32Array, texture: C3TextureEditor, blendMode: BlendMode) {
this.renderer.ResetColor();
this.renderer.SetBlendMode(BlendingModeSpineToC3[blendMode]);
this.renderer.SetTextureFillMode();
this.renderer.SetTexture(texture.texture);
this.renderer.DrawMesh(vertices, uvs, indices, colors);
};
public renderGameObjectBounds (x: number, y: number, quad: SDK.Quad): void {
const { renderer, matrix } = this;
renderer.SetAlphaBlend();
renderer.SetColorFillMode();
renderer.SetColorRgba(0.25, 0, 0, 0.25);
renderer.LineQuad(quad);
renderer.Line(x, y, matrix.tx, matrix.ty);
}
}

View File

@ -61,7 +61,7 @@ export class C3TextureEditor extends Texture {
}
}
export class C3Texture extends Texture {
export class C3TextureRuntime extends Texture {
texture: ITexture;
renderer?: IRenderer;

View File

@ -1,3 +1,32 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, Esoteric Software LLC
*
* Integration of the Spine Runtimes into software or otherwise creating
* derivative works of the Spine Runtimes is permitted under the terms and
* conditions of Section 2 of the Spine Editor License Agreement:
* http://esotericsoftware.com/spine-editor-license
*
* Otherwise, it is permitted to integrate the Spine Runtimes into software
* or otherwise create derivative works of the Spine Runtimes (collectively,
* "Products"), provided that each user of the Products must obtain their own
* Spine Editor license and redistribution of the Products in any form must
* include this license and copyright notice.
*
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
import { AnimationState, Physics, Skeleton, SkeletonClipping, Skin } from "@esotericsoftware/spine-core";
interface Rectangle {

View File

@ -1,4 +1,6 @@
export * from "@esotericsoftware/spine-core";
export * from './AssetLoader.js';
export * from './C3Matrix.js';
export * from './C3SkeletonRenderer.js';
export * from './C3Texture.js';
export * from './SpineBoundsProvider.js';

View File

@ -1,4 +1,4 @@
import type { AnimationState, AnimationStateListener, AssetLoader, Bone, Event, NumberArrayLike, RegionAttachment, Skeleton, SkeletonRendererCore, Skin, Slot, TextureAtlas, } from "@esotericsoftware/spine-construct3-lib";
import type { AnimationState, AnimationStateListener, AssetLoader, Bone, C3Matrix, C3RendererRuntime, Event, NumberArrayLike, RegionAttachment, Skeleton, Skin, Slot, TextureAtlas, } from "@esotericsoftware/spine-construct3-lib";
const C3 = globalThis.C3;
const spine = globalThis.spine;
@ -39,12 +39,8 @@ class SpineC3Instance extends globalThis.ISDKWorldInstanceBase {
public triggeredEventData?: Event;
private assetLoader: AssetLoader;
private skeletonRenderer: SkeletonRendererCore;
private a = 0;
private b = 0;
private c = 0;
private d = 0;
private skeletonRenderer?: C3RendererRuntime;
private matrix: C3Matrix;
private tempVertices = new Float32Array(4096);
private tempColors = new Float32Array(4096);
@ -86,7 +82,7 @@ class SpineC3Instance extends globalThis.ISDKWorldInstanceBase {
}
this.assetLoader = new spine.AssetLoader();
this.skeletonRenderer = new spine.SkeletonRendererCore();
this.matrix = new spine.C3Matrix();
this._setTicking(true);
}
@ -125,7 +121,7 @@ class SpineC3Instance extends globalThis.ISDKWorldInstanceBase {
}
private update (delta: number) {
const { state, skeleton, animationSpeed, physicsMode } = this;
const { state, skeleton, animationSpeed, physicsMode, matrix } = this;
if (!skeleton || !state) return;
@ -133,17 +129,13 @@ class SpineC3Instance extends globalThis.ISDKWorldInstanceBase {
state.update(adjustedDelta);
skeleton.update(adjustedDelta);
state.apply(skeleton);
const cos = Math.cos(this.angle + this.propOffsetAngle);
const sin = Math.sin(this.angle + this.propOffsetAngle);
this.a = cos;
this.b = sin;
this.c = -sin;
this.d = cos;
matrix.update(
this.x + this.propOffsetX,
this.y + this.propOffsetY,
this.angle + this.propOffsetAngle);
skeleton.updateWorldTransform(physicsMode);
this.updateHandles(skeleton);
this.updateHandles(skeleton, matrix);
this.updateBoneFollowers();
}
@ -156,134 +148,19 @@ class SpineC3Instance extends globalThis.ISDKWorldInstanceBase {
const { skeleton } = this;
if (!skeleton) return;
this.renderSkeleton(renderer, skeleton);
this.renderDragHandles(renderer);
this.renderDebugSkeleton(renderer, skeleton);
this.skeletonRenderer ||= new spine.C3RendererRuntime(renderer, skeleton, this.matrix);
this.skeletonRenderer.draw(this.opacity);
if (this.propDebugSkeleton) this.skeletonRenderer.drawDebug(this.x, this.y, this.getBoundingQuad(false));
this.renderDragHandles();
}
private renderSkeleton (renderer: IRenderer, skeleton: Skeleton) {
let command = this.skeletonRenderer.render(skeleton);
const opacity = this.opacity;
const inv255 = 1 / 255;
while (command) {
const { numVertices, positions, uvs, colors, indices, numIndices, blendMode } = command;
const vertices = this.tempVertices.length < numVertices * 3
? (this.tempVertices = new Float32Array(numVertices * 3))
: this.tempVertices;
const c3colors = this.tempColors.length < numVertices * 4
? (this.tempColors = new Float32Array(numVertices * 4))
: this.tempColors;
for (let i = 0; i < numVertices; i++) {
const srcIndex = i * 2;
const { x, y } = this.skeletonToC3WorldCoordinates(positions[srcIndex], positions[srcIndex + 1]);
const dstIndex = i * 3;
vertices[dstIndex] = x;
vertices[dstIndex + 1] = y;
vertices[dstIndex + 2] = 0;
const color = colors[i];
const colorDst = i * 4;
const alpha = (color >>> 24 & 0xFF) * inv255 * opacity;
const alphaInverse = inv255 * alpha;
c3colors[colorDst] = (color >>> 16 & 0xFF) * alphaInverse;
c3colors[colorDst + 1] = (color >>> 8 & 0xFF) * alphaInverse;
c3colors[colorDst + 2] = (color & 0xFF) * alphaInverse;
c3colors[colorDst + 3] = alpha;
}
renderer.setTexture(command.texture.texture);
renderer.setBlendMode(spine.BlendingModeSpineToC3[blendMode]);
renderer.drawMesh(
vertices.subarray(0, numVertices * 3),
uvs.subarray(0, numVertices * 2),
indices.subarray(0, numIndices),
c3colors.subarray(0, numVertices * 4),
);
command = command.next;
}
}
private renderDragHandles (renderer: IRenderer) {
private renderDragHandles () {
for (const { bone, radius, debug } of this.dragHandles) {
if (!debug) continue;
const boneApplied = bone.applied;
const { x: x1, y: y1 } = this.skeletonToC3WorldCoordinates(boneApplied.worldX, boneApplied.worldY);
renderer.setColorFillMode();
renderer.setColor([1, 0, 0, .2]);
renderer.convexPoly(this.circle(x1, y1, radius));
this.skeletonRenderer?.renderDragHandles(bone, radius);
}
}
private renderDebugSkeleton (renderer: IRenderer, skeleton: Skeleton) {
if (!this.propDebugSkeleton) return;
const bones = skeleton.bones;
for (let i = 0, n = bones.length; i < n; i++) {
const bone = bones[i];
if (!bone.parent) continue;
const boneApplied = bone.applied;
const { x: x1, y: y1 } = this.skeletonToC3WorldCoordinates(boneApplied.worldX, boneApplied.worldY);
const x2 = bone.data.length * boneApplied.a + x1;
const y2 = bone.data.length * boneApplied.c + y1;
renderer.setColor([1, 0, 0, 1]);
renderer.setColorFillMode();
const t = this.tempPoint.set(y2 - y1, x1 - x2);
t.normalize();
const width = 1 * 0.5;
const tx = t.x * width;
const ty = t.y * width;
renderer.convexPoly([
x1 + tx, y1 + ty,
x1 - tx, y1 - ty,
x2 + tx, y2 + ty,
x2 - tx, y2 - ty,
x2 + tx, y2 + ty,
x1 - tx, y1 - ty,
]);
renderer.setColor([0, 1, 0, 1]);
renderer.convexPoly(this.circle(x1, y1, 2));
}
// debug bounds
renderer.setAlphaBlendMode();
renderer.setColorFillMode();
renderer.setColorRgba(0.25, 0, 0, 0.25);
renderer.lineQuad(this.getBoundingQuad(false));
renderer.line(this.x, this.y, this.x + this.propOffsetX, this.y + this.propOffsetY);
}
private circle (x: number, y: number, radius: number) {
let segments = Math.max(1, (6 * spine.MathUtils.cbrt(radius)) | 0);
if (segments <= 0) throw new Error("segments must be > 0.");
const angle = 2 * spine.MathUtils.PI / segments;
const cos = Math.cos(angle);
const sin = Math.sin(angle);
let cx = radius, cy = 0;
segments--;
const poly = [];
for (let i = 0; i < segments; i++) {
poly.push(x, y);
poly.push(x + cx, y + cy);
const temp = cx;
cx = cos * cx - sin * cy;
cy = sin * temp + cos * cy;
poly.push(x + cx, y + cy);
}
poly.push(x, y, x + cx, y + cy);
cx = radius;
cy = 0;
poly.push(x + cx, y + cy);
return poly;
}
/**********/
@ -327,7 +204,7 @@ class SpineC3Instance extends globalThis.ISDKWorldInstanceBase {
}
}
private updateHandles (skeleton: Skeleton) {
private updateHandles (skeleton: Skeleton, matrix: C3Matrix) {
if (this.dragHandles.size === 0) return;
// accessing mouse without having a mouse object will throw an error
@ -357,16 +234,16 @@ class SpineC3Instance extends globalThis.ISDKWorldInstanceBase {
if (handleObject.dragging) {
if (bone.parent) {
const { x, y } = this.c3WorldCoordinatesToBone(mx - handleObject.offsetX, my - handleObject.offsetY, bone);
const { x, y } = matrix.gameToBone(mx - handleObject.offsetX, my - handleObject.offsetY, bone);
boneApplied.x = x;
boneApplied.y = y;
} else {
const { x, y } = this.c3WorldCoordinatesToSkeleton(mx - handleObject.offsetX, my - handleObject.offsetY);
const { x, y } = matrix.gameToSkeleton(mx - handleObject.offsetX, my - handleObject.offsetY);
boneApplied.x = x / skeleton.scaleX;
boneApplied.y = -y / skeleton.scaleY * spine.Skeleton.yDir;
}
} else if (!this.prevLeftClickDown) {
const { x, y } = this.c3WorldCoordinatesToSkeleton(mx, my);
const { x, y } = matrix.gameToSkeleton(mx, my);
const inside = handleObject.slot
? this.isInsideSlot(x, y, handleObject.slot, true)
: this.inRadius(x, y, boneApplied.worldX, boneApplied.worldY, handleObject.radius);
@ -402,7 +279,7 @@ class SpineC3Instance extends globalThis.ISDKWorldInstanceBase {
if (skeletonCoordinate) return this.isPointInPolygon(vertices, hullLength, x, y);
const coords = this.c3WorldCoordinatesToSkeleton(x, y);
const coords = this.matrix.gameToSkeleton(x, y);
return this.isPointInPolygon(vertices, hullLength, coords.x, coords.y);
}
@ -806,7 +683,7 @@ class SpineC3Instance extends globalThis.ISDKWorldInstanceBase {
const instance = this.runtime.getInstanceByUid(follower.uid) as IWorldInstance;
if (!instance) continue;
const { x, y } = this.boneToC3WorldCoordinates(bone);
const { x, y } = this.matrix.boneToGame(bone);
const boneRotation = bone.applied.getWorldRotationX();
// Apply rotation to offset
@ -824,41 +701,6 @@ class SpineC3Instance extends globalThis.ISDKWorldInstanceBase {
/**********/
/*
* Coordinates transformation
*/
private c3WorldCoordinatesToSkeleton (x: number, y: number) {
const tx = x - (this.x + this.propOffsetX);
const ty = y - (this.y + this.propOffsetY);
const { a: ta, b: tb, c: tc, d: td, tempPoint } = this;
const delta = ta * td - tb * tc;
tempPoint.x = (td * tx - tc * ty) / delta;
tempPoint.y = (ta * ty - tb * tx) / delta;
return this.tempPoint;
}
private c3WorldCoordinatesToBone (x: number, y: number, bone: Bone) {
const point = this.c3WorldCoordinatesToSkeleton(x, y);
if (bone.parent)
return bone.parent.applied.worldToLocal(point);
return bone.applied.worldToLocal(point);
}
private skeletonToC3WorldCoordinates (skeletonX: number, skeletonY: number) {
const { a, b, c, d, tempPoint } = this;
tempPoint.x = a * skeletonX + c * skeletonY + this.x + this.propOffsetX;
tempPoint.y = b * skeletonX + d * skeletonY + this.y + this.propOffsetY;
return tempPoint;
}
private boneToC3WorldCoordinates (bone: Bone) {
const { applied } = bone;
return this.skeletonToC3WorldCoordinates(applied.worldX, applied.worldY);
}
/**********/
/*
* Bone
*/
@ -947,7 +789,7 @@ class SpineC3Instance extends globalThis.ISDKWorldInstanceBase {
return 0;
}
const point = this.boneToC3WorldCoordinates(bone);
const point = this.matrix.boneToGame(bone);
return point.x;
}
@ -964,7 +806,7 @@ class SpineC3Instance extends globalThis.ISDKWorldInstanceBase {
return 0;
}
const point = this.boneToC3WorldCoordinates(bone);
const point = this.matrix.boneToGame(bone);
return point.y;
}
@ -972,7 +814,7 @@ class SpineC3Instance extends globalThis.ISDKWorldInstanceBase {
const bone = this.getBone(boneName);
if (!bone) return;
const { x, y } = this.c3WorldCoordinatesToBone(c3X, c3Y, bone);
const { x, y } = this.matrix.gameToBone(c3X, c3Y, bone);
bone.applied.x = x;
bone.applied.y = y;
}

View File

@ -1,6 +1,6 @@
// / <reference types="editor/sdk" />
import type { AnimationState, AssetLoader, Skeleton, SkeletonRendererCore, SpineBoundsProvider, TextureAtlas } from "@esotericsoftware/spine-construct3-lib";
import type { AnimationState, AssetLoader, C3Matrix, C3RendererEditor, Skeleton, SpineBoundsProvider, TextureAtlas, } from "@esotericsoftware/spine-construct3-lib";
import type { SpineC3PluginType } from "./type";
const SDK = globalThis.SDK;
@ -25,7 +25,8 @@ class SpineC3PluginInstance extends SDK.IWorldInstanceBase {
animation?: string;
private assetLoader: AssetLoader;
private skeletonRenderer: SkeletonRendererCore;
private skeletonRenderer?: C3RendererEditor;
private matrix: C3Matrix;
// position mode
private positioningBounds = false;
@ -40,10 +41,6 @@ class SpineC3PluginInstance extends SDK.IWorldInstanceBase {
height: 200,
};
// utils for drawing
private tempVertices = new Float32Array(4096);
private tempColors = new Float32Array(4096);
// errors
private errorTextureAtlas?: string;
private errorSkeleton?: string;
@ -56,7 +53,7 @@ class SpineC3PluginInstance extends SDK.IWorldInstanceBase {
spine.Skeleton.yDown = true;
this.assetLoader = new spine.AssetLoader();
this.skeletonRenderer = new spine.SkeletonRendererCore();
this.matrix = new spine.C3Matrix();
}
Release () {
@ -118,64 +115,13 @@ class SpineC3PluginInstance extends SDK.IWorldInstanceBase {
skeleton.scaleY = _inst.GetPropertyValue(PLUGIN_CLASS.PROP_SKELETON_SCALE_Y) as number;
}
const opacity = _inst.GetOpacity();
const cos = Math.cos(offsetAngle);
const sin = Math.sin(offsetAngle);
const inv255 = 1 / 255;
this.update(0);
let command = this.skeletonRenderer.render(skeleton);
while (command) {
const { numVertices, positions, uvs, colors, indices, numIndices, blendMode } = command;
const vertices = this.tempVertices.length < numVertices * 3
? (this.tempVertices = new Float32Array(numVertices * 3))
: this.tempVertices;
const c3colors = this.tempColors.length < numVertices * 4
? (this.tempColors = new Float32Array(numVertices * 4))
: this.tempColors;
for (let i = 0; i < numVertices; i++) {
const srcIndex = i * 2;
const dstIndex = i * 3;
const x = positions[srcIndex];
const y = positions[srcIndex + 1];
vertices[dstIndex] = x * cos - y * sin + offsetX;
vertices[dstIndex + 1] = x * sin + y * cos + offsetY;
vertices[dstIndex + 2] = 0;
const color = colors[i];
const colorDst = i * 4;
const alpha = (color >>> 24 & 0xFF) * inv255 * opacity;
const alphaInverse = inv255 * alpha;
c3colors[colorDst] = (color >>> 16 & 0xFF) * alphaInverse;
c3colors[colorDst + 1] = (color >>> 8 & 0xFF) * alphaInverse;
c3colors[colorDst + 2] = (color & 0xFF) * alphaInverse;
c3colors[colorDst + 3] = alpha;
}
iRenderer.ResetColor();
iRenderer.SetBlendMode(spine.BlendingModeSpineToC3[blendMode]);
iRenderer.SetTextureFillMode();
iRenderer.SetTexture(command.texture.texture);
iRenderer.DrawMesh(
vertices.subarray(0, numVertices * 3),
uvs.subarray(0, numVertices * 2),
indices.subarray(0, numIndices),
c3colors,
);
command = command.next;
}
iRenderer.SetAlphaBlend();
iRenderer.SetColorFillMode();
iRenderer.SetColorRgba(0.25, 0, 0, 0.25);
iRenderer.LineQuad(_inst.GetQuad());
iRenderer.Line(rectX, rectY, offsetX, offsetY);
this.skeletonRenderer ||= new spine.C3RendererEditor(iRenderer, skeleton, this.matrix);
this.skeletonRenderer.draw(_inst.GetOpacity());
const quad = _inst.GetQuad();
if (_inst.GetPropertyValue(PLUGIN_CLASS.PROP_DEBUG_SKELETON) as boolean)
this.skeletonRenderer.drawDebug(rectX, rectY, quad);
this.skeletonRenderer.renderGameObjectBounds(rectX, rectY, quad);
} else {
iRenderer.SetAlphaBlend();
@ -501,6 +447,10 @@ class SpineC3PluginInstance extends SDK.IWorldInstanceBase {
state.update(delta);
skeleton.update(delta);
state.apply(skeleton);
this.matrix.update(
this._inst.GetX() + (this._inst.GetPropertyValue(PLUGIN_CLASS.PROP_BOUNDS_OFFSET_X) as number),
this._inst.GetY() + (this._inst.GetPropertyValue(PLUGIN_CLASS.PROP_BOUNDS_OFFSET_Y) as number),
this._inst.GetAngle() + (this._inst.GetPropertyValue(PLUGIN_CLASS.PROP_BOUNDS_OFFSET_ANGLE) as number));
skeleton.updateWorldTransform(spine.Physics.update);
}