Collide test not 100% accurate

I currently studying at collision detection. I searched on web and build a small test problem. As some website says, I use three.js raycast from object center to its vertex . I test if it intersect with the bounding box of the satellite which is a line mesh .The code is as following

let point;//vector3 (0,2,0)
let origin = new THREE.Vector3();
function test(A, B) {

    let local = point.clone();
    A.updateMatrixWorld();
    origin.set(A.position.x, A.position.y, 2);
    let global = local.applyMatrix4(A.matrix);
    global.z = 2;
    var direct = global.sub(origin);

    var ray = new THREE.Raycaster(origin, direct.normalize());
    var result = ray.intersectObjects(B);
    if (result.length > 0 && result[0].distance < direct.length()) {
        console.log("bang!")
    }

}

I only test one point on each bullet. In order to see the lines completely , I set all the points’ position.z to 2
The problem is some times it test ok sometimes not.

As you can see in the video. I fire 5 bullet but sometimes it only hit 4.

Unfortunately, I don’t understand what you are doing in your code so let me provide you a general guide for implementing a robust bullet collision detection.

  • The line of fire is usually represented as a ray.
  • In each simulation step, the origin of the ray is the previous position of the bullet.
  • You now perform an intersection test with the obstacles in your game environment.
  • If there is an intersection, computed the (squared) euclidian distance between the origin of the ray and the intersection point. Let’s call this distance d1.
  • Now compute the “new” position of the bullet for this simulation step and the (squared) euclidian distance between the new position and the previous one. Let’s call this distance d2.
  • If d1 <= d2, you have a hit.

A working example of this code is presented in the following demo of Yuka, a library of developing game AI. The API is similar to three.js, so it should be no problem use the code as a template.

FPS Example

The related code section is encapsulated in the update method of the Bullet class.

2 Likes

thank you @Mugen87 I think I should first build a smaller debug scene and find the problem

After watching your video,I think if you want to check the bullet in 2D scene ,It is not an appropriate way to use ‘‘raycaster’’,It checks all vertices in the parameterwhich your pass and cost a lot of performance.It is a good idea to use a 2D collide algorithm to check if the collision happen.

if( (target.position.x - bullet.position.x)^2 + (target.position.y -bullet.position.y)^2 < R^2){
     collision_function()
}

:joy:sorry for my bad English.

sounds good , I would have a try,BTW ,your english is better than me :rofl:

Keep in mind that the first step of a ray intersection test normally happens against a bounding volume (like a bounding sphere or an AABB) which is very fast. For an accurate collision detection against a geometry, using a ray is definitely a valid approach

Oh,thanks, I remember .It is too careless of me!

I build a small test program,here is the code , I failed to build it in fiddle. So I post here
html:

<!DOCTYPE html>
<html>

<head>
<title>ray intersect line</title>
<link rel="stylesheet" href="/threejsExample/f06-ray/main.css">
<script src="/lib/three.js"></script>
<script src="/lib/TrackballControls.js"></script>
<script src="/lib/tween.js"></script>

</head>

<body>
<div class="container">
    <div class="fire"></div><div class="hit"></div>
    <canvas class="scene"></canvas>
</div>
<script src="/threejsExample/f06-ray/main.js"></script>
</body>

</html>

css:

body{
margin: 0;
padding: 0;
}
.container {
width: 640px;
height: 480px;
position: relative;
overflow: hidden;
border: solid 1px blue;
}
.fire{
position: absolute;
right: 0;
bottom: 0;
}
.hit{
position: absolute;
right: 0;
bottom: 30px;
}

js:

function main() {
    init();
    animate();
}
let container;
let scene, camera, renderer;
let controls;
let clock;

