Three.js and fabric.js

Hi there, I’m making a t-shirt configurator with three and fabric.js, Bit there is one problem, When i click on the t-shirt model to place an image on the mouse position it is not being perfectly place, Can anyone help me in this issue?

here is the model:
Tshirt.glb (1.2 MB)

here is the 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>

</head>
<style>
    * {
        margin: 0;
        padding: 0;
        background: rgb(0, 0, 0);
    }

    #wiki {
        display: none;
    }

    #canvas_container {
        display: none;
    }
</style>

<body>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/90/three.min.js"></script>
    <!-- Include other Three.js loaders -->


    <script src="GLTFLoader.js"></script>

    <script src="OrbitControls.js"></script>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.7.1/dat.gui.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/2.2.0/fabric.min.js"></script>

    <script type="text/javascript"
        src="https://custom.speedjersey.com/designer/assets/javascripts/three/OBJLoader2.js"></script>
    <script type="text/javascript"
        src="https://custom.speedjersey.com/designer/assets/javascripts/three/SVGLoader.js"></script>


    <canvas style="display: none;" id="canvas" height="1024" width="1024"></canvas>
    <img crossorigin="anonymous" id="wiki"
        src="https://fiverr-res.cloudinary.com/t_profile_original,q_auto,f_auto/attachments/profile/photo/19d960fe641d49e4c77288d4b5e90283-1690885352403/9c2d1c91-ea9a-4f74-822f-3f27f1b6f0c7.jpg"
        width="100" height="100" />

    <img crossorigin="anonymous" id="wiki1" src="https://www.imgacademy.com/sites/default/files/2009-stadium-about.jpg"
        width="20" height="20" />

    <div id="renderer">
    </div>
    <br />
    <div id="canvas_container">
        <canvas id="cnvs"></canvas>

    </div>

    <script>

        /**
         * FabricJS  
         **/

        /**
  * FabricJS  
  **/





  var canvas = new fabric.Canvas("cnvs", { width: window.innerWidth, height: window.innerHeight });
window.addEventListener('resize', function () {
    canvas.setDimensions({ width: window.innerWidth, height: window.innerHeight });
});

