Custom shape in image not working

From this documentation three.js docs

I tried to make a custom shape with an image however the image seems like it doesn’t fit to the custom shape I’d like to happen. Like is there anyway can I make custom image shapes? I want to make it something like KPR website where you see its a custom shape image folder like.

and here is my source code

And this is the main code of it.

function CustomShapeImage() {
  const shape = useMemo(() => {
    const x = 0,
      y = 0;

    const heartShape = new THREE.Shape();

    heartShape.moveTo(x + 5, y + 5);
    heartShape.bezierCurveTo(x + 5, y + 5, x + 4, y, x, y);
    heartShape.bezierCurveTo(x - 6, y, x - 6, y + 7, x - 6, y + 7);
    heartShape.bezierCurveTo(x - 6, y + 11, x - 3, y + 15.4, x + 5, y + 19);
    heartShape.bezierCurveTo(x + 12, y + 15.4, x + 16, y + 11, x + 16, y + 7);
    heartShape.bezierCurveTo(x + 16, y + 7, x + 16, y, x + 10, y);
    heartShape.bezierCurveTo(x + 7, y, x + 5, y + 5, x + 5, y + 5);

    const geometry = new THREE.ShapeGeometry(heartShape);
    return geometry;
  }, []);

  return (
      <mesh geometry={shape}>
        {/* <shapeGeometry args={[10,10]}/> */}

I think you just need to scale the image mapping, because the image is there, it is just too small:

Increasing the image size 20 times (see line 29):


Dear Myth Xenodo,

Firstly, from what I see the problem in your code is all about texture mapping. I believe the UV coordinates are not generated for a shapegeometry or extrude geometry. You have to take care of generating the UV coordinates for your shape. Then the image will start showing up. Currently all the uv coordinates are assigned to 0,0.

Secondly, to achieve the effect based on the example URL you have provided. You need to approach it differently. One way to achieve this by having two different threejs scenes rendered to a single canvas as an image. However, ensure to move the cameras in these scenes with your logic to paint the canvas image. There will be a third scene with a custom shapegeometry with proper uv coordinates that will paint the image in the canvas. Hence, you paint the canvas dynamically it is going to reflect in the texture of the custom shapegeometry you are creating.

I will show you how to update the uv coordinates for your shapegeometry below

projected2DCoordinates(coordinate: Vector3, normal: Vector3): Vector2{
        function projectOnPlane(point: Vector3, u: Vector3, v: Vector3): Vector2{
            return new Vector2(,;
        function getUVVectors(normal: Vector3): Array<Vector3>{
            let u: Vector3;
            let v: Vector3;
            let dot: number = Math.abs(;
            let dot2: number = Math.abs(;

             * If the angle between FORWARD and NORMAL is 0 degrees
             * then FORWARD is the NORMAL itself, in that case 
             * use the orthogonal vectors RIGHT and cross(RIGHT, FORWARD)
            if(dot == 1){
                return [RIGHT.clone(), RIGHT.clone().cross(normal)];
             * If the angle between RIGHT and NORMAL is 0 degrees
             * then RIGHT is the NORMAL itself, in that case 
             * use the orthogonal vectors FORWARD and cross(FORWARD, FORWARD)
            else if(dot2 == 1){
                return [FORWARD.clone(), FORWARD.clone().cross(normal)];
            if(dot < 0.2 || dot > 1.0-1e-6){
                u = RIGHT.clone().projectOnPlane(normal).normalize();
                u = FORWARD.clone().projectOnPlane(normal).normalize();
            v = u.clone().cross(normal).normalize();
            return [u, v];
        let [u, v] = getUVVectors(normal);
        return projectOnPlane(coordinate, u, v);

function textureDefinitionShapeGeometry() {
let dim: Vector3 = dimension of your shapegeometry computed from bounding box;
let inverseDim: Vector3 = new Vector3(1 / dim.x, 1 / dim.y, 1 / dim.z);
shapeGeometry.groups.forEach((group: any) =>{
                for (i =  group.start; i < (group.start + group.count); i+= 3){
                    a.fromBufferAttribute(positionAttribute, i);
                    b.fromBufferAttribute(positionAttribute, i + 1);
                    c.fromBufferAttribute(positionAttribute, i + 2);
                    ab = b.clone().sub(a);     
                    bc = c.clone().sub(b);     
                    ca = a.clone().sub(c);     
                    ac = c.clone().sub(a);
                    normal = ab.clone().cross(ac).normalize();

                    a2D = projected2DCoordinates(a, normal);
                    b2D = projected2DCoordinates(b, normal);
                    c2D = projected2DCoordinates(c, normal);
                    dim2D = projected2DCoordinates(dim, normal);
                    inverseDim2D = projected2DCoordinates(inverseDim, normal);


                    uvAttribute.setXY(i, Math.abs(a2D.x), Math.abs(a2D.y));
                    uvAttribute.setXY(i + 1, Math.abs(b2D.x), Math.abs(b2D.y));
                    uvAttribute.setXY(i + 2, Math.abs(c2D.x), Math.abs(c2D.y));       

I hope this will solve your problem of UV coordinates for your custom shape. Doing the effect is a different story altogether based on the strategy I proposed earlier. Feel free to ask for more explanation if you need it.


1 Like

is this too rich/bad when it comes to performance?

I feel so small with this code I don’t understand hahaha.


Texture scaling does not affect performance. Preferably, the dimensions of the texture must be powers of 2.

1 Like

likewise :smiley: i feel so low whenever i see real math. i wish i didn’t fall asleep in math class.

1 Like

Re-compute of UVs:

    const geometry = new THREE.ShapeGeometry(heartShape);
    let pos = geometry.attributes.position;
    let b3 = new THREE.Box3().setFromBufferAttribute(pos);
    let b3size = new THREE.Vector3();
    let uv = [];
    for(let i = 0; i < pos.count; i++){
      let x = pos.getX(i);
      let y = pos.getY(i);
      let u = (x - b3.min.x) / b3size.x;
      let v = (y - b3.min.y) / b3size.y;
      uv.push(u, v);
    geometry.setAttribute("uv", new THREE.Float32BufferAttribute(uv, 2));

gives this:


:ok_hand: Simply ideal.

1 Like

@prisoner849 @Myth_Xenodo i could add this code to drei so nobody has to fight with this again. it already exposes shapes (box, sphere, …) but shapegeometry was missing for some reason.

like this: nice-thunder-4ic9zz - CodeSandbox

can i assume that the code will apply to every custom shape or must it change once the user uses different paths?

ps @Myth_Xenodo you don’t need all the code for setting camera up and orbitcontrols. this is all taken care of by canvas and the orbitcontrols component in drei.


yeah I think @prisoner849 code is just to center the image.

1 Like

ok, i’ll add it, it will auto calculate uvs for drei/Shape then


it’s in: nice-thunder-4ic9zz - CodeSandbox

now it’s just

import { Shape } from '@react-three/drei'

<Shape args={[path]}>
  <meshStandardMaterial map={foo} />

though this should probably be in three-core, if all other geometries calculate uv’s why not shapegeo?

1 Like

I’m unprofessional and I don’t know the answer although I can think that what @aalavandhaann said that

I believe the UV coordinates are not generated for a shapegeometry or extrude geometry

meaning its not default

I also tried extrudegeometry and yet he is right it doesn’t work for both.

1 Like

They are, but in world coords: three.js/ShapeGeometry.js at 47b28bc564b438bf2b80d6e5baf90235292fcbd7 · mrdoob/three.js · GitHub

1 Like

@prisoner849 credited you here drei/shapes.tsx at d7bf03e0077612a03ce057914e613dbda67bf20c · pmndrs/drei · GitHub


Actually not. The performance depends on how you strategize it. In my opinion the performance could be saved in many ways. Starting with a good planing of implementing the 3D scenes. To me they resemble more of a parallax than a complete 3D scene itself. Another way would be the usage of shaders to optimize certain aspects.


Yes, this code as pointed by drcmda also is an excellent fit. The one I posted is for objects with thickness or objects with faces spanning in all 3 dimensions. In that case take the normal of a face and project them on a plane parallel to that face. This way you can produce an atlas type UV coordinates for all the faces.


Ooops, I was speaking from my experience of using ExtrudeGeometry. Thank you master for teaching me this :wink: