Getting vertex colors from texture

I am continuing working in parametric surfaces visualization and would like to export surface to ZBrush. Althout this can be done via OBJ, I’d prefer FBX or GoZ formats, because they supports vertex colors. Now the surface is colored by texture and the goal is to create color attribute for BufferGeometry by getting texture color at vertex location.

Unfortunately I don’t see how to do this and searching the forum gives no result.
Could someone point me the right method ?

As far as I know there isn’t a single method that does it…
But you can do it manually… by reading the pixel data for the texture, and using the UVs to look up each pixel value, and generate the color attribute.

1 Like

Thanks, in general I understand the idea, but don’t know how to make it technically. Could you please provide a little bit more details or maybe a code sample that makes something similar as starting point ?

Yup.

Here is some untested code from chatGPT:

function applyVertexColorsFromTextures(scene) {
    let geometries = [];
    let textures = [];
    let textureCache = new Map(); // Cache for processed texture data

    // Collect meshes with textures
    scene.traverse(e => {
        if (e.isMesh && e.material.map) {
            geometries.push(e.geometry);
            textures.push(e.material.map);
        }
    });

    // Function to prepare texture cache
    function processTexture(texture) {
        if (!texture.image || textureCache.has(texture)) return textureCache.get(texture);

        let { width, height } = texture.image;
        let canvas = document.createElement("canvas");
        let ctx = canvas.getContext("2d");
        canvas.width = width;
        canvas.height = height;
        ctx.drawImage(texture.image, 0, 0);
        let imgData = ctx.getImageData(0, 0, width, height).data;

        // Store image data lookup function
        let getColor = (u, v) => {
            let x = Math.floor(u * width);
            let y = Math.floor((1 - v) * height); // Flip V coordinate
            let index = (y * width + x) * 4;
            return [
                imgData[index] / 255,
                imgData[index + 1] / 255,
                imgData[index + 2] / 255
            ]; // Normalized RGB
        };

        textureCache.set(texture, getColor);
        return getColor;
    }

    // Apply vertex colors to geometries
    geometries.forEach((geo, i) => {
        let uvArray = geo.attributes.uv.array;
        let colorArray = new Float32Array((uvArray.length / 2) * 3);
        let getColor = processTexture(textures[i]);

        if (!getColor) return;

        for (let j = 0; j < uvArray.length; j += 2) {
            let [r, g, b] = getColor(uvArray[j], uvArray[j + 1]);
            let idx = (j / 2) * 3;
            colorArray[idx] = r;
            colorArray[idx + 1] = g;
            colorArray[idx + 2] = b;
        }

        geo.setAttribute("color", new THREE.BufferAttribute(colorArray, 3));
        geo.attributes.color.needsUpdate = true;
    });

    console.log("Vertex colors applied from textures.");
}

Also, make sure that before you export or whatever you’re going to do with them, that you then remove the .map from the mesh.materials, and enable vertexColors:true on the materials as well.

1 Like

Thanks a lot, I will try it now.
Very intersting, never seen code from chatGPT before :).

lol bro… you better get on it.
It can crank out threejs code like crazy.

1 Like

Unbelievable, but it works !
The only issues are the lack of color on the vertical segment (loop index ?) and the changed colorspace, but I will fix them without problem. Thanks again, you opened my eyes on AI coding.

How does this request to chatGPT look ?

Well I started writing the code manually and realized it would take me less time to paste what I had into chatGPT:

So first I asked:

can you expand this into a function to create color attributes for the meshes, from the texture data on the scenes meshes? Here is my post where I started...

//Collect all the geometries/textures
let geometries;
let textures;
scene.traverse(e =>{
if(e.isMesh ){
geometries.push(e.geometry);
textures.push(e.material.texture);
}
}

textures.forEach((texture,i)=>{
// Here grab the texture.source.data to retrieve the texture image...
// Draw it to a canvas, and use canvas .getImgData()  to retrieve the pixels...
})

//Loop through the geometries...
geometries.forEach((geo,i)=>{
   let uvarray = geo.attributes.uv.array;
   for(let i=0;i<uvarray.length;i++){

   }
})

It then generated:

function applyVertexColorsFromTextures(scene) {
    let geometries = [];
    let textures = [];

    // Collect geometries and their associated textures
    scene.traverse(e => {
        if (e.isMesh && e.material.map) {
            geometries.push(e.geometry);
            textures.push(e.material.map);
        }
    });

    // Function to get pixel color from a texture
    function getPixelColor(texture) {
        let image = texture.image;
        if (!image) return null; // Ensure image is loaded

        let canvas = document.createElement("canvas");
        let ctx = canvas.getContext("2d");
        canvas.width = image.width;
        canvas.height = image.height;
        ctx.drawImage(image, 0, 0);
        let imgData = ctx.getImageData(0, 0, image.width, image.height).data;

        return (u, v) => {
            let x = Math.floor(u * image.width);
            let y = Math.floor((1 - v) * image.height); // Flip V coordinate
            let index = (y * image.width + x) * 4;
            return [
                imgData[index] / 255,
                imgData[index + 1] / 255,
                imgData[index + 2] / 255
            ]; // Return normalized RGB
        };
    }

    // Apply vertex colors to geometries
    geometries.forEach((geo, i) => {
        let uvArray = geo.attributes.uv.array;
        let colorArray = new Float32Array((uvArray.length / 2) * 3);
        let getColor = getPixelColor(textures[i]);

        if (!getColor) return;

        for (let j = 0; j < uvArray.length; j += 2) {
            let [r, g, b] = getColor(uvArray[j], uvArray[j + 1]);
            colorArray[(j / 2) * 3] = r;
            colorArray[(j / 2) * 3 + 1] = g;
            colorArray[(j / 2) * 3 + 2] = b;
        }

        geo.setAttribute("color", new THREE.BufferAttribute(colorArray, 3));
        geo.attributes.color.needsUpdate = true;
    });

    console.log("Vertex colors applied from textures.");
}

Which would have worked, but probably would have been incredibly slow since it was creating a canvas for every texel fetch…

So I replied:

can you refactor that to avoid creating the canvas and imgdata for every pixel, and cache them instead?

And it produced the output I sent you.

1 Like

It’s fantastic, I really need it !

Black gap is fixed:

    function processTexture(texture) {
        if (!texture.image || textureCache.has(texture)) return textureCache.get(texture);

        let { width, height } = texture.image;
        let canvas = document.createElement("canvas");
        let ctx = canvas.getContext("2d");
        canvas.width = width;
        canvas.height = height;
        ctx.drawImage(texture.image, 0, 0);
        let imgData = ctx.getImageData(0, 0, width, height).data;
        width--;  // <<<<<<<< GAP FIX <<<<<<<<
        height--; //
        // Store image data lookup function
        let getColor = (u, v) => {
            let x = Math.ceil(u * width);
            let y = Math.ceil((1 - v) * height); // Flip V coordinate
            let index = (y * width + x) * 4;
            return [
                imgData[index] / 255,
                imgData[index + 1] / 255,
                imgData[index + 2] / 255,
            ]; // Normalized RGB
        };

        textureCache.set(texture, getColor);
        return getColor;
    }

3 Likes