Extrude Geometry with transparent texture and white background

Hello,
I draw an image on a html canvas object. The unpainted areas are transparent in the resulting image.
I managed to find a very simple method to find the pixels that will create the bounds of the painted surface even if not all areas are touching. I also wrote a small function to reduce the amount of vectors used to build this outline.
I also managed to build an extrude geometry and give it shape of the outline.
Next I mapped the texture on the geometry and organized the uv-mapping.
This really works just fine so far.
But on the geometry the transparent parts of the geometry appear black. I want those black parts to appear white.

If I enable transparency the black parts disappear but unfortunattaly the parts of the geometry that are not coverd by the image also disappear (turn transparent)

And if I just color this without attaching the texture I do get the outline geometry in any color I define (white in my develpment stage).

createFromImage(imageInfo) { //Ist fĂĽr Testzwecke, um zu sehen, wie die Konturen ĂĽberhaupt aussehen
        let dataUrl = imageInfo.base64Url; //base64;png data-url
        let wPixels = parseFloat(imageInfo.width); //Image Width in pixels
        let hPixels = parseFloat(imageInfo.height); //Image Height in pixels
        let imageData = imageInfo.imageData; // ctx = canvas.getContext('2d'); ctx.getImageData(0 , 0 , w , h); 
        let contourPoints = this.createContourFromImageData(imageData);
        const shape = this.contourToShape(contourPoints);
        const extrudeSettings = {
            steps: 1,
            depth: 1,
            bevelEnabled: true,
            bevelThickness: 1,
            bevelSize: 1,
            bevelOffset: 0,
            bevelSegments: 3,
            UVGenerator: {
                generateTopUV: function (geometry, vertices, indexA, indexB, indexC) {
                    const ax = vertices[indexA * 3];
                    const ay = vertices[indexA * 3 + 1];
                    const bx = vertices[indexB * 3];
                    const by = vertices[indexB * 3 + 1];
                    const cx = vertices[indexC * 3];
                    const cy = vertices[indexC * 3 + 1];
    
                    return [
                        new THREE.Vector2(ax / wPixels, 1 - ay / hPixels),
                        new THREE.Vector2(bx / wPixels, 1 - by / hPixels),
                        new THREE.Vector2(cx / wPixels, 1 - cy / hPixels)
                    ];
                },
                generateSideWallUV: function (geometry, vertices, indexA, indexB, indexC, indexD) {
                    // UV-Koordinaten für die Seitenwände definieren
                    const ax = vertices[indexA * 3];
                    const ay = vertices[indexA * 3 + 1];
                    const bx = vertices[indexB * 3];
                    const by = vertices[indexB * 3 + 1];
    
                    return [
                        new THREE.Vector2(ax / wPixels, ay / hPixels),
                        new THREE.Vector2(bx / wPixels, by / hPixels),
                        new THREE.Vector2(ax / wPixels, ay / hPixels),
                        new THREE.Vector2(bx / wPixels, by / hPixels)
                    ];
                }
            }
        };


        let widthMesh = 5.0;
        let heightMesh = widthMesh * (hPixels / wPixels);
        let texture = new THREE.TextureLoader().load(dataUrl);
        //texture.encoding = THREE.sRGBEncoding;
        //texture.needsUpdate = true;
        let geometry = new THREE.ExtrudeGeometry(shape , extrudeSettings);
        geometry.attributes.uv.array.forEach((_, i, uvArray) => {
            if (i % 2 === 1) {
                // Y-Koordinate (v-Wert) invertieren
                uvArray[i] = 1 - uvArray[i];
            }
        });
        let material = new THREE.MeshStandardMaterial({
            color: 0xffffff,         // WeiĂź als Grundfarbe fĂĽr den Hintergrund
            map: texture,            // PNG-Textur
            transparent: false,       // Aktiviere Transparenz
            opacity: 1,              // Setze die Gesamt-Deckkraft
            side: THREE.DoubleSide   // Beide Seiten sichtbar
        });
        let mesh = new THREE.Mesh(geometry , material);
        geometry.computeBoundingSphere();
        let radius = geometry.boundingSphere.radius;
        let scaleFactor = widthMesh / radius;
        mesh.scale.set(scaleFactor , scaleFactor, scaleFactor);
        mesh.position.set(0 , 4 , 0);
        this.m_stage.add(mesh);
    }

    contourToShape(contourPoints) {
        if(!contourPoints || contourPoints.length < 3) return null;
        const shape = new THREE.Shape();
        for(let i = 0 ; i < contourPoints.length ; i++) { //Kontur spiegeln
            let pi = contourPoints[i];
            pi.y *= -1;
        }
        let p0 = contourPoints[0];
        let minX = p0.x;
        let minY = p0.y;
        for(let i = 1 ; i < contourPoints.length ; i++) {
            let pi = contourPoints[i];
            if(pi.x < minX) minX = pi.x;
            if(pi.y < minY) minY = pi.y;
        }
        for(let i = 0 ; i < contourPoints.length ; i++) {
            let pi = contourPoints[i];
            pi.x = pi.x - minX;
            pi.y = pi.y - minY;
        }
        shape.moveTo(p0.x , p0.y);
        for(let i = 1 ; i < contourPoints.length ; i++) {
            let pi = contourPoints[i];
            shape.lineTo(pi.x , pi.y);
            
        }
        shape.lineTo(p0.x , p0.y);
        return shape;
    }

    createContourFromImageData(imageData) {
        let wPixels = imageData.width;
        let hPixels = imageData.height;
        const data = imageData.data;

        const pointsL = [];
        const pointsR = [];
        for(let y = 0 ; y < hPixels ; y++) {
            let left = null; 
            let right = null;
            for(let x = 0 ; x < wPixels ; x++) {
                const alpha = data[(y * wPixels + x) * 4 + 3];
                if(alpha > 0) {
                    if(left === null) left = x;
                    right = x;
                }
            }
            if(left !== null) {
                pointsL.push({x: left , y:y});
                pointsR.unshift({x:right , y:y});
            }
        }
        let points = pointsL.concat(pointsR); //Die Punkte bilden nun ein linksläufiges Polygon
        this.minimizeEdges(points);
        return points;
    }

    //Betrachtet wird der Vector P[i]->P[i+1] und verglichen mit P[i+1] -> P[i+2]
    //Haben beide Vektoren die gleiche Richtung kann der Punkt P[i+1] entfallen
    minimizeEdges(points) {
        let i = 0;
        while(i < points.length - 2) {
            let p0 = points[i];
            let p1 = points[i+1];
            let p2 = points[i+2];
            let p10 = {x: p1.x-p0.x , y: p1.y-p0.y};
            let p11 = {x: p2.x-p1.x , y: p2.y-p1.y};
            //Normalisierung hier nicht notwendig und sorgt fĂĽr Rundungsfehler.
            //Nicht notwendig, weil ein Vektor immer nur von einem Pix zu seinem Nachbarpixel geht.
            //Im Zweifelsfall bleiben ein paar Vectoren übrig, die man auch noch hätte vereinheitlichen können,
            //aber das ist erst interessant, wenn das System unbedingt optimiert werden soll.
            let crossProduct = (p10.x * p11.y) - (p10.y * p11.x);
            if(crossProduct == 0) {
                points.splice(i , 1);
            }else {
                i++;
            }
        }
    }

