Three.js First Person Controls - javascript

I'm playing around with Three.js and WebGL and can't quite get the controls the way I want. I chose to try to "roll my own" controls since Three.js's FirstPersonControls do not use pointer lock.
Anyway, I took most of my code from the built-in FirstPersonControls, converted it to use pointer lock (movementX instead of pageX - offset), but I am having trouble smoothing the look motion.
Here is my onMouseMove (using originalEvent since it is a jquery event):
onMouseMove: function(e) {
if(!document.pointerLockElement) return;
var moveX = e.originalEvent.movementX ||
e.originalEvent.mozMovementX ||
e.originalEvent.webkitMovementX ||
0,
moveY = e.originalEvent.movementY ||
e.originalEvent.mozMovementY ||
e.originalEvent.webkitMovementY ||
0;
//Update the mouse movement for coming frames
this.mouseMovementX = moveX;
this.mouseMovementY = moveY;
}
And my Controls.update() (called on each animation frame, with the THREE.Clock delta):
update: function(delta) {
if(this.freeze) {
return;
}
//movement, works fine
if(this.moveForward) this.camera.translateZ(-(actualMoveSpeed + this.autoSpeedFactor));
if(this.moveBackward) this.camera.translateZ(actualMoveSpeed);
if(this.moveLeft) this.camera.translateX(-actualMoveSpeed);
if(this.moveRight) this.camera.translateX(actualMoveSpeed);
/////////
//ISSUES ARE WITH THIS CODE:
/////////
//look movement, really jumpy
this.lon += this.mouseMovementX;
this.lat -= this.mouseMovementY;
this.lat = Math.max(-85, Math.min(85, this.lat));
this.phi = (90 - this.lat) * Math.PI / 180;
this.theta = this.lon * Math.PI / 180;
this.target.x = this.camera.position.x + 100 * Math.sin(this.phi) * Math.cos(this.theta);
this.target.y = this.camera.position.y + 100 * Math.cos(this.phi);
this.target.z = this.camera.position.z + 100 * Math.sin(this.phi) * Math.sin(this.theta);
this.camera.lookAt(this.target);
}
This code does work, but moving the camera is jumpy as the mouse moves around. I could really use some help figuring out how to smooth it.
You can see what I mean by "jumpy" here. I'm new to Three.js, WebGL, and just 3D in general so any help is appreciated.
Thanks,
-Chad
EDIT After working with #przemo_li, here is the working code he came up with:
onMouseMove: function(e) {
if(!document.pointerLockElement) return;
var moveX = e.originalEvent.movementX ||
e.originalEvent.mozMovementX ||
e.originalEvent.webkitMovementX ||
0,
moveY = e.originalEvent.movementY ||
e.originalEvent.mozMovementY ||
e.originalEvent.webkitMovementY ||
0;
//Update the initial coords on mouse move
this.mouseMovementX += moveX; //aggregate mouse movements as a total delta delta
this.mouseMovementY += moveY;
},
update: function(delta) {
if(this.freeze) {
return;
}
//movement
if(this.moveForward) this.camera.translateZ(-(actualMoveSpeed + this.autoSpeedFactor));
if(this.moveBackward) this.camera.translateZ(actualMoveSpeed);
if(this.moveLeft) this.camera.translateX(-actualMoveSpeed);
if(this.moveRight) this.camera.translateX(actualMoveSpeed);
//look movement
this.lon += this.mouseMovementX;
this.lat -= this.mouseMovementY;
this.mouseMovementX = 0; //reset mouse deltas to 0 each rendered frame
this.mouseMovementY = 0;
this.phi = (90 - this.lat) * Math.PI / 180;
this.theta = this.lon * Math.PI / 180;
if(this.constrainVertical) {
this.phi = THREE.Math.mapLinear(this.phi, 0, Math.PI, this.verticalMin, this.verticalMax);
}
this.target.x = this.camera.position.x + 100 * Math.sin(this.phi) * Math.cos(this.theta);
this.target.y = this.camera.position.y + 100 * Math.cos(this.phi);
this.target.z = this.camera.position.z + 100 * Math.sin(this.phi) * Math.sin(this.theta);
this.camera.lookAt(this.target);
}

