Three.js: RayCasting fails under certain circumstances

Created on 5 Jun 2017  路  9Comments  路  Source: mrdoob/three.js

Description of the problem

When casting a ray with Raycaster, it happens under some conditions in my test example that the caster fails to detect the object on top of it.

In the test example, when casting UP from one (or multiple) vertices of the rectangular block top, it does not detect the sphere above.

When varying the "influence" parameter, one can see the points that failed to being detected, as they are not morphed to match the sphere shape.

The error can be reproduced if the sphere is 70 unit of radius. However, if the radius is set to 100 units, then there are no errors.

var sphereGeo = new THREE.SphereGeometry(70); // if 100, no issue

Also, if the tessalation of the block is changed, by dividing the edges by 10 points per edge instead of 15, then the issue appears twice!

var geometry = new THREE.BoxGeometry(200, 100, 200, 15, 1, 15); // Change to 10, 1, 10 for double trouble!
Three.js version
  • [x] r85
Browser
  • [x] Chrome
OS
  • [x] Windows 10
Hardware Requirements (graphics card, VR Device, ...)

None observed.

[CODE]



three.js - Morph by Raycasting

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

<script src="OrbitControls.js"></script>
<script src="Detector.js"></script>
<script src="dat.gui.min.js"></script>

<script>
    if (!Detector.webgl) Detector.addGetWebGLMessage();

    var container;

    var camera, scene, renderer;

    var mesh;
    var morphMesh;
    var sphere;

    init();
    animate();

    function init() {

        container = document.getElementById('container');

        camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 15000);
        camera.position.z = 500;

        scene = new THREE.Scene();

        var light = new THREE.PointLight(0xff2200);
        light.position.set(100, 100, 100);
        scene.add(light);

        var light = new THREE.AmbientLight(0x111111);
        scene.add(light);

        //

        var geometry = new THREE.BoxGeometry(200, 100, 200, 15, 1, 15);
        var material = new THREE.MeshLambertMaterial({ color: 0xffffff, 
                                                       morphTargets: true ,
                                                       wireframe: true});

        var sphereGeo = new THREE.SphereGeometry(70);
        var sphereMat = new THREE.MeshPhongMaterial({ color: 0xff00ff, morphTargets: false });

        var morphGeo = geometry.clone();
        var morphMat = new THREE.MeshLambertMaterial({ color: 0xff00ff, morphTargets: false});

        {
            var vertices = [];

            for (var v = 0; v < morphGeo.vertices.length; v++) {
                vertices.push(morphGeo.vertices[v]);
            }

            geometry.morphTargets.push({ name: "sphere", vertices: vertices });
        }

        mesh = new THREE.Mesh(geometry, material);
        morphMesh = new THREE.Mesh(morphGeo, morphMat);
        sphere = new THREE.Mesh(sphereGeo, sphereMat);
        sphere.geometry.translate(0, 200, 0);

        mesh.geometry.morphTargets.push({name: "morphed", vertices: morphMesh.geometry.vertices});

        scene.add(mesh);
        scene.add(sphere);

        //

        findIntersection(mesh.geometry, sphere);

        //

        var params = {
            influence1: 0,
        };

        var gui = new dat.GUI();

        var folder = gui.addFolder('Morph Targets');
        folder.add(params, 'influence1', 0, 1).step(0.01).onChange(function (value) { mesh.morphTargetInfluences[0] = value; });
        folder.open();

        //

        renderer = new THREE.WebGLRenderer();
        renderer.setClearColor(0x222222);
        renderer.setPixelRatio(window.devicePixelRatio);
        renderer.setSize(window.innerWidth, window.innerHeight);
        renderer.sortObjects = false;
        container.appendChild(renderer.domElement);

        //

        controls = new THREE.OrbitControls(camera, renderer.domElement);

        //

        window.addEventListener('resize', onWindowResize, false);

    }

    function findIntersection(baseGeo, targetMesh) {

        var rayCaster = new THREE.Raycaster();
        var UP = new THREE.Vector3(0, 1, 0);

        var counter = 0;

        for (var v = 0; v < baseGeo.vertices.length; v++)
        {

            var vertex = baseGeo.vertices[v]; 

            // Reduce computation by only looking for top vertices
            if (vertex.y > 0)
            {
                rayCaster.set(vertex, UP);

                var intersects = rayCaster.intersectObject(targetMesh);

                if (intersects.length > 0)
                {

                    // Distance is hardcoded, but delta could be calculated
                    // relative to a target bounding box
                    var delta = 150 - intersects[0].distance;

                    morphMesh.geometry.vertices[v].y -= delta;
                }
            }
        }
    }

    function onWindowResize() {

        camera.aspect = window.innerWidth / window.innerHeight;
        camera.updateProjectionMatrix();

        renderer.setSize(window.innerWidth, window.innerHeight);

    }

    function animate() {

        requestAnimationFrame(animate);
        render();

    }

    function render() {

        renderer.render(scene, camera);

    }
