Updated material does not show new texture (image)?

In my world I build walls that are actually cubes but with one side made very small (so it’s no longer a cube but it looks like a wall/panel). One and only one face of the cube gets an image assigned to it, loaded via aTHREE.TextureLoader().load() call using a URL to a JPG file.

I have added code so that when I press the “C” key the surface with an image texture assigned to it is updated with a new image. However, when I execute the code everything works fine (no errors), but I don’t see the new image in the world. I still see the old one. I tried setting the needsUpdate property on the new material to true but that doesn’t help.

What is the most likely cause of this problem and how do I fix it?

Here is the code that builds and assigns the texture. I know it works because it is the same call that is used to build the original images and they all display fine. I just can’t seem to update an image (texture).

/**
 * Given a URL to an image, build a ThreeJS texture from it.
 *
 * @param {String} srcUrl - A URL to an image.
 * @param {Boolean} bIsRepeated - Whether or not the texture should be repeated.
 * @param {Object} theTexture - A ThreeJS texture object.
 *
 * @return {MeshBasicMaterial} - Returns a ThreeJS MeshBasicMaterial object
 *  built from the image at the given URL.
 */
function createMaterialFromImage(srcUrl, bIsRepeated=false) {
    const errPrefix = `(createMaterialFromImage) `;

    if (misc_shared_lib.isEmptySafeString(srcUrl))
        throw new Error(errPrefix + `The srcUrl parameter is empty.`);
    // Make sure an attempt to load a GIF file is not made with
    //  this function.
    if (srcUrl.toLowerCase().endsWith('.gif'))
        throw new Error(errPrefix + `The srcUrl parameter is a GIF file: ${srcUrl}.`);

    if (typeof bIsRepeated !== 'boolean')
    	throw new Error(errPrefix + `The value in the bIsRepeated parameter is not boolean.`);

    const threeJsMaterial = new THREE.MeshBasicMaterial();

    if (bVerbose) {
        console.info(`${errPrefix}Loading image: ${srcUrl}.`);
    }

    const loader = new THREE.TextureLoader().load(
        // resource URL
        srcUrl,
        // This function fires when the resource is loaded.
        function ( theTexture ) {
            // If the image is to be repeated, set the wrap
            //  properties to THREE.RepeatWrapping, otherwise
            //  use the default wrapping which is THREE.ClampToEdgeWrapping.
            theTexture.wrapS = bIsRepeated ? THREE.RepeatWrapping : THREE.ClampToEdgeWrapping;
            theTexture.wrapT = bIsRepeated ? THREE.RepeatWrapping : THREE.ClampToEdgeWrapping;

            // Assign the texture value to the material map when the texture is loaded.
            threeJsMaterial.map = theTexture;

            if (bVerbose)
                console.info(`${errPrefix}Resource LOADED: ${srcUrl}.`);
        },
        // This function will be called as the download of an
        //  image progresses.
        function ( xhr ) {
            if (bVerbose) {
                const pctLoaded = xhr.loaded / xhr.total * 100;

                console.info(`${errPrefix}${pctLoaded}}% loaded.  Resource: ${srcUrl}.`);
            }
        },
        // This function will be called in the event of an error.
        function ( xhr ) {
            console.error( `${errPrefix} Download failed for resource: ${srcUrl}.`);
        }
    );

    // Return the threeJsMaterial we created the desired image.
    return threeJsMaterial;
}

    /**
     * Assign the given color value or image URL to the desired
     *  surface.  Create either a THREE.COLOR() or THREE.MeshBasicMaterial()
     *  object as needed.
     *
     * @param {String} surfaceName - The name of the surface to assign the asset to.
     * @param {String|Number} numberOrStringVal - Either a color value in numeric
     *  form, or in string form, or a URL.
     *
     * @return {string|null} - Returns the ID of the previous materials object
     *  found in our collection that was disposed of or NULL if there
     *  was no previous materials object.  This can be used to
     *  seek out other occurrences of the now defunct materials object
     *  like when updating the aryCubeMaterialsExt property found in
     *  a PictureDisplay object.
     */
    this.assignAsset = function(surfaceName, numberOrStringVal) {
        const methodName = self.constructor.name + '::' + `assignAsset`;
        let idOfPreviousSurfaceObj = null;

        if (!THREEJSSUPPORT.isValidSurfaceName(surfaceName))
            throw new Error(errPrefix + `The surfaceName parameter is not a valid surface name.`);

        if (misc_shared_lib.isEmptySafeString(numberOrStringVal))
            throw new Error(errPrefix + `The newAsset parameter is empty.`);

        // If there is an existing asset, then remove it.
        if (self[surfaceName] && self[surfaceName].materialContent !== null) {
            // Is it a ThreeJS object or any other object that has a dispose method?
            if (typeof self[surfaceName].materialContent.dispose !== undefined) {
                idOfPreviousSurfaceObj = self[surfaceName].id;

                console.info('dispose', `Disposing of current asset for picture(${self.id}) for surface name: ${surfaceName}`);

                // Yes. Call its dispose method.
                self[surfaceName].materialContent.dispose();
            }
        }

        console.warn('dispose', `Disposing of assets is disabled!`);

        // Assign the new asset to the surface.
        self[surfaceName] = new ExtendedMaterial();
        self[surfaceName].surfaceName = surfaceName;

        // Is it a URL?
        if (isStringAUrl(numberOrStringVal)) {
            try {
                // Yes, it is a URL.  Create a new material from the URL
                //  and assign it to the surface.

                console.info(`${errPrefix} - Creating a new THREE.MeshBasicMaterial() object from the URL: ${numberOrStringVal}.`);

                let theMaterial = null;

                // Is it a GIF URL?
                console.info(`[Booth: IMAGE LOAD ----->>>>> ${numberOrStringVal}`);
                if (numberOrStringVal.toLowerCase().endsWith('.gif'))
                    // Yes, use the GIF loader.
                    theMaterial = createMaterialFromGif(numberOrStringVal);
                else
                    // No, use the regular image loader.
                    theMaterial = createMaterialFromImage(numberOrStringVal);

                if (!theMaterial)
                    throw new Error(errPrefix + `Unable to find a suitable image loader for URL: ${numberOrStringVal}.`);

                self[surfaceName].materialContent = theMaterial;
                self[surfaceName].isImage = true;

                // Set the needsUpdate flag to TRUE.
                self[surfaceName].materialContent.needsUpdate = true;
            } catch (err) {
                // Assume the URL is invalid.  Just assign the default
                //  color to the surface.
                self[surfaceName].materialContent = new THREE.MeshBasicMaterial( {color: new THREE.Color(DEFAULT_COLOR_FOR_CUBE_SURFACES) } );
            }
        }
        else {
            // No, it is not a URL.  Assume it is a color value
            //  and assign a color to the surface.

            // Is it a number or a string?
            if (typeof numberOrStringVal === 'number' || typeof numberOrStringVal === 'string') {
                // Yes.  Assume it is a color value.
                self[surfaceName].materialContent = new THREE.MeshBasicMaterial( {color: new THREE.Color(numberOrStringVal) } );
            }
            else {
                // No, it is not a number or a string.  For now that
                //  is an error.
                throw new Error(errPrefix + `The new asset value is not a number or a string.`);
            }
        }

        return idOfPreviousSurfaceObj;
    };

