Three.js中Fabric.js画布作为3D模型纹理时图片不显示问题
问题分析与解决方案
核心问题排查
- 全局变量缺失:
material14是loadModel函数内的局部变量,文件上传回调无法直接访问,导致更新材质的代码完全无效。 - 纹理重复创建:每次上传图片都新建
CanvasTexture,替换掉原材质的纹理引用,但未正确触发材质更新,且浪费资源。 - 材质更新不彻底:仅设置纹理的
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
相关产品推荐
相关产品推荐

