javascriptthree.js3dfabricjs

Update Three.js 3d model texture + Fabric.js canvas as texture


I'm quite new to Three.js. I'm trying to develop a project where I use a pre-existing 3D model and modify one of its materials so that it uses a Fabric.js canvas as its texture. However, I've encountered an issue where the texture takes on the color of the canvas, but none of the images I add are visible. Here's my code:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>

    <style>
        body { margin: 0; }

        #controls {
            position: absolute;
            top: 10px;
            left: 10px;
            z-index: 1;
        }
    </style>
</head>
<body>
    <div id="controls">
        <div class="colorPicker"></div>
        <canvas id="c"></canvas>
        <button id="clear">Borrar</button>
        <input type="file" id="file">
    </div>
    

    <script src="https://cdn.jsdelivr.net/npm/@jaames/iro@5"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r134/three.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/three@0.134.0/examples/js/loaders/GLTFLoader.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/three@0.134.0/examples/js/controls/OrbitControls.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.7.7/dat.gui.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/5.3.1/fabric.min.js"></script>

    <script>
        let canvas = new fabric.Canvas('c',  {
            backgroundColor: 'white',
        });
        canvas.setHeight(512);
        canvas.setWidth(512);

        document.getElementById('clear').addEventListener('click', () => {
            !deleteActiveObjects()
        });

        function deleteActiveObjects() {
            const activeObjects = canvas.getActiveObjects();
            if(!activeObjects.length) return false;
            
            if(activeObjects.length) {
                activeObjects.forEach(function(object) {
                    canvas.remove(object);
                });
            } else {
                canvas.remove(activeObjects);
            }
            
            return true;
        }

        document.getElementById('file').addEventListener('change', (e) => {
            const file = e.target.files[0]; // Get the file from the input
            if (file) {
                const reader = new FileReader();
                reader.onload = function(event) {
                    fabric.Image.fromURL(event.target.result, function(img) {
                        img.set({
                            angle: 0,
                            padding: 10,
                            flipX: false
                        });
                        img.scaleToHeight(200);
                        img.scaleToWidth(200);
                        canvas.add(img);
                        canvas.renderAll(); // Redibujar el canvas después de añadir la imagen
                        
                        // Actualizar la textura del material 14
                        const texture = new THREE.CanvasTexture(canvas.getElement());
                        material14.map = texture;
                        material14.map.needsUpdate = true; // Esto asegura que la textura se actualice
                    });
                };
                reader.readAsDataURL(file); // Read the file as a data URL
            }
        });

        function colorPicker(material14) {
            let colorPicker = new iro.ColorPicker(".colorPicker", {
                width: 280,
                color: "rgb(255, 0, 0)",
                borderWidth: 1,
                borderColor: "#fff",
            });
        
            colorPicker.on(["color:change"], function(color) {
                canvas.backgroundColor = color.hexString;
                canvas.renderAll();
        
                // Actualiza la textura del material
                material14.map.needsUpdate = true;
            });
        }

        function initScene() {
            const scene = new THREE.Scene();
            scene.background = new THREE.Color(0xd3d3d3);

            const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
            camera.position.set(0, 1, 5);

            const renderer = new THREE.WebGLRenderer({ antialias: true });
            renderer.setSize(window.innerWidth, window.innerHeight);
            document.body.appendChild(renderer.domElement);

            const controls = new THREE.OrbitControls(camera, renderer.domElement);
            controls.enableDamping = true;
            controls.dampingFactor = 0.05;
            controls.screenSpacePanning = false;
            controls.minDistance = 2;
            controls.maxDistance = 50;
            controls.maxPolarAngle = Math.PI / 2;

            return { scene, camera, renderer, controls };
        }

        function addLights(scene) {
            const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
            scene.add(ambientLight);

            const light = new THREE.DirectionalLight(0xffffff, 1);
            light.position.set(10, 10, 10).normalize();
            scene.add(light);
        }

        function loadModel(scene) {
            const loader = new THREE.GLTFLoader();
            loader.load('assets/models/2x2.glb', function(gltf) {
                model = gltf.scene;
                model.scale.set(0.001, 0.001, 0.001);
                scene.add(model);
                console.log("Modelo cargado correctamente");
        
                const material14 = getMaterialByIndex(model, 14);
                if (material14) {
                    console.log("Material 14 encontrado:", material14);
        
                    // Usa el canvas de Fabric.js como textura
                    const texture = new THREE.CanvasTexture(canvas.getElement());
                    material14.map = texture;
                    material14.map.wrapS = THREE.RepeatWrapping;
                    material14.map.wrapT = THREE.RepeatWrapping;
                    material14.map.minFilter = THREE.LinearFilter;
                    material14.map.magFilter = THREE.LinearFilter;
                    material14.needsUpdate = true;
        
                    colorPicker(material14);
                }
            }, undefined, function(error) {
                console.error("Error al cargar el modelo:", error);
            });
        }

        function getMaterialByIndex(model, index) {
            let materials = [];
            model.traverse(function(child) {
                if (child.isMesh) {
                    materials = materials.concat(child.material);
                }
            });
            return materials[index] || null;
        }

        function animate(scene, camera, renderer, controls) {
            function render() {
                requestAnimationFrame(render);
                controls.update();
                renderer.render(scene, camera);
            }
            render();
        }

        function onWindowResize(camera, renderer) {
            window.addEventListener('resize', function() {
                camera.aspect = window.innerWidth / window.innerHeight;
                camera.updateProjectionMatrix();
                renderer.setSize(window.innerWidth, window.innerHeight);
            });
        }

        function init() {
            const { scene, camera, renderer, controls } = initScene();
            addLights(scene);
            loadModel(scene);
            animate(scene, camera, renderer, controls);
            onWindowResize(camera, renderer);
        }

        init();
    </script>
</body>
</html>

I've try updating the material when an image is uploaded but it does nothing.


Solution

  • The issue was more with the 3D modeling than with the code itself. After adjusting the model, I realized it didn’t have UV mapping, which is necessary for Three.js to properly apply the texture to the material. This is the final code that worked:

    function loadModel() {
        const loader = new THREE.GLTFLoader();
        loader.load('assets/models/2x2.glb', function (gltf) {
            model = gltf.scene;
            model.scale.set(0.001, 0.001, 0.001);
            model.position.set(0, 0, 0);
    
            model.traverse(function (child) {
    
                if (child.isMesh) {
    
                    if (child.material) {
                        if (child.material.name == "mat14.003") {
                            const basicMaterial = new THREE.MeshBasicMaterial({
                                map: child.material.map, // Aplica la textura del material original si existe
                                side: THREE.DoubleSide // Aplica textura a ambos lados si es necesario
                            });
    
                            child.material = basicMaterial;
    
                            child.material.map = texture;
                            child.material.map.wrapS = THREE.RepeatWrapping;
                            child.material.map.wrapT = THREE.RepeatWrapping;
                            child.material.map.needsUpdate = true;
                        }
                    }
                }
            });
            
            scene.add(model);
        }, undefined, function (error) {
            console.error(error);
        });
    }