You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Three.js中Fabric.js画布作为3D模型纹理时图片不显示问题

问题分析与解决方案

核心问题排查

  1. 全局变量缺失:material14是loadModel函数内的局部变量,文件上传回调无法直接访问,导致更新材质的代码完全无效。
  2. 纹理重复创建:每次上传图片都新建CanvasTexture,替换掉原材质的纹理引用,但未正确触发材质更新,且浪费资源。
  3. 材质更新不彻底:仅设置纹理的needsUpdate,未同步设置材质本身的needsUpdate = true,Three.js无法感知材质变化。

修复步骤

1. 声明全局变量存储关键对象

在脚本最顶部添加全局变量,统一管理目标材质和对应的Three.js纹理:

let material14 = null; // 全局存储目标材质
let fabricTexture = null; // 全局存储Fabric画布对应的Three.js纹理

2. 初始化纹理时复用引用

在loadModel函数中,将纹理赋值给全局变量,后续更新直接复用该引用:

// 替换原纹理初始化代码
fabricTexture = new THREE.CanvasTexture(canvas.getElement());
fabricTexture.wrapS = THREE.RepeatWrapping;
fabricTexture.wrapT = THREE.RepeatWrapping;
fabricTexture.minFilter = THREE.LinearFilter;
fabricTexture.magFilter = THREE.LinearFilter;
fabricTexture.flipY = false; // 解决Fabric画布与Three.js纹理坐标系倒置问题
material14.map = fabricTexture;
material14.needsUpdate = true;

3. 修改文件上传回调逻辑

不再新建纹理,直接更新现有纹理并触发材质更新:

document.getElementById('file').addEventListener('change', (e) => {
    const file = e.target.files[0];
    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();
                
                // 仅更新现有纹理,无需新建
                if (fabricTexture) {
                    fabricTexture.needsUpdate = true;
                    material14.needsUpdate = true; // 触发材质更新
                }
            });
        };
        reader.readAsDataURL(file);
    }
});

4. 优化Fabric画布尺寸设置

使用setDimensions确保画布元素和Fabric内部尺寸一致:

let canvas = new fabric.Canvas('c',  {
    backgroundColor: 'white',
});
canvas.setDimensions({ width: 512, height: 512 }); // 替换原setWidth/setHeight

5. 修复清除按钮逻辑

确保清除画布后同步更新纹理:

function deleteActiveObjects() {
    const activeObjects = canvas.getActiveObjects();
    if(!activeObjects.length) return false;
    
    activeObjects.forEach(function(object) {
        canvas.remove(object);
    });
    canvas.renderAll();
    // 清除后更新纹理
    if (fabricTexture) {
        fabricTexture.needsUpdate = true;
        material14.needsUpdate = true;
    }
    return true;
}

完整修复后的代码

<!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 material14 = null;
        let fabricTexture = null;
        let canvas = new fabric.Canvas('c',  {
            backgroundColor: 'white',
        });
        canvas.setDimensions({ width: 512, height: 512 });

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

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

        document.getElementById('file').addEventListener('change', (e) => {
            const file = e.target.files[0];
            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();
                        
                        if (fabricTexture) {
                            fabricTexture.needsUpdate = true;
                            material14.needsUpdate = true;
                        }
                    });
                };
                reader.readAsDataURL(file);
            }
        });

        function colorPicker() {
            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();

                if (fabricTexture) {
                    fabricTexture.needsUpdate = true;
                    material14.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 materials = [];
                model.traverse(function(child) {
                    if (child.isMesh) {
                        materials.push(...child.material);
                    }
                });
                material14 = materials[14] || null;
                
                if (material14) {
                    console.log("Material 14 encontrado:", material14);
        
                    fabricTexture = new THREE.CanvasTexture(canvas.getElement());
                    fabricTexture.wrapS = THREE.RepeatWrapping;
                    fabricTexture.wrapT = THREE.RepeatWrapping;
                    fabricTexture.minFilter = THREE.LinearFilter;
                    fabricTexture.magFilter = THREE.LinearFilter;
                    fabricTexture.flipY = false;
                    material14.map = fabricTexture;
                    material14.needsUpdate = true;
        
                    colorPicker();
                }
            }, undefined, function(error) {
                console.error("Error al cargar el modelo:", error);
            });
        }

        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>

额外说明

  • 全局变量material14和fabricTexture确保所有回调都能访问到目标材质和对应的纹理。
  • 设置fabricTexture.flipY = false解决Fabric画布与Three.js纹理的坐标系差异,避免图片倒置。
  • 每次修改Fabric画布后,只需设置fabricTexture.needsUpdate = true和material14.needsUpdate = true,即可让Three.js重新渲染纹理。

内容的提问来源于stack exchange,提问作者ludivik

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.19 16:17:02