'Official' version just added: https://github.com/mrdoob/three.js/blob/master/examples/js/controls/PointerLockControls.js

1)Constraints?
In your code you limit mouse X movement to -|+ 85 Its unlikely that such constraint is needed.
2)Aggregate all events that happen during frame
In your code you override mouse movement with each new event. So if you get 3 events during frame only most recent will be stored.
Add those movements. Than after rendering frame you can clear count. And start gathering events again.

Related

Calculate angle based on x, y position

I am trying to calculate the angle for an arrow on a ball, based on the position where it is going to.
The arrow moves, but in a total unexplainable direction, can anybody give some pointers?
Codepen available: Codepen
I added the full code on here (EDITED based on input):
I added a step to make the difference bigger for the angle calculation, not sure if that is the right way to go, but it seems a bit more functional. Plus added the +/- 90 in the angle method, but that doesnt seem to fix it. It is still feeling odd.
class Throwable {
constructor(){
this.throwObject = null;
this.canDrag = null;
this.initialDiffX = 0;
this.initialDiffY = 0;
this.previousX = 0;
this.previousY = 0;
this.intervalCounter = 0;
}
set x(input) {
this.throwObject.style.left = input + 'px';
}
set y(input) {
this.throwObject.style.top = input + 'px';
}
set rotation(input) {
this.throwObject.style.transform = `rotate(${input}deg)`;
}
init(){
this.throwObject = document.querySelector('.throwable');
this.throwObject.addEventListener('mousedown', this.activateDrag.bind(this));
this.throwObject.addEventListener('mouseup', this.deactivateDrag.bind(this));
document.addEventListener('mousemove', this.drag.bind(this));
}
activateDrag(event) {
this.canDrag = true;
this.initialDiffX = event.clientX - this.throwObject.offsetLeft;
this.initialDiffY = event.clientY - this.throwObject.offsetTop;
}
deactivateDrag() {
this.canDrag = false;
}
drag(event) {
if(this.canDrag === true) {
if(this.intervalCounter >= 30) {
this.intervalCounter = 0;
}
if(this.intervalCounter === 0) {
this.previousX = event.clientX;
this.previousY = event.clientY;
}
this.intervalCounter++;
this.y = event.clientY- this.initialDiffY;
this.x = event.clientX - this.initialDiffX;
this.rotation = this.angle(event.clientX, event.clientY, this.previousX, this.previousY);
}
}
angle(ex, ey, cx, cy) {
var dy = ey - cy;
var dx = ex - cx;
return Math.atan2(dy, dx) * 180 / Math.PI + 90;
}
// Untility
log(logObject) {
let logStr = '';
for(let key in logObject) {
logStr += `${key}: ${logObject[key]}<br>`;
}
document.getElementById('log').innerHTML = logStr;
}
}
let throwable = new Throwable();
throwable.init();
I made a mistake in comparing two different values, I fixed that, it is working way better, still have some odd behavior sometimes, seems like it doesnt know where to go in some points. But working better than before.
Maybe you have some mistakes in your angle function. This works for me:
angle(cx, cy, ex, ey) {
var dy = ey - cy ;
var dx = cx - ex ;
return Math.atan2(dx, dy) * 180 / Math.PI;
}
When you call this.angle() you give it twice this.throwObject.offset..., once directly and once via px and py:
let px = this.throwObject.offsetLeft;
let py = this.throwObject.offsetTop;
this.rotation = this.angle(this.throwObject.offsetLeft, this.throwObject.offsetTop, px, py)
That will result in dx and dy to be 0 in angle() making the result of Math.atan2() unpredictable.
I'm not sure about the rest of your code, but maybe you meant to call angle() like this:
this.rotation = this.angle(this.x, this.y, px, py);
There are a couple small issues that I can see.
First, the angle method is calculating radians in range of -180 to 180 and you want it to be 0 to 360. So after angle calculation you'll want to convert something like this:
angle(ex, ey, cx, cy) {
var dy = ey - cy;
var dx = ex - cx;
var theta = Math.atan2(dy, dx) * 180 / Math.PI;
if (theta < 0) theta += 360; // convert to [0, 360]
return theta;
}
Second, the starting angle of your element at 0 degrees is not the actual 0 degrees calculated by this method due to how js coordinates work. A quick fix is to add 90 degrees to make it match:
set rotation(input) {
this.throwObject.style.transform = `rotate(${input + 90}deg)`;
}
It's still a little janky after these conversion but I think it's a start on the right calculations. My guess is part of the issue is having such close points for calculation.
This happens because there's a difference how angles are measured between Math.atan2() and the CSS rotate transformation.
For us humans it's natural that the 12 o' clock position on an analog clock refers to the angle 0 - same for CSS rotate.
Math.atan2() however measures the angle starting from the horizontal x axis. So depending on your input coordinates it would be the 3 or 9 o' clock position.
There's an easy fix however.
After calculating the angle
Math.atan2(dy, dx) * 180 / Math.PI
just subtract 90 degrees like
Math.atan2(dy, dx) * 180 / Math.PI - 90
What happens when intervalCounter become 0? The previus point moved to the event point, so dy, dx becomes 0 and you have a jitter: -180 + 90, +180 + 90, 0 + 90 as defined in Math.atan2. After that, the previus point is fixed until intervalCounter < 30 and you have some inceasing distance between the previus and event points, so the angle is close to the expected one.
Anyway, this is a bad coordinate filter. You can improve it by implementing simple exponential filtering or by using fixed size (30 in your case) queue for event point.

