chandlerprall / Physijs

Physics plugin for Three.js
MIT License
2.77k stars 455 forks source link

Movement on terrain #268

Open SET001 opened 8 years ago

SET001 commented 8 years ago

I'm trying to implement players ability to move among terrain (based on heightmap) and experiencing few issues here:

  1. I want to avoid players rotation. It should fall, it should slide down but never rotate.
  2. It should not slide down from easy hills.
  3. It should not be able to climb on steep hills.

drawing 2

Here is a simple video that demonstrate this problems: https://www.youtube.com/watch?v=15X7tLMjsPg

  1. I was manage to avoid rotations doing something like this on EACH frame call:

        this.get('mesh').object.rotation.x=0;
        this.get('mesh').object.rotation.y=0;
        this.get('mesh').object.rotation.z=0;
        this.get('mesh').object.__dirtyRotation = true;

Are there any other ways to avoid object rotation without having to execute some code each frame call? Also I thought I can set setAngularVelocity to 0

this.get('mesh').object.setAngularVelocity(new THREE.Vector3(0, 0, 0));

but for some reason it does not helped

2, 3, I don't have any idea how to solve this.

Terrain and player materials are set to

1, // medium friction
0 // low restitution

gravity is set to (0, -30, 0 )

Any help are highly appreciated.

ghost commented 8 years ago

Get ready @SET001, because this may take a while...

For your first problem, if you want a solution without putting it in the physics loop you can set the angular factor of your physijs mesh:

this.get("mesh").object.addEventListener("ready", function(){
    this.get("mesh").object.setAngularFactor(new THREE.Vector3(0, 0, 0));
});

The angular factor of an object is basically a vector determining whether or not the final angular velocities should be applied on the mesh (one number for each axis). So, if the factor is an empty vector, no force would be applied for rotation, thus eliminating rotation. This method only needs to be called once (when your object is added to the world), which is why it is put in a 'ready' event.

For your second and third problem, you're going to have to use a little more thinking. First off, you need to determine at what slope angle the character will stop climbing and start falling. I recommend a number between 40 and 45 degrees, but it is up to you. I'll use 45 degrees here. Next, for every frame, you need to:

Now that we've got our priorities in order, we can begin. You should probably create a new function dedicated to this process. Then, you need to create a THREE.Raycaster (preferably outside of the new function) with its first parameter, ray origin, set to the character position, and its second parameter, ray direction, set to new THREE.Vector3(0, -1, 0), which will make the ray point directly down. In the function, make the raycaster intersect the heightmap mesh with the .intersectObject method, explained here. After getting an array of intersections, you simply take the first intersection (which is the closest) and pass it on to the next part.

We have the intersecting triangle, but what do we do with it? Well, our next challenge is to find out how steep the triangle is. Fortunatly for us, THREE.js provides a utility function in THREE.Vector3 called .angleTo. Just use the upwards unit vector (a.k.a. new THREE.Vector3(0, 1, 0)) and set the function parameter to the triangle normal.

Finally, we know where the character is on the heightmap and how steep the place where it's standing. All we have to do now is take action, which can be done with a simple if statement. Check if the slope angle is higher than your limit, in radians (in this case 45 degrees, so about 0.707 radians I think), or if it is lower than your limit, again in radians, and disable climbing / sliding. To achieve the first situation, you can ignore all key inputs so that no code will be run. To achieve the second situation, you can set the linear factor to new THREE.Vector3(0, 0, 0). Feel free to make your own handler.

Some example code:

// Note: I refer to 'player' as character object and 'heightmap' to heightmap object.

// Raycaster init code
var raycaster = new THREE.Raycaster(player.position.clone(), new THREE.Vector3(0, -1, 0));

// Slope limit - the angle that tells when players start sliding down (in radians)
var SLOPE_LIMIT = 0.707; // 45 deg

// Call this every frame!
function heightmapPhysics(){
    var panel, intersects;

    // Always update player position to raycaster!
    raycaster.ray.origin.copy(player.position);

    // Find closest intersection
    intersects = raycaster.intersectObject(heightmap, false);

    if(intersects.length){
        panel = raycaster.intersectObject(heightmap, false)[0].face;
    }

    // Get angle between triangle normal and upwards unit vector
    var slope_ang = new THREE.Vector3(0, 1, 0).angleTo(panel.normal);

    // Take appropriate action
    if(slope_ang < SLOPE_LIMIT){
        // Stop sliding
    } else {
        // Stop climbing
    }
}

Looks easier in JS, right? Hope this helps.

SET001 commented 8 years ago

