323 lines
11 KiB
Vue
323 lines
11 KiB
Vue
|
<script setup lang="ts">
|
|||
|
import AtlasRenderArgument from "../entities/AtlasRenderArgument.ts";
|
|||
|
import {computed, onMounted, onUnmounted, ref} from "vue";
|
|||
|
import {
|
|||
|
AnimationState, AnimationStateData,
|
|||
|
AssetManager, AtlasAttachmentLoader,
|
|||
|
ManagedWebGLRenderingContext, Matrix4, Physics,
|
|||
|
PolygonBatcher,
|
|||
|
Shader, ShapeRenderer, Skeleton, SkeletonDebugRenderer, SkeletonJson,
|
|||
|
SkeletonRenderer, Vector2
|
|||
|
} from "@esotericsoftware/spine-webgl";
|
|||
|
|
|||
|
type Props = {
|
|||
|
atlas: AtlasRenderArgument[],
|
|||
|
premultipliedAlpha: boolean,
|
|||
|
debug: boolean
|
|||
|
}
|
|||
|
type Bound = {
|
|||
|
width: number,
|
|||
|
height: number,
|
|||
|
x: number,
|
|||
|
y: number
|
|||
|
}
|
|||
|
type RenderArgument = {
|
|||
|
skel: Skeleton,
|
|||
|
state: AnimationState,
|
|||
|
setupBound: Bound,
|
|||
|
edit: boolean
|
|||
|
}
|
|||
|
const props = defineProps<Props>()
|
|||
|
const mainCanvas = ref<HTMLCanvasElement>();
|
|||
|
let mounted = false;
|
|||
|
|
|||
|
|
|||
|
const webGlCtx = computed<WebGLRenderingContext>(() => mainCanvas.value?.getContext("webgl") as WebGLRenderingContext);
|
|||
|
const managedCtx = computed(() => new ManagedWebGLRenderingContext(webGlCtx.value));
|
|||
|
const assetManager = computed<AssetManager>(() => new AssetManager(managedCtx.value));
|
|||
|
const shader = computed<Shader>(() => Shader.newTwoColoredTextured(managedCtx.value));
|
|||
|
const batcher = computed<PolygonBatcher>(() => new PolygonBatcher(managedCtx.value));
|
|||
|
const renderer = computed<SkeletonRenderer>(() => new SkeletonRenderer(managedCtx.value));
|
|||
|
const rendererArguments = computed<RenderArgument[]>(() => {
|
|||
|
return props.atlas.map((argument): RenderArgument => {
|
|||
|
const attachmentLoader = new AtlasAttachmentLoader(assetManager.value.require(argument.texture));
|
|||
|
const skeletonLoader = new SkeletonJson(attachmentLoader);
|
|||
|
skeletonLoader.scale = argument.scale;
|
|||
|
const skeletonData = skeletonLoader.readSkeletonData(assetManager.value.require(argument.skel));
|
|||
|
const skeleton = new Skeleton(skeletonData);
|
|||
|
skeleton.x += argument.x;
|
|||
|
skeleton.y += argument.y;
|
|||
|
skeleton.setToSetupPose();
|
|||
|
skeleton.updateWorldTransform(Physics.update);
|
|||
|
let offset = new Vector2();
|
|||
|
let size = new Vector2();
|
|||
|
skeleton.getBounds(offset, size, []);
|
|||
|
const setupBound: Bound = {width: size.x, height: size.y, x: offset.x, y: offset.y};
|
|||
|
const animationStateData = new AnimationStateData(skeletonData);
|
|||
|
const animationState = new AnimationState(animationStateData);
|
|||
|
animationState.setAnimation(0, argument.animation, true);
|
|||
|
return {skel: skeleton, setupBound, state: animationState, edit: !!argument.edit};
|
|||
|
})
|
|||
|
})
|
|||
|
let timer = Date.now() / 1000;
|
|||
|
let mvp = new Matrix4();
|
|||
|
|
|||
|
// debugger
|
|||
|
const debugRenderer = computed<SkeletonDebugRenderer>(() => {
|
|||
|
const result = new SkeletonDebugRenderer(managedCtx.value);
|
|||
|
result.drawRegionAttachments = true;
|
|||
|
result.drawBoundingBoxes = true;
|
|||
|
result.drawMeshHull = true;
|
|||
|
result.drawMeshTriangles = true;
|
|||
|
result.drawPaths = true;
|
|||
|
return result;
|
|||
|
});
|
|||
|
const debugShader = computed<Shader>(() => Shader.newColored(managedCtx.value));
|
|||
|
const shape = computed<ShapeRenderer>(() => new ShapeRenderer(managedCtx.value));
|
|||
|
|
|||
|
|
|||
|
const loadingResources = async () => {
|
|||
|
props.atlas.forEach((argument) => {
|
|||
|
for (let originalImageName in argument.textureImage) {
|
|||
|
const blobUrl = argument.textureImage[originalImageName];
|
|||
|
const filenameSplit = argument.texture.split("/");
|
|||
|
const filename = filenameSplit[filenameSplit.length - 1];
|
|||
|
const defaultUrl = argument.texture.replace(filename, originalImageName);
|
|||
|
console.log(defaultUrl, filename, originalImageName)
|
|||
|
assetManager.value.setRawDataURI(defaultUrl, blobUrl);
|
|||
|
}
|
|||
|
assetManager.value.loadJson(argument.skel);
|
|||
|
assetManager.value.loadTextureAtlas(argument.texture);
|
|||
|
});
|
|||
|
await assetManager.value.loadAll();
|
|||
|
};
|
|||
|
const initializationRender = () => {
|
|||
|
if (!mainCanvas.value){
|
|||
|
console.error("浏览器不支持canvas");
|
|||
|
return;
|
|||
|
}
|
|||
|
mainCanvas.value.getContext("webgl");
|
|||
|
}
|
|||
|
let viewport: {width: number, height: number} = {width: 0, height: 0};
|
|||
|
let selectSkel: Skeleton | null = null;
|
|||
|
let isDragging: boolean = false;
|
|||
|
const resize = () => {
|
|||
|
if (!mainCanvas.value){
|
|||
|
return;
|
|||
|
}
|
|||
|
const w = mainCanvas.value.clientWidth;
|
|||
|
const h = mainCanvas.value.clientHeight;
|
|||
|
if (mainCanvas.value.width != w || mainCanvas.value.height != h) {
|
|||
|
mainCanvas.value.width = w;
|
|||
|
mainCanvas.value.height = h;
|
|||
|
}
|
|||
|
const width = w * 2;
|
|||
|
const height = h * 2;
|
|||
|
|
|||
|
mvp.ortho2d(-(width / 2), -(height / 2), width, height);
|
|||
|
viewport.width = w;
|
|||
|
viewport.height = h;
|
|||
|
managedCtx.value.gl.viewport(0, 0, w, h);
|
|||
|
}
|
|||
|
const createShaderProgram = (r: number, g: number, b: number) => {
|
|||
|
const vsSource = `
|
|||
|
attribute vec2 a_position;
|
|||
|
void main() {
|
|||
|
gl_Position = vec4(a_position, 0.0, 1.0);
|
|||
|
}
|
|||
|
`;
|
|||
|
const fsSource = `
|
|||
|
void main() {
|
|||
|
gl_FragColor = vec4(${r}, ${g}, ${b}, 1.0);
|
|||
|
}
|
|||
|
`;
|
|||
|
const vertexShader = webGlCtx.value.createShader(webGlCtx.value.VERTEX_SHADER) as WebGLShader;
|
|||
|
webGlCtx.value.shaderSource(vertexShader, vsSource);
|
|||
|
webGlCtx.value.compileShader(vertexShader);
|
|||
|
|
|||
|
const fragmentShader = webGlCtx.value.createShader(webGlCtx.value.FRAGMENT_SHADER) as WebGLShader;
|
|||
|
webGlCtx.value.shaderSource(fragmentShader, fsSource);
|
|||
|
webGlCtx.value.compileShader(fragmentShader);
|
|||
|
|
|||
|
const shaderProgram = webGlCtx.value.createProgram() as WebGLProgram;
|
|||
|
webGlCtx.value.attachShader(shaderProgram, vertexShader);
|
|||
|
webGlCtx.value.attachShader(shaderProgram, fragmentShader);
|
|||
|
webGlCtx.value.linkProgram(shaderProgram);
|
|||
|
|
|||
|
if (!webGlCtx.value.getProgramParameter(shaderProgram, webGlCtx.value.LINK_STATUS)) {
|
|||
|
alert('Unable to initialize the shader program: ' + webGlCtx.value.getProgramInfoLog(shaderProgram));
|
|||
|
return null;
|
|||
|
}
|
|||
|
|
|||
|
webGlCtx.value.useProgram(shaderProgram);
|
|||
|
|
|||
|
return shaderProgram;
|
|||
|
};
|
|||
|
const initBuffers = (x: number, y: number, width: number, height: number, r: number, g: number, b: number) => {
|
|||
|
// 创建一个缓冲区对象
|
|||
|
const vertexBuffer = webGlCtx.value.createBuffer();
|
|||
|
// 绑定缓冲区
|
|||
|
webGlCtx.value.bindBuffer(webGlCtx.value.ARRAY_BUFFER, vertexBuffer);
|
|||
|
const relativeX = x / viewport.width;
|
|||
|
const relativeY = y / viewport.height;
|
|||
|
const leftX = relativeX;
|
|||
|
const topY = relativeY;
|
|||
|
const rightX = relativeX + width / viewport.width;
|
|||
|
const bottomY = relativeY + height / viewport.height;
|
|||
|
// 定义四个顶点的位置
|
|||
|
const vertices = new Float32Array([
|
|||
|
leftX, topY, // 第一个顶点(x, y)
|
|||
|
rightX, topY, // 第二个顶点
|
|||
|
rightX, bottomY, // 第三个顶点
|
|||
|
leftX, bottomY // 第四个顶点
|
|||
|
]);
|
|||
|
|
|||
|
// 将顶点数据传到缓冲区
|
|||
|
webGlCtx.value.bufferData(webGlCtx.value.ARRAY_BUFFER, vertices, webGlCtx.value.STATIC_DRAW);
|
|||
|
const shaderProgram = createShaderProgram(r, g, b) as WebGLProgram;
|
|||
|
// 获取顶点着色器的位置属性
|
|||
|
const positionAttribLocation = webGlCtx.value.getAttribLocation(shaderProgram, 'a_position');
|
|||
|
webGlCtx.value.enableVertexAttribArray(positionAttribLocation);
|
|||
|
webGlCtx.value.vertexAttribPointer(positionAttribLocation, 2, webGlCtx.value.FLOAT, false, 0, 0);
|
|||
|
|
|||
|
return vertexBuffer;
|
|||
|
};
|
|||
|
const render = () => {
|
|||
|
if (!mainCanvas.value){
|
|||
|
console.error("浏览器不支持canvas");
|
|||
|
return;
|
|||
|
}
|
|||
|
if (!mounted){
|
|||
|
return;
|
|||
|
}
|
|||
|
resize();
|
|||
|
let now = Date.now() / 1000;
|
|||
|
let delta = now - timer;
|
|||
|
timer = now;
|
|||
|
webGlCtx.value.clearColor(0, 0, 0, 0);
|
|||
|
webGlCtx.value.clear(webGlCtx.value.COLOR_BUFFER_BIT);
|
|||
|
|
|||
|
for (let renderObj of rendererArguments.value) {
|
|||
|
let skeleton = renderObj.skel;
|
|||
|
let state = renderObj.state;
|
|||
|
// let bounds = renderObj.setupBound;
|
|||
|
state.update(delta);
|
|||
|
state.apply(skeleton);
|
|||
|
skeleton.updateWorldTransform(Physics.update);
|
|||
|
if (renderObj.edit){
|
|||
|
let offset = new Vector2();
|
|||
|
let size = new Vector2();
|
|||
|
skeleton.getBounds(offset, size, []);
|
|||
|
renderObj.setupBound = {width: size.x, height: size.y, x: offset.x, y: offset.y};
|
|||
|
}
|
|||
|
|
|||
|
shader.value.bind();
|
|||
|
shader.value.setUniformi(Shader.SAMPLER, 0);
|
|||
|
shader.value.setUniform4x4f(Shader.MVP_MATRIX, mvp.values);
|
|||
|
batcher.value.begin(shader.value);
|
|||
|
renderer.value.premultipliedAlpha = props.premultipliedAlpha;
|
|||
|
renderer.value.draw(batcher.value, skeleton);
|
|||
|
batcher.value.end();
|
|||
|
shader.value.unbind();
|
|||
|
if (props.debug) {
|
|||
|
debugShader.value.bind();
|
|||
|
debugShader.value.setUniform4x4f(Shader.MVP_MATRIX, mvp.values);
|
|||
|
debugRenderer.value.premultipliedAlpha = props.premultipliedAlpha;
|
|||
|
shape.value.begin(debugShader.value);
|
|||
|
debugRenderer.value.draw(shape.value, skeleton);
|
|||
|
shape.value.end();
|
|||
|
debugShader.value.unbind();
|
|||
|
}
|
|||
|
}
|
|||
|
let hasTouched = false;
|
|||
|
selectSkel = null;
|
|||
|
for (let renderObj of rendererArguments.value) {
|
|||
|
if (!renderObj.edit){
|
|||
|
continue;
|
|||
|
}
|
|||
|
const xMin = Math.min(renderObj.setupBound.x + renderObj.setupBound.width, renderObj.setupBound.x);
|
|||
|
const yMin = Math.min(renderObj.setupBound.y + renderObj.setupBound.height, renderObj.setupBound.y);
|
|||
|
const xMax = Math.max(renderObj.setupBound.x + renderObj.setupBound.width, renderObj.setupBound.x);
|
|||
|
const yMax = Math.max(renderObj.setupBound.y + renderObj.setupBound.height, renderObj.setupBound.y);
|
|||
|
if (mousePosition.x > xMin && mousePosition.x < xMax && mousePosition.y > yMin && mousePosition.y < yMax) {
|
|||
|
if (!hasTouched){
|
|||
|
selectSkel = renderObj.skel;
|
|||
|
webGlCtx.value.bindBuffer(webGlCtx.value.ARRAY_BUFFER, initBuffers(renderObj.setupBound.x,renderObj.setupBound.y,renderObj.setupBound.width,renderObj.setupBound.height, 0, 0, 0));
|
|||
|
webGlCtx.value.drawArrays(webGlCtx.value.LINE_LOOP, 0, 4);
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
requestAnimationFrame(render);
|
|||
|
}
|
|||
|
onMounted(async () => {
|
|||
|
if (!mainCanvas.value){
|
|||
|
return;
|
|||
|
}
|
|||
|
mainCanvas.value.addEventListener("mousemove", mouseMove);
|
|||
|
mainCanvas.value.addEventListener("mousedown", mouseDown);
|
|||
|
mainCanvas.value.addEventListener("mouseup", mouseUp);
|
|||
|
window.addEventListener("wheel", wheel, {passive: false});
|
|||
|
initializationRender()
|
|||
|
await loadingResources();
|
|||
|
mounted = true;
|
|||
|
requestAnimationFrame(render);
|
|||
|
})
|
|||
|
onUnmounted(() => {
|
|||
|
mounted = false;
|
|||
|
});
|
|||
|
const mousePosition = new Vector2(0,0);
|
|||
|
const mouseMove = (evt: MouseEvent) => {
|
|||
|
if (viewport.height === 0 || viewport.width === 0){
|
|||
|
return;
|
|||
|
}
|
|||
|
if (!mainCanvas.value){
|
|||
|
return;
|
|||
|
}
|
|||
|
mousePosition.x = (evt.offsetX - mainCanvas.value.clientWidth / 2) * 2;
|
|||
|
mousePosition.y = (-(evt.offsetY - mainCanvas.value.clientHeight / 2)) * 2;
|
|||
|
if (isDragging){
|
|||
|
const mX = evt.movementX;
|
|||
|
const mY = evt.movementY;
|
|||
|
if (selectSkel){
|
|||
|
selectSkel.x += mX * 2;
|
|||
|
selectSkel.y -= mY * 2;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
const mouseDown = () => {
|
|||
|
isDragging = true;
|
|||
|
}
|
|||
|
const mouseUp = () => {
|
|||
|
isDragging = false;
|
|||
|
}
|
|||
|
const wheel = (evt: WheelEvent) => {
|
|||
|
if (evt.ctrlKey){
|
|||
|
evt.preventDefault();
|
|||
|
evt.stopPropagation();
|
|||
|
if (selectSkel){
|
|||
|
const deltaScale = -evt.deltaY / 1000;
|
|||
|
selectSkel.scaleX += deltaScale;
|
|||
|
if (selectSkel.scaleX < 0.1){
|
|||
|
selectSkel.scaleX = 0.1;
|
|||
|
}
|
|||
|
if (selectSkel.scaleX > 10){
|
|||
|
selectSkel.scaleX = 10;
|
|||
|
}
|
|||
|
selectSkel.scaleY += deltaScale;
|
|||
|
if (selectSkel.scaleY < 0.1){
|
|||
|
selectSkel.scaleY = 0.1;
|
|||
|
}
|
|||
|
if (selectSkel.scaleY > 10){
|
|||
|
selectSkel.scaleY = 10;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
</script>
|
|||
|
|
|||
|
<template>
|
|||
|
<canvas id="mainCanvas" ref="mainCanvas"></canvas>
|
|||
|
</template>
|
|||
|
|
|||
|
<style scoped>
|
|||
|
</style>
|