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.
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);
});
}