Update moment.js duration with ionic touch event

I am building an iOS application with Ionic. The application is a timer where the user will specify a time limit and then countdown that time for a certain activity. I am trying to achieve an interaction where the user will drag a handle around the outside of a circle and each clockwise rotation will increment the time limit by one minute, and going the opposite direction will decrement by one.
I have the circle working, where you can drag the handle and it will adhere to the bounds of the container. Now I am trying to use Moment.js to create the countdown, but am having a tough time getting the timer values to update inside of the touch event.
The $scope.duration variable is what I am using to track the timer value. I have tried using the moment().duration() method to specify a duration object and am initializing it to '00:00:00'. When I try to update that value in the touch gesture event, I am unable to update the timer value. I am assuming I either don't understand how to correctly apply updated scope values in Angular/Ionic, I don't know how to correctly use Moment.js, or quite possibly - both.
Here is my template code:
<ion-view hide-nav-bar="true" view-title="Dashboard">
<ion-content>
<div class="timer">
<div class="timer-slider"></div>
<span class="timer-countdown">
{{duration}}
</span>
</div>
</ion-content>
</ion-view>
And my large controller:
.controller('DashCtrl', function($scope, $ionicGesture) {
var $timer = angular.element(document.getElementsByClassName('timer')[0]);
var $timerSlider = angular.element(document.getElementsByClassName('timer-slider')[0]);
var timerWidth = $timer[0].getBoundingClientRect().width;
var sliderWidth = $timerSlider[0].getBoundingClientRect().width;
var radius = timerWidth / 2;
var deg = 0;
var X = Math.round(radius * Math.sin(deg*Math.PI/180));
var Y = Math.round(radius * -Math.cos(deg*Math.PI/180));
var counter = 0;
$scope.duration = moment().hour(0).minute(0).second(0).format('HH : mm : ss');
// Set timer circle aspect ratio
$timer.css('height', timerWidth + 'px');
$timerSlider.css({
left: X + radius - sliderWidth / 2 + 'px',
top: Y + radius - sliderWidth / 2 + 'px'
});
$ionicGesture.on('drag', function(e) {
e.preventDefault();
var pos = {
x: e.gesture.touches[0].clientX,
y: e.gesture.touches[0].clientY
};
var atan = Math.atan2(pos.x - radius, pos.y - radius);
deg = -atan/(Math.PI/180) + 180; // final (0-360 positive) degrees from mouse position
// for attraction to multiple of 90 degrees
var distance = Math.abs( deg - ( Math.round(deg / 90) * 90 ) );
if ( distance <= 5 || distance >= 355 )
deg = Math.round(deg / 90) * 90;
if(Math.floor(deg) % 6 === 0) {
console.log(Math.floor(deg));
$scope.duration = moment().hour(0).minute(0).second(counter++).format('HH : mm : ss');
}
if (deg === 360)
deg = 0;
X = Math.round(radius * Math.sin(deg * Math.PI / 180));
Y = Math.round(radius * -Math.cos(deg * Math.PI / 180));
$timerSlider.css({
left: X + radius - sliderWidth / 2 + 'px',
top: Y + radius - sliderWidth / 2 + 'px'
});
}, $timerSlider);
})
I hacked up a CodePen demo. It doesn't track the drag event all that well without a mobile format, but you can get the idea of what I am going for.
http://codepen.io/stat30fbliss/pen/xZRrXY
Here's a screenshot of the app in-progress
After updating $scope.duration, run $scope.$apply() and it should start working :)

