简体   繁体   中英

Transparent drawing on canvas-sourced texture in THREE.js

I'm creating 2d surfaces in my THREE.js app by creating PlaneGeometry / BasicMaterial meshes and backing their texture with a canvas:

this.canvas = makeCanvas(canvasWidth, canvasHeight);
this.ctx = this.canvas.getContext('2d');

this.texture = new THREE.Texture(this.canvas);
this.texture.minFilter = THREE.LinearFilter;
this.texture.magFilter = THREE.LinearFilter;
this.texture.anisotropy = 16;

this.mesh = new THREE.Mesh(new THREE.PlaneGeometry(width, height), new THREE.MeshBasicMaterial({
    map: this.texture,
    overdraw: true,
    side: THREE.DoubleSide,
    transparent: true
}));

This works fine - unless I want to draw transparently. Then, I need to create another texture to bind as alphaMap , and duplicate all my drawing operations between the two canvas contexts. The performance is fine, but the code looks absolutely horrendous:

var circleAlpha = 'rgb(100, 100, 100);',
    segmentAlpha = 'rgb(200, 200, 200);';
ctx.fillStyle = 'rgb(0, 255, 255);';
if (this.segment === -1) {
    circleAlpha = segmentAlpha;
}
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.alphaCtx.clearRect(0, 0, this.canvas.width, this.canvas.height);
var drawArc = function (c, w, h, piece) {
    var angw = 2 * Math.PI / 6;
    c.beginPath();
    c.arc(w / 2, h / 2, 512, angw * piece, angw * (piece + 1), false);
    c.arc(w / 2, h / 2, 300, angw * (piece + 1), angw * piece, true);
    c.closePath();
    c.fill();
};
for (var i = 0; i < 6; i++) {
    this.alphaCtx.fillStyle = i == this.segment ? segmentAlpha : circleAlpha;
    drawArc(ctx, this.canvas.width, this.canvas.height, i);
    drawArc(this.alphaCtx, this.canvas.width, this.canvas.height, i);
}
this.updateTexture();
this.alphaTexture.needsUpdate = true;

I've been planning on writing a small utility library to handle this automatically, but before I did I was wondering if maybe I'm just being silly and there's an easier way to do this.

You could write a split function instead of rendering to two canvases. This approach will allow you to issue multiple draw operations to one canvas using alpha channel as intended.

When done, just run it through the splitter which returns two canvases, one for color and one gray-scale that can be used for alpha channel.

(You could of course reuse the main for color, just note the alpha channel would be gone).

Live Splitter Example:

 var ctx = document.querySelector("canvas").getContext("2d"), gr = ctx.createLinearGradient(0, 0, 300, 0); // draw something with alpha channel to main canvas gr.addColorStop(0, "rgba(255,140,0,1)"); gr.addColorStop(1, "rgba(255,0,0,0)"); ctx.fillStyle = gr; ctx.fillRect(0, 0, 300, 150); // split the canvas to two new canvas, one for color, one for alpha var maps = splitTexture(ctx); document.body.appendChild(maps.color); // show in DOM for demo document.body.appendChild(maps.alpha); // Split texture: function splitTexture(ctx) { var w = ctx.canvas.width, h = ctx.canvas.height, canvasColor = document.createElement("canvas"), canvasAlpha = document.createElement("canvas"), ctxc = canvasColor.getContext("2d"), ctxa = canvasAlpha.getContext("2d"), idata = ctx.getImageData(0, 0, w, h), data32 = new Uint32Array(idata.data.buffer), // use uint-32 buffer (faster!) len = data32.length, i = 0, p, a, g, adata, adata32, cdata, cdata32; // destinations canvasColor.width = canvasAlpha.width = w; // set same size as source canvasColor.height = canvasAlpha.height = h; cdata = ctxc.createImageData(w, h); // create buffers and uint32 views cdata32 = new Uint32Array(cdata.data.buffer); adata = ctxa.createImageData(w, h); adata32 = new Uint32Array(adata.data.buffer); // splitter loop while(i < len) { p = data32[i]; // source pixel as 32-bit ABGR a = p & 0xff000000; // mask out alpha g = 0xff000000 | (a >>> 8) | (a >>> 16) | (a >>> 24); // grey value adata32[i] = g; // set gray value cdata32[i++] = 0xff000000 | (p & 0xffffff); // set color value } ctxc.putImageData(cdata, 0, 0); // update destinations ctxa.putImageData(adata, 0, 0); return { color: canvasColor, alpha: canvasAlpha } } 
 body {background:#79c} 
 <canvas></canvas> 

I ended up writing a proxy context that splits the drawing operations between the two canvases. Needs the csscolorparser` node module (it's on NPM).

var FakeCtx = function (canvasA, canvasB) {
    this.ctxA = canvasA.getContext('2d');
    this.ctxB = canvasB.getContext('2d');

    this.colorStore = {};
};

var assignContextProperty = function (property, propertyType) {
    if (propertyType === 'function') {
        FakeCtx.prototype[property] = function () {
            var argArray = Array.prototype.slice.call(arguments),
                ra = this.ctxA[property].apply(this.ctxA, argArray),
                rb = this.ctxB[property].apply(this.ctxB, argArray);
            if (ra !== rb) {
                var argString = argArray.join(', ');
                debug.warn('Console proxy got two different results for calling ' + property + ':');
                debug.warn('    Canvas A: ' + property + '(' + argString + ') = ' + ra);
                debug.warn('    Canvas B: ' + property + '(' + argString + ') = ' + rb);
            }
            return ra;
        };
    } else {
        if (property != 'fillStyle' && property != 'strokeStyle') {
            FakeCtx.prototype.__defineGetter__(property, function () {
                return this.ctxA[property];
            });
            FakeCtx.prototype.__defineSetter__(property, function (value) {
                this.ctxA[property] = this.ctxB[property] = value;
            });
        } else {
            FakeCtx.prototype.__defineGetter__(property, function () {
                return this.colorStore[property] || this.ctxA[property];
            });
            FakeCtx.prototype.__defineSetter__(property, function (value) {
                var color = csscolor.parseCSSColor(value);
                if (color === null || color.length < 3) {
                    throw new Error('Invalid color ' + value + ': ' + color);
                } else {
                    this.colorStore[property] = value;
                    if (color.length === 3 || color[3] === 1) { // no alpha
                        this.ctxA[property] = value;
                        this.ctxB[property] = 'rgb(255, 255, 255)'; // white = full alpha
                    } else {
                        this.ctxA[property] = 'rgb(' + color.slice(0, 3).join(', ') + ')';
                        var alphaValue = Math.round(255 * color[3]);
                        this.ctxB[property] = 'rgb(' + [ alphaValue, alphaValue, alphaValue ].join(', ') + ')';
                        // console.log('got color with alpha ' + value + ', splitting to ' + 'rgb(' + color.slice(0, 3).join(', ') + ');' + ' and ' + 'rgb(' + [ alphaValue, alphaValue, alphaValue ].join(', ') + ');');
                    }
                }
            });
        }
    }
}

var _ctx = makeCanvas(0, 0).getContext('2d');
for (var property in _ctx) {
    assignContextProperty(property, typeof _ctx[property]);
}

The code probably has a couple bugs, I haven't run it through any rigorous tests yet, but it worked for the simple bits.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM