[ts][threejs] Allow to load pma textures. Non-pma textures are pma on upload. Fixed blending modes. Add physics example.

Close #2503.
This commit is contained in:
Davide Tantillo 2024-07-02 16:49:35 +02:00
parent 06dc94ee11
commit f3b3cb429a
13 changed files with 4307 additions and 20 deletions

View File

@ -456,6 +456,7 @@ rm "$ROOT/spine-ts/spine-threejs/example/assets/"*
cp -f ../raptor/export/raptor-pro.json "$ROOT/spine-ts/spine-threejs/example/assets/"
cp -f ../raptor/export/raptor.atlas "$ROOT/spine-ts/spine-threejs/example/assets/"
cp -f ../raptor/export/raptor.png "$ROOT/spine-ts/spine-threejs/example/assets/"
cp -f ../celestial-circus/export/* "$ROOT/spine-ts/spine-threejs/example/assets/"
rm "$ROOT/spine-ts/spine-player/example/assets/"*
cp -f ../raptor/export/raptor-pro.json "$ROOT/spine-ts/spine-player/example/assets/"

View File

@ -178,6 +178,9 @@
<li>
<a href="/spine-threejs/example/logarithmic-depth-buffer.html">Logarithmic depth buffer</a>
</li>
<li>
<a href="/spine-threejs/example/physics.html">Physics</a>
</li>
</ul>
</ul>
</div>

View File

@ -0,0 +1,174 @@
celestial-circus-pma.png
size: 1024, 1024
filter: Linear, Linear
pma: true
scale: 0.4
arm-back-down
bounds: 324, 401, 38, 82
rotate: 90
arm-back-up
bounds: 290, 44, 83, 116
rotate: 90
arm-front-down
bounds: 706, 2, 36, 78
rotate: 90
arm-front-up
bounds: 860, 138, 77, 116
bench
bounds: 725, 256, 189, 48
body-bottom
bounds: 879, 868, 154, 124
rotate: 90
body-top
bounds: 725, 128, 126, 133
rotate: 90
chest
bounds: 408, 26, 104, 93
cloud-back
bounds: 752, 378, 202, 165
cloud-front
bounds: 2, 2, 325, 196
rotate: 90
collar
bounds: 786, 13, 47, 26
ear
bounds: 1002, 643, 20, 28
eye-back-shadow
bounds: 428, 395, 14, 10
eye-front-shadow
bounds: 704, 529, 24, 14
eye-reflex-back
bounds: 860, 128, 8, 7
rotate: 90
eye-reflex-front
bounds: 726, 386, 10, 7
eye-white-back
bounds: 835, 23, 13, 16
eye-white-front
bounds: 1005, 1000, 22, 17
rotate: 90
eyelashes-down-back
bounds: 232, 329, 11, 6
rotate: 90
eyelashes-down-front
bounds: 913, 851, 15, 6
rotate: 90
eyelashes-top-back
bounds: 408, 395, 18, 10
eyelashes-top-front
bounds: 702, 179, 30, 16
rotate: 90
face
bounds: 514, 26, 93, 102
rotate: 90
feathers-back
bounds: 954, 625, 46, 46
feathers-front
bounds: 706, 40, 72, 86
fringe-middle-back
bounds: 200, 6, 33, 52
rotate: 90
fringe-middle-front
bounds: 878, 76, 60, 50
rotate: 90
fringe-side-back
bounds: 780, 41, 27, 94
rotate: 90
fringe-side-front
bounds: 939, 161, 26, 93
glove-bottom-back
bounds: 954, 572, 51, 41
rotate: 90
glove-bottom-front
bounds: 916, 256, 47, 48
hair-back-1
bounds: 444, 395, 132, 306
rotate: 90
hair-back-2
bounds: 438, 211, 80, 285
rotate: 90
hair-back-3
bounds: 719, 306, 70, 268
rotate: 90
hair-back-4
bounds: 438, 121, 88, 262
rotate: 90
hair-back-5
bounds: 438, 293, 88, 279
rotate: 90
hair-back-6
bounds: 200, 41, 88, 286
hair-hat-shadow
bounds: 232, 398, 90, 41
hand-back
bounds: 954, 673, 60, 47
rotate: 90
hand-front
bounds: 967, 172, 53, 60
hat-back
bounds: 954, 802, 64, 45
rotate: 90
hat-front
bounds: 780, 70, 96, 56
head-back
bounds: 618, 17, 102, 86
rotate: 90
jabot
bounds: 967, 234, 70, 55
rotate: 90
leg-back
bounds: 232, 441, 210, 333
leg-front
bounds: 444, 529, 258, 320
logo-brooch
bounds: 954, 545, 16, 25
mouth
bounds: 408, 121, 22, 6
neck
bounds: 232, 342, 39, 56
rotate: 90
nose
bounds: 742, 529, 6, 7
rotate: 90
nose-highlight
bounds: 719, 300, 4, 4
nose-shadow
bounds: 869, 128, 7, 8
pupil-back
bounds: 730, 529, 10, 14
pupil-front
bounds: 254, 21, 12, 18
rope-back
bounds: 232, 383, 10, 492
rotate: 90
rope-front
bounds: 232, 383, 10, 492
rotate: 90
rope-front-bottom
bounds: 954, 735, 42, 65
skirt
bounds: 2, 776, 440, 246
sock-bow
bounds: 408, 407, 33, 32
spine-logo-body
bounds: 879, 853, 13, 32
rotate: 90
star-big
bounds: 939, 141, 18, 24
rotate: 90
star-medium
bounds: 742, 537, 6, 8
rotate: 90
star-small
bounds: 719, 378, 3, 4
rotate: 90
underskirt
bounds: 2, 329, 445, 228
rotate: 90
underskirt-back
bounds: 444, 851, 433, 171
wing-back
bounds: 290, 129, 146, 252
wing-front
bounds: 704, 545, 304, 248
rotate: 90

Binary file not shown.

After

Width:  |  Height:  |  Size: 799 KiB

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,173 @@
celestial-circus.png
size: 1024, 1024
filter: Linear, Linear
scale: 0.4
arm-back-down
bounds: 324, 401, 38, 82
rotate: 90
arm-back-up
bounds: 290, 44, 83, 116
rotate: 90
arm-front-down
bounds: 706, 2, 36, 78
rotate: 90
arm-front-up
bounds: 860, 138, 77, 116
bench
bounds: 725, 256, 189, 48
body-bottom
bounds: 879, 868, 154, 124
rotate: 90
body-top
bounds: 725, 128, 126, 133
rotate: 90
chest
bounds: 408, 26, 104, 93
cloud-back
bounds: 752, 378, 202, 165
cloud-front
bounds: 2, 2, 325, 196
rotate: 90
collar
bounds: 786, 13, 47, 26
ear
bounds: 1002, 643, 20, 28
eye-back-shadow
bounds: 428, 395, 14, 10
eye-front-shadow
bounds: 704, 529, 24, 14
eye-reflex-back
bounds: 860, 128, 8, 7
rotate: 90
eye-reflex-front
bounds: 726, 386, 10, 7
eye-white-back
bounds: 835, 23, 13, 16
eye-white-front
bounds: 1005, 1000, 22, 17
rotate: 90
eyelashes-down-back
bounds: 232, 329, 11, 6
rotate: 90
eyelashes-down-front
bounds: 913, 851, 15, 6
rotate: 90
eyelashes-top-back
bounds: 408, 395, 18, 10
eyelashes-top-front
bounds: 702, 179, 30, 16
rotate: 90
face
bounds: 514, 26, 93, 102
rotate: 90
feathers-back
bounds: 954, 625, 46, 46
feathers-front
bounds: 706, 40, 72, 86
fringe-middle-back
bounds: 200, 6, 33, 52
rotate: 90
fringe-middle-front
bounds: 878, 76, 60, 50
rotate: 90
fringe-side-back
bounds: 780, 41, 27, 94
rotate: 90
fringe-side-front
bounds: 939, 161, 26, 93
glove-bottom-back
bounds: 954, 572, 51, 41
rotate: 90
glove-bottom-front
bounds: 916, 256, 47, 48
hair-back-1
bounds: 444, 395, 132, 306
rotate: 90
hair-back-2
bounds: 438, 211, 80, 285
rotate: 90
hair-back-3
bounds: 719, 306, 70, 268
rotate: 90
hair-back-4
bounds: 438, 121, 88, 262
rotate: 90
hair-back-5
bounds: 438, 293, 88, 279
rotate: 90
hair-back-6
bounds: 200, 41, 88, 286
hair-hat-shadow
bounds: 232, 398, 90, 41
hand-back
bounds: 954, 673, 60, 47
rotate: 90
hand-front
bounds: 967, 172, 53, 60
hat-back
bounds: 954, 802, 64, 45
rotate: 90
hat-front
bounds: 780, 70, 96, 56
head-back
bounds: 618, 17, 102, 86
rotate: 90
jabot
bounds: 967, 234, 70, 55
rotate: 90
leg-back
bounds: 232, 441, 210, 333
leg-front
bounds: 444, 529, 258, 320
logo-brooch
bounds: 954, 545, 16, 25
mouth
bounds: 408, 121, 22, 6
neck
bounds: 232, 342, 39, 56
rotate: 90
nose
bounds: 742, 529, 6, 7
rotate: 90
nose-highlight
bounds: 719, 300, 4, 4
nose-shadow
bounds: 869, 128, 7, 8
pupil-back
bounds: 730, 529, 10, 14
pupil-front
bounds: 254, 21, 12, 18
rope-back
bounds: 232, 383, 10, 492
rotate: 90
rope-front
bounds: 232, 383, 10, 492
rotate: 90
rope-front-bottom
bounds: 954, 735, 42, 65
skirt
bounds: 2, 776, 440, 246
sock-bow
bounds: 408, 407, 33, 32
spine-logo-body
bounds: 879, 853, 13, 32
rotate: 90
star-big
bounds: 939, 141, 18, 24
rotate: 90
star-medium
bounds: 742, 537, 6, 8
rotate: 90
star-small
bounds: 719, 378, 3, 4
rotate: 90
underskirt
bounds: 2, 329, 445, 228
rotate: 90
underskirt-back
bounds: 444, 851, 433, 171
wing-back
bounds: 290, 129, 146, 252
wing-front
bounds: 704, 545, 304, 248
rotate: 90

Binary file not shown.

After

Width:  |  Height:  |  Size: 790 KiB

View File

@ -0,0 +1,150 @@
<html>
<head>
<meta charset="UTF-8" />
<title>spine-threejs</title>
<script src="https://unpkg.com/three@0.141.0/build/three.js"></script>
<script src="../dist/iife/spine-threejs.js"></script>
<script src="./OrbitalControls.js"></script>
</head>
<style>
* {
margin: 0;
padding: 0;
}
body,
html {
height: 100%;
}
canvas {
position: absolute;
width: 100%;
height: 100%;
}
</style>
<body>
<script>
(function () {
let scene, camera, renderer;
let geometry, material, mesh, skeletonMesh;
let assetManager;
let canvas;
let controls;
let lastFrameTime = Date.now() / 1000;
let pma = false;
let baseUrl = "assets/";
let skeletonFile = "celestial-circus-pro.json";
let atlasFile = `celestial-circus${pma ? "-pma" : ""}.atlas`;
function init() {
// create the THREE.JS camera, scene and renderer (WebGL)
let width = window.innerWidth,
height = window.innerHeight;
camera = new THREE.PerspectiveCamera(75, width / height, 1, 3000);
camera.position.y = 0;
camera.position.z = 800;
scene = new THREE.Scene();
renderer = new THREE.WebGLRenderer();
renderer.setSize(width, height);
document.body.appendChild(renderer.domElement);
canvas = renderer.domElement;
controls = new OrbitControls(camera, renderer.domElement);
// load the assets required to display the Raptor model
assetManager = new spine.AssetManager(baseUrl, undefined, pma);
assetManager.loadText(skeletonFile);
assetManager.loadTextureAtlas(atlasFile);
requestAnimationFrame(load);
}
function load(name, scale) {
if (assetManager.isLoadingComplete()) {
// Add a box to the scene to which we attach the skeleton mesh
geometry = new THREE.BoxGeometry(200, 200, 200);
material = new THREE.MeshBasicMaterial({
color: 0xff0000,
wireframe: true,
});
mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
// Load the texture atlas using name.atlas and name.png from the AssetManager.
// The function passed to TextureAtlas is used to resolve relative paths.
atlas = assetManager.require(atlasFile);
// Create a AtlasAttachmentLoader that resolves region, mesh, boundingbox and path attachments
atlasLoader = new spine.AtlasAttachmentLoader(atlas);
// Create a SkeletonJson instance for parsing the .json file.
let skeletonJson = new spine.SkeletonJson(atlasLoader);
// Set the scale to apply during parsing, parse the file, and create a new skeleton.
skeletonJson.scale = 0.4;
let skeletonData = skeletonJson.readSkeletonData(
assetManager.require(skeletonFile)
);
// Create a SkeletonMesh from the data and attach it to the scene
skeletonMesh = new spine.SkeletonMesh(
skeletonData,
(parameters) => {
parameters.depthTest = true;
parameters.depthWrite = true;
parameters.alphaTest = 0.001;
}
);
skeletonMesh.state.setAnimation(0, "swing", true);
skeletonMesh.state.setAnimation(1, "eyeblink-long", true);
mesh.add(skeletonMesh);
skeletonMesh.position.y = -300;
requestAnimationFrame(render);
} else requestAnimationFrame(load);
}
let lastTime = Date.now();
function render() {
// calculate delta time for animation purposes
let now = Date.now() / 1000;
let delta = now - lastFrameTime;
lastFrameTime = now;
// resize canvas to use full page, adjust camera/renderer
resize();
// Update orbital controls
controls.update();
// update the animation
skeletonMesh.update(delta);
// render the scene
renderer.render(scene, camera);
requestAnimationFrame(render);
}
function resize() {
let w = window.innerWidth;
let h = window.innerHeight;
if (canvas.width != w || canvas.height != h) {
canvas.width = w;
canvas.height = h;
}
camera.aspect = w / h;
camera.updateProjectionMatrix();
renderer.setSize(w, h);
}
init();
})();
</script>
</body>
</html>

View File

@ -31,9 +31,9 @@ import { AssetManagerBase, Downloader } from "@esotericsoftware/spine-core"
import { ThreeJsTexture } from "./ThreeJsTexture.js";
export class AssetManager extends AssetManagerBase {
constructor (pathPrefix: string = "", downloader: Downloader = new Downloader()) {
constructor (pathPrefix: string = "", downloader: Downloader = new Downloader(), pma = false) {
super((image: HTMLImageElement | ImageBitmap) => {
return new ThreeJsTexture(image);
return new ThreeJsTexture(image, pma);
}, pathPrefix, downloader);
}
}

View File

@ -29,7 +29,7 @@
import { SkeletonMeshMaterial, SkeletonMeshMaterialParametersCustomizer } from "./SkeletonMesh.js";
import * as THREE from "three"
import { ThreeJsTexture } from "./ThreeJsTexture.js";
import { ThreeJsTexture, ThreeBlendOptions } from "./ThreeJsTexture.js";
import { BlendMode } from "@esotericsoftware/spine-core";
export class MeshBatcher extends THREE.Mesh {
@ -163,7 +163,7 @@ export class MeshBatcher extends THREE.Mesh {
}
findMaterialGroup (slotTexture: THREE.Texture, slotBlendMode: BlendMode) {
const blending = ThreeJsTexture.toThreeJsBlending(slotBlendMode);
const blendingObject = ThreeJsTexture.toThreeJsBlending(slotBlendMode);
let group = -1;
if (Array.isArray(this.material)) {
@ -171,17 +171,23 @@ export class MeshBatcher extends THREE.Mesh {
const meshMaterial = this.material[i] as SkeletonMeshMaterial;
if (!meshMaterial.uniforms.map.value) {
updateMeshMaterial(meshMaterial, slotTexture, blending);
updateMeshMaterial(meshMaterial, slotTexture, blendingObject);
return i;
}
if (meshMaterial.uniforms.map.value === slotTexture && meshMaterial.blending === blending) {
if (meshMaterial.uniforms.map.value === slotTexture
&& blendingObject.blending === meshMaterial.blending
&& (blendingObject.blendSrc === undefined || blendingObject.blendSrc === meshMaterial.blendSrc)
&& (blendingObject.blendDst === undefined || blendingObject.blendDst === meshMaterial.blendDst)
&& (blendingObject.blendSrcAlpha === undefined || blendingObject.blendSrcAlpha === meshMaterial.blendSrcAlpha)
&& (blendingObject.blendDstAlpha === undefined || blendingObject.blendDstAlpha === meshMaterial.blendDstAlpha)
) {
return i;
}
}
const meshMaterial = new SkeletonMeshMaterial(this.materialCustomizer);
updateMeshMaterial(meshMaterial, slotTexture, blending);
updateMeshMaterial(meshMaterial, slotTexture, blendingObject);
this.material.push(meshMaterial);
group = this.material.length - 1;
} else {
@ -192,10 +198,8 @@ export class MeshBatcher extends THREE.Mesh {
}
}
function updateMeshMaterial (meshMaterial: SkeletonMeshMaterial, slotTexture: THREE.Texture, blending: THREE.Blending) {
function updateMeshMaterial (meshMaterial: SkeletonMeshMaterial, slotTexture: THREE.Texture, blending: ThreeBlendOptions) {
meshMaterial.uniforms.map.value = slotTexture;
meshMaterial.blending = blending;
meshMaterial.blendDst = blending === THREE.CustomBlending ? THREE.OneMinusSrcColorFactor : THREE.OneMinusSrcAlphaFactor;
meshMaterial.blendSrc = blending === THREE.CustomBlending ? THREE.OneFactor : THREE.SrcAlphaFactor;
Object.assign(meshMaterial, blending);
meshMaterial.needsUpdate = true;
}

View File

@ -97,6 +97,8 @@ export class SkeletonMeshMaterial extends THREE.ShaderMaterial {
parameters.uniforms["alphaTest"] = { value: parameters.alphaTest };
}
super(parameters);
// non-pma textures are premultiply on upload, so we set premultipliedAlpha to true
this.premultipliedAlpha = true;
}
}
@ -241,9 +243,9 @@ export class SkeletonMesh extends THREE.Object3D {
let alpha = skeletonColor.a * slotColor.a * attachmentColor.a;
let color = this.tempColor;
color.set(
skeletonColor.r * slotColor.r * attachmentColor.r,
skeletonColor.g * slotColor.g * attachmentColor.g,
skeletonColor.b * slotColor.b * attachmentColor.b,
skeletonColor.r * slotColor.r * attachmentColor.r * alpha,
skeletonColor.g * slotColor.g * attachmentColor.g * alpha,
skeletonColor.b * slotColor.b * attachmentColor.b * alpha,
alpha
);

View File

@ -33,12 +33,14 @@ import * as THREE from "three";
export class ThreeJsTexture extends Texture {
texture: THREE.Texture;
constructor (image: HTMLImageElement | ImageBitmap) {
constructor (image: HTMLImageElement | ImageBitmap, pma = false) {
super(image);
if (image instanceof ImageBitmap)
this.texture = new THREE.CanvasTexture(image);
else
this.texture = new THREE.Texture(image);
// if the texture is not pma, we ask to threejs to premultiply on upload
this.texture.premultiplyAlpha = !pma;
this.texture.flipY = false;
this.texture.needsUpdate = true;
}
@ -74,11 +76,31 @@ export class ThreeJsTexture extends Texture {
else throw new Error("Unknown texture wrap: " + wrap);
}
static toThreeJsBlending (blend: BlendMode) {
if (blend === BlendMode.Normal) return THREE.NormalBlending;
else if (blend === BlendMode.Additive) return THREE.AdditiveBlending;
else if (blend === BlendMode.Multiply) return THREE.MultiplyBlending;
else if (blend === BlendMode.Screen) return THREE.CustomBlending;
static toThreeJsBlending (blend: BlendMode): ThreeBlendOptions {
if (blend === BlendMode.Normal) return { blending: THREE.NormalBlending };
else if (blend === BlendMode.Additive) return { blending: THREE.AdditiveBlending };
else if (blend === BlendMode.Multiply) return {
blending: THREE.CustomBlending,
blendSrc: THREE.DstColorFactor,
blendDst: THREE.OneMinusSrcAlphaFactor,
blendSrcAlpha: THREE.OneFactor,
blendDstAlpha: THREE.OneMinusSrcAlphaFactor,
}
else if (blend === BlendMode.Screen) return {
blending: THREE.CustomBlending,
blendSrc: THREE.OneFactor,
blendDst: THREE.OneMinusSrcColorFactor,
blendSrcAlpha: THREE.OneFactor,
blendDstAlpha: THREE.OneMinusSrcColorFactor,
}
else throw new Error("Unknown blendMode: " + blend);
}
}
export type ThreeBlendOptions = {
blending: THREE.Blending,
blendSrc?: THREE.BlendingDstFactor,
blendDst?: THREE.BlendingDstFactor,
blendSrcAlpha?: THREE.BlendingDstFactor,
blendDstAlpha?: THREE.BlendingDstFactor,
}