Returning precise vector components in js canvas

I have been wrestling with rendering an animation that fires a projectile accurately from an "enemy" node to a "player" node in a 2D 11:11 grid (0:0 = top-left) in JS/Canvas. After a lot of reading up I've managed to get the shots close, but not quite bang on. I think my velocity function is a little out but I really don't know why. This is the trigonometric function:
this.getVelocityComponents = function(speed){
// loc (location of enemy actor) = array(2) [X_coord, Y_coord]
// des (destination (ie. player in this instance)) = array(2) [X_coord, Y_coord]
var i, sum, hyp, output = [], dis = [];
var higher = false;
for (i in loc) {
sum = 0;
if (loc[i] > des[i])
sum = loc[i] - des[i];
if (loc[i] < des[i])
sum = des[i] - loc[i];
dis.push(sum);
}
hyp = Math.sqrt(Math.pow(dis[X], 2) + Math.pow(dis[Y], 2));
if (dis[X] > dis[Y]) {
output[X] = (speed * Math.cos(dis[X]/hyp))
output[Y] = (speed * Math.sin(dis[Y]/hyp))
} else if (dis[X] < dis[Y]) {
output[X] = (speed * Math.cos(dis[Y]/hyp))
output[Y] = (speed * Math.sin(dis[X]/hyp))
}
return output;
}
and this is the instruction that tells the X and the Y of the projectile frame to advance:
var distance = [];
for (i in loc) {
var sum = 0;
if (loc[i] > des[i])
sum = loc[i] - des[i];
if (loc[i] < des[i])
sum = des[i] - loc[i];
distance.push(sum);
}
if (distance[X] > distance[Y]) {
frm[X] += (loc[X] < des[X]) ? v[X] : -v[X];
frm[Y] += (loc[Y] < des[Y]) ? v[Y] : -v[Y];
} else {
frm[Y] += (loc[Y] < des[Y]) ? v[X] : -v[X];
frm[X] += (loc[X] < des[X]) ? v[Y] : -v[Y];
}
Below is a screenshot. Blue is player, pink enemy and the yellow circles are projectiles
as you can see, it's almost on the mark.
Have I done something wrong? what do I need to do?
To calculate the direction from enemy to player you can simplify the calculations a little.
Find direction angle
var diffX = Player.x - Enemy.x, // difference in position
diffY = Player.y - Enemy.y,
angle = Math.atan2(diffY, diffX); // atan2 will give the angle in radians
Notice also difference for Y comes first for atan2 as canvas is oriented 0° pointing right.
Velocity vector
Then calculate the velocity vector using angle and speed:
// calculate velocity vector
var speed = 8,
vx = Math.cos(angle) * speed, // angle x speed
vy = Math.sin(angle) * speed;
You might want to consider using time as a factor if that is important. You can see my answer from a while back here for an example on this.
Demo
Using these calculations you will be able to always "hit" the player with the projectile (reload demo to change enemy position to random y):
var ctx = document.querySelector("canvas").getContext("2d"),
Player = {
x: 470,
y: 75
},
Enemy = {
x: 100,
y: Math.random() * 150 // reload demo to change y-position
};
// calculate angle
var diffX = Player.x - Enemy.x,
diffY = Player.y - Enemy.y,
angle = Math.atan2(diffY, diffX);
// calculate velocity vector
var speed = 8,
vx = Math.cos(angle) * speed, // angle x speed
vy = Math.sin(angle) * speed,
x = Enemy.x, // projectil start
y = Enemy.y + 50;
// render
(function loop() {
ctx.clearRect(0, 0, 500, 300);
ctx.fillRect(Player.x, Player.y, 30, 100);
ctx.fillRect(Enemy.x, Enemy.y, 30, 100);
ctx.fillRect(x - 3, y -3, 6, 6);
x += vx;
y += vy;
if (x < 500) requestAnimationFrame(loop);
})();
<canvas width=500 height=300></canvas>
The solution is much simpler than that.
What should you do ?
1) compute the vector that leads from you enemy to the player. That will be the shooting direction.
2) normalize the vector : meaning you build a vector that has a length of 1, with the same direction.
3) multiply that vector by your speed : now you have a correct speed vector, with the right norm, aimed at the player.
Below some code to help you understand :
function spawnBullet(enemy, player) {
var shootVector = [];
shootVector[0] = player[0] - enemy[0];
shootVector[1] = player[1] - enemy[1];
var shootVectorLength = Math.sqrt(Math.pow(shootVector[0], 2) + Math.pow(shootVector[1],2));
shootVector[0]/=shootVectorLength;
shootVector[1]/=shootVectorLength;
shootVector[0]*=bulletSpeed;
shootVector[1]*=bulletSpeed;
// ... here return an object that has the enemy's coordinate
// and shootVector as speed
}
Then, since you don't use time in your computations (!! wrooong !! ;-) ) you will make the bullet move with the straightforward :
bullet[0] += bullet.speed[0];
bullet[1] += bullet.speed[1];
Now the issue with fixed-step is that your game will run, say, twice slower on a 30fps device than on a 60fps device. The solution is to compute how much time elapsed since the last refresh, let's call this time 'dt'. Using that time will lead you to an update like :
bullet[0] += dt * bullet.speed[0];
bullet[1] += dt * bullet.speed[1];
and now you'll be framerate-agnostic, your game will feel the same on any device.

rotation issue on physijs

I am working with this library for a short time, and i have a issue that is killing me.
I have 2 cubes, one with phisy.js and another with three.js, and i have a function for rotate them when you press the A, S, D, or W key depending on the rotation of the camera, my code is like this:
var v = new THREE.Vector3(0, 0, 0);
if (keys.forward === 1)
v.x = -1;
if (keys.right === 1)
v.z = 1;
if (keys.backward === 1)
v.x = 1;
if (keys.left === 1)
v.z = -1;
//get the searched angle
var angle = camera.rotation.y + Math.atan2(v.x, v.z);
var rot = normalMesh.rotation.y;
var diff = angle - rot;
if (Math.abs(diff) > Math.PI) {
//find the shortest way to rotate
if (diff > 0)
rot += 2 * Math.PI;
else
rot -= 2 * Math.PI;
diff = angle - rot;
}
//get the /2 for a smooth rotation, i really need this
if (diff !== 0)
rot += diff / 2;
normalMesh.rotation.set(0, rot, 0);
on the three.js cube works fine, but on the physi.js cube i doesn't work.
I am created a demo for this (i can´t create it on jsfiddle because the web worker)
http://demo.cristobaldiaz.cl/test/
also i left the source code on a zip file ---> http://demo.cristobaldiaz.cl/test/issue.zip
you can check the movement function on http://demo.cristobaldiaz.cl/test/src/app.js line 95
Anyway, if you rotate the camera with the mouse, you can check that the problem occurs when you rotate the cube to the direction of the red mesh.
You can remove you rotation update from the animation loop and move it to a physical loop:
scene.addEventListener('update', function(){
actor.onRender();
})
This way it will always be in sync.
See this page https://github.com/chandlerprall/Physijs/wiki/Callbacks-&-Events for more details.

