I have searched around but I can't find anything like what I'm trying to do that doesn't use Three.js in some way (I can't use Three.js because my computer is too old to support Webgl). Here's what I've got so far:
HTML:
<!DOCTYPE html>
<html>
<head>
<script type="text/javascript" src="terrain.js"></script>
<title>Terrain</title>
</head>
<body>
<canvas id="canvas" height="400" width="400"></canvas>
</body>
</html>
Javascript:
var canvas, ctx, row1 = [], row2 = [], intensity = 15, width = 20, height = 20, centery = 200, centerx = 200, minus, delta = 1.6, nu = .02;
window.onload = function() {
canvas = document.getElementById('canvas'), ctx = canvas.getContext('2d');
ctx.lineStyle = '#000'
for (var i = 0; i < height; i++) {
row2 = [];
minus = 200
for (var j = 0; j < width; j++) {
row2[j] = {
x: centerx - (minus * (delta * (nu * i))),
y: Math.floor(Math.random() * intensity) + (height * i)
}
minus -= height;
}
ctx.beginPath();
ctx.moveTo(row2[0].x,row2[0].y)
for (var k = 1; k < row2.length; k++) {
ctx.lineTo(row2[k].x,row2[k].y)
if (k == row2.length) {ctx.clostPath()}
}
ctx.stroke();
if (row1[0] && row2[0]) {
for (var l = 0; l < row2.length; l++) {
ctx.beginPath();
ctx.moveTo(row2[l].x,row2[l].y)
ctx.lineTo(row1[l].x,row1[l].y)
ctx.closePath();
ctx.stroke();
}
}
row1 = row2;
}
}
Currently, the result looks like a Christmas tree but I want it to look more like actual 3d wireframe terrain.
3D wire frame basics
3D can be done on any systems that can move pixels. Thought not by dedicated hardware Javascript can do alright if you are after simple 3d.
This answers shows how to create a mesh, rotate and move it, create a camera and move it, and project the whole lot onto the 2D canvas using simple moveTo, and lineTo calls.
This answer is a real rush job so apologies for the typos (if any) and messy code. Will clean it up in the come few days (if time permits). Any questions please do ask in the comments.
Update
I have not done any basic 3D for some time so having a little fun I have added to the answer with more comments in the code and added some extra functionality.
vec3 now has normalise, dot, cross functions.
mat now has lookat function and is ready for much more if needed.
mesh now maintains its own world matrix
Added box, and line that create box and line meshs
Created a second vector type vec3S (S for simple) that is just coordinates no functionality
Demo now shows how to add more objects, position them in the scene, use a lookat transform
Details about the code.
The code below is the basics of 3D. It has a mesh object to create objects out of 3D points (vertices) connected via lines.
Simple transformation for rotating, moving and scaling a model so it can be placed in the scene.
A very very basic camera that can only look forward, move up,down, left,right, in and out. And the focal length can be changed.
Only for lines as there is no depth sorting.
The demo does not clip to the camera front plane, but rather just ignores lines that have any part behind the camera;
You will have to work out the rest from the comments, 3D is a big subject and any one of the features is worth a question / answer all its own.
Oh and coordinates in 3D are origin in center of canvas. Y positive down, x positive right, and z positive into the screen. projection is basic so when you have perspective set to 400 than a object at 400 units out from camera will have a one to one match with pixel size.
var ctx = canvas.getContext("2d");
// some usage of vecs does not need the added functionality
// and will use the basic version
const vec3Basic = { x : 0, y : 0, z: 0};
const vec3Def = {
// Sets the vector scalars
// Has two signatures
// setVal(x,y,z) sets vector to {x,y,z}
// setVal(vec) set this vector to vec
setVal(x,y = x.y,z = x.z + (x = x.x) * 0){
this.x = x;
this.y = y;
this.z = z;
},
// subtract v from this vector
// Has two signatures
// setVal(v) subtract v from this returning a new vec3
// setVal(v,vec) subtract v from this returning result in retVec
sub(v,retVec = vec3()){
retVec.x = this.x - v.x;
retVec.y = this.y - v.y;
retVec.z = this.z - v.z;
return retVec;
},
// Cross product of two vectors this and v.
// Cross product can be thought of as get the vector
// that is perpendicular to the plane described by the two vector we are crossing
// Has two signatures
// cross(vec); // returns a new vec3 as the cross product of this and vec
// cross(vec, retVec); // set retVec as the cross product
cross (v, retVec = vec3()){
retVec.x = this.y * v.z - this.z * v.y;
retVec.y = this.z * v.x - this.x * v.z;
retVec.z = this.x * v.y - this.y * v.x;
return retVec;
},
// Dot product
// Dot product of two vectors if both normalized can be thought of as finding the cos of the angle
// between two vectors. If not normalised the dot product will give you < 0 if v points away from
// the plane that this vector is perpendicular to, if > 0 the v points in the same direction as the
// plane perpendicular to this vector. if 0 then v is at 90 degs to the plane this is perpendicular to
// Using vector dot on its self is the same as getting the length squared
// dot(vec3); // returns a number as a float
dot (v){ return this.x * v.x + this.y * v.y + this.z * this.z },
// normalize normalizes a vector. A normalized vector has length equale to 1 unit
// Has two signitures
// normalise(); normalises this vector returning this
// normalize(retVec); normalises this vector but puts the normalised vector in retVec returning
// returning retVec. Thiis is unchanged.
normalize(retVec = this){
// could have used len = this.dot(this) but for speed all functions will do calcs internaly
const len = Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z);
// it is assumed that all vector are valid (have length) so no test is made to avoid
// the divide by zero that will happen for invalid vectors.
retVec.x = this.x / len;
retVec.y = this.y / len;
retVec.z = this.z / len;
}
}
// Created as a singleton to close over working constants
const matDef = (()=>{
// to seed up vector math the following closed over vectors are used
// rather than create and dispose of vectors for every operation needing them
// Currently not used
const V1 = vec3();
return {
// The matrix is just 3 pointers one for each axis
// They represent the direction and scale in 3D of each axis
// when you transform a point x,y,z you move x along the x axis,
// then y along y and z along the z axis
xAxis : null,
yAxis : null,
zAxis : null,
// this is a position x,y,z and represents where in 3D space an objects
// center coordinate (0,0,0) will be. It is simply added to a point
// after it has been moved along the 3 axis.
pos : null,
// This function does most of the 3D work in most 3D environments.
// It rotates, scales, translates, and a whole lot more.
// It is a cut down of the full 4 by 4 3D matrix you will find in
// Libraries like three.js
transformVec3(vec,retVec = {}){
retVec.x = vec.x * this.xAxis.x + vec.y * this.yAxis.x + vec.z * this.zAxis.x + this.pos.x;
retVec.y = vec.x * this.xAxis.y + vec.y * this.yAxis.y + vec.z * this.zAxis.y + this.pos.y;
retVec.z = vec.x * this.xAxis.z + vec.y * this.yAxis.z + vec.z * this.zAxis.z + this.pos.z;
return retVec;
},
// resets the matrix
identity(){ // default matrix
this.xAxis.setVal(1,0,0); // x 1 unit long in the x direction
this.yAxis.setVal(0,1,0); // y 1 unit long in the y direction
this.zAxis.setVal(0,0,1); // z 1 unit long in the z direction
this.pos.setVal(0,0,0); // and position at the origin.
},
init(){ // need to call this before using due to the way I create these
// objects.
this.xAxis = vec3(1,0,0);
this.yAxis = vec3(0,1,0);
this.zAxis = vec3(0,0,1);
this.pos = vec3(0,0,0);
return this; // must have this line for the constructor function to return
},
setRotateY(amount){
var x = Math.cos(amount);
var y = Math.sin(amount);
this.xAxis.x = x;
this.xAxis.y = 0;
this.xAxis.z = y;
this.zAxis.x = -y;
this.zAxis.y = 0;
this.zAxis.z = x;
},
// creates a look at transform from the current position
// point is a vec3.
// No check is made to see if look at is at pos which will invalidate this matrix
// Note scale is lost in this operation.
lookAt(point){
// zAxis along vector from pos to point
this.pos.sub(point,this.zAxis).normalize();
// use y as vertical reference
this.yAxis.x = 0;
this.yAxis.y = 1;
this.yAxis.z = 0;
// get x axis perpendicular to the plane described by z and y axis
// need to normalise as z and y axis may not be at 90 deg
this.yAxis.cross(this.zAxis,this.xAxis).normalize();
// Get the y axis that is perpendicular to z and x axis
// Normalise is not really needed but rounding errors can be problematic
// so the normalise just fixes some of the rounding errors.
this.zAxis.cross(this.xAxis,this.yAxis).normalize();
},
}
})();
// Mesh object has buffers for the
// model as verts
// transformed mesh as tVerts
// projected 2D verts as dVerts (d for display)
// An a array of lines. Each line has two indexes that point to the
// vert that define their ends.
// Buffers are all preallocated to stop GC slowing everything down.
const meshDef = {
addVert(vec){
this.verts.push(vec);
// vec3(vec) in next line makes a copy of the vec. This is important
// as using the same vert in the two buffers will result in strange happenings.
this.tVerts.push(vec3S(vec)); // transformed verts pre allocated so GC does not bite
this.dVerts.push({x:0,y:0}); // preallocated memory for displaying 2d projection
// when x and y are zero this means that it is not visible
return this.verts.length - 1;
},
addLine(index1,index2){
this.lines.push(index1,index2);
},
transform(matrix = this.matrix){
for(var i = 0; i < this.verts.length; i++){
matrix.transformVec3(this.verts[i],this.tVerts[i]);
}
},
eachVert(callback){
for(var i = 0; i < this.verts.length; i++){
callback(this.tVerts[i],i);
}
},
eachLine(callback){
for(var i = 0; i < this.lines.length; i+= 2){
var ind1 = this.lines[i];
var v1 = this.dVerts[ind1]; // get the start
if(v1.x !== 0 && v1.y !== 0){ // is valid
var ind2 = this.lines[i+ 1]; // get end of line
var v2 = this.dVerts[ind2];
if(v2.x !== 0 && v2.y !== 0){ // is valid
callback(v1,v2);
}
}
}
},
init(){ // need to call this befor using
this.verts = [];
this.lines = [];
this.dVerts = [];
this.tVerts = [];
this.matrix = mat();
return this; // must have this line for the construtor function to return
}
}
const cameraDef = {
projectMesh(mesh){ // create a 2D mesh
mesh.eachVert((vert,i)=>{
var z = (vert.z + this.position.z);
if(z < 0){ // is behind the camera then ignor it
mesh.dVerts[i].x = mesh.dVerts[i].y = 0;
}else{
var s = this.perspective / z;
mesh.dVerts[i].x = (vert.x + this.position.x) * s;
mesh.dVerts[i].y = (vert.y + this.position.y) * s;
}
})
},
drawMesh(mesh){ // renders the 2D mesh
ctx.beginPath();
mesh.eachLine((v1,v2)=>{
ctx.moveTo(v1.x,v1.y);
ctx.lineTo(v2.x,v2.y);
})
ctx.stroke();
}
}
// vec3S creates a basic (simple) vector
// 3 signatures
//vec3S(); // return vec 1,0,0
//vec3S(vec); // returns copy of vec
//vec3S(x,y,z); // returns {x,y,z}
function vec3S(x = {x:1,y:0,z:0},y = x.y ,z = x.z + (x = x.x) * 0){ // a 3d point
return Object.assign({},vec3Basic,{x, y, z});
}
// vec3S creates a basic (simple) vector
// 3 signatures
//vec3S(); // return vec 1,0,0
//vec3S(vec); // returns copy of vec
//vec3S(x,y,z); // returns {x,y,z}
function vec3(x = {x:1,y:0,z:0},y = x.y ,z = x.z + (x = x.x) * 0){ // a 3d point
return Object.assign({},vec3Def,{x,y,z});
}
function mat(){ // matrix used to rotate scale and move a 3d point
return Object.assign({},matDef).init();
}
function mesh(){ // this is for storing objects as points in 3d and lines conecting points
return Object.assign({},meshDef).init();
}
function camera(perspective,position){ // this is for displaying 3D
return Object.assign({},cameraDef,{perspective,position});
}
// grid is the number of grids x,z and size is the overal size for x
function createLandMesh(gridx,gridz,size,maxHeight){
var m = mesh(); // create a mesh
var hs = size/2 ;
var step = size / gridx;
for(var z = 0; z < gridz; z ++){
for(var x = 0; x < gridx; x ++){
// create a vertex. Y is random
m.addVert(vec3S(x * step - hs, (Math.random() * maxHeight), z * step-hs)); // create a vert
}
}
for(var z = 0; z < gridz-1; z ++){
for(var x = 0; x < gridx-1; x ++){
if(x < gridx -1){ // dont go past end
m.addLine(x + z * gridx,x + 1 + z * gridx); // add line across
}
if(z < gridz - 1){ // dont go past end
m.addLine(x + z * (gridx-1),x + 1 + (z + 1) * (gridx-1));
}
}
}
return m;
}
function createBoxMesh(size){
var s = size / 2;
var m = mesh(); // create a mesh
// add bottom
m.addVert(vec3S(-s,-s,-s));
m.addVert(vec3S( s,-s,-s));
m.addVert(vec3S( s, s,-s));
m.addVert(vec3S(-s, s,-s));
// add top verts
m.addVert(vec3S(-s,-s, s));
m.addVert(vec3S( s,-s, s));
m.addVert(vec3S( s, s, s));
m.addVert(vec3S(-s, s, s));
// add lines
/// bottom lines
m.addLine(0,1);
m.addLine(1,2);
m.addLine(2,3);
m.addLine(3,0);
/// top lines
m.addLine(4,5);
m.addLine(5,6);
m.addLine(6,7);
m.addLine(7,4);
// side lines
m.addLine(0,4);
m.addLine(1,5);
m.addLine(2,6);
m.addLine(3,7);
return m;
}
function createLineMesh(v1 = vec3S(),v2 = vec3S()){
const m = mesh();
m.addVert(v1);
m.addVert(v2);
m.addLine(0,1);
return m;
}
//Create a land mesh grid 20 by 20 and 400 units by 400 units in size
var land = createLandMesh(20,20,400,20); // create a land mesh
var box = createBoxMesh(50);
var box1 = createBoxMesh(25);
var line = createLineMesh(); // line conecting boxes
line.tVerts[0] = box.matrix.pos; // set the line transformed tVect[0] to box matrix.pos
line.tVerts[1] = box1.matrix.pos; // set the line transformed tVect[0] to box1 matrix.pos
var cam = camera(200,vec3(0,0,0)); // create a projection with focal len 200 and at 0,0,0
box.matrix.pos.setVal(0,-100,400);
box1.matrix.pos.setVal(0,-100,400);
land.matrix.pos.setVal(0,100,300); // move down 100, move away 300
var w = canvas.width;
var h = canvas.height;
var cw = w / 2; // center of canvas
var ch = h / 2;
function update(timer){
// next section just maintains canvas size and resets state and clears display
if (canvas.width !== innerWidth || canvas.height !== innerHeight) {
cw = (w = canvas.width = innerWidth) /2;
ch = (h = canvas.height = innerHeight) /2;
}
ctx.setTransform(1,0,0,1,0,0); // reset transform
ctx.globalAlpha = 1; // reset alpha
ctx.fillStyle = "black";
ctx.fillRect(0,0,canvas.width,canvas.height);
// end of standard canvas maintenance
// render from center of canvas by setting canvas origin to center
ctx.setTransform(1,0,0,1,canvas.width / 2,canvas.height / 2)
land.matrix.setRotateY(timer/1000); // set matrix to rotation position
land.transform();
// move the blue box
var t = timer/1000;
box1.matrix.pos.setVal(Math.sin(t / 2.1) * 100,Math.sin( t / 3.2) * 100, Math.sin(t /5.3) * 90+300);
// Make the cyan box look at the blue box
box.matrix.lookAt(box1.matrix.pos);
// Transform boxes from local to world space
box1.transform();
box.transform();
// set camera x,y pos to mouse pos;
cam.position.x = mouse.x - cw;
cam.position.y = mouse.y - ch;
// move in and out
if (mouse.buttonRaw === 1) { cam.position.z -= 1 }
if (mouse.buttonRaw === 4) {cam.position.z += 1 }
// Converts mesh transformed verts to 2D screen coordinates
cam.projectMesh(land);
cam.projectMesh(box);
cam.projectMesh(box1);
cam.projectMesh(line);
// Draw each mesh in turn
ctx.strokeStyle = "#0F0";
cam.drawMesh(land);
ctx.strokeStyle = "#0FF";
cam.drawMesh(box);
ctx.strokeStyle = "#00F";
cam.drawMesh(box1);
ctx.strokeStyle = "#F00";
cam.drawMesh(line);
ctx.setTransform(1,0,0,1,cw,ch / 4);
ctx.font = "20px arial";
ctx.textAlign = "center";
ctx.fillStyle = "yellow";
ctx.fillText("Move mouse to move camera. Left right mouse move in out",0,0)
requestAnimationFrame(update);
}
requestAnimationFrame(update);
// A mouse handler from old lib of mine just to give some interaction
// not needed for the 3d
var mouse = (function () {
var m; // alias for mouse
var mouse = {
x : 0, y : 0, // mouse position
buttonRaw : 0,
buttonOnMasks : [0b1, 0b10, 0b100], // mouse button on masks
buttonOffMasks : [0b110, 0b101, 0b011], // mouse button off masks
bounds : null,
event(e) {
m.bounds = m.element.getBoundingClientRect();
m.x = e.pageX - m.bounds.left - scrollX;
m.y = e.pageY - m.bounds.top - scrollY;
if (e.type === "mousedown") { m.buttonRaw |= m.buttonOnMasks[e.which - 1] }
else if (e.type === "mouseup") { m.buttonRaw &= m.buttonOffMasks[e.which - 1] }
e.preventDefault();
},
start(element) {
m.element = element === undefined ? document : element;
"mousemove,mousedown,mouseup".split(",").forEach(name => document.addEventListener(name, mouse.event) );
document.addEventListener("contextmenu", (e) => { e.preventDefault() }, false);
return mouse;
},
}
m = mouse;
return mouse;
})().start(canvas);
canvas { position:absolute; top : 0px; left : 0px;}
<canvas id="canvas"></canvas>
So I'm creating a brick breaker game, and I need some help finding an angle.
Pretty much the game consists of blocks that, when hit, will cause you to lose 1 health. The point of the game is to hit the blocks with the balls to break them before they reach the bottom. If the ball hits a wall or a block, its trajectory is reversed.
I want the user to be able to click someone within the html canvas. Then the balls, which start in the center of the screen at the bottom of the canvas, will follow that angle. In other words, the user will click and the balls will move to that spot and then continue until it hits something.
I have some code here, But it probably won't help on how to achieve the angle thing.
function animate(callback) {
window.requestAnimationFrame(function() {
window.setTimeout(callback, 1000/60);
});
}
// canvas
var canvas = document.getElementById('canvas');
var context = canvas.getContext('2d');
// variables
var ballList = [];
var maxBalls = 1;
var checkAmount = 0;
var interval;
// onload/refresh/update/render
window.onload = function() {
refresh();
}
function refresh() {
update();
render();
animate(refresh);
}
function update() {
document.addEventListener("click", spawn);
for(var i = 0; i < ballList.length; i++) {
ballList[i].move();
}
}
function render() {
context.fillStyle = '#000';
context.fillRect(0, 0, canvas.width, canvas.height);
for(var i = 0; i < ballList.length; i++) {
ballList[i].show();
}
}
// ball
function Ball() {
this.x = canvas.width / 2;
this.y = canvas.height - 50;
this.width = 10;
this.height = 10;
this.xVel = 5;
this.yVel = -10;
this.show = function() {
context.fillStyle = '#fff';
context.fillRect(this.x, this.y, this.width, this.height);
}
this.move = function() {
this.x += this.xVel;
this.y += this.yVel;
if(this.x >= canvas.width || this.x <= 0) {
this.xVel *= -1;
}
if(this.y >= canvas.height || this.y <= 0) {
this.yVel *= -1;
}
}
}
function spawn(event) {
var xVel = (event.clientX - canvas.width / 2) / 90;
if(ballList.length < maxBalls) {
if(checkAmount < maxBalls) {
interval = setInterval(function() {
ballList.push(new Ball((event.clientX)));
checkAmount++;
if(checkAmount > maxBalls) {
clearInterval(interval);
checkAmount = 0;
}
}, 10);
}
}
}
Thanks in advance.
Unit Vectors
To move an object from one point towards another you use a vector. A vector is just two numbers that represent a direction and a speed. It can be polar in that one number is an angle and the other is a distance, or cartesian that represent the vector as the amount of change in x and y.
Cartesian unit vector
For this you can use either but I prefer the cartesian vector and a particular type called a unit vector. The unit vector is 1 unit long. In computer graphics the unit is normally the pixel.
So we have a point to start at
var startX = ?
var startY = ?
And a point the we want to head towards
var targetX = ?
var targetY = ?
We want the unit vector from start to target,
var vectorX = targetX - startX;
var vectorY = targetY - startY;
The vector's length is the distance between the two points. This is not so handy so we will turn it into a unit vector by dividing both the x and y by the length
var length = Math.sqrt(vectorX * vectorX + vectorY * vectorY);
var unitVectX = vectorX / length;
var unitVectY = vectorY / length;
Now we have a one pixel long unit vector.
The Ball will start at start
var ballX = startX
var ballY = startY
And will move at a speed of 200 pixels per second (assuming 60fps)
var ballSpeed = 200 / 60;
Now to move the ball just add the unit vector times the speed and you are done. Well till the next frame that is.
ballX += unitVectX * ballSpeed;
ballY += unitVectY * ballSpeed;
Using the cartesian makes it very easy to bounce off of walls that are aligned to the x or y axis.
if(ballX + ballRadius > canvas.width){
ballX = canvas.width - ballRadius;
unitVectX = - unitVectX;
}
Polar vector
You can also use polar coordinates. As we use a unit vector the polar unit vector just needs the direction. You use the trig function atan2
// get the direction in radians
var polarDirection = Math.atan2(targetY - startY, targetX - startX);
The direction is in radians, many poeple don't like radians and convert to degrees, but there is no need to know which way it is going just as long as it goes in the correct direction. To remember radians is easy. 360 degrees is 2 radian 180 is 1 randian 90 is 0.5. The actual units used are PI (not many people know many of the digits of pi but you don't need to). So 270 degree is 1.5 radians or as a number 1.5 * Math.PI.
The angles start at the 3 o'clock point (pointing to the right of screen) as 0 radians or 0 deg then clockwise 90deg is at 6 o'clock 0.5 radian, and 180deg 1 radian at 6 o'clock and so on.
To move the ball with the polarDirection you need to use some more trig.
// do this once a frame
ballX += Math.cos(polarDirection) * ballSpeed;
ballY += Math.sin(polarDirection) * ballSpeed;
// note that the cos and sin actually generate the cartesian unit vector
/**
* #param {number} x1 - x coordinate of the first point
* #param {number} y1 - y coordinate of the first point
* #param {number} x2 - x coordinate of the second point
* #param {number} y2 - y coordinate of the second point
* #return {number} - the angle (between 0 and 360)
*/
function getDirection(x1, y1, x2, y2) {
// might be negative:
var angle = Math.atan2(y2 - y1, x2 - x1) * 180 / Math.PI;
// correct, positive angle:
return (angle + 360) % 360;
}
I wrote this function for a similar purpose. Don't forget that you might have to negate x.
I'm trying to find the distance that the mouse has traveled along a normal vector.
The idea is to move a set of vertices within an object along the intersecting face's normal vector.
Currently, I have an onmousedown event handler that finds the intersecting face, adjacent faces with the same normal, and the vertices associated to those faces. I also have an onmousemove event handler that moves the vertices along the normal.
Right now, the onmousemove just moves the vertices 1 unit along the face normal every time the event is fired. I'd like them to move with the mouse.
The code that I am working off of came largely from the three.js editor. Any help is very much appreciated, thanks!
var object; // Set outside this code
var camera; // Set outside this code
var viewport; // Set outside this code
var raycaster = new THREE.Raycaster();
var point = new THREE.Vector2();
var mouse = new THREE.Vector2();
var _dragging = false;
var faces = [];
var vertices = [];
function onMouseDown(event) {
if (object === undefined || _dragging === true) {
return;
}
event.preventDefault();
event.stopPropagation();
var intersect = getIntersects(event, object)[0];
if (intersect && intersect.face) {
faces = getAdjacentNormalFaces(intersect.object.geometry, intersect.face);
vertices = getFaceVertices(intersect.object.geometry, self.faces);
}
_dragging = true;
}
function onMouseMove(event) {
if (object === undefined || vertices.length === 0 || _dragging === false) {
return;
}
event.preventDefault();
event.stopPropagation();
var normal = faces[0].normal;
/*
* Get the distance to move the vertices
*/
var distance = 1;
var i;
for (i = 0; i < self.vertices.length; i++) {
self.vertices[i].x += (normal.x * distance);
self.vertices[i].y += (normal.y * distance);
self.vertices[i].z += (normal.z * distance);
}
object.geometry.verticesNeedUpdate = true;
object.geometry.computeBoundingBox();
object.geometry.computeBoundingSphere();
}
var getIntersects = function (event, object) {
var rect = viewport.getBoundingClientRect();
point.fromArray([
( event.clientX - rect.left ) / rect.width,
( event.clientY - rect.top ) / rect.height
]);
mouse.set(( point.x * 2 ) - 1, -( point.y * 2 ) + 1);
raycaster.setFromCamera(mouse, camera);
if (object instanceof Array) {
return raycaster.intersectObjects(object);
}
return raycaster.intersectObject(object);
};
var getAdjacentNormalFaces = function (geometry, face) {
// Returns an array of all faces that are adjacent and share the same normal vector
};
var getFaceVertices = function (geometry, faces) {
// Returns an array of vertices that belong to the array of faces
};
Update:
As a summary... I have the event handlers, the set of vertices that need to be moved and the normal vector that the vertices should be moved on. What I need is the offset distance that the vertices should be moved based on where the mouse is.
My first thought is to create a plane perpendicular to the normal vector and track the mouse position on that plane. However, I am not sure 1. how to create the perpendicular plane where the largest side is visible to the camera and 2. how to translate the x/y coordinates of the mouse on the plane to the distance the vertices should be moved.
The way I solved this is to map the zero and normal points on the 2D plane and then use the inverse slope to find the perpendicular line that intersects the normal line. I can then use the starting point and the point of intersection to find the distance the mouse moved. I also had to scale the final distance using the camera.
For a quick reference:
// linear slope/intercept: y = mx + b
// solve for b: b = y - mx
// solve for m: (y2 - y1) / (x2 - x1)
// get inverse slope: -1 / m
// get intersect point: (b2 - b1) / (m1 - m2)
There may be an easier way but this is what I did and hopefully it helps others:
On Mousedown
Project the center (0,0,0) vector, the face normal vector and an arbitrary 1 unit vector (1,0,0) onto the camera and get the screen position of the three points
var zero2D = toScreenPosition(0, 0, 0);
var one2D = toScreenPosition(1, 0, 0);
var normal2D = toScreenPosition(intersect.face.normal.x, intersect.face.normal.y, intersect.face.normal.z);
/ ***** /
var toScreenPosition = function (x, y, z) {
var rect = viewport.getBoundingClientRect();
var point = new THREE.Vector2();
screenPositionVector.set(x || 0, y || 0, z || 0);
screenPositionVector.project(camera);
point.set((screenPositionVector.x + 1) / 2 * rect.width, -(screenPositionVector.y - 1) / 2 * rect.height);
return point;
};
Store the mouse starting point and the x direction of the normal (1 or -1).
start2D.set(event.clientX, event.clientY);
normalDir = zero2D.x < normal2D.x ? 1 : -1;
Store the slope and inverse slope of the zero/normal line.
slope = (normal2D.y - zero2D.y) / (normal2D.x - zero2D.x); // TODO: Handle zero slope
inverseSlope = -1 / slope; // TODO: If slope is 0, inverse is infinity
Store the y intercept of the normal line based on the mouse coordinates.
startingYIntercept = event.clientY - (slope * event.clientX);
Use the zero2D and one2D point to find the camera scale. The camera scale is the distance between the two 2D points divided by the distance between the two 3D points (1).
cameraScale = one2D.distanceTo(zero2D);
For better accuracy, we are going to move the vertices based on total movement, not the delta between event handler calls. Because of this, we need to track the starting position of all the vertices.
startingVertices = [];
var i;
for (i = 0; i < vertices.length; i++) {
startingVertices.push({x: vertices[i].x, y: vertices[i].y, z: vertices[i].z});
}
On Mousemove
Using the mouse position and the inverse slope, find the perpendicular line's y intercept.
var endingYIntercept = event.clientY - (inverseSlope * event.clientX);
Use the intercept equation to find the x location where the normal line and perpendicular line intercept.
var endingX = (endingYIntercept - startingYIntercept) / (slope / inverseSlope);
Plug x back in to find the y point. Since the lines intercept at x, you can use either the normal line or perpendicular line. Set the end point based on this.
var endingY = (slope * endingX) + startingYIntercept;
end2D.set(endingX, endingY);
Find the distance between the points and divide by the camera scale.
var distance = end2D.distanceTo(start2D) / cameraScale;
If the normal is in the opposite direction of the mouse movement, multiply distance by -1.
if ((normalDir > 0 && endingX < start2D.x) || (normalDir < 0 && endingX > start2D.x)) {
distance = distance * -1;
}
Since we are moving the vertices by a total distance and not the delta between event handlers, the vertex update code is a little different.
var i;
for (i = 0; i < self.vertices.length; i++) {
vertices[i].x = startingVertices[i].x + (normal.x * distance);
vertices[i].y = startingVertices[i].y + (normal.y * distance);
vertices[i].z = startingVertices[i].z + (normal.z * distance);
}
Extra Credit On Mouseup
When the vertices are moved, the geometry's center is not changed and needs to be updated. To update the center, I can call geometry.center(), however, in Three.js, the geometry's position is based off of its center so this will effectively move the center and the position of the geometry in the opposite direction at half the distance of the vertex move. I don't want this, I want the geometry to stay in the same position when I move the vertices. To do this, I take the first vertex's ending position minus its start position divided by 2 and add that vector to the geometry's position. I then recenter the geometry.
if (_dragging && self.vertices.length > 0) {
offset.set(self.vertices[0].x - startingVertices[0].x, self.vertices[0].y - startingVertices[0].y, self.vertices[0].z - startingVertices[0].z);
offset.divideScalar(2);
object.position.add(offset);
object.geometry.center();
}
All Together
var object; // Set outside this code
var camera; // Set outside this code
var viewport; // Set outside this code
var raycaster = new THREE.Raycaster();
var point = new THREE.Vector2();
var mouse = new THREE.Vector2();
var _dragging = false;
var faces = [];
var vertices = [];
var startingVertices = [];
var slope = 0;
var inverseSlope;
var startingYIntercept = 0;
var normalDir = 1;
var cameraScale = 1;
var start2D = new THREE.Vector2();
var end2D = new THREE.Vector2();
var offset = new THREE.Vector3();
var onMouseDown = function (event) {
if (object === undefined || _dragging === true) {
return;
}
event.preventDefault();
event.stopPropagation();
var intersect = getIntersects(event, object)[0];
if (intersect && intersect.face) {
var zero2D = toScreenPosition(0, 0, 0);
var one2D = toScreenPosition(1, 0, 0);
var normal2D = toScreenPosition(intersect.face.normal.x, intersect.face.normal.y, intersect.face.normal.z);
start2D.set(event.clientX, event.clientY);
normalDir = zero2D.x < normal2D.x ? 1 : -1;
slope = (normal2D.y - zero2D.y) / (normal2D.x - zero2D.x); // TODO: Handle zero slope
inverseSlope = -1 / slope; // TODO: If slope is 0, inverse is infinity
startingYIntercept = event.clientY - (slope * event.clientX);
cameraScale = one2D.distanceTo(zero2D);
faces = getAdjacentNormalFaces(intersect.object.geometry, intersect.face);
vertices = getFaceVertices(intersect.object.geometry, self.faces);
startingVertices = [];
var i;
for (i = 0; i < vertices.length; i++) {
startingVertices.push({x: vertices[i].x, y: vertices[i].y, z: vertices[i].z});
}
}
_dragging = true;
}
var onMouseMove = function (event) {
if (object === undefined || vertices.length === 0 || _dragging === false) {
return;
}
event.preventDefault();
event.stopPropagation();
var normal = faces[0].normal;
var endingYIntercept = event.clientY - (inverseSlope * event.clientX);
var endingX = (endingYIntercept - startingYIntercept) / (slope / inverseSlope);
var endingY = (slope * endingX) + startingYIntercept;
end2D.set(endingX, endingY);
var distance = end2D.distanceTo(start2D) / cameraScale;
if ((normalDir > 0 && endingX < start2D.x) || (normalDir < 0 && endingX > start2D.x)) {
distance = distance * -1;
}
var i;
for (i = 0; i < self.vertices.length; i++) {
vertices[i].x = startingVertices[i].x + (normal.x * distance);
vertices[i].y = startingVertices[i].y + (normal.y * distance);
vertices[i].z = startingVertices[i].z + (normal.z * distance);
}
object.geometry.verticesNeedUpdate = true;
object.geometry.computeBoundingBox();
object.geometry.computeBoundingSphere();
}
var onMouseUp = function (event) {
if (_dragging && vertices.length > 0) {
offset.set(vertices[0].x - startingVertices[0].x, vertices[0].y - startingVertices[0].y, vertices[0].z - startingVertices[0].z);
offset.divideScalar(2);
object.position.add(offset);
object.geometry.center();
}
}
var getIntersects = function (event, object) {
var rect = viewport.getBoundingClientRect();
point.fromArray([
( event.clientX - rect.left ) / rect.width,
( event.clientY - rect.top ) / rect.height
]);
mouse.set(( point.x * 2 ) - 1, -( point.y * 2 ) + 1);
raycaster.setFromCamera(mouse, camera);
if (object instanceof Array) {
return raycaster.intersectObjects(object);
}
return raycaster.intersectObject(object);
};
var toScreenPosition = function (x, y, z) {
var rect = viewport.getBoundingClientRect();
var point = new THREE.Vector2();
screenPositionVector.set(x || 0, y || 0, z || 0);
screenPositionVector.project(camera);
point.set((screenPositionVector.x + 1) / 2 * rect.width, -(screenPositionVector.y - 1) / 2 * rect.height);
return point;
};
var getAdjacentNormalFaces = function (geometry, face) {
// Returns an array of all faces that are adjacent and share the same normal vector
};
var getFaceVertices = function (geometry, faces) {
// Returns an array of vertices that belong to the array of faces
};
You could achieve two ways, on mouse move or in animationframe.
onmouseMove(){
mouseX = ( event.clientX - windowHalfX ) / resistanceh;
mouseY = ( event.clientY - windowHalfY ) / resistancew;
var raycaster = new THREE.Raycaster();
raycaster.setFromCamera(mouse, camera);
var intersects = raycaster.intersectObjects(objects);
if ( intersects.length > 0 ) {
if(mousedown){
//do your thing
}
or in your animation updating these values is more accurate I found.
AnimationFrame(){
mouseX = ( event.clientX - windowHalfX ) / resistanceh;
mouseY = ( event.clientY - windowHalfY ) / resistancew;