So this is the code I am using. I really hope you could use that code for your purposes and I also hope that you come up with any ideas to get the black parts white (or in fact card-box colored)

I was already asking ChatGPT but it wasn’t able to solve my problem. And now I am out of ideas except writing custom shaders, maybe, which I have never done before.

Just in case someone is wondering, I cropped the image and this is the function to create the “imageInfo” object that I take all the information from.

cropImageFromCanvas(ctx) {
        var canvas = ctx.canvas, 
          w = canvas.width, h = canvas.height,
          pix = {x:[], y:[]},
          imageData = ctx.getImageData(0,0,canvas.width,canvas.height),
          x, y, index;
        
        for (y = 0; y < h; y++) {
          for (x = 0; x < w; x++) {
            index = (y * w + x) * 4;
            if (imageData.data[index+3] > 0) {
              pix.x.push(x);
              pix.y.push(y);
            } 
          }
        }
        pix.x.sort(function(a,b){return a-b});
        pix.y.sort(function(a,b){return a-b});
        var n = pix.x.length-1;
        
        w = 1 + pix.x[n] - pix.x[0];
        h = 1 + pix.y[n] - pix.y[0];
        var cut = ctx.getImageData(pix.x[0], pix.y[0], w, h);
      
        canvas.width = w;
        canvas.height = h;
        ctx.putImageData(cut, 0, 0);
              
        var base64Url = canvas.toDataURL();
        let imageDataCropped = ctx.getImageData(0 , 0 , w , h);
        return {base64Url: base64Url , width: w , height: h , imageData: imageDataCropped};
    }

Thanks in advice :slight_smile:

