预览一致性修复
This commit is contained in:
1 parent
11b1c95d98
commit
f0c7e27552
7 files changed
+189
-36
No files matched your search
+18
-11
@@ -80,7 +80,7 @@ function unpackVertices(vertices, count) {
|
|||||||
return { positions, uvs };
|
return { positions, uvs };
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawInterleaved(base, image, vertices, triangles) {
|
function drawInterleaved(base, image, vertices, triangles, darkColor) {
|
||||||
for (let i = 0; i < triangles.length; i += 3) {
|
for (let i = 0; i < triangles.length; i += 3) {
|
||||||
const a = triangles[i] * 8;
|
const a = triangles[i] * 8;
|
||||||
const b = triangles[i + 1] * 8;
|
const b = triangles[i + 1] * 8;
|
||||||
@@ -88,7 +88,9 @@ function drawInterleaved(base, image, vertices, triangles) {
|
|||||||
base.drawTriangle(image,
|
base.drawTriangle(image,
|
||||||
vertices[a], vertices[a + 1], vertices[a + 6], vertices[a + 7],
|
vertices[a], vertices[a + 1], vertices[a + 6], vertices[a + 7],
|
||||||
vertices[b], vertices[b + 1], vertices[b + 6], vertices[b + 7],
|
vertices[b], vertices[b + 1], vertices[b + 6], vertices[b + 7],
|
||||||
vertices[c], vertices[c + 1], vertices[c + 6], vertices[c + 7]);
|
vertices[c], vertices[c + 1], vertices[c + 6], vertices[c + 7],
|
||||||
|
[...vertices.slice(a+2,a+6), ...vertices.slice(b+2,b+6), ...vertices.slice(c+2,c+6)],
|
||||||
|
darkColor ? Array.from({length:3}, () => [darkColor.r,darkColor.g,darkColor.b,0]).flat() : undefined);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,18 +126,19 @@ function createLegacyRenderer(spine, context) {
|
|||||||
let triangles;
|
let triangles;
|
||||||
let vertexCount;
|
let vertexCount;
|
||||||
if (attachment instanceof spine.RegionAttachment) {
|
if (attachment instanceof spine.RegionAttachment) {
|
||||||
vertices = base.computeRegionVertices(slot, attachment, pma);
|
vertices = base.computeRegionVertices(slot, attachment, false);
|
||||||
triangles = Renderer.QUAD_TRIANGLES || QUAD_TRIANGLES;
|
triangles = Renderer.QUAD_TRIANGLES || QUAD_TRIANGLES;
|
||||||
vertexCount = 4;
|
vertexCount = 4;
|
||||||
} else if (attachment instanceof spine.MeshAttachment) {
|
} else if (attachment instanceof spine.MeshAttachment) {
|
||||||
vertices = base.computeMeshVertices(slot, attachment, pma);
|
vertices = base.computeMeshVertices(slot, attachment, false);
|
||||||
triangles = attachment.triangles;
|
triangles = attachment.triangles;
|
||||||
vertexCount = attachment.worldVerticesLength >> 1;
|
vertexCount = attachment.worldVerticesLength >> 1;
|
||||||
} else {
|
} else {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const image = imageForCanvas(imageFromAttachment(attachment), context, pma);
|
const sourceImage = imageFromAttachment(attachment);
|
||||||
|
const image = base.gpuDrawing ? sourceImage : imageForCanvas(sourceImage, context, pma);
|
||||||
if (!image) continue;
|
if (!image) continue;
|
||||||
context.globalAlpha = base.tempColor.a;
|
context.globalAlpha = base.tempColor.a;
|
||||||
context.globalCompositeOperation = BLEND_MODES[slot.data.blendMode] || 'source-over';
|
context.globalCompositeOperation = BLEND_MODES[slot.data.blendMode] || 'source-over';
|
||||||
@@ -143,9 +146,9 @@ function createLegacyRenderer(spine, context) {
|
|||||||
const unpacked = unpackVertices(vertices, vertexCount);
|
const unpacked = unpackVertices(vertices, vertexCount);
|
||||||
clipper.clipTriangles(unpacked.positions, unpacked.positions.length,
|
clipper.clipTriangles(unpacked.positions, unpacked.positions.length,
|
||||||
triangles, triangles.length, unpacked.uvs, base.tempColor, dark, false);
|
triangles, triangles.length, unpacked.uvs, base.tempColor, dark, false);
|
||||||
drawInterleaved(base, image, clipper.clippedVertices, clipper.clippedTriangles);
|
drawInterleaved(base, image, clipper.clippedVertices, clipper.clippedTriangles, slot.darkColor);
|
||||||
} else {
|
} else {
|
||||||
drawInterleaved(base, image, vertices, triangles);
|
drawInterleaved(base, image, vertices, triangles, slot.darkColor);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
clipper.clipEndWithSlot(slot);
|
clipper.clipEndWithSlot(slot);
|
||||||
@@ -173,7 +176,8 @@ function create34Renderer(spine, context) {
|
|||||||
const attachment = slot.getAttachment();
|
const attachment = slot.getAttachment();
|
||||||
if (!(attachment instanceof spine.RegionAttachment) && !(attachment instanceof spine.MeshAttachment)) continue;
|
if (!(attachment instanceof spine.RegionAttachment) && !(attachment instanceof spine.MeshAttachment)) continue;
|
||||||
const vertices = attachment.updateWorldVertices(slot, false);
|
const vertices = attachment.updateWorldVertices(slot, false);
|
||||||
const image = imageForCanvas(imageFromAttachment(attachment), context, base.premultipliedAlpha);
|
const sourceImage = imageFromAttachment(attachment);
|
||||||
|
const image = base.gpuDrawing ? sourceImage : imageForCanvas(sourceImage, context, base.premultipliedAlpha);
|
||||||
if (!image) continue;
|
if (!image) continue;
|
||||||
context.globalAlpha = vertices[5];
|
context.globalAlpha = vertices[5];
|
||||||
context.globalCompositeOperation = BLEND_MODES[slot.data.blendMode] || 'source-over';
|
context.globalCompositeOperation = BLEND_MODES[slot.data.blendMode] || 'source-over';
|
||||||
@@ -196,9 +200,9 @@ function create43Renderer(spine, context) {
|
|||||||
const composite = context.globalCompositeOperation;
|
const composite = context.globalCompositeOperation;
|
||||||
try {
|
try {
|
||||||
const pma = !!base.premultipliedAlpha;
|
const pma = !!base.premultipliedAlpha;
|
||||||
for (let command = core.render(skeleton, pma); command; command = command.next) {
|
for (let command = core.render(skeleton, false); command; command = command.next) {
|
||||||
const sourceImage = command.texture && command.texture.getImage && command.texture.getImage();
|
const sourceImage = command.texture && command.texture.getImage && command.texture.getImage();
|
||||||
const image = imageForCanvas(sourceImage, context, pma);
|
const image = base.gpuDrawing ? sourceImage : imageForCanvas(sourceImage, context, pma);
|
||||||
if (!image) continue;
|
if (!image) continue;
|
||||||
context.globalAlpha = ((command.colors[0] >>> 24) & 255) / 255;
|
context.globalAlpha = ((command.colors[0] >>> 24) & 255) / 255;
|
||||||
context.globalCompositeOperation = BLEND_MODES[command.blendMode] || 'source-over';
|
context.globalCompositeOperation = BLEND_MODES[command.blendMode] || 'source-over';
|
||||||
@@ -209,10 +213,13 @@ function create43Renderer(spine, context) {
|
|||||||
const a = indices[i] * 2;
|
const a = indices[i] * 2;
|
||||||
const b = indices[i + 1] * 2;
|
const b = indices[i + 1] * 2;
|
||||||
const c = indices[i + 2] * 2;
|
const c = indices[i + 2] * 2;
|
||||||
|
const unpack = packed => [((packed>>>16)&255)/255,((packed>>>8)&255)/255,(packed&255)/255,((packed>>>24)&255)/255];
|
||||||
|
const colors = [a,b,c].flatMap(index => unpack(command.colors[index/2]));
|
||||||
|
const darkColors = [a,b,c].flatMap(index => unpack(command.darkColors[index/2]));
|
||||||
base.drawTriangle(image,
|
base.drawTriangle(image,
|
||||||
positions[a], positions[a + 1], uvs[a], uvs[a + 1],
|
positions[a], positions[a + 1], uvs[a], uvs[a + 1],
|
||||||
positions[b], positions[b + 1], uvs[b], uvs[b + 1],
|
positions[b], positions[b + 1], uvs[b], uvs[b + 1],
|
||||||
positions[c], positions[c + 1], uvs[c], uvs[c + 1]);
|
positions[c], positions[c + 1], uvs[c], uvs[c + 1], colors, darkColors);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -1,5 +1,18 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
// Decode file bytes without the Canvas 2D premultiply/unpremultiply round trip.
|
||||||
|
// In low-alpha PMA pixels that round trip destroys RGB precision before blending.
|
||||||
|
const preparedTextures = new WeakMap();
|
||||||
|
async function prepareTexture(image, blob) {
|
||||||
|
if (typeof createImageBitmap !== 'function') return;
|
||||||
|
const options = { colorSpaceConversion: 'none' };
|
||||||
|
const [pma, straight] = await Promise.all([
|
||||||
|
createImageBitmap(blob, { ...options, premultiplyAlpha: 'none' }),
|
||||||
|
createImageBitmap(blob, { ...options, premultiplyAlpha: 'premultiply' })
|
||||||
|
]);
|
||||||
|
preparedTextures.set(image, { pma, straight });
|
||||||
|
}
|
||||||
|
|
||||||
// Rasterize adjacent mesh triangles together, with GPU edge ownership rules.
|
// Rasterize adjacent mesh triangles together, with GPU edge ownership rules.
|
||||||
// Canvas clip()+drawImage() anti-aliases each triangle separately and exposes
|
// Canvas clip()+drawImage() anti-aliases each triangle separately and exposes
|
||||||
// the background along shared edges, which looks like a wireframe overlay.
|
// the background along shared edges, which looks like a wireframe overlay.
|
||||||
@@ -21,50 +34,64 @@ function createRasterizer(context) {
|
|||||||
const vertex = shader(gl.VERTEX_SHADER, `
|
const vertex = shader(gl.VERTEX_SHADER, `
|
||||||
attribute vec2 position;
|
attribute vec2 position;
|
||||||
attribute vec2 uv;
|
attribute vec2 uv;
|
||||||
|
attribute vec4 light;
|
||||||
|
attribute vec4 dark;
|
||||||
uniform vec2 resolution;
|
uniform vec2 resolution;
|
||||||
varying vec2 textureUV;
|
varying vec2 textureUV;
|
||||||
|
varying vec4 lightColor;
|
||||||
|
varying vec4 darkColor;
|
||||||
void main() {
|
void main() {
|
||||||
vec2 p = position / resolution * 2.0 - 1.0;
|
vec2 p = position / resolution * 2.0 - 1.0;
|
||||||
gl_Position = vec4(p.x, -p.y, 0.0, 1.0);
|
gl_Position = vec4(p.x, -p.y, 0.0, 1.0);
|
||||||
textureUV = uv;
|
textureUV = uv;
|
||||||
|
lightColor = light; darkColor = dark;
|
||||||
}`);
|
}`);
|
||||||
const fragment = shader(gl.FRAGMENT_SHADER, `
|
const fragment = shader(gl.FRAGMENT_SHADER, `
|
||||||
precision mediump float;
|
precision mediump float;
|
||||||
uniform sampler2D image;
|
uniform sampler2D image;
|
||||||
uniform float opacity;
|
|
||||||
varying vec2 textureUV;
|
varying vec2 textureUV;
|
||||||
|
varying vec4 lightColor;
|
||||||
|
varying vec4 darkColor;
|
||||||
void main() {
|
void main() {
|
||||||
vec4 color = texture2D(image, textureUV);
|
vec4 color = texture2D(image, textureUV);
|
||||||
float alpha = color.a * opacity;
|
float alpha = color.a * lightColor.a;
|
||||||
gl_FragColor = vec4(color.rgb * opacity, alpha);
|
// Textures are PMA in GPU memory. Light/dark colors arrive straight.
|
||||||
|
vec3 rgb = (color.rgb * lightColor.rgb + (color.a - color.rgb) * darkColor.rgb) * lightColor.a;
|
||||||
|
gl_FragColor = vec4(rgb, alpha);
|
||||||
}`);
|
}`);
|
||||||
const program = gl.createProgram(); gl.attachShader(program, vertex); gl.attachShader(program, fragment); gl.linkProgram(program);
|
const program = gl.createProgram(); gl.attachShader(program, vertex); gl.attachShader(program, fragment); gl.linkProgram(program);
|
||||||
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(program));
|
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(program));
|
||||||
gl.deleteShader(vertex); gl.deleteShader(fragment);
|
gl.deleteShader(vertex); gl.deleteShader(fragment);
|
||||||
const buffer = gl.createBuffer();
|
const buffer = gl.createBuffer();
|
||||||
const position = gl.getAttribLocation(program, 'position'), uv = gl.getAttribLocation(program, 'uv');
|
const position = gl.getAttribLocation(program, 'position'), uv = gl.getAttribLocation(program, 'uv');
|
||||||
const resolution = gl.getUniformLocation(program, 'resolution'), opacity = gl.getUniformLocation(program, 'opacity');
|
const resolution = gl.getUniformLocation(program, 'resolution');
|
||||||
|
const light = gl.getAttribLocation(program, 'light'), dark = gl.getAttribLocation(program, 'dark');
|
||||||
const textures = new Map();
|
const textures = new Map();
|
||||||
let coordinates = [], currentImage = null, currentAlpha = 1, currentMode = 'source-over', transform;
|
let coordinates = [], currentImage = null, currentAlpha = 1, currentMode = 'source-over', transform;
|
||||||
let disposed = false, lost = false;
|
let disposed = false, lost = false;
|
||||||
|
let premultipliedAlpha = false;
|
||||||
canvas.addEventListener('webglcontextlost', event => { event.preventDefault(); lost = true; });
|
canvas.addEventListener('webglcontextlost', event => { event.preventDefault(); lost = true; });
|
||||||
function texture(image) {
|
function texture(image) {
|
||||||
if (textures.has(image) && image !== context.canvas) return textures.get(image);
|
const pma = image !== context.canvas && premultipliedAlpha;
|
||||||
const t = textures.get(image) || gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, t);
|
let pair = textures.get(image);
|
||||||
|
if (!pair) { pair = {}; textures.set(image, pair); }
|
||||||
|
const key = pma ? 'pma' : 'straight';
|
||||||
|
if (pair[key] && image !== context.canvas) return pair[key];
|
||||||
|
const t = pair[key] || gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, t);
|
||||||
// Premultiply before interpolation so translucent edges do not darken.
|
// Premultiply before interpolation so translucent edges do not darken.
|
||||||
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);
|
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, !pma);
|
||||||
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
|
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
|
||||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
||||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
||||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
||||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
||||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
|
const prepared = preparedTextures.get(image);
|
||||||
textures.set(image, t); return t;
|
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, prepared ? prepared[key] : image);
|
||||||
|
pair[key] = t; return t;
|
||||||
}
|
}
|
||||||
function flush() {
|
function flush() {
|
||||||
if (!coordinates.length) return;
|
if (!coordinates.length) return;
|
||||||
gl.bindTexture(gl.TEXTURE_2D, texture(currentImage));
|
gl.bindTexture(gl.TEXTURE_2D, texture(currentImage));
|
||||||
gl.uniform1f(opacity, currentAlpha);
|
|
||||||
// Premultiplied RGB factors; keep source-over alpha independently.
|
// Premultiplied RGB factors; keep source-over alpha independently.
|
||||||
const factors = currentMode === 'lighter' ? [gl.ONE, gl.ONE]
|
const factors = currentMode === 'lighter' ? [gl.ONE, gl.ONE]
|
||||||
: currentMode === 'multiply' ? [gl.DST_COLOR, gl.ONE_MINUS_SRC_ALPHA]
|
: currentMode === 'multiply' ? [gl.DST_COLOR, gl.ONE_MINUS_SRC_ALPHA]
|
||||||
@@ -72,12 +99,13 @@ function createRasterizer(context) {
|
|||||||
: [gl.ONE, gl.ONE_MINUS_SRC_ALPHA];
|
: [gl.ONE, gl.ONE_MINUS_SRC_ALPHA];
|
||||||
gl.blendFuncSeparate(factors[0], factors[1], gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
|
gl.blendFuncSeparate(factors[0], factors[1], gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
|
||||||
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(coordinates), gl.STREAM_DRAW);
|
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(coordinates), gl.STREAM_DRAW);
|
||||||
gl.drawArrays(gl.TRIANGLES, 0, coordinates.length / 4);
|
gl.drawArrays(gl.TRIANGLES, 0, coordinates.length / 12);
|
||||||
coordinates.length = 0;
|
coordinates.length = 0;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
begin() {
|
begin(pma = false) {
|
||||||
if (disposed || lost || gl.isContextLost()) return false;
|
if (disposed || lost || gl.isContextLost()) return false;
|
||||||
|
premultipliedAlpha = pma;
|
||||||
const target = context.canvas;
|
const target = context.canvas;
|
||||||
if (canvas.width !== target.width) canvas.width = target.width;
|
if (canvas.width !== target.width) canvas.width = target.width;
|
||||||
if (canvas.height !== target.height) canvas.height = target.height;
|
if (canvas.height !== target.height) canvas.height = target.height;
|
||||||
@@ -85,8 +113,10 @@ function createRasterizer(context) {
|
|||||||
gl.viewport(0, 0, canvas.width, canvas.height);
|
gl.viewport(0, 0, canvas.width, canvas.height);
|
||||||
gl.clearColor(0, 0, 0, 0); gl.clear(gl.COLOR_BUFFER_BIT);
|
gl.clearColor(0, 0, 0, 0); gl.clear(gl.COLOR_BUFFER_BIT);
|
||||||
gl.useProgram(program); gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
gl.useProgram(program); gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
||||||
gl.enableVertexAttribArray(position); gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 16, 0);
|
gl.enableVertexAttribArray(position); gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 48, 0);
|
||||||
gl.enableVertexAttribArray(uv); gl.vertexAttribPointer(uv, 2, gl.FLOAT, false, 16, 8);
|
gl.enableVertexAttribArray(uv); gl.vertexAttribPointer(uv, 2, gl.FLOAT, false, 48, 8);
|
||||||
|
gl.enableVertexAttribArray(light); gl.vertexAttribPointer(light, 4, gl.FLOAT, false, 48, 16);
|
||||||
|
gl.enableVertexAttribArray(dark); gl.vertexAttribPointer(dark, 4, gl.FLOAT, false, 48, 32);
|
||||||
gl.uniform2f(resolution, canvas.width, canvas.height);
|
gl.uniform2f(resolution, canvas.width, canvas.height);
|
||||||
gl.activeTexture(gl.TEXTURE0); gl.uniform1i(gl.getUniformLocation(program, 'image'), 0);
|
gl.activeTexture(gl.TEXTURE0); gl.uniform1i(gl.getUniformLocation(program, 'image'), 0);
|
||||||
gl.enable(gl.BLEND); gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
|
gl.enable(gl.BLEND); gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
|
||||||
@@ -94,19 +124,23 @@ function createRasterizer(context) {
|
|||||||
// checkerboard background). CSS backgrounds cannot participate in blending.
|
// checkerboard background). CSS backgrounds cannot participate in blending.
|
||||||
currentImage = target; currentAlpha = 1; currentMode = 'source-over';
|
currentImage = target; currentAlpha = 1; currentMode = 'source-over';
|
||||||
const w = canvas.width, h = canvas.height;
|
const w = canvas.width, h = canvas.height;
|
||||||
coordinates.push(0,0,0,0, w,0,1,0, w,h,1,1, 0,0,0,0, w,h,1,1, 0,h,0,1);
|
for (const vertex of [[0,0,0,0],[w,0,1,0],[w,h,1,1],[0,0,0,0],[w,h,1,1],[0,h,0,1]])
|
||||||
|
coordinates.push(...vertex, 1,1,1,1, 0,0,0,0);
|
||||||
flush(); currentImage = null;
|
flush(); currentImage = null;
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
triangle(image, x0, y0, u0, v0, x1, y1, u1, v1, x2, y2, u2, v2) {
|
triangle(image, x0, y0, u0, v0, x1, y1, u1, v1, x2, y2, u2, v2, colors, darkColors) {
|
||||||
const alpha = context.globalAlpha;
|
const alpha = context.globalAlpha;
|
||||||
const mode = context.globalCompositeOperation || 'source-over';
|
const mode = context.globalCompositeOperation || 'source-over';
|
||||||
if (image !== currentImage || alpha !== currentAlpha || mode !== currentMode) { flush(); currentImage = image; currentAlpha = alpha; currentMode = mode; }
|
if (image !== currentImage || alpha !== currentAlpha || mode !== currentMode) { flush(); currentImage = image; currentAlpha = alpha; currentMode = mode; }
|
||||||
const t = transform;
|
const t = transform;
|
||||||
coordinates.push(
|
const points = [[x0,y0,u0,v0],[x1,y1,u1,v1],[x2,y2,u2,v2]];
|
||||||
t.a * x0 + t.c * y0 + t.e, t.b * x0 + t.d * y0 + t.f, u0, v0,
|
for (let i=0; i<3; i++) {
|
||||||
t.a * x1 + t.c * y1 + t.e, t.b * x1 + t.d * y1 + t.f, u1, v1,
|
const [x,y,u,v] = points[i], j=i*4;
|
||||||
t.a * x2 + t.c * y2 + t.e, t.b * x2 + t.d * y2 + t.f, u2, v2);
|
coordinates.push(t.a*x+t.c*y+t.e, t.b*x+t.d*y+t.f, u,v,
|
||||||
|
...(colors ? colors.slice(j,j+4) : [1,1,1,alpha]),
|
||||||
|
...(darkColors ? darkColors.slice(j,j+4) : [0,0,0,0]));
|
||||||
|
}
|
||||||
},
|
},
|
||||||
end() {
|
end() {
|
||||||
flush();
|
flush();
|
||||||
@@ -116,7 +150,7 @@ function createRasterizer(context) {
|
|||||||
},
|
},
|
||||||
dispose() {
|
dispose() {
|
||||||
if (disposed) return; disposed = true;
|
if (disposed) return; disposed = true;
|
||||||
for (const value of textures.values()) gl.deleteTexture(value);
|
for (const pair of textures.values()) for (const value of Object.values(pair)) gl.deleteTexture(value);
|
||||||
textures.clear(); gl.deleteBuffer(buffer); gl.deleteProgram(program);
|
textures.clear(); gl.deleteBuffer(buffer); gl.deleteProgram(program);
|
||||||
const extension = gl.getExtension('WEBGL_lose_context'); if (extension) extension.loseContext();
|
const extension = gl.getExtension('WEBGL_lose_context'); if (extension) extension.loseContext();
|
||||||
}
|
}
|
||||||
@@ -129,14 +163,15 @@ function attach(renderer, context) {
|
|||||||
if (!rasterizer) return renderer;
|
if (!rasterizer) return renderer;
|
||||||
const draw = renderer.draw.bind(renderer), triangle = renderer.drawTriangle.bind(renderer);
|
const draw = renderer.draw.bind(renderer), triangle = renderer.drawTriangle.bind(renderer);
|
||||||
renderer.draw = skeleton => {
|
renderer.draw = skeleton => {
|
||||||
if (!rasterizer.begin()) return draw(skeleton);
|
if (!rasterizer.begin(!!renderer.premultipliedAlpha)) return draw(skeleton);
|
||||||
|
renderer.gpuDrawing = true;
|
||||||
renderer.drawTriangle = rasterizer.triangle;
|
renderer.drawTriangle = rasterizer.triangle;
|
||||||
try { draw(skeleton); rasterizer.end(); }
|
try { draw(skeleton); rasterizer.end(); }
|
||||||
finally { renderer.drawTriangle = triangle; }
|
finally { renderer.drawTriangle = triangle; renderer.gpuDrawing = false; }
|
||||||
};
|
};
|
||||||
renderer.dispose = rasterizer.dispose;
|
renderer.dispose = rasterizer.dispose;
|
||||||
renderer.rasterization = 'webgl';
|
renderer.rasterization = 'webgl';
|
||||||
return renderer;
|
return renderer;
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { createRasterizer, attach };
|
module.exports = { createRasterizer, attach, prepareTexture };
|
||||||
@@ -12,6 +12,7 @@ const frames=versions.map(version=>{
|
|||||||
html+=script(fs.readFileSync(path.join(root,'lib',version,'spine-canvas.js'),'utf8'));
|
html+=script(fs.readFileSync(path.join(root,'lib',version,'spine-canvas.js'),'utf8'));
|
||||||
html+=script(`(async()=>{try{
|
html+=script(`(async()=>{try{
|
||||||
const image=new Image();image.src='data:image/png;base64,${fs.readFileSync(path.join(dir,'spineboy.png')).toString('base64')}';await image.decode();
|
const image=new Image();image.src='data:image/png;base64,${fs.readFileSync(path.join(dir,'spineboy.png')).toString('base64')}';await image.decode();
|
||||||
|
await modules['./spine-webgl-rasterizer.js'].prepareTexture(image,await (await fetch(image.src)).blob());
|
||||||
const text=${JSON.stringify(fs.readFileSync(path.join(dir,'spineboy.atlas'),'utf8'))};
|
const text=${JSON.stringify(fs.readFileSync(path.join(dir,'spineboy.atlas'),'utf8'))};
|
||||||
const texture=new (spine.CanvasTexture||spine.canvas.CanvasTexture)(image);let atlas;
|
const texture=new (spine.CanvasTexture||spine.canvas.CanvasTexture)(image);let atlas;
|
||||||
if(spine.TextureAtlas.length<=1){atlas=new spine.TextureAtlas(text);for(const page of atlas.pages){if(page.setTexture)page.setTexture(texture);else page.texture=texture}}
|
if(spine.TextureAtlas.length<=1){atlas=new spine.TextureAtlas(text);for(const page of atlas.pages){if(page.setTexture)page.setTexture(texture);else page.texture=texture}}
|
||||||
@@ -33,6 +34,18 @@ const frames=versions.map(version=>{
|
|||||||
renderer.draw(skeleton);const pixels=ctx.getImageData(0,0,400,400).data;
|
renderer.draw(skeleton);const pixels=ctx.getImageData(0,0,400,400).data;
|
||||||
if(mode===0)normal=pixels;else{let changed=0;for(let i=0;i<pixels.length;i+=4)if(Math.abs(pixels[i]-normal[i])>3)changed++;if(changed<100)throw Error('Slot multiply did not change image');}
|
if(mode===0)normal=pixels;else{let changed=0;for(let i=0;i<pixels.length;i+=4)if(Math.abs(pixels[i]-normal[i])>3)changed++;if(changed<100)throw Error('Slot multiply did not change image');}
|
||||||
}
|
}
|
||||||
|
let black;
|
||||||
|
for(const tint of [[0,0,0],[0.25,0.5,0.75]]){
|
||||||
|
ctx.setTransform(1,0,0,1,0,0);ctx.fillStyle='#1e1e1e';ctx.fillRect(0,0,400,400);
|
||||||
|
const scale=320/Math.max(size.x,size.y);ctx.translate(200,200);ctx.scale(scale,-scale);ctx.translate(-offset.x-size.x/2,-offset.y-size.y/2);
|
||||||
|
modules['./spine-background.js'].drawCheckerboard(ctx,400,400);
|
||||||
|
for(const slot of skeleton.slots)slot.data.blendMode=0;
|
||||||
|
[skeleton.color.r,skeleton.color.g,skeleton.color.b]=tint;
|
||||||
|
renderer.draw(skeleton);const pixels=ctx.getImageData(0,0,400,400).data;
|
||||||
|
if(tint[0]===0)black=pixels;
|
||||||
|
else{let checked=0,wrong=0;for(let i=0;i<pixels.length;i+=4)for(let c=0;c<3;c++)if(normal[i+c]-black[i+c]>30){checked++;const expected=black[i+c]+(normal[i+c]-black[i+c])*tint[c];if(Math.abs(pixels[i+c]-expected)>5)wrong++;}
|
||||||
|
if(checked<100||wrong>checked*0.01)throw Error('Vertex tint mismatch: '+wrong+'/'+checked);}
|
||||||
|
}
|
||||||
renderer.premultipliedAlpha=true;renderer.draw(skeleton);renderer.dispose();
|
renderer.premultipliedAlpha=true;renderer.draw(skeleton);renderer.dispose();
|
||||||
parent.postMessage({version,result:'PASS'},'*');
|
parent.postMessage({version,result:'PASS'},'*');
|
||||||
}catch(error){parent.postMessage({version,error:error.stack},'*')}})();`);
|
}catch(error){parent.postMessage({version,error:error.stack},'*')}})();`);
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
'use strict';
|
||||||
|
// Local visual diagnostic. The private project/export remain outside version control.
|
||||||
|
const fs = require('fs'), path = require('path');
|
||||||
|
const root = path.resolve(__dirname, '..');
|
||||||
|
const library = '/Users/tianmokeji/Desktop/特效/特效.library/images';
|
||||||
|
const source = process.argv[2] || path.join(library, 'MTV7M6B96MAP6.info/x10.skel');
|
||||||
|
const atlasPath = process.argv[3] || path.join(library, 'MTV7M6B96RPMA.info/x10.atlas');
|
||||||
|
const texturePath = process.argv[4] || path.join(library, 'MTV7M6B9BJ9MS.info/x10.png');
|
||||||
|
const output = process.argv[5] || '/tmp/spine-x10-comparison.html';
|
||||||
|
const baseline = process.env.SPINE_COMPARE_BASELINE === '1';
|
||||||
|
const script = s => '<script>' + s.replace(/<\/script/gi, '<\\/script') + '</script>';
|
||||||
|
let html = '<!doctype html><meta charset="utf-8"><style>body{margin:0;background:#555;color:white;font:16px sans-serif}canvas{width:486.5px;height:278.5px}section{display:inline-block}pre{white-space:pre-wrap}</style><pre id="result">RUNNING</pre>';
|
||||||
|
html += script('var module={exports:{}};const modules={};function require(n){return modules[n]}');
|
||||||
|
for (const name of ['spine-webgl-rasterizer', 'spine-canvas-clipping'])
|
||||||
|
html += script('module={exports:{}};\n' + (baseline ? require('child_process').execFileSync('git',['show','HEAD:js/'+name+'.js'],{cwd:root,encoding:'utf8'}) : fs.readFileSync(path.join(root, 'js', name+'.js'), 'utf8')) + `\nmodules['./${name}.js']=module.exports;`);
|
||||||
|
html += script(fs.readFileSync(path.join(root, 'lib/4.2/spine-canvas.js'), 'utf8'));
|
||||||
|
html += script(`(async()=>{try{
|
||||||
|
const raw=Uint8Array.from(atob('${fs.readFileSync(texturePath).toString('base64')}'),c=>c.charCodeAt(0));
|
||||||
|
const image=new Image();image.src='data:image/png;base64,${fs.readFileSync(texturePath).toString('base64')}';await image.decode();
|
||||||
|
if(modules['./spine-webgl-rasterizer.js'].prepareTexture) await modules['./spine-webgl-rasterizer.js'].prepareTexture(image,new Blob([raw],{type:'image/png'}));
|
||||||
|
const atlas=new spine.TextureAtlas(${JSON.stringify(fs.readFileSync(atlasPath,'utf8'))});for(const page of atlas.pages)page.setTexture(new spine.CanvasTexture(image));
|
||||||
|
const data=new spine.SkeletonBinary(new spine.AtlasAttachmentLoader(atlas)).readSkeletonData(Uint8Array.from(atob('${fs.readFileSync(source).toString('base64')}'),c=>c.charCodeAt(0)));
|
||||||
|
const reference=new Image();reference.src='data:image/png;base64,${fs.readFileSync(path.join(__dirname,'对比测试.png')).toString('base64')}';await reference.decode();
|
||||||
|
const reports=[];
|
||||||
|
function panel(name){const e=document.createElement('section');e.innerHTML='<div>'+name+'</div>';const c=document.createElement('canvas');c.width=973;c.height=557;e.appendChild(c);document.body.appendChild(e);return c;}
|
||||||
|
const rc=panel('Spine editor export'),rx=rc.getContext('2d');rx.fillStyle='#555';rx.fillRect(0,0,973,557);rx.drawImage(reference,0,0);const expected=rx.getImageData(0,0,973,557).data;
|
||||||
|
function redBounds(pixels){let x0=973,y0=557,x1=0,y1=0;for(let y=0;y<557;y++){const xs=[];for(let x=0;x<973;x++){const i=(y*973+x)*4;if(pixels[i]>100&&pixels[i]>pixels[i+1]*2&&pixels[i]>pixels[i+2]*2)xs.push(x)}if(xs.length>24){x0=Math.min(x0,...xs);y0=Math.min(y0,y);x1=Math.max(x1,...xs);y1=Math.max(y1,y)}}return [x0,y0,x1,y1];}
|
||||||
|
const rb=redBounds(expected);
|
||||||
|
for(const frame of [7,8]){
|
||||||
|
const sk=new spine.Skeleton(data),state=new spine.AnimationState(new spine.AnimationStateData(data));state.setAnimation(0,'word_fly',false);state.update(frame/30);state.apply(sk);sk.updateWorldTransform(spine.Physics.update);
|
||||||
|
const off=new spine.Vector2(),size=new spine.Vector2();sk.getBounds(off,size,[]);
|
||||||
|
const c=panel('Plugin frame '+frame),ctx=c.getContext('2d');ctx.fillStyle='#555';ctx.fillRect(0,0,973,557);
|
||||||
|
// Editor exports tight bounds plus its export padding. Record bounds for registration.
|
||||||
|
const scale=Math.min(973/size.x,557/size.y);ctx.translate(973/2,557/2);ctx.scale(scale,-scale);ctx.translate(-off.x-size.x/2,-off.y-size.y/2);
|
||||||
|
const renderer=modules['./spine-canvas-clipping.js'].createRenderer(spine,ctx,'4.2');renderer.premultipliedAlpha=true;renderer.draw(sk);
|
||||||
|
const ab=redBounds(ctx.getImageData(0,0,973,557).data),ratio=(rb[2]-rb[0])/(ab[2]-ab[0]);
|
||||||
|
const transform=ctx.getTransform();ctx.setTransform(1,0,0,1,0,0);ctx.fillStyle='#555';ctx.fillRect(0,0,973,557);
|
||||||
|
ctx.translate((rb[0]+rb[2])/2-ratio*(ab[0]+ab[2])/2,(rb[1]+rb[3])/2-ratio*(ab[1]+ab[3])/2);ctx.scale(ratio,ratio);ctx.transform(transform.a,transform.b,transform.c,transform.d,transform.e,transform.f);renderer.draw(sk);renderer.dispose();
|
||||||
|
const actual=ctx.getImageData(0,0,973,557).data;let sum=0;for(let i=0;i<actual.length;i++)if(i%4!==3)sum+=Math.abs(actual[i]-expected[i]);
|
||||||
|
reports.push({frame,scale:scale*ratio,mae:sum/(973*557*3)});
|
||||||
|
}
|
||||||
|
document.getElementById('result').textContent=JSON.stringify(reports);
|
||||||
|
}catch(e){document.getElementById('result').textContent='FAIL '+e.stack}})();`);
|
||||||
|
fs.writeFileSync(output,html);console.log(output);
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
<!doctype html><meta charset="utf-8"><title>PMA and vertex color regression</title>
|
||||||
|
<pre id="result">RUNNING</pre>
|
||||||
|
<script>var module={exports:{}};</script>
|
||||||
|
<script src="../js/spine-webgl-rasterizer.js"></script>
|
||||||
|
<script>
|
||||||
|
(async()=>{try{
|
||||||
|
const api=module.exports;
|
||||||
|
function check(ok,message){if(!ok)throw Error(message)}
|
||||||
|
function canvas(){const c=document.createElement('canvas');c.width=c.height=32;return c.getContext('2d')}
|
||||||
|
// Produce a PNG whose low-alpha RGB values can be represented exactly by
|
||||||
|
// Canvas's 8-bit storage; PMA consumers must preserve those encoded values.
|
||||||
|
const encoded=canvas(),pixels=encoded.createImageData(32,32);
|
||||||
|
for(let i=0;i<pixels.data.length;i+=4)pixels.data.set([64,32,16,8],i);
|
||||||
|
encoded.putImageData(pixels,0,0);
|
||||||
|
const blob=await new Promise(resolve=>encoded.canvas.toBlob(resolve));
|
||||||
|
const image=new Image();image.src=URL.createObjectURL(blob);await image.decode();
|
||||||
|
await api.prepareTexture(image,blob);
|
||||||
|
// Read the encoded PNG samples via a non-premultiplied ImageBitmap in WebGL.
|
||||||
|
const rawCanvas=document.createElement('canvas'),gl=rawCanvas.getContext('webgl');
|
||||||
|
const t=gl.createTexture();gl.bindTexture(gl.TEXTURE_2D,t);
|
||||||
|
gl.texImage2D(gl.TEXTURE_2D,0,gl.RGBA,gl.RGBA,gl.UNSIGNED_BYTE,await createImageBitmap(blob,{premultiplyAlpha:'none'}));
|
||||||
|
const fb=gl.createFramebuffer();gl.bindFramebuffer(gl.FRAMEBUFFER,fb);gl.framebufferTexture2D(gl.FRAMEBUFFER,gl.COLOR_ATTACHMENT0,gl.TEXTURE_2D,t,0);
|
||||||
|
const samples=new Uint8Array(4);gl.readPixels(0,0,1,1,gl.RGBA,gl.UNSIGNED_BYTE,samples);
|
||||||
|
gl.deleteFramebuffer(fb);gl.deleteTexture(t);
|
||||||
|
const target=canvas(),raster=api.createRasterizer(target);check(raster,'WebGL unavailable');
|
||||||
|
const modes=['source-over','lighter','multiply','screen'];
|
||||||
|
const light=[0.8,0.5,0.25,0.6],dark=[0.1,0.2,0.3,0];
|
||||||
|
for(const pma of [true,false,true])for(const mode of modes){
|
||||||
|
target.setTransform(1,0,0,1,0,0);target.globalCompositeOperation='source-over';target.fillStyle='rgb(50,60,70)';target.fillRect(0,0,32,32);
|
||||||
|
check(raster.begin(pma),'begin');target.globalCompositeOperation=mode;
|
||||||
|
const colors=[...light,...light,...light],darks=[...dark,...dark,...dark];
|
||||||
|
raster.triangle(image,0,0,0,0,32,0,1,0,32,32,1,1,colors,darks);
|
||||||
|
raster.triangle(image,0,0,0,0,32,32,1,1,0,32,0,1,colors,darks);raster.end();
|
||||||
|
const actual=target.getImageData(16,16,1,1).data,alpha=samples[3]/255*light[3];
|
||||||
|
for(let c=0;c<3;c++){
|
||||||
|
const tex=samples[c]*(pma?1:samples[3]/255),s=(tex*light[c]+(samples[3]-tex)*dark[c])*light[3],d=[50,60,70][c];
|
||||||
|
const expected=mode==='lighter'?s+d:mode==='multiply'?s*d/255+d*(1-alpha):mode==='screen'?s+d*(1-s/255):s+d*(1-alpha);
|
||||||
|
check(Math.abs(actual[c]-expected)<2.5,JSON.stringify({pma,mode,c,actual:actual[c],expected}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
raster.dispose();URL.revokeObjectURL(image.src);
|
||||||
|
// Different per-vertex colors in the same batch must interpolate separately.
|
||||||
|
const white=canvas();white.fillStyle='white';white.fillRect(0,0,32,32);
|
||||||
|
const gradient=canvas(),g=api.createRasterizer(gradient);g.begin();
|
||||||
|
g.triangle(white.canvas,0,0,0,0,32,0,1,0,0,32,0,1,[1,0,0,1,0,1,0,1,0,0,1,1]);g.end();
|
||||||
|
const pixel=gradient.getImageData(8,8,1,1).data;
|
||||||
|
check(pixel[0]>100&&pixel[1]>50&&pixel[2]>50,'per-vertex color interpolation');g.dispose();
|
||||||
|
document.getElementById('result').textContent='PASS: direct PMA samples, straight/PMA toggles, light/dark tint, four blend modes and per-vertex colors';
|
||||||
|
}catch(e){document.getElementById('result').textContent='FAIL '+e.stack}})();
|
||||||
|
</script>
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 511 KiB |
+5
-1
@@ -307,7 +307,11 @@
|
|||||||
try {
|
try {
|
||||||
const data = fs.readFileSync(p);
|
const data = fs.readFileSync(p);
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
img.onload = () => { imgMap[name] = img; res(); };
|
img.onload = async () => {
|
||||||
|
try { await require('../js/spine-webgl-rasterizer.js').prepareTexture(img, new Blob([data], { type: mime })); }
|
||||||
|
catch (e) { log('无损贴图解码不可用,使用浏览器贴图: ' + e.message); }
|
||||||
|
imgMap[name] = img; res();
|
||||||
|
};
|
||||||
img.onerror = () => res();
|
img.onerror = () => res();
|
||||||
img.src = 'data:' + mime + ';base64,' + data.toString('base64');
|
img.src = 'data:' + mime + ';base64,' + data.toString('base64');
|
||||||
} catch (e) { res(); }
|
} catch (e) { res(); }
|
||||||
|
|||||||
Reference in new issue
Block a user