Add pma, wraps and filter management.

This commit is contained in:
Davide Tantillo 2025-08-18 18:21:23 +02:00
parent 235a96ca5c
commit 75f30e297d
4 changed files with 94 additions and 18 deletions

View File

@ -65,7 +65,7 @@ export class AssetLoader {
await Promise.all(textureAtlas.pages.map(async page => { await Promise.all(textureAtlas.pages.map(async page => {
const texture = await this.loadSpineTextureEditor(page.name, page.pma, instance); const texture = await this.loadSpineTextureEditor(page.name, page.pma, instance);
if (texture) { if (texture) {
const spineTexture = new C3TextureEditor(texture, renderer); const spineTexture = new C3TextureEditor(texture, renderer, page);
page.setTexture(spineTexture); page.setTexture(spineTexture);
} }
return texture; return texture;
@ -116,7 +116,7 @@ export class AssetLoader {
await Promise.all(textureAtlas.pages.map(async page => { await Promise.all(textureAtlas.pages.map(async page => {
const texture = await this.loadSpineTextureRuntime(page.name, page.pma, instance); const texture = await this.loadSpineTextureRuntime(page.name, page.pma, instance);
if (texture) { if (texture) {
const spineTexture = new C3Texture(texture, renderer); const spineTexture = new C3Texture(texture, renderer, page);
page.setTexture(spineTexture); page.setTexture(spineTexture);
} }
return texture; return texture;
@ -136,6 +136,7 @@ export class AssetLoader {
static async createImageBitmapFromBlob (blob: Blob, pma: boolean): Promise<ImageBitmap | null> { static async createImageBitmapFromBlob (blob: Blob, pma: boolean): Promise<ImageBitmap | null> {
try { try {
// pma parameters seems to do not matter here. It matters in C3 Texture creation
return createImageBitmap(blob, { premultiplyAlpha: pma ? "none" : "premultiply" }); return createImageBitmap(blob, { premultiplyAlpha: pma ? "none" : "premultiply" });
} catch (e) { } catch (e) {
console.error("Failed to create ImageBitmap from blob:", e); console.error("Failed to create ImageBitmap from blob:", e);

View File

@ -27,25 +27,34 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/ *****************************************************************************/
import { BlendMode, Texture, type TextureFilter, type TextureWrap } from "@esotericsoftware/spine-core"; import { BlendMode, Texture, type TextureAtlasPage, TextureFilter, TextureWrap } from "@esotericsoftware/spine-core";
export class C3TextureEditor extends Texture { export class C3TextureEditor extends Texture {
texture: SDK.Gfx.IWebGLTexture; texture: SDK.Gfx.IWebGLTexture;
renderer: SDK.Gfx.IWebGLRenderer; renderer: SDK.Gfx.IWebGLRenderer;
constructor (image: HTMLImageElement | ImageBitmap, renderer: SDK.Gfx.IWebGLRenderer) { constructor (image: HTMLImageElement | ImageBitmap, renderer: SDK.Gfx.IWebGLRenderer, page: TextureAtlasPage) {
super(image); super(image);
this.renderer = renderer; this.renderer = renderer;
this.texture = renderer.CreateDynamicTexture(image.width, image.height); const options: TextureCreateOptions = {
this.renderer.UpdateTexture(image, this.texture); wrapX: toC3TextureWrap(page.uWrap),
wrapY: toC3TextureWrap(page.vWrap),
sampling: toC3Filter(page.minFilter),
mipMap: toC3MipMap(page.minFilter),
}
this.texture = renderer.CreateDynamicTexture(image.width, image.height, options);
this.renderer.UpdateTexture(image, this.texture, { premultiplyAlpha: !page.pma });
} }
setFilters (minFilter: TextureFilter, magFilter: TextureFilter) { setFilters () {
// cannot change filter after texture creation
} }
setWraps (uWrap: TextureWrap, vWrap: TextureWrap) { setWraps () {
// cannot change wraps after texture creation
} }
dispose () { dispose () {
this.renderer.DeleteTexture(this.texture); this.renderer.DeleteTexture(this.texture);
} }
@ -55,17 +64,26 @@ export class C3Texture extends Texture {
texture: ITexture; texture: ITexture;
renderer: IRenderer; renderer: IRenderer;
constructor (image: HTMLImageElement | ImageBitmap, renderer: IRenderer) { constructor (image: HTMLImageElement | ImageBitmap, renderer: IRenderer, page: TextureAtlasPage) {
super(image); super(image);
this.renderer = renderer; this.renderer = renderer;
this.texture = renderer.createDynamicTexture(image.width, image.height); const options: TextureCreateOptions = {
this.renderer.updateTexture(image, this.texture); wrapX: toC3TextureWrap(page.uWrap),
wrapY: toC3TextureWrap(page.vWrap),
sampling: toC3Filter(page.minFilter),
mipMap: toC3MipMap(page.minFilter),
}
this.texture = renderer.createDynamicTexture(image.width, image.height, options);
this.renderer.updateTexture(image, this.texture, { premultiplyAlpha: !page.pma });
} }
setFilters (minFilter: TextureFilter, magFilter: TextureFilter) {
setFilters () {
// cannot change filter after texture creation
} }
setWraps (uWrap: TextureWrap, vWrap: TextureWrap) { setWraps () {
// cannot change wraps after texture creation
} }
dispose () { dispose () {
@ -73,6 +91,49 @@ export class C3Texture extends Texture {
} }
} }
function toC3TextureWrap (wrap: TextureWrap): TextureWrapMode {
if (wrap === TextureWrap.ClampToEdge) return "clamp-to-edge";
else if (wrap === TextureWrap.MirroredRepeat) return "mirror-repeat";
else if (wrap === TextureWrap.Repeat) return "repeat";
else throw new Error(`Unknown texture wrap: ${wrap}`);
}
function toC3MipMap (filter: TextureFilter): boolean {
switch (filter) {
case TextureFilter.MipMap:
case TextureFilter.MipMapLinearNearest:
case TextureFilter.MipMapNearestLinear:
case TextureFilter.MipMapNearestNearest:
return true;
case TextureFilter.Linear:
case TextureFilter.Nearest:
return false;
default:
throw new Error(`Unknown texture filter: ${filter}`);
}
}
function toC3Filter (filter: TextureFilter): TextureSamplingMode {
switch (filter) {
case TextureFilter.Nearest:
case TextureFilter.MipMapNearestNearest:
return "nearest";
case TextureFilter.Linear:
case TextureFilter.MipMapLinearNearest:
case TextureFilter.MipMapNearestLinear:
return "bilinear";
case TextureFilter.MipMap:
case TextureFilter.MipMapLinearLinear:
return "trilinear";
default:
throw new Error(`Unknown texture filter: ${filter}`);
}
}
export const BlendingModeSpineToC3: Record<BlendMode, BlendModeParameter> = { export const BlendingModeSpineToC3: Record<BlendMode, BlendModeParameter> = {
[BlendMode.Normal]: "normal", [BlendMode.Normal]: "normal",
[BlendMode.Additive]: "additive", [BlendMode.Additive]: "additive",

View File

@ -71,8 +71,10 @@ class DrawingInstance extends globalThis.ISDKWorldInstanceBase {
return; return;
} }
// workaround to request a redraw: https://github.com/Scirra/Construct-feature-requests/issues/615
this.x++; this.x++;
this.x--; this.x--;
this.update(this.dt); this.update(this.dt);
} }
@ -224,8 +226,12 @@ class DrawingInstance extends globalThis.ISDKWorldInstanceBase {
const offsetY = this.y + this.propOffsetY; const offsetY = this.y + this.propOffsetY;
const offsetAngle = this.angle + this.propOffsetAngle; const offsetAngle = this.angle + this.propOffsetAngle;
const cos = Math.cos(offsetAngle); let cos = 0;
const sin = Math.sin(offsetAngle); let sin = 0;
if (offsetAngle) {
cos = Math.cos(offsetAngle);
sin = Math.sin(offsetAngle);
}
while (command) { while (command) {
const { numVertices, positions, uvs, colors, indices, numIndices, blendMode } = command; const { numVertices, positions, uvs, colors, indices, numIndices, blendMode } = command;
@ -236,10 +242,17 @@ class DrawingInstance extends globalThis.ISDKWorldInstanceBase {
const dstIndex = i * 3; const dstIndex = i * 3;
const x = positions[srcIndex]; const x = positions[srcIndex];
const y = positions[srcIndex + 1]; const y = positions[srcIndex + 1];
if (offsetAngle) {
vertices[dstIndex] = x * cos - y * sin + offsetX; vertices[dstIndex] = x * cos - y * sin + offsetX;
vertices[dstIndex + 1] = x * sin + y * cos + offsetY; vertices[dstIndex + 1] = x * sin + y * cos + offsetY;
} else {
vertices[dstIndex] = x + offsetX;
vertices[dstIndex + 1] = y + offsetY;
}
vertices[dstIndex + 2] = 0; vertices[dstIndex + 2] = 0;
// there's something wrong with the hand after adding the colors on spineboy portal animation // there's something wrong with the hand after adding the colors on spineboy portal animation
const color = colors[i]; const color = colors[i];
const colorDst = i * 4; const colorDst = i * 4;
@ -254,6 +267,7 @@ class DrawingInstance extends globalThis.ISDKWorldInstanceBase {
renderer.drawMesh( renderer.drawMesh(
vertices.subarray(0, numVertices * 3), vertices.subarray(0, numVertices * 3),
uvs.subarray(0, numVertices * 2), uvs.subarray(0, numVertices * 2),
// workaround for this bug: https://github.com/Scirra/Construct-bugs/issues/8746
this.padUint16ArrayForWebGPU(indices.subarray(0, numIndices)), this.padUint16ArrayForWebGPU(indices.subarray(0, numIndices)),
c3colors.subarray(0, numVertices * 4), c3colors.subarray(0, numVertices * 4),
); );

View File

@ -136,10 +136,10 @@ class MyDrawingInstance extends SDK.IWorldInstanceBase {
iRenderer.SetTextureFillMode(); iRenderer.SetTextureFillMode();
iRenderer.SetTexture(command.texture.texture); iRenderer.SetTexture(command.texture.texture);
const padded = this.padUint16ArrayForWebGPU(indices.subarray(0, numIndices));
iRenderer.DrawMesh( iRenderer.DrawMesh(
vertices.subarray(0, numVertices * 3), vertices.subarray(0, numVertices * 3),
uvs.subarray(0, numVertices * 2), uvs.subarray(0, numVertices * 2),
// workaround for this bug: https://github.com/Scirra/Construct-bugs/issues/8746
this.padUint16ArrayForWebGPU(indices.subarray(0, numIndices)), this.padUint16ArrayForWebGPU(indices.subarray(0, numIndices)),
); );