diff --git a/js/spine-canvas-clipping.js b/js/spine-canvas-clipping.js index ae70202..e055548 100644 --- a/js/spine-canvas-clipping.js +++ b/js/spine-canvas-clipping.js @@ -80,7 +80,7 @@ function unpackVertices(vertices, count) { 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) { const a = triangles[i] * 8; const b = triangles[i + 1] * 8; @@ -88,7 +88,9 @@ function drawInterleaved(base, image, vertices, triangles) { base.drawTriangle(image, vertices[a], vertices[a + 1], vertices[a + 6], vertices[a + 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 vertexCount; if (attachment instanceof spine.RegionAttachment) { - vertices = base.computeRegionVertices(slot, attachment, pma); + vertices = base.computeRegionVertices(slot, attachment, false); triangles = Renderer.QUAD_TRIANGLES || QUAD_TRIANGLES; vertexCount = 4; } else if (attachment instanceof spine.MeshAttachment) { - vertices = base.computeMeshVertices(slot, attachment, pma); + vertices = base.computeMeshVertices(slot, attachment, false); triangles = attachment.triangles; vertexCount = attachment.worldVerticesLength >> 1; } else { continue; } - const image = imageForCanvas(imageFromAttachment(attachment), context, pma); + const sourceImage = imageFromAttachment(attachment); + const image = base.gpuDrawing ? sourceImage : imageForCanvas(sourceImage, context, pma); if (!image) continue; context.globalAlpha = base.tempColor.a; context.globalCompositeOperation = BLEND_MODES[slot.data.blendMode] || 'source-over'; @@ -143,9 +146,9 @@ function createLegacyRenderer(spine, context) { const unpacked = unpackVertices(vertices, vertexCount); clipper.clipTriangles(unpacked.positions, unpacked.positions.length, 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 { - drawInterleaved(base, image, vertices, triangles); + drawInterleaved(base, image, vertices, triangles, slot.darkColor); } } finally { clipper.clipEndWithSlot(slot); @@ -173,7 +176,8 @@ function create34Renderer(spine, context) { const attachment = slot.getAttachment(); if (!(attachment instanceof spine.RegionAttachment) && !(attachment instanceof spine.MeshAttachment)) continue; 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; context.globalAlpha = vertices[5]; context.globalCompositeOperation = BLEND_MODES[slot.data.blendMode] || 'source-over'; @@ -196,9 +200,9 @@ function create43Renderer(spine, context) { const composite = context.globalCompositeOperation; try { 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 image = imageForCanvas(sourceImage, context, pma); + const image = base.gpuDrawing ? sourceImage : imageForCanvas(sourceImage, context, pma); if (!image) continue; context.globalAlpha = ((command.colors[0] >>> 24) & 255) / 255; context.globalCompositeOperation = BLEND_MODES[command.blendMode] || 'source-over'; @@ -209,10 +213,13 @@ function create43Renderer(spine, context) { const a = indices[i] * 2; const b = indices[i + 1] * 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, positions[a], positions[a + 1], uvs[a], uvs[a + 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 { diff --git a/js/spine-webgl-rasterizer.js b/js/spine-webgl-rasterizer.js index 7a11969..8aa147d 100644 --- a/js/spine-webgl-rasterizer.js +++ b/js/spine-webgl-rasterizer.js @@ -1,5 +1,18 @@ '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. // Canvas clip()+drawImage() anti-aliases each triangle separately and exposes // the background along shared edges, which looks like a wireframe overlay. @@ -21,50 +34,64 @@ function createRasterizer(context) { const vertex = shader(gl.VERTEX_SHADER, ` attribute vec2 position; attribute vec2 uv; + attribute vec4 light; + attribute vec4 dark; uniform vec2 resolution; varying vec2 textureUV; + varying vec4 lightColor; + varying vec4 darkColor; void main() { vec2 p = position / resolution * 2.0 - 1.0; gl_Position = vec4(p.x, -p.y, 0.0, 1.0); textureUV = uv; + lightColor = light; darkColor = dark; }`); const fragment = shader(gl.FRAGMENT_SHADER, ` precision mediump float; uniform sampler2D image; - uniform float opacity; varying vec2 textureUV; + varying vec4 lightColor; + varying vec4 darkColor; void main() { vec4 color = texture2D(image, textureUV); - float alpha = color.a * opacity; - gl_FragColor = vec4(color.rgb * opacity, alpha); + float alpha = color.a * lightColor.a; + // 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); if (!gl.getProgramParameter(program, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(program)); gl.deleteShader(vertex); gl.deleteShader(fragment); const buffer = gl.createBuffer(); 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(); let coordinates = [], currentImage = null, currentAlpha = 1, currentMode = 'source-over', transform; let disposed = false, lost = false; + let premultipliedAlpha = false; canvas.addEventListener('webglcontextlost', event => { event.preventDefault(); lost = true; }); function texture(image) { - if (textures.has(image) && image !== context.canvas) return textures.get(image); - const t = textures.get(image) || gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, t); + const pma = image !== context.canvas && premultipliedAlpha; + 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. - 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.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_WRAP_S, 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); - textures.set(image, t); return t; + const prepared = preparedTextures.get(image); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, prepared ? prepared[key] : image); + pair[key] = t; return t; } function flush() { if (!coordinates.length) return; gl.bindTexture(gl.TEXTURE_2D, texture(currentImage)); - gl.uniform1f(opacity, currentAlpha); // Premultiplied RGB factors; keep source-over alpha independently. const factors = currentMode === 'lighter' ? [gl.ONE, gl.ONE] : 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.blendFuncSeparate(factors[0], factors[1], gl.ONE, gl.ONE_MINUS_SRC_ALPHA); 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; } return { - begin() { + begin(pma = false) { if (disposed || lost || gl.isContextLost()) return false; + premultipliedAlpha = pma; const target = context.canvas; if (canvas.width !== target.width) canvas.width = target.width; 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.clearColor(0, 0, 0, 0); gl.clear(gl.COLOR_BUFFER_BIT); gl.useProgram(program); gl.bindBuffer(gl.ARRAY_BUFFER, buffer); - gl.enableVertexAttribArray(position); gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 16, 0); - gl.enableVertexAttribArray(uv); gl.vertexAttribPointer(uv, 2, gl.FLOAT, false, 16, 8); + gl.enableVertexAttribArray(position); gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 48, 0); + 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.activeTexture(gl.TEXTURE0); gl.uniform1i(gl.getUniformLocation(program, 'image'), 0); 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. currentImage = target; currentAlpha = 1; currentMode = 'source-over'; 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; 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 mode = context.globalCompositeOperation || 'source-over'; if (image !== currentImage || alpha !== currentAlpha || mode !== currentMode) { flush(); currentImage = image; currentAlpha = alpha; currentMode = mode; } const t = transform; - coordinates.push( - t.a * x0 + t.c * y0 + t.e, t.b * x0 + t.d * y0 + t.f, u0, v0, - t.a * x1 + t.c * y1 + t.e, t.b * x1 + t.d * y1 + t.f, u1, v1, - t.a * x2 + t.c * y2 + t.e, t.b * x2 + t.d * y2 + t.f, u2, v2); + const points = [[x0,y0,u0,v0],[x1,y1,u1,v1],[x2,y2,u2,v2]]; + for (let i=0; i<3; i++) { + const [x,y,u,v] = points[i], j=i*4; + 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() { flush(); @@ -116,7 +150,7 @@ function createRasterizer(context) { }, dispose() { 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); const extension = gl.getExtension('WEBGL_lose_context'); if (extension) extension.loseContext(); } @@ -129,14 +163,15 @@ function attach(renderer, context) { if (!rasterizer) return renderer; const draw = renderer.draw.bind(renderer), triangle = renderer.drawTriangle.bind(renderer); renderer.draw = skeleton => { - if (!rasterizer.begin()) return draw(skeleton); + if (!rasterizer.begin(!!renderer.premultipliedAlpha)) return draw(skeleton); + renderer.gpuDrawing = true; renderer.drawTriangle = rasterizer.triangle; try { draw(skeleton); rasterizer.end(); } - finally { renderer.drawTriangle = triangle; } + finally { renderer.drawTriangle = triangle; renderer.gpuDrawing = false; } }; renderer.dispose = rasterizer.dispose; renderer.rasterization = 'webgl'; return renderer; } -module.exports = { createRasterizer, attach }; +module.exports = { createRasterizer, attach, prepareTexture }; diff --git a/tests/build-browser-preview-test.js b/tests/build-browser-preview-test.js index 975ecb4..f8da2fa 100644 --- a/tests/build-browser-preview-test.js +++ b/tests/build-browser-preview-test.js @@ -12,6 +12,7 @@ const frames=versions.map(version=>{ html+=script(fs.readFileSync(path.join(root,'lib',version,'spine-canvas.js'),'utf8')); 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(); + 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 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}} @@ -33,6 +34,18 @@ const frames=versions.map(version=>{ 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;i3)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;i30){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(); parent.postMessage({version,result:'PASS'},'*'); }catch(error){parent.postMessage({version,error:error.stack},'*')}})();`); diff --git a/tests/build-x10-comparison.js b/tests/build-x10-comparison.js new file mode 100644 index 0000000..49b3c58 --- /dev/null +++ b/tests/build-x10-comparison.js @@ -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 => ''; +let html = '
RUNNING
'; +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='
'+name+'
';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;iPMA and vertex color regression +
RUNNING
+ + + diff --git a/tests/对比测试.png b/tests/对比测试.png new file mode 100644 index 0000000..b8e5124 Binary files /dev/null and b/tests/对比测试.png differ diff --git a/viewer/spine.html b/viewer/spine.html index 9097c4d..de3d4f5 100644 --- a/viewer/spine.html +++ b/viewer/spine.html @@ -307,7 +307,11 @@ try { const data = fs.readFileSync(p); 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.src = 'data:' + mime + ';base64,' + data.toString('base64'); } catch (e) { res(); }