canvas.backgroundColor = "white";
canvas.on("after:render", function () {
    if (model) {
        canvasTexture.needsUpdate = true;
    }
});



        var text = new fabric.IText('Three.js\n', {
            fontSize: 40,
            textAlign: 'center',
            fontWeight: 'bold',
            left: 128,
            top: 128,
            angle: 0,
            originX: 'center',
            originY: 'center',
            shadow: 'blue -5px 6px 5px'
        });
        canvas.add(text);

        var imgElement = document.getElementById("wiki1");
        var imageinstance = new fabric.Image(imgElement, {
            angle: 0,
            left: 0,
            opacity: 1,
            cornerSize: 10,
        });
        canvas.add(imageinstance);

        /**
         * ThreeJS
         **/
        var containerHeight = "512";
        var containerWidth = "512";
        var camera, renderer, container, scene, texture, material, geometry;

        scene = new THREE.Scene();
        camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 1, 1000);
        camera.position.set(0, 0, 10);

        scene.background = new THREE.Color("#4C489B");

        /* Raycaster */
        var raycaster = new THREE.Raycaster();
        var mouse = new THREE.Vector2();
        var onClickPosition = new THREE.Vector2();
        var isMobile = false;
        /**
         ** Renderer
         **/
        container = document.getElementById("renderer");
        renderer = new THREE.WebGLRenderer({ antialias: true, preserveDrawingBuffer: true });
        renderer.setPixelRatio(window.devicePixelRatio);
        renderer.setSize(window.innerWidth, window.innerHeight);
        camera.updateProjectionMatrix();
        container.appendChild(renderer.domElement);


        function resizeRendererToDisplaySize(renderer) {
            const canvas = renderer.domElement;
            var width = window.innerWidth;
            var height = window.innerHeight;
            var canvasPixelWidth = canvas.width / window.devicePixelRatio;
            var canvasPixelHeight = canvas.height / window.devicePixelRatio;

            const needResize = canvasPixelWidth !== width || canvasPixelHeight !== height;
            if (needResize) {

                renderer.setSize(width, height, false);
            }
            return needResize;
        }


        /* 
         * End Renderer 
         */

        var controls = new THREE.OrbitControls(camera, renderer.domElement);
        controls.enableDamping = true; // an animation loop is required when either damping or auto-rotation are enabled
        controls.dampingFactor = 0.5;
        controls.screenSpacePanning = false;
        controls.maxPolarAngle = Math.PI / 2;


        var light = new THREE.DirectionalLight(0xffffff, 0.5);
        light.position.setScalar(10);
        scene.add(light);
        scene.add(new THREE.AmbientLight(0xffffff, 1));

        var canvasTexture = new THREE.CanvasTexture(cnvs);
        canvasTexture.flipY = false;
        canvasTexture.wrapT = THREE.RepeatWrapping;
        canvasTexture.minFilter = THREE.LinearFilter;
        canvasTexture.generateMipmaps = false;
        canvasTexture.wrapT = THREE.ClampToEdgeWrapping;
        canvasTexture.anisotropy = renderer.capabilities.getMaxAnisotropy();
        canvasTexture.magFilter = THREE.NearestFilter;
        canvasTexture.anisotropy = renderer.capabilities.getMaxAnisotropy();
        canvasTexture.needsUpdate = true;


        var newMaterial = new THREE.MeshPhysicalMaterial({});

        var loader = new THREE.GLTFLoader();
        var model;

        loader.load('Tshirt.glb', function (gltf) {
            let initialAngle = scene.rotation.y;
            model = gltf.scene;

            // Set the canvas texture to the material of the 3D model
            model.traverse(function (child) {
                if (child.isMesh) {
                    // Check if the mesh has a name that corresponds to the type
                    if (child.name.includes(scene.name)) {
                        // Ensure the material is MeshBasicMaterial for texture mapping
                        if (child.material instanceof THREE.MeshStandardMaterial) {
                            child.material.map = canvasTexture;
                            child.material.side = THREE.DoubleSide; // Ensure both sides of the geometry are rendered
                            child.material.needsUpdate = true;
                        } else {
                            // If the material is not MeshBasicMaterial, create a new one
                            const newMaterial = new THREE.MeshStandardMaterial({
                                map: canvasTexture,
                                side: THREE.DoubleSide,
                            });
                            child.material = newMaterial;
                        }

                        // Assuming the UV mapping is correct, no additional adjustments needed
                        child.nameID = scene.name; // Set a new property to identify this object
                    }
                }
            });

            model.scale.set(6, 6, 6);

            model.position.set(0, -8, 0)
            // Add the loaded 3D model to the scene
            scene.add(model);
        });







        //         var geometry = new THREE.PlaneGeometry(20, 20, 30, 30);
        //         // geometry.vertices.forEach(v => {
        //         //   v.z = Math.cos(v.x) * Math.sin(-v.y * 1) * 1;
        //         // });
        //         // geometry.computeFaceNormals();
        //         // geometry.computeVertexNormals();

        //         var mesh = new THREE.Mesh(geometry, new THREE.MeshStandardMaterial({
        //     map: canvasTexture,
        //     metalness: 0.25,
        //     roughness: 0.25,
        //     magFilter: THREE.NearestFilter, // Use NearestFilter for magnification
        //     minFilter: THREE.LinearMipmapLinearFilter, // Use LinearMipmapLinearFilter for minification
        //     anisotropy: renderer.capabilities.getMaxAnisotropy(), // Anisotropic filtering
        // }));

        //         scene.add(mesh);




        function animateRandom() {
            var randomX = THREE.Math.randInt(50, 206);
            var randomY = THREE.Math.randInt(10, 206);
        }

        animateRandom();
        setInterval(animateRandom, 1000);

        var clock = new THREE.Clock();
        var time = 0;

        // controls.update();

        function render() {
            requestAnimationFrame(render);
            time += clock.getDelta();
            renderer.render(scene, camera);
        }

        render();

        /**
        * Fabric.js patch
        */
        fabric.Canvas.prototype.getPointer = function (e, ignoreZoom) {
            if (this._absolutePointer && !ignoreZoom) {
                return this._absolutePointer;
            }
            if (this._pointer && ignoreZoom) {
                return this._pointer;
            }
            var simEvt;
            if (e.touches != undefined) {
                simEvt = new MouseEvent({
                    touchstart: "mousedown",
                    touchmove: "mousemove",
                    touchend: "mouseup"
                }[e.type], {
                    bubbles: true,
                    cancelable: true,
                    view: window,
                    detail: 1,
                    screenX: Math.round(e.changedTouches[0].screenX),
                    screenY: Math.round(e.changedTouches[0].screenY),
                    clientX: Math.round(e.changedTouches[0].clientX),
                    clientY: Math.round(e.changedTouches[0].clientY),
                    ctrlKey: false,
                    altKey: false,
                    shiftKey: false,
                    metaKey: false,
                    button: 0,
                    relatedTarget: null
                });
                var pointer = fabric.util.getPointer(simEvt),
                    upperCanvasEl = this.upperCanvasEl,
                    bounds = upperCanvasEl.getBoundingClientRect(),
                    boundsWidth = bounds.width || 0,
                    boundsHeight = bounds.height || 0,
                    cssScale;
            } else {
                var pointer = fabric.util.getPointer(e),
                    upperCanvasEl = this.upperCanvasEl,
                    bounds = upperCanvasEl.getBoundingClientRect(),
                    boundsWidth = bounds.width || 0,
                    boundsHeight = bounds.height || 0,
                    cssScale;
            }
            if (!boundsWidth || !boundsHeight) {
                if ('top' in bounds && 'bottom' in bounds) {
                    boundsHeight = Math.abs(bounds.top - bounds.bottom);
                }
                if ('right' in bounds && 'left' in bounds) {
                    boundsWidth = Math.abs(bounds.right - bounds.left);
                }
            }
            this.calcOffset();
            pointer.x = pointer.x - this._offset.left;
            pointer.y = pointer.y - this._offset.top;
            /* BEGIN PATCH CODE */
            if (e.target !== this.upperCanvasEl) {
                var positionOnScene;
                if (isMobile == true) {
                    positionOnScene = getPositionOnSceneTouch(container, e);
                    if (positionOnScene) {
                        console.log(positionOnScene);
                        pointer.x = positionOnScene.x;
                        pointer.y = positionOnScene.y;
                    }
                } else {
                    positionOnScene = getPositionOnScene(container, e);
                    if (positionOnScene) {
                        console.log(positionOnScene);
                        pointer.x = positionOnScene.x;
                        pointer.y = positionOnScene.y;
                    }
                }
            }
            /* END PATCH CODE */
            if (!ignoreZoom) {
                pointer = this.restorePointerVpt(pointer);
            }

            if (boundsWidth === 0 || boundsHeight === 0) {
                cssScale = { width: 1, height: 1 };
            }
            else {
                cssScale = {
                    width: upperCanvasEl.width / boundsWidth,
                    height: upperCanvasEl.height / boundsHeight
                };
            }

            return {
                x: pointer.x * cssScale.width,
                y: pointer.y * cssScale.height
            };
        }

        /**
         * Listeners
         */

        container.addEventListener("mousedown", onMouseEvt, false);

        if (
            /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|ipad|iris|kindle|Android|Silk|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i.test(
                navigator.userAgent,
            ) ||
            /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(
                navigator.userAgent.substr(0, 4),
            )
        ) {
            isMobile = true;
            container.addEventListener("touchstart", onTouch, false);
        }
        /**
         * Event handler
         */

        function onTouch(evt) {
            evt.preventDefault();
            const positionOnScene = getPositionOnSceneTouch(container, evt);
            if (positionOnScene) {
                const canvasRect = canvas._offset;
                const simEvt = new MouseEvent(evt.type, {
                    clientX: canvasRect.left + positionOnScene.x,
                    clientY: canvasRect.top + positionOnScene.y,
                });
                canvas.upperCanvasEl.dispatchEvent(simEvt);
            }
        }

        function getPositionOnScene(sceneContainer, evt) {
    var array = getMousePosition(container, evt.clientX, evt.clientY);
    onClickPosition.fromArray(array);

    var intersects = getIntersects(onClickPosition, scene.children);

    if (intersects.length > 0 && intersects[0].uv) {
        var uv = intersects[0].uv;
        intersects[0].object.material.map.transformUv(uv);

        // Get the canvas position based on UV coordinates
        var canvasX = Math.round(uv.x * canvas.width);
        var canvasY = Math.round(uv.y * canvas.height);

        // Get the intersection point on the canvas using the raycaster
        var intersectionPoint = intersects[0].point;
        var canvasPosition = convertWorldToCanvas(intersectionPoint);

        return {
            x: canvasPosition.x,
            y: canvasPosition.y,
        };
    }

    return null;
}