</script>

Bug

Most helpful comment

Yes, testing against EPSILON will help here. I'm not 100% sure about this but there might be also other methods of three.jss math code where the usage of EPSILON is missing. A revision in this context is maybe a good idea...

All 9 comments

Apparently, Raycaster is hitting the seam between two triangles in the sphere, and recording no detection.

The ray is emanating from ( 20, 50, 20 ).

Try sphere.geometry.rotateY( 0.00001 );. That should work.

Try THREE.SphereBufferGeometry( 70, 8, 6 ), instead. That should work, too.

/ping @Mugen87 Not merging vertices in the SphereGeometry constructor also makes the problem go away.

This has come up before. I do not recall what we decided.

Thank you for the info!

Indeed, modifying the inputs does prevent the issue (or amplify). But this is a small proof of concept example. I'm more concerned about what could happen for more complex models. There could be cases where modifying, let's say by slightly rotating the mesh, could make one casting to succeed, but maybe another in the model would then fail.

It could be an effective workaround, but it does not guarantee the success of the algorithm.

As a more generic solution, shouldn't the edges also be part of the detection? They are definitely part of the mesh.

Thanks!
S1m

My comments were not intended as a workaround; they were intended as evidence that my conjecture is true.

You can help by creating a simpler example, with just a single ray and a sphere -- and from that, identifying the failure point.

You can help by creating a simpler example, with just a single ray and a sphere -- and from that, identifying the failure point.

Yes please. @S1m do you mind creating a jsfiddle? You can use this one as base.

Sure thing. @WestLangley @mrdoob I've tweaked the base fiddle to show a case of the issue with a single ray.

The flPos parameter can be set to a different X|Z value to move the Flashlight and see the expected behavior. Basically, the ray stops at the mesh. But, in that specific case, the ray doesn't and go through.

I've left the code I used to find the failure point, if it could be helpful.

On a strange note... I was not able to reproduce the issue when the ray was casted downwards, only upwards.

Here what is happening: It is possible for a point on an edge between two faces to be computed as "outside" the perimeter of _both_ of the neighboring faces. This can happen due to roundoff. Consequently, a ray can pass through a supposedly-solid mesh without intersecting it.

/ping @Mugen87 The only solution to this is -- I expect -- to allow a tolerance of EPSILON when testing for intersections. However, this can lead to a ray intersecting both a face, and the faces neighbor, simultaneously. (Actually, due to roundoff, that is likely happening with the current code anyway.)

Yes, testing against EPSILON will help here. I'm not 100% sure about this but there might be also other methods of three.jss math code where the usage of EPSILON is missing. A revision in this context is maybe a good idea...

I have small spheres (radius:3) what should I do to make raycaster work?

@hardikgw please, use the forum for help.

Was this page helpful?
0 / 5 - 0 ratings