let bullets = [];
bullets.remove = removeElemtFromArray;
function removeElemtFromArray(elem) {
    this.splice(this.indexOf(elem), 1);
}
let bBoxs = [];
function init() {
    container = document.querySelector(".container");
    camera = new THREE.PerspectiveCamera(60, container.clientWidth / container.clientHeight, 0.1, 2000);
    camera.position.z = 200;
    camera.position.y = 50;
    control3D = new THREE.TrackballControls(camera);
    control3D.rotateSpeed = 1.0;
    control3D.zoomSpeed = 1.2;
    control3D.panSpeed = 0.8;
    control3D.noZoom = false;
    control3D.noPan = false;
    control3D.staticMoving = true;
    control3D.dynamicDampingFactor = 0.3;
    control3D.target.set(0, 50, 0);
    scene = new THREE.Scene();
    let canvas = document.querySelector(".scene")
    let context = canvas.getContext('webgl2');
    renderer = new THREE.WebGLRenderer({ canvas: canvas, context: context });
    renderer.setPixelRatio(window.devicePixelRatio);
    renderer.setSize(container.clientWidth, container.clientHeight);
    renderer.setClearColor(0x777777, 0);
    container.appendChild(renderer.domElement);

    window.addEventListener('resize', onWindowResize, false);
    clock = new THREE.Clock();


    //bullet
    new ControlGun();

    //boundingBox
    let wid = 50;
    let hei = 50;
    let hwid = wid / 2;
    let hhei = hei / 2;
    let topLeft = new THREE.Vector3(-hwid, hhei, 0);
    let bottomLeft = new THREE.Vector3(-hwid, -hhei, 0);
    let topRight = new THREE.Vector3(hwid, hhei, 0);
    let bottomRight = new THREE.Vector3(hwid, -hhei, 0);
    material = new THREE.LineBasicMaterial({
        color: 0xff0000
    });
    geometry = new THREE.Geometry();
    geometry.vertices.push(
        bottomRight,
        bottomLeft,
        topLeft,


    );
    boundingBox = new THREE.Line(geometry, material);
    boundingBox.position.set(100, 100, 0);
    scene.add(boundingBox);
    bBoxs.push(boundingBox);
}
function onWindowResize() {
    camera.aspect = container.clientWidth / container.clientHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(container.clientWidth, container.clientHeight);
}
function animate() {
    var delta = clock.getDelta();
    requestAnimationFrame(animate);
    ControlGun.update(delta);
    TWEEN.update();

    testCollide()
    control3D.update();
    render();
}
function render() {
    renderer.render(scene, camera);
}
let point = new THREE.Vector3(0, 2, 0);//test point on bullet
let hit = 0;
let hitinfo = document.querySelector(".hit");
function testCollide() {
    for (let i = 0; i < bullets.length; i++) {
        let bullet = bullets[i];
        bullet.updateMatrixWorld();
        let local = point.clone();
        let global = local.applyMatrix4(bullet.matrix);
        var direct = global.sub(bullet.position);

        var ray = new THREE.Raycaster(bullet.position, direct.normalize());
        var result = ray.intersectObjects(bBoxs);
        if (result.length > 0 && result[0].distance < direct.length()) {
            hit++;
            hitinfo.textContent = "hit:" + hit;
            console.log("bang!");
        }
    }

}

function ControlGun() {
    let shotInterval = 0.3;
    let count = 0;

    ControlGun.update = function updateGunControl(delta) {
        if (count == 0)
            fire();
        count += delta;
        if (count >= shotInterval) {
            count = 0;
        }

    }


    function createBullet() {
        let geometry = new THREE.CircleGeometry(2, 4);
        let material = new THREE.MeshBasicMaterial({
            color: 0x000000,
        })
        bullet = new THREE.Mesh(geometry, material);
        scene.add(bullet);
        return bullet;

    }
    let target = new THREE.Vector3();
    let direct = new THREE.Vector3();
    let initDirect = new THREE.Vector3(0, 1, 0);
    let total = 0;
    let info = document.querySelector(".fire");
    function fire() {
        let bullet = createBullet();
        if (bullet == null) return;
        //bullet init position
        bullet.position.set(0, 0, 0);
        //bullet target
        target.set(450, 500, 0);
        direct.subVectors(target, bullet.position);
        let angle = initDirect.angleTo(direct);
        bullet.rotation.z = angle;

        scene.add(bullet);
        bullets.push(bullet);
        new TWEEN.Tween(bullet.position)
            .to({ x: target.x, y: target.y }, 1000)
            .start()
            .onComplete(function () {
                bullets.remove(bullet);
            });

        total++;
        info.textContent = "total:" + total;
    }

}
main();

you can see the problem that is the hit and fire are not same . But I will continue try methods you mentioned before

sorry about my math , I try to understand your approach , So I draw this picture. But I still don’t understand why d1<d2 then hit. d1 < d2 looks impossible in my comprehension
aa

sorry I make mistake about your approach . The correct pic should be like this. Now I can understand ,Thank you, let me continue my test
aa

Hi, @Mugen87. I finally did it. Your method is awesome. It is very simple and accurate (never lose a hit). I think it could handle all the collision detection in my game. Here is the result!


The code change to the following

    let hit = 0;
    let hitinfo = document.querySelector(".hit");
    let d1;
    let d2;
    let d2vec = new THREE.Vector3();
   function testCollide() {
    let i = bullets.length;
    while (i--) {
        let bullet = bullets[i];
        bullet.updateMatrixWorld();
        var ray = new THREE.Raycaster(bullet.lastPosition, bullet.lastDirect);
        var intersects = ray.intersectObjects(bBoxs);
        if (intersects[0] == undefined) return;
        d1 = intersects[0].distance;
        d2vec.subVectors(bullet.position, bullet.lastPosition);
        d2 = d2vec.length();
        if (d1 <= d2) {
            scene.remove(bullet);
            bullets.remove(bullet);
            bullet.tween.stop();

            hit++;
            hitinfo.textContent = "hit:" + hit;
        }
        bullet.lastPosition.copy(bullet.position);
        bullet.lastDirect.copy(bullet.direct);
    }
}

