317 lines
11 KiB
Vue
317 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";
|
||
import {RenderArgument} from "../entities/RenderArgument.ts";
|
||
|
||
type Props = {
|
||
atlas: AtlasRenderArgument[],
|
||
premultipliedAlpha: boolean,
|
||
debug: boolean
|
||
}
|
||
|
||
|
||
const props = defineProps<Props>();
|
||
const emit = defineEmits(['edit']);
|
||
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, originalRender: argument};
|
||
})
|
||
})
|
||
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: {skel: Skeleton, obj: RenderArgument} | 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 = {skel: renderObj.skel, obj: renderObj};
|
||
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.skel.x += mX * 2;
|
||
selectSkel.skel.y -= mY * 2;
|
||
emit("edit", selectSkel);
|
||
}
|
||
}
|
||
}
|
||
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.skel.scaleX += deltaScale;
|
||
if (selectSkel.skel.scaleX < 0.1){
|
||
selectSkel.skel.scaleX = 0.1;
|
||
}
|
||
if (selectSkel.skel.scaleX > 10){
|
||
selectSkel.skel.scaleX = 10;
|
||
}
|
||
selectSkel.skel.scaleY += deltaScale;
|
||
if (selectSkel.skel.scaleY < 0.1){
|
||
selectSkel.skel.scaleY = 0.1;
|
||
}
|
||
if (selectSkel.skel.scaleY > 10){
|
||
selectSkel.skel.scaleY = 10;
|
||
}
|
||
emit("edit", selectSkel);
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<canvas id="mainCanvas" ref="mainCanvas" v-once></canvas>
|
||
</template>
|
||
|
||
<style scoped>
|
||
</style> |