It is hard to tell. This is not the full code and it is ‘bloated’ with checks. Checks are good thing, but now they only make a lot harder to spot the bug.

Personally, if I want to change textures of an object, I’d do this:

  • I will load all textures in an array texture, so individual textures are texture[0], texture[1] and so on
  • When I need to change the texture of object, I will just do object.material.map = texture[i];

UPDATE: I am putting the solution here in case it helps others.

The problem was an async one. I was setting the new texture’s needsUpdate flag to true before the async callback in the TextureLoader.load() call in the createMaterialFromImage() function had completed (i.e. - had finished loading the image).

By setting the new texture’s needsUpdate flag to true from inside the body of that callback, function everything works now. Below is the updated code from above:

    const loader = new THREE.TextureLoader().load(
        // resource URL
        srcUrl,
        // This function fires when the resource is loaded.
        function ( theTexture ) {
            theTexture.wrapS = bIsRepeated ? THREE.RepeatWrapping : THREE.ClampToEdgeWrapping;
            theTexture.wrapT = bIsRepeated ? THREE.RepeatWrapping : THREE.ClampToEdgeWrapping;

            // Assign the texture value to the material map when the texture is loaded.
            threeJsMaterial.map = theTexture;

            // Set the needsUpdate flag in case this was a replace operation.
            theTexture.needsUpdate = true;

            if (bVerbose)
                console.info(`${errPrefix}Resource LOADED: ${srcUrl}.`);
        },
1 Like