function convertWorldToCanvas(worldPosition) {
    var vector = worldPosition.clone();
    vector.project(camera);

    vector.x = Math.round((vector.x + 1) / 2 * canvas.width);
    vector.y = Math.round((-vector.y + 1) / 2 * canvas.height);

    return vector;
}




        function onMouseEvt(evt) {
            evt.preventDefault();
            const positionOnScene = getPositionOnScene(container, evt)
            if (positionOnScene) {
                const canvasRect = canvas._offset;
                const simEvt = new MouseEvent(evt.type, {
                    clientX: canvasRect.left + positionOnScene.x,
                    clientY: canvasRect.top + positionOnScene.y
                });

                console.log(simEvt);

                canvas.upperCanvasEl.dispatchEvent(simEvt);
            }
        }

        /**
         * Add a flag to track whether an image is being dragged
         */
        let isDragging = false;

        /**
         * Event handler
         */
        container.addEventListener("mousedown", onMouseDown, false);

        /**
         * Modified onMouseDown function
         */
        // Use Vector2 for onClickPosition
        var onClickPosition = new THREE.Vector2();

        function getRealPosition(axis, value) {
            let CORRECTION_VALUE = axis === "x" ? 512 / 2 : 512 / 2;
            return Math.round(value * 512) - CORRECTION_VALUE;
        }


        
        function onMouseDown(evt) {
    evt.preventDefault();
    const positionOnScene = getPositionOnScene(container, evt);

    if (positionOnScene && model) {
        const imgElement = document.getElementById("wiki");
        const newImageInstance = new fabric.Image(imgElement, {
            angle: 0,
            left: positionOnScene.x,
            top: positionOnScene.y,
            opacity: 1,
            cornerSize: 30,
        });

        // Convert screen coordinates to 3D world coordinates
        const vector = new THREE.Vector3(positionOnScene.x, positionOnScene.y, 0.5);
        vector.unproject(camera);

        // Raycast to find intersection with the model
        const raycaster = new THREE.Raycaster(camera.position, vector.sub(camera.position).normalize());
        const intersects = raycaster.intersectObject(model, true);

        if (intersects.length > 0) {
            // Get the intersection point in world coordinates
            const intersectionPoint = intersects[0].point;

            // Project the 3D intersection point back to screen coordinates
            const screenPosition = intersectionPoint.clone().project(camera);

            // Calculate the offset between screen and fabric coordinates
            const offset = {
                x: positionOnScene.x - screenPosition.x * window.innerWidth,
                y: positionOnScene.y - screenPosition.y * window.innerHeight,
            };

            // Set fabric image position considering the offset
            newImageInstance.left = positionOnScene.x - offset.x;
            newImageInstance.top = positionOnScene.y - offset.y;
            newImageInstance.setCoords();

            model.add(newImageInstance);
        }
    }
}





        /**
         * Event handler for mouseup
         */
        container.addEventListener("mouseup", onMouseUp, false);

        /**
         * Modified onMouseUp function
         */
        function onMouseUp(evt) {
            // Reset the flag when the mouse button is released
            isDragging = false;
        }

        /**
         * Event handler for object:selected in Fabric.js
         */
        canvas.on('object:selected', function (options) {
            if (options.target && options.target.type === 'image') {
                // Set the flag to indicate that an image is being dragged
                isDragging = true;
            }
        });

        /**
         * Event handler for object:modified in Fabric.js
         */
        canvas.on('object:modified', function (options) {
            // Reset the flag after the image has been modified (including drag)
            isDragging = false;
        });

        /**
         * Modified onTouch function
         */
        function onTouch(evt) {
            // Check the flag before processing touch events
            if (isDragging) {
                return;
            }

            // ... (Your existing touch event handling code)
        }

        /**
         * Three.js Helper functions
         */
        function getPositionOnScene(sceneContainer, evt) {
            console.log("Getting position on scene...");
            var array = getMousePosition(sceneContainer, evt.clientX, evt.clientY);
            onClickPosition.fromArray(array);

            // Get all objects in the scene (including the 3D model and other elements)
            var objects = [];
            scene.traverse(function (child) {
                if (child.isMesh) {
                    objects.push(child);
                }
            });

            console.log("Number of objects in the scene:", objects.length);

            var intersects = getIntersects(onClickPosition, objects);

            if (intersects.length > 0) {
                console.log("Intersection found:", intersects[0].point);
                return intersects[0].point;
            }

            console.log("No intersection found.");
            return null;
        }


        function getRealPosition(axis, value) {
            let CORRECTION_VALUE = axis === "x" ? 4.5 : 5.5;

            return Math.round(value * 512) - CORRECTION_VALUE;
        }

        var getMousePosition = function (dom, x, y) {
            var rect = dom.getBoundingClientRect();
            return [(x - rect.left) / rect.width, (y - rect.top) / rect.height];
        };

        var getIntersects = function (point, objects) {
            mouse.set(point.x * 2 - 1, -(point.y * 2) + 1);
            raycaster.setFromCamera(mouse, camera);
            return raycaster.intersectObjects(objects);
        };

    </script>
