mirror of
https://github.com/EsotericSoftware/spine-runtimes.git
synced 2026-02-04 06:14:53 +08:00
[ts][webgl] Allow to define a custom Shader for SceneRenderer.
This commit is contained in:
parent
c07d8a3bf4
commit
3136e1d33f
137
spine-ts/spine-player/example/custom-shader.html
Normal file
137
spine-ts/spine-player/example/custom-shader.html
Normal file
@ -0,0 +1,137 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<script src="../dist/iife/spine-player.js"></script>
|
||||
<link rel="stylesheet" href="../css/spine-player.css">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="../../index.css" />
|
||||
</head>
|
||||
|
||||
<body class="flex flex-col justify-center items-center">
|
||||
<div id="container-raptor" style="max-width:640px; aspect-ratio: 16/9"></div>
|
||||
<div style="color: white;">
|
||||
Red: <input id="red-input" type="range" min="0" max="100" value="100" style="appearance: revert;" /> <output id="red-value"></output>
|
||||
<br>
|
||||
Green: <input id="green-input" type="range" min="0" max="100" value="100" style="appearance: revert;" /> <output id="green-value"></output>
|
||||
<br>
|
||||
Blue: <input id="blue-input" type="range" min="0" max="100" value="100" style="appearance: revert;" /> <output id="blue-value"></output>
|
||||
</div>
|
||||
</body>
|
||||
<script>
|
||||
|
||||
const inputToValue = input => parseFloat(input) / 100
|
||||
const redInput = document.querySelector("#red-input");
|
||||
const redValue = document.querySelector("#red-value");
|
||||
redValue.textContent = redInput.value;
|
||||
|
||||
const greenInput = document.querySelector("#green-input");
|
||||
const greenValue = document.querySelector("#green-value");
|
||||
greenValue.textContent = greenInput.value;
|
||||
|
||||
const blueInput = document.querySelector("#blue-input");
|
||||
const blueValue = document.querySelector("#blue-value");
|
||||
blueValue.textContent = blueInput.value;
|
||||
|
||||
redInput.addEventListener("input", (event) => {
|
||||
redValue.textContent = event.target.value;
|
||||
});
|
||||
greenInput.addEventListener("input", (event) => {
|
||||
greenValue.textContent = event.target.value;
|
||||
});
|
||||
blueInput.addEventListener("input", (event) => {
|
||||
blueValue.textContent = event.target.value;
|
||||
});
|
||||
|
||||
redInput.addEventListener("input", (event) => {
|
||||
redValue.textContent = event.target.value;
|
||||
});
|
||||
greenInput.addEventListener("input", (event) => {
|
||||
greenValue.textContent = event.target.value;
|
||||
});
|
||||
blueInput.addEventListener("input", (event) => {
|
||||
blueValue.textContent = event.target.value;
|
||||
});
|
||||
|
||||
let vertexShader = `
|
||||
attribute vec4 ${spine.Shader.POSITION};
|
||||
attribute vec4 ${spine.Shader.COLOR};
|
||||
attribute vec4 ${spine.Shader.COLOR2};
|
||||
attribute vec2 ${spine.Shader.TEXCOORDS};
|
||||
uniform mat4 ${spine.Shader.MVP_MATRIX};
|
||||
varying vec4 v_light;
|
||||
varying vec4 v_dark;
|
||||
varying vec2 v_texCoords;
|
||||
|
||||
void main () {
|
||||
v_light = ${spine.Shader.COLOR};
|
||||
v_dark = ${spine.Shader.COLOR2};
|
||||
v_texCoords = ${spine.Shader.TEXCOORDS};
|
||||
gl_Position = ${spine.Shader.MVP_MATRIX} * ${spine.Shader.POSITION};
|
||||
}
|
||||
`;
|
||||
|
||||
let fragmentShader = `
|
||||
#ifdef GL_ES
|
||||
#define LOWP lowp
|
||||
precision mediump float;
|
||||
#else
|
||||
#define LOWP
|
||||
#endif
|
||||
varying LOWP vec4 v_light;
|
||||
varying LOWP vec4 v_dark;
|
||||
varying vec2 v_texCoords;
|
||||
uniform sampler2D u_texture;
|
||||
uniform float red_multiplier;
|
||||
uniform float green_multiplier;
|
||||
uniform float blue_multiplier;
|
||||
|
||||
void main () {
|
||||
vec4 texColor = texture2D(u_texture, v_texCoords);
|
||||
gl_FragColor.a = texColor.a * v_light.a;
|
||||
vec3 multipliers = vec3(clamp(red_multiplier, 0.0, 1.0), clamp(green_multiplier, 0.0, 1.0), clamp(blue_multiplier, 0.0, 1.0));
|
||||
gl_FragColor.rgb = ((texColor.a - 1.0) * v_dark.a + 1.0 - texColor.rgb) * v_dark.rgb + texColor.rgb * v_light.rgb * multipliers;
|
||||
}
|
||||
`;
|
||||
|
||||
// Creates a new spine player with a transparent background,
|
||||
// so content from the website shines through. Hides the controls.
|
||||
// Instead, the user can control the animation via buttons.
|
||||
var jsControlledPlayer = new spine.SpinePlayer("container-raptor", {
|
||||
skeleton: "assets/raptor-pro.json",
|
||||
atlas: "assets/raptor-pma.atlas",
|
||||
animation: "walk",
|
||||
showControls: false,
|
||||
premultipliedAlpha: true,
|
||||
backgroundColor: "#00000000",
|
||||
alpha: true,
|
||||
defaultMix: 1,
|
||||
controlBones: ["root"],
|
||||
success: (player) => {
|
||||
const inputToValue = input => parseFloat(input) / 100;
|
||||
player.sceneRenderer.setCustomBatcherShader(new spine.CustomShader(
|
||||
player.sceneRenderer.context,
|
||||
vertexShader,
|
||||
fragmentShader,
|
||||
(shader) => {
|
||||
shader.setUniformf("red_multiplier", inputToValue(redInput.value));
|
||||
shader.setUniformf("green_multiplier", inputToValue(greenInput.value));
|
||||
shader.setUniformf("blue_multiplier", inputToValue(blueInput.value));
|
||||
},
|
||||
));
|
||||
|
||||
const head = player.skeleton.findSlot("head");
|
||||
console.log(head.color)
|
||||
|
||||
setTimeout(() => {
|
||||
head.color.set(1, 0, 0, 1)
|
||||
player.skeleton.setToSetupPose()
|
||||
})
|
||||
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
186
spine-ts/spine-webgl/example/custom-shader.html
Normal file
186
spine-ts/spine-webgl/example/custom-shader.html
Normal file
@ -0,0 +1,186 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
</head>
|
||||
<script src="../dist/iife/spine-webgl.js"></script>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<body>
|
||||
<canvas id="canvas" style="position: absolute; width: 100%; height: 100%;"></canvas>
|
||||
<div style="position: absolute; color: white;">
|
||||
Red: <input id="red-input" type="range" min="0" max="100" value="100" /> <output id="red-value"></output>
|
||||
<br>
|
||||
Green: <input id="green-input" type="range" min="0" max="100" value="100" /> <output id="green-value"></output>
|
||||
<br>
|
||||
Blue: <input id="blue-input" type="range" min="0" max="100" value="100" /> <output id="blue-value"></output>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
class App {
|
||||
constructor() {
|
||||
this.skeleton = null;
|
||||
this.animationState = null;
|
||||
this.shaderValues = { r: 0, g: 0, b: 0, growing: true, color: "r" };
|
||||
}
|
||||
|
||||
loadAssets(canvas) {
|
||||
// Load the skeleton file.
|
||||
canvas.assetManager.loadBinary("assets/raptor-pro.skel");
|
||||
// Load the atlas and its pages.
|
||||
canvas.assetManager.loadTextureAtlas("assets/raptor-pma.atlas");
|
||||
}
|
||||
|
||||
initialize(canvas) {
|
||||
let assetManager = canvas.assetManager;
|
||||
|
||||
// Create the texture atlas.
|
||||
var atlas = assetManager.require("assets/raptor-pma.atlas");
|
||||
|
||||
// Create a AtlasAttachmentLoader that resolves region, mesh, boundingbox and path attachments
|
||||
var atlasLoader = new spine.AtlasAttachmentLoader(atlas);
|
||||
|
||||
// Create a SkeletonBinary instance for parsing the .skel file.
|
||||
var skeletonBinary = new spine.SkeletonBinary(atlasLoader);
|
||||
|
||||
// Set the scale to apply during parsing, parse the file, and create a new skeleton.
|
||||
skeletonBinary.scale = 0.6;
|
||||
var skeletonData = skeletonBinary.readSkeletonData(assetManager.require("assets/raptor-pro.skel"));
|
||||
this.skeleton = new spine.Skeleton(skeletonData);
|
||||
|
||||
// Create an AnimationState, and set the "cape-follow-example" animation in looping mode.
|
||||
var animationStateData = new spine.AnimationStateData(skeletonData);
|
||||
this.animationState = new spine.AnimationState(animationStateData);
|
||||
this.animationState.setAnimation(0, "walk", true);
|
||||
|
||||
// Center the camera on the skeleton
|
||||
const offset = new spine.Vector2();
|
||||
const size = new spine.Vector2();
|
||||
this.skeleton.setToSetupPose();
|
||||
this.skeleton.update(0);
|
||||
this.skeleton.updateWorldTransform(spine.Physics.update);
|
||||
this.skeleton.getBounds(offset, size);
|
||||
canvas.renderer.camera.position.x = offset.x + size.x / 2;
|
||||
canvas.renderer.camera.position.y = offset.y + size.y / 2;
|
||||
}
|
||||
|
||||
update(canvas, delta) {
|
||||
// Update the animation state using the delta time.
|
||||
this.animationState.update(delta);
|
||||
// Apply the animation state to the skeleton.
|
||||
this.animationState.apply(this.skeleton);
|
||||
// Let the skeleton update the transforms of its bones and apply physics
|
||||
this.skeleton.update(delta);
|
||||
this.skeleton.updateWorldTransform(spine.Physics.update);
|
||||
}
|
||||
|
||||
render(canvas) {
|
||||
let renderer = canvas.renderer;
|
||||
// Resize the viewport to the full canvas.
|
||||
renderer.resize(spine.ResizeMode.Expand);
|
||||
|
||||
// Clear the canvas with a light gray color.
|
||||
canvas.clear(0.2, 0.2, 0.2, 1);
|
||||
|
||||
// Begin rendering.
|
||||
renderer.begin();
|
||||
|
||||
// updateShader(canvas.shader, this.shaderValues);
|
||||
|
||||
// Draw the skeleton
|
||||
renderer.drawSkeleton(this.skeleton, true);
|
||||
// Complete rendering.
|
||||
renderer.end();
|
||||
}
|
||||
}
|
||||
|
||||
// Vertex and Fragment shader has been copied from src/Shader.ts#newTwoColoredTextured
|
||||
// Fragment shader has been slightly changed to add uniform red_multiplier, green_multiplier, blue_multiplier uniforms
|
||||
// that are used to multiply the rgb colors
|
||||
let vertexShader = `
|
||||
attribute vec4 ${spine.Shader.POSITION};
|
||||
attribute vec4 ${spine.Shader.COLOR};
|
||||
attribute vec4 ${spine.Shader.COLOR2};
|
||||
attribute vec2 ${spine.Shader.TEXCOORDS};
|
||||
uniform mat4 ${spine.Shader.MVP_MATRIX};
|
||||
varying vec4 v_light;
|
||||
varying vec4 v_dark;
|
||||
varying vec2 v_texCoords;
|
||||
|
||||
void main () {
|
||||
v_light = ${spine.Shader.COLOR};
|
||||
v_dark = ${spine.Shader.COLOR2};
|
||||
v_texCoords = ${spine.Shader.TEXCOORDS};
|
||||
gl_Position = ${spine.Shader.MVP_MATRIX} * ${spine.Shader.POSITION};
|
||||
}
|
||||
`;
|
||||
|
||||
let fragmentShader = `
|
||||
#ifdef GL_ES
|
||||
#define LOWP lowp
|
||||
precision mediump float;
|
||||
#else
|
||||
#define LOWP
|
||||
#endif
|
||||
varying LOWP vec4 v_light;
|
||||
varying LOWP vec4 v_dark;
|
||||
varying vec2 v_texCoords;
|
||||
uniform sampler2D u_texture;
|
||||
uniform float red_multiplier;
|
||||
uniform float green_multiplier;
|
||||
uniform float blue_multiplier;
|
||||
|
||||
void main () {
|
||||
vec4 texColor = texture2D(u_texture, v_texCoords);
|
||||
gl_FragColor.a = texColor.a * v_light.a;
|
||||
vec3 multipliers = vec3(clamp(red_multiplier, 0.0, 1.0), clamp(green_multiplier, 0.0, 1.0), clamp(blue_multiplier, 0.0, 1.0));
|
||||
gl_FragColor.rgb = ((texColor.a - 1.0) * v_dark.a + 1.0 - texColor.rgb) * v_dark.rgb + texColor.rgb * v_light.rgb * multipliers;
|
||||
}
|
||||
`;
|
||||
|
||||
const inputToValue = input => parseFloat(input) / 100;
|
||||
const redInput = document.querySelector("#red-input");
|
||||
const redValue = document.querySelector("#red-value");
|
||||
redValue.textContent = redInput.value;
|
||||
|
||||
const greenInput = document.querySelector("#green-input");
|
||||
const greenValue = document.querySelector("#green-value");
|
||||
greenValue.textContent = greenInput.value;
|
||||
|
||||
const blueInput = document.querySelector("#blue-input");
|
||||
const blueValue = document.querySelector("#blue-value");
|
||||
blueValue.textContent = blueInput.value;
|
||||
|
||||
redInput.addEventListener("input", (event) => {
|
||||
redValue.textContent = event.target.value;
|
||||
});
|
||||
greenInput.addEventListener("input", (event) => {
|
||||
greenValue.textContent = event.target.value;
|
||||
});
|
||||
blueInput.addEventListener("input", (event) => {
|
||||
blueValue.textContent = event.target.value;
|
||||
});
|
||||
|
||||
const shader = {
|
||||
vertexShader,
|
||||
fragmentShader,
|
||||
setUniformCallback: (shader) => {
|
||||
shader.setUniformf("red_multiplier", inputToValue(redInput.value));
|
||||
shader.setUniformf("green_multiplier", inputToValue(greenInput.value));
|
||||
shader.setUniformf("blue_multiplier", inputToValue(blueInput.value));
|
||||
}
|
||||
}
|
||||
const app = new spine.SpineCanvas(document.getElementById("canvas"), {
|
||||
app: new App(),
|
||||
shader,
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@ -31,7 +31,7 @@ import { Color, Disposable, Skeleton, MathUtils, TextureAtlasRegion } from "@eso
|
||||
import { OrthoCamera } from "./Camera.js";
|
||||
import { GLTexture } from "./GLTexture.js";
|
||||
import { PolygonBatcher } from "./PolygonBatcher.js";
|
||||
import { Shader } from "./Shader.js";
|
||||
import { CustomShader, Shader } from "./Shader.js";
|
||||
import { ShapeRenderer } from "./ShapeRenderer.js";
|
||||
import { SkeletonDebugRenderer } from "./SkeletonDebugRenderer.js";
|
||||
import { SkeletonRenderer, VertexTransformer } from "./SkeletonRenderer.js";
|
||||
@ -60,12 +60,15 @@ export class SceneRenderer implements Disposable {
|
||||
skeletonRenderer: SkeletonRenderer;
|
||||
skeletonDebugRenderer: SkeletonDebugRenderer;
|
||||
|
||||
constructor (canvas: HTMLCanvasElement, context: ManagedWebGLRenderingContext | WebGLRenderingContext, twoColorTint: boolean = true) {
|
||||
constructor (canvas: HTMLCanvasElement, context: ManagedWebGLRenderingContext | WebGLRenderingContext, twoColorTint: boolean = true, customShader?: Shader) {
|
||||
this.canvas = canvas;
|
||||
this.context = context instanceof ManagedWebGLRenderingContext ? context : new ManagedWebGLRenderingContext(context);
|
||||
this.twoColorTint = twoColorTint;
|
||||
this.camera = new OrthoCamera(canvas.width, canvas.height);
|
||||
this.batcherShader = twoColorTint ? Shader.newTwoColoredTextured(this.context) : Shader.newColoredTextured(this.context);
|
||||
if (customShader)
|
||||
this.batcherShader = customShader;
|
||||
else
|
||||
this.batcherShader = twoColorTint ? Shader.newTwoColoredTextured(this.context) : Shader.newColoredTextured(this.context);
|
||||
this.batcher = new PolygonBatcher(this.context, twoColorTint);
|
||||
this.shapesShader = Shader.newColored(this.context);
|
||||
this.shapes = new ShapeRenderer(this.context);
|
||||
@ -489,6 +492,10 @@ export class SceneRenderer implements Disposable {
|
||||
this.camera.update();
|
||||
}
|
||||
|
||||
setCustomBatcherShader(shader: CustomShader) {
|
||||
this.batcherShader = shader;
|
||||
}
|
||||
|
||||
private enableRenderer (renderer: PolygonBatcher | ShapeRenderer | SkeletonDebugRenderer) {
|
||||
if (this.activeRenderer === renderer) return;
|
||||
this.end();
|
||||
@ -496,6 +503,8 @@ export class SceneRenderer implements Disposable {
|
||||
this.batcherShader.bind();
|
||||
this.batcherShader.setUniform4x4f(Shader.MVP_MATRIX, this.camera.projectionView.values);
|
||||
this.batcherShader.setUniformi("u_texture", 0);
|
||||
if (this.batcherShader instanceof CustomShader && this.batcherShader.setUniformsCallback !== undefined)
|
||||
this.batcherShader.setUniformsCallback(this.batcherShader);
|
||||
this.batcher.begin(this.batcherShader);
|
||||
this.activeRenderer = this.batcher;
|
||||
} else if (renderer instanceof ShapeRenderer) {
|
||||
|
||||
@ -298,3 +298,14 @@ void main () {
|
||||
return new Shader(context, vs, fs);
|
||||
}
|
||||
}
|
||||
|
||||
export class CustomShader extends Shader {
|
||||
constructor (
|
||||
context: ManagedWebGLRenderingContext | WebGLRenderingContext,
|
||||
vertexShader: string,
|
||||
fragmentShader: string,
|
||||
public setUniformsCallback?: (shader: Shader) => void,
|
||||
) {
|
||||
super(context, vertexShader, fragmentShader);
|
||||
}
|
||||
}
|
||||
@ -27,7 +27,7 @@
|
||||
* SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*****************************************************************************/
|
||||
|
||||
import { TimeKeeper, AssetManager, ManagedWebGLRenderingContext, SceneRenderer, Input, StringMap } from "./index.js";
|
||||
import { TimeKeeper, AssetManager, ManagedWebGLRenderingContext, SceneRenderer, Input, StringMap, CustomShader, Shader } from "./index.js";
|
||||
|
||||
/** An app running inside a {@link SpineCanvas}. The app life-cycle
|
||||
* is as follows:
|
||||
@ -56,6 +56,12 @@ export interface SpineCanvasConfig {
|
||||
pathPrefix?: string;
|
||||
/* The WebGL context configuration */
|
||||
webglConfig?: any;
|
||||
/** Your custom Shader configuration. See {@link CustomShader} */
|
||||
shader?: {
|
||||
vertexShader: string,
|
||||
fragmentShader: string,
|
||||
setUniformCallback: (shader: Shader) => void,
|
||||
};
|
||||
}
|
||||
|
||||
/** Manages the life-cycle and WebGL context of a {@link SpineCanvasApp}. The app loads
|
||||
@ -75,6 +81,8 @@ export class SpineCanvas {
|
||||
readonly assetManager: AssetManager;
|
||||
/** The input processor used to listen to mouse, touch, and keyboard events. */
|
||||
readonly input: Input;
|
||||
/** The custom shader, if {@link SpineCanvasConfig.shader} config is passed. */
|
||||
readonly shader?: Shader;
|
||||
|
||||
private disposed = false;
|
||||
|
||||
@ -93,7 +101,8 @@ export class SpineCanvas {
|
||||
|
||||
this.htmlCanvas = canvas;
|
||||
this.context = new ManagedWebGLRenderingContext(canvas, config.webglConfig);
|
||||
this.renderer = new SceneRenderer(canvas, this.context);
|
||||
this.shader = config.shader ? new CustomShader(this.context, config.shader.vertexShader, config.shader.fragmentShader, config.shader.setUniformCallback) : undefined;
|
||||
this.renderer = new SceneRenderer(canvas, this.context, true, this.shader);
|
||||
this.gl = this.context.gl;
|
||||
this.assetManager = new AssetManager(this.context, config.pathPrefix);
|
||||
this.input = new Input(canvas);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user