Linear movement between two points that constantly move position

I currently have an issue with my code (written in Javascript); I have arrays objects that keep filling as the time goes. An example of an object:
monster.push({
range: 200,
attackSpeed: 500,
lastFire: 100,
id: 'ogre',
speed : 50,
pos:[canvas.width*Math.random(), canvas.height*Math.random()],
sprite: new Sprite('images/sheet_characters.png',[508,224],64,64],6,[0])
and
hero={
attackSpeed: 200,
lastGetHit: Date.now(),
lastFire: Date.now(),
health : 100,
speed: 256, //pixel/second
pos:[canvas.width/2,canvas.height/2],
sprite: new Sprite('images/sheet_characters.png',[256,0],[32,32],8,[0]) };
The position field of the objects change quite often and I want to add a function that determines the slope between the monster and the hero (we want the monster to fire at the hero) and then the attack should follow a linear movement.
What I currently have
for(var i=0; i<monster.length; i++){
var mob = monster[i];
mob.sprite.update(delta); //animatie
var newPos = moveTowards(mob, hero, delta);
mob.pos[0] = newPos[0]
mob.pos[1] = newPos[1]
if(checkBounds(mob.pos,mob.sprite.size)){
monster.splice(i,1);
}
mobAttacks(mob);
var attack = enemyAttacks[i]; //atacks updaten
attack.sprite.update(delta);
attack.pos[0] = attack.speed * Math.cos(attack.direction)));
attack.pos[1] = attack.speed * Math.sin(attack.direction)));
if(checkBounds(attack.pos,attack.sprite.sieze)){
enemyAttacks.splice(i,1);
}
}
In this for-loop I can access the position of the monster that fires and also the hero position as it is a global variable. Now the function to attack is :
function mobAttacks(object)
{
var distance = Math.sqrt(Math.pow((hero.pos[0]-object.pos[0]),2) + Math.pow((hero.pos[1]-object.pos[1]),2));
if( Date.now() - object.lastFire > object.attackSpeed && object.range >= distance)
{
deltaY = hero.pos[1] - object.pos[1];
deltaX = hero.pos[0] - object.pos[0];
var direction = Math.atan(deltaY/deltaX);
enemyAttacks.push({
pos:[(object.pos[0]+object.sprite.size[0]/2), (object.pos[1]+object.sprite.size[1]/2)],
direction: direction,
speed: 128, //pixel/s
sprite: new Sprite('images/sheet_objects.png', [231,3],[24,24],6,[0])
});
object.lastFire = Date.now();
}
}
The angle between both objects is calculated and I make a new object (the attack) with the start position of the monster.
The result is quite odd:
The slope is off, so is the Y position of the boulder. Also when the hero is on the left side of the monster, there is no boulder to be spotted.
After some hours of tinkering with the code I came to the conclusion that I couldn't solve my current problem.
EDIT:
attack.pos[0] += attack.speed * Math.cos(attack.direction)*delta;
attack.pos[1] += attack.speed * Math.sin(attack.direction)*delta;
Solved the issue that the boulders are no longer cast from a random position.
Now the angle is a not going negative when I'm in the 2nd or 3rd kwadrant (position left when viewed from the monster perspective)
Get all the trig out of your code, it's unnecessary. Let
deltaX = hero.pos[0] - object.pos[0];
deltaY = hero.pos[1] - object.pos[1];
then
distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
deltaX /= distance;
deltaY /= distance;
will make <deltaX,deltaY> a normalized vector (one with a length of 1).
Then you can update the position of the attack for delta time using simply:
attack.pos[0] += attack.speed * attack.deltaX * delta;
attack.pos[1] += attack.speed * attack.deltaY * delta;
If you don't have any use for the speed and direction separately, you can also pre-multiply speed into deltaX and deltaY when you initialize the attack, meaning that the update becomes only
attack.pos[0] += attack.deltaX * delta;
attack.pos[1] += attack.deltaY * delta;
which is nice and simple.

Categories