</body>

</html>`

it was working perfectly when I was testing it here: https://codepen.io/usman9602/pen/ZEwaZBb?editors=0010

anyone there?

cat-patience-cat


It seems to be working in the codepen indeed - could you share a video or a more compact example of how it’s not working in your code? Debugging 600 lines of unformatted code is a bit much :face_holding_back_tears:

:joy: let me send you a video:

my way to solve this was to set controls.enabled = false when the canvas.on('selection:update') fired. The issue was because of the orbit controls still moving while you also move the fabric object. anyway take a look at my demo 3D Merch Configurator

So, I should disable the control’s as soon I Select or Move the canvas object, And enable it when I click out side the fabric canvas, Right?

Also, Can you tell me if I’m mapping the canvas on the T-shirt the right way?

yes, that’s correct

Okay thanks, I’ll let you know :v:

i can see you are using my starter code, it should be correct

Yep, Your code was the best that I found.

I tried this method but its still same as before, Can you please give a sneak peek of object dragging function? :sweat_smile:

i only did this on my configurator demo


        canvas.on("mouse:down", (e) => {
            if (e.target && e.target.text || e.target && e.target._element) {
                controls.enabled = false;
            } else {
                controls.enabled = true;
            }
        })

also don’t forget to set enabled to true on mouse:up

Thanks man :wink:

Okay perfect :ok_hand:

Hi @naonao_naonvl, The logic is working when I click on the fabric canvas but not working when i click on the T-shirt model.


I mean how?

you should double check this condition e.target && e.target.text || e.target && e.target._element it should true when you are clicking a text or an image object if not then maybe there’s something missing on your code as it’s working on mine with the same code