Trying to understand the coordinate system and relationship between uv and NDC

I have an orthographic camera that is looking perpendicular at a sprite
the intersection with some point on the sprite is at intersects[0].point
NDC is computed from intersects[0].point.
In my case:
the camera is orthographic
the camera view is perpendicualr to the sprite
the aspect ratio of the sprite is equal to the aspect ratio of the camera fustrum

To get from uv to NDC_Coord I have to transform uv → worldCoords → NDC_Coord
Why is it that uv and NDC_Coord are not mapped to each other up to shift and scale?

ThreejsUtil.js:368 uv2 Vector2 {x: 0.46508622967278285, y: 0.5292560967357548}
ThreejsUtil.js:375 threejsViewport Vector4 {x: -0, y: -815, z: 2469, w: 2276}
ThreejsUtil.js:379 sprite.position Vector3 {x: 468, y: 0, z: 431.5}
ThreejsUtil.js:380 sprite.scale Vector3 {x: 936, y: 863, z: 1}
ThreejsUtil.js:387 worldCoord3 Vector3 {x: 435.3207109737248, y: 0, z: 456.7480114829564}
ThreejsUtil.js:389 NDC_Coord3 Vector3 {x: -0.0198460915350345, y: -0.016695957737587717, z: -0.9800020305980534}
ThreejsUtil.js:391 camera.zoom 0.2842158172697181
ThreejsUtil.js:393 intersects[0].point Vector3 {x: 435.32071097372483, y: -0.002524801135450616, z: 456.74801135671635}
ThreejsUtil.js:396 NDC_Coord Vector3 {x: -0.019846091535034464, y: -0.01669595782073819, z: -0.9800019801019801}

related functions are

    static GetPositionRelativeToViewportInPixels(mouseNDC, camera, threejsViewport, children) {

        // clientX, clientY - screen coordinates in pixel of the client window, e.g. 2461
        console.log('xxxxxxxxxxxxxxxxxxxxxxxxxxx');
        
        let raycaster = new THREE_Raycaster();
        console.log('mouseNDC', mouseNDC);
        raycaster.setFromCamera(mouseNDC, camera);
        let intersects = raycaster.intersectObjects(children);
        
        if (intersects.length > 0 && intersects[0].uv) {
            // https://threejs.org/docs/#api/en/core/Raycaster.intersectObject
            // The intesectionPoint intersects[0] contains:
            // point - point of interection in world coordinates
            // uv - U,V coordinates at point of intersection in respect to the object
            //      e.g. if intersecting with circle object (laid on a surface (sprite)) on the left part of the circle, then x will be 0
            //           and if intersecting with circle object on the right part of the circle, then x will be 1
            //           this is unrelated to the position of the circle on the surface (sprite)
            var uv = intersects[0].uv;
            // Transform the uv based on the value of this texture's 
            // .offset, .repeat, .wrapS, .wrapT and .flipY properties.
            // The value of uv changes in-place.
            intersects[0].object.material.map.transformUv(uv);

            // console.log('intersects[0]', intersects[0]);
            console.log('uv2', uv);
        
            let point2d_relativeToViewportInPixels = new THREE_Vector2(threejsViewport.z * uv.x,
                threejsViewport.w * uv.y);

            {

                console.log('threejsViewport', threejsViewport);
                
                let sprite = COL.model.selectedSite.selectedLayer.getPlanViewSurface();
                // console.log('sprite', sprite);
                console.log('sprite.position', sprite.position);
                console.log('sprite.scale', sprite.scale);

                let worldX3 = sprite.position.x + (uv.x - 0.5) * sprite.scale.x;
                let worldY3 = sprite.position.y; // Assuming the sprite lies in the XZ plane
                let worldZ3 = sprite.position.z + (uv.y - 0.5) * sprite.scale.y;
                
                let worldCoord3 = new THREE_Vector3(worldX3, worldY3, worldZ3);
                console.log('worldCoord3', worldCoord3);
                let NDC_Coord3 = worldCoord3.project(camera);
                console.log('NDC_Coord3', NDC_Coord3);
                
                console.log('camera.zoom', camera.zoom);

                console.log('intersects[0].point', intersects[0].point);

                let NDC_Coord = ThreejsUtil.WorldCoord_to_NDC_coord(camera, intersects[0].point);
                console.log('NDC_Coord', NDC_Coord);


                let worldCoord2 = ThreejsUtil.NDC_Coord_to_WorldCoord(camera, NDC_Coord);
                console.log('worldCoord2', worldCoord2);
        
            }

            console.log('xxxxxxxxxxxxxxxxxxxxxxxxxxx');
            
            return point2d_relativeToViewportInPixels;
        }

        return null;
    }

    static WorldCoord_to_NDC_coord(camera, worldCoord) {
        
        // If you transform the camera in 3D space and you directly use Vector3.project() or Vector3.unproject(), you have to call updateMatrixWorld().
        // https://discourse.threejs.org/t/ortho-camera-pixel-to-world-coordinate-world-coordinate-to-pixel/20719
        let NDC_coord = new THREE_Vector3( worldCoord.x, worldCoord.y, worldCoord.z ).project( camera );

        return NDC_coord;
    };

It’s hard to imagine what you’ve got and what result you want to achieve, having no visual explanation.
But, if the sprite takes all the viewport and has the same aspect as the screen, then NDC (which is in range [-1..1] on XY) from UV (which is in range [0..1] on XY) is:

let ndc = intersects[0].uv.clone().multiplyScalar(2).subScalar(1);

That’s all.
And this result should be equal to the result of how you compute pointer position for Raycaster:

	// calculate pointer position in normalized device coordinates
	// (-1 to +1) for both components

	pointer.x = ( event.clientX / window.innerWidth ) * 2 - 1;
	pointer.y = - ( event.clientY / window.innerHeight ) * 2 + 1;

:thinking:

1 Like