thanks again
BTW, I see you build your own library YUKA, Can I use it without three.js or combine with three.js . How to install it . I don’t find document for novice

1 Like

Hi , I have an additional problem. Now I can test hit well. But In real game. There should not be a bounding box displayed. How can I hide the line while still collide with the bullet. I tried set material to transparent, but it seems no effect to lineMaterial. I also try set mesh .visible =false, thus the raycast function don’t test it any more . Help!:sweat_smile:

Yuka is a new project and we have not provided tutorials so far. But as mentioned at github you can download the repo and then execute npm install && npm start in order to start a local server and work with the examples locally.

Yuka is not intended to replace three.js. It’s a pure AI engine. You still need a 3D engine like three.js or BabylonJS to visually represent your game entities. You can combine Yuka with any JavaScript 3D engine you like. The idea is to map the transformation of your game entities on their respective render components per frame. This could be for example a THREE.SkinnedMesh or any other type of THREE.Object3D.

A bounding box is normally of type THREE.Box3 which has not geometry. So it can’t be rendered. Do you visualize the AABB with THREE.Box3Helper?

I don’t use box3 display my boundingbox. I just use a line mesh whose points just form a rectangle. And I use three.js Raycast to test the line . It successed , but with a price that the line must be displayed:sweat_smile: I current plan to change the method and use THREE.Ray and ray.distanceSqToSegment to test line segment directly. I think Box3 is for 3D game . And my mesh are all plane object that do not have a thickness

after a whole day of struggle of search web and thinking. I finally work out a solution use ray.distanceSqToSegment .

the code change to following

 let d1;
let d2;
let d2vec = new THREE.Vector3();
Collide.update = function testCollide(delta) {
    if (bullets.length == 0 || bBoxs.length == 0) return;
    let i = bullets.length;
    while (i--) {
        let bullet = bullets[i];
        bullet.updateMatrixWorld();
        let testPoint = bullet.position.clone();
        testPoint.z = 0;
        let ray = new THREE.Ray(bullet.lastPosition, bullet.lastDirect);
        d2vec.subVectors(testPoint, bullet.lastPosition);
        d2 = d2vec.lengthSq();
        if (d2 == 0) return;
        intersectBboxs(ray, bBoxs, bullet);

        bullet.lastPosition.copy(testPoint);
        bullet.lastDirect.copy(bullet.direct);
    }
}
let intersetPoint = new THREE.Vector3();
let satPosRot = new THREE.Vector3();
let quaternion = new THREE.Quaternion();

function intersectBboxs(ray, bBoxs, bullet) {
    for (let i = 0; i < bBoxs.length; i++) {
        let bBox = bBoxs[i];
        for (let j = 0; j < bBox.length; j++) {
            let segPoints = bBox[j];
            let v1 = segPoints[0].clone();
            let v2 = segPoints[1].clone();
            satPosRot.setFromMatrixPosition(bBox.host.scene.matrixWorld);
            bBox.host.scene.getWorldQuaternion(quaternion);
            satPosRot.applyQuaternion(quaternion);
            v1.add(satPosRot);
            v2.add(satPosRot);
            if (!facePoint(v1, v2, bullet.lastPosition)) continue;
            let distSq = ray.distanceSqToSegment(v1, v2, intersetPoint);
            console.log(distSq);
            if (distSq <= 0.1) {
                d1 = ray.origin.clone().sub(intersetPoint).lengthSq();
                if (d1 <= d2) {
                    BulletsMaker.reclaim(bullet);
                    //SatelliteMaker.reclaim(bBox.host);
                    console.log("bang");
                    return;
                }
            }
        }
        console.log('-----------------');
    }

}

function facePoint(v1, v2, p) {
    let d = (p.x - v1.x) * (v2.y - v1.y) - (p.y - v1.y) * (v2.x - v1.x);
    if (d <0) return true;
    else return false;
}

There is a interesting fact that as you watching the above video you may notice I output the result of ray.distanceSqToSegment. I find it not accurate .it is not alway zero as expected when the ray intersect segment. I must use a threshold value. But it is more accurate when the bullet get closer to the target.

I’m currently studying at box3 in order to max leverage the convinience of three.js . My question is : how to let the box3 follow to object. I find applyMatrix4. I originly thought It could let the box3 follow the objects transformation include translate rotate and scale. When I test this function I found its not working. So What is the proper approach that could let the box3 follow the object. and What is the applyMatrix4 used for ?

Hi, @Mugen87. I find accurate problems again. Now I am sure that It will be 100% accurate when the obj I shoot is static . But , if the target move,then there will be chance of losing hit. What is your thought?? thanks :pray:
I draw a picture :

losthit

If the geometry of the object does not change then it should be sufficient to use Box3.applyMatrix4 with the world matrix of the respective 3D object.

I build a fiddle , Why the box3 fly away instead of follow the plane ? I use it in a tween . Is it mean I can’t use with tween.js?
https://jsfiddle.net/relaxslow/wk5x7fby/