@xprogram , thank you so much for you expanded response. Actually just using setAngularFactor inside ready callback solved almost all my problems - player is not sliding on small landscape angles and is not climbing on steep. However it still behaves very strange in many cases and I does not have control over exact landscape angles. All this appeared very tricky.

ghost commented 8 years ago

@SET001 I'm glad to know my solution is working! I'm just having trouble understanding your problem with control over landscape angles. Could you explain what seems tricky?

SET001 commented 8 years ago

@xprogram the tricky is a whole player controls based on physijs. I have so much problems and so little progress here:

  1. About 1 of 5 times my player falling through landscape. I enabled motion clamping as suggested in docs but this does not change a thing. Collision event triggered and maybe I can do something manually to prevent falling but this is not good.
  2. About landscape angles - I mean I want to have some control over that on which landscape angle player should slide down. On which it should not be possible to move forward. For this I should play with your approach.
  3. Movements based on setLinearVelocity are not smooth. I also tried applyCentralImpulse but still they are not smooth. I'm using following approach:

    moveForward(){
        var box = this.component.entity.get('mesh').object;
        this.velocity = Math.abs(box.getLinearVelocity().z) + Math.abs(box.getLinearVelocity().x);
        if (!this.isJumping && this.velocity < this.moveSpeed){
            var matrix = (new THREE.Matrix4()).makeRotationFromEuler( box.rotation ); 
            var velocity = (new THREE.Vector3( 0, 0, -1 ).applyMatrix4(matrix)).normalize().multiplyScalar(this.moveSpeed-this.velocity);
            box.setLinearVelocity(box.getLinearVelocity().add(velocity));
        }
    }

you can try this here http://smartass.su/terrain_movements/ (use mouse and wasd+space)

ghost commented 8 years ago

@SET001 I found a great article about FPS physics (not for JS though, but explains concept well), I think it is something you want to look at (might help with problem 2): http://oxleygamedev.blogspot.ca/2011/04/player-physics-part-2.html

For your 1st problem, a working solution with some event listeners:

// Note: I reference the scene as 'scene' and player as 'box'

var prevPos = new THREE.Vector3(0, 0, 0);

scene.addEventListener("update", function(){
    prevPos.copy(box.position);
});

box.addEventListener("collision", function(other_obj){
    if(box.position.distanceTo(prevPos) > 10){
        box.__dirtyPosition = true;
        box.position.copy(prevPos);
        box.y += box.geometry.boundingBox.size().y;
        box.setLinearVelocity(new THREE.Vector3(0, 0, 0));
    }
});

About your 3rd problem: in the function moveForward you are checking if the velocity is slower than the speed required in an incorrect way. Instead, try this:

this.velocity = box.getLinearVelocity().length();
if(!this.isJumping && this.velocity < this.moveSpeed){
    // ...
}
SET001 commented 8 years ago

@xprogram, About first problem - why it does not work without that hack you proposed? Why something sometimes keep moving after collision?

ghost commented 8 years ago

@SET001 I wonder about that myself...

With CCD (Continuous Collision Detection) objects moving at high speed should be stopped from any object in its way, regardless of velocity. As long as your swept sphere radius is less than your player size (box size) and your threshold is about player size, CCD should function normally (according to Physijs and Bullet Physics docs). I haven't really played with it, but maybe setting swept sphere radius and motion threshold for the heightmap can help too.

SET001 commented 8 years ago

@xprogram, I found that falling through the ground happens if I will set __dirtyRotation on each frame. Here is my test case for this situation - http://smartass.su/terrain_movements/raw.html If you only remove that line from render loop - it will never fall through the ground. Why I need to set __dirtyRotation on each frame is because without it my mouse controls, that binded to physijs object, behaves strange when rotating more that 180`. Here is the demonstration of problem with mouse rotation - http://smartass.su/terrain_movements/raw_mouse.html

SET001 commented 8 years ago

Eventually I found the secret formula for camera rotations:

var mesh = this.component.entity.get('mesh').object;
this.rotation.y += this.mouseAcceleration*Math.abs(foo.mouseLeft);
mesh.rotation.copy(this.rotation);
mesh.__dirtyRotation = true;
Thundros commented 5 years ago

Does anyone have an example for the above code? I am looking for one. Thanks!

kourindouhime commented 4 years ago

I've noticed reducing terrain resolution (for example going from 512x512 to 128x128 bitmap) increases chances of successful landing. With 512x512 capsule collider almost always gets stuck/falls through for me

kourindouhime commented 4 years ago

Also reducing timeStep helps too. Switched from fixedTimeStep 1/60 to 1/180 and objects no longer fall through terrain.