It’s great to see the detailed work you’ve put into generating and mapping textures onto your HTML canvas geometry. The issue you’re facing with black appearing in the transparent parts can be tricky. Here are a few potential solutions to consider: NextCare com

  1. Adjust the Texture Background: Ensure the texture background is white instead of black. This can be done by modifying the image before loading it into the texture.
  2. Modify Material Settings: Instead of using a standard material, you might want to try using a MeshBasicMaterial with a white background and transparency enabled. For example:
    let material = new THREE.MeshBasicMaterial({
    color: 0xffffff,
    map: texture,
    transparent: true,
    opacity: 1,
    side: THREE.DoubleSide,
    alphaTest: 0.5 // Adjust this value as needed
    });
  3. Custom Shader: If you decide to delve into writing custom shaders, you could create a shader material that sets the alpha channel to 1 (fully opaque) for all non-transparent parts and to a specific color (white) for transparent parts.

For instance, a basic fragment shader could look something like this:
uniform sampler2D texture;
varying vec2 vUv;

void main() {
vec4 color = texture2D(texture, vUv);
if (color.a < 0.5) {
color = vec4(1.0, 1.0, 1.0, 1.0); // Set transparent parts to white
}
gl_FragColor = color;
}
These steps should help you eliminate the unwanted black areas, making them white or any desired color.

Best regards,
AliceR.

1 Like

Thing is, and I know this is a bitt off-topic but I am doing for a little that died from blood cancer a few weeks ago. She died in quarantine all alone, without her family. The nurse that took care of the girl told me about it, and asked for such a “game” if you might wanna call it like that. Friends / Family having a video chat and “play” painting funny things together. I hope to have this done by next easter, as there are quite a lot of kids in quarantine out there.
So I really appreachiate your ideas. I will give them a try :slight_smile:

So, I kinda solved it, not very elegant though, but at least it works.
the trick is, I take the countour, create a second canvas object and fill a polygon with that contour. Then I run over each pixel from the original image and copy the pixel data if the pixels alpha is not 0 (>0.5 to be precise).
That give me a texture where the parts that the underlying extrude geometry would show as black, will the show white, or whatever color I assign to it.


Therefore I moved the contour finding algorithm from my 3D-Scene class to my “Paint-Dialog” class.

The function now looks like (as said, not elegant but it does the trick)

cropImageFromCanvas(ctx) {
        var canvas = ctx.canvas, 
          w = canvas.width, h = canvas.height,
          pix = {x:[], y:[]},
          imageData = ctx.getImageData(0,0,canvas.width,canvas.height),
          x, y, index;
        
        for (y = 0; y < h; y++) {
          for (x = 0; x < w; x++) {
            index = (y * w + x) * 4;
            if (imageData.data[index+3] > 0) {
              pix.x.push(x);
              pix.y.push(y);
            } 
          }
        }
        pix.x.sort(function(a,b){return a-b});
        pix.y.sort(function(a,b){return a-b});
        var n = pix.x.length-1;
        
        w = 1 + pix.x[n] - pix.x[0];
        h = 1 + pix.y[n] - pix.y[0];
        var cut = ctx.getImageData(pix.x[0], pix.y[0], w, h);
      
        canvas.width = w;
        canvas.height = h;
        ctx.putImageData(cut, 0, 0);
              
        
        let imageDataCropped = ctx.getImageData(0 , 0 , w , h);
        let contour = this.createContourFromImageData(imageDataCropped);
        let cv2 = document.createElement("canvas");
        cv2.width = w;
        cv2.height = h;
        let ctx2 = cv2.getContext("2d");
        ctx2.fillStyle = '#fff';
        ctx2.beginPath();
        ctx2.moveTo(contour[0].x , contour[0].y);
        for(let i = 1 ; i < contour.length ; i++) {
            let pt = contour[i];
            ctx2.lineTo(pt.x , pt.y);
        }
        ctx2.closePath();
        ctx2.fill();
        for(let iRow = 0 ; iRow < h ; iRow++) {
            for(let iCol = 0 ; iCol < w ; iCol++) {
                let data = ctx.getImageData(iCol , iRow , 1 , 1);
                if(data.data[3] > 0.5) {
                    ctx2.putImageData(data , iCol , iRow);
                }
            }
        }
        imageDataCropped = ctx2.getImageData(0 , 0 , w , h);

        var base64Url = cv2.toDataURL();
        return {base64Url: base64Url , width: w , height: h , imageData: imageDataCropped, contourPoints: contour};
    }

The rest is just as described in the top of this post, just in case somebody could also make use of this code :slight_smile: