So I got the following animation working (x of the projection shadow is a little off):
It creates a renderTexture of the spine and uses that plus a shadow of that...?
Little bit wonky code (I don't understand many parts regarding the shader and rendering parts):
`<html>
<head>
<meta charset="UTF-8" />
<title>spine-pixi-v8 with Shadow Fix</title>
<script src="https://cdn.jsdelivr.net/npm/pixi.js@8.4.1/dist/pixi.js"></script>
<script src="../dist/iife/spine-pixi-v8.js"></script>
<script src="https://cdn.jsdelivr.net/npm/lil-gui@0.20.0/dist/lil-gui.umd.min.js"></script>
<link href="https://cdn.jsdelivr.net/npm/lil-gui@0.20.0/dist/lil-gui.min.css" rel="stylesheet">
<link rel="stylesheet" href="../../index.css">
</head>
<body>
<script>
(async function () {
var app = new PIXI.Application();
await app.init({
width: window.innerWidth,
height: window.innerHeight,
resolution: window.devicePixelRatio || 1,
autoDensity: true,
resizeTo: window,
backgroundColor: 0xffffff, // White for better contrast
preference: 'webgl', // Force WebGL
hello: true,
})
document.body.appendChild(app.canvas);
// Load assets
PIXI.Assets.add({alias: "coinData", src: "/assets/coin-pro.json"});
PIXI.Assets.add({alias: "coinAtlas", src: "/assets/coin-pma.atlas"});
await PIXI.Assets.load(["coinData", "coinAtlas"]);
// Create Spine object
const coin = spine.Spine.from({
skeleton: "coinData",
atlas: "coinAtlas",
scale: 1,
});
coin.autoUpdate = false;
coin.state.data.defaultMix = 0.2;
coin.state.setAnimation(0, "animation", true);
coin.update(0); // Initial update for bounds
// Compute base Y from local bounds
const localBounds = coin.getLocalBounds();
const baseLocalY = localBounds.y + localBounds.height;
const padding = 300;
const rtWidth = localBounds.width + padding * 2;
const rtHeight = localBounds.height + padding * 2; // Extra for shadow downward
// Create RenderTexture for composing the Spine render
const rt = PIXI.RenderTexture.create({ width: rtWidth, height: rtHeight });
// Create a Sprite to display the composed texture
const composedSprite = new PIXI.Sprite(rt);
composedSprite.anchor.set(0.5);
// Hide the original Spine object
coin.visible = false;
app.stage.addChild(coin); // Still add to stage for transform calculations
app.stage.addChild(composedSprite);
// Vertex shader with mediump precision
const myVertex = `
#version 300 es
precision mediump float;
in vec2 aPosition;
uniform vec4 uInputSize;
uniform vec4 uOutputFrame;
uniform vec4 uOutputTexture;
out vec2 vTextureCoord;
vec4 filterVertexPosition(void) {
vec2 position = aPosition * uOutputFrame.zw + uOutputFrame.xy;
position.x = position.x * (2.0 / uOutputTexture.x) - 1.0;
position.y = position.y * (2.0 * uOutputTexture.z / uOutputTexture.y) - uOutputTexture.z;
return vec4(position, 0.0, 1.0);
}
vec2 filterTextureCoord(void) {
return aPosition * (uOutputFrame.zw * uInputSize.zw);
}
void main(void) {
gl_Position = filterVertexPosition();
vTextureCoord = filterTextureCoord();
}
`;
// Fragment shader
const myFragment = `
#version 300 es
precision mediump float;
in vec2 vTextureCoord;
uniform sampler2D uSampler;
uniform vec4 uInputSize;
uniform vec4 uOutputFrame;
uniform vec2 shadowDirection;
uniform float floorY;
out vec4 outColor;
void main(void) {
vec2 screenCoord = vTextureCoord * uInputSize.xy + uOutputFrame.xy;
vec2 shadow;
float paramY = (screenCoord.y - floorY) / shadowDirection.y;
shadow.y = paramY + floorY;
shadow.x = screenCoord.x + paramY * shadowDirection.x;
vec2 bodyFilterCoord = (shadow - uOutputFrame.xy) * uInputSize.zw;
vec4 originalColor = texture(uSampler, vTextureCoord);
vec4 shadowColor = texture(uSampler, bodyFilterCoord);
shadowColor.rgb = vec3(0.0);
shadowColor.a *= 0.8;
outColor = originalColor + shadowColor * (1.0 - originalColor.a);
}
`;
const glProgram = new PIXI.GlProgram({
vertex: myVertex,
fragment: myFragment,
});
const shadowUniforms = {
shadowDirection: { value: [-0.3, -0.7], type: 'vec2<f32>' },
floorY: { value: 0.0, type: 'f32' },
};
const filter = new PIXI.Filter({
glProgram,
resources: {
shadowUniforms,
},
});
filter.padding = 300;
composedSprite.filters = [filter];
// Function to update positions
function updatePositions() {
const centerX = window.innerWidth / 2;
const centerY = window.innerHeight / 2;
coin.x = centerX;
coin.y = centerY;
composedSprite.x = centerX;
composedSprite.y = centerY;
}
updatePositions();
// Add resize listener
window.addEventListener('resize', updatePositions);
app.ticker.add((ticker) => {
coin.update(ticker.deltaMS / 1000);
// Render Spine to RenderTexture with offset to center it
const offsetX = -localBounds.x - localBounds.width / 2 + rtWidth / 2;
const offsetY = -localBounds.y - localBounds.height / 2 + rtHeight / 2;
const matrix = new PIXI.Matrix().translate(offsetX, offsetY);
app.renderer.render(coin, {
renderTexture: rt,
clear: true,
transform: matrix
});
coin.x=400
coin.y=300
// Update floorY using global position
filter.resources.shadowUniforms.uniforms.floorY = coin.toGlobal(new PIXI.Point(0, baseLocalY)).y;
});
})();
</script>
</body>
</html>
`
Is this the best approach...?