How do I implement a function grapher in a way that it plots a function that is in the form
z=f(x,y) using three.js API. This program should:
generate input values between -1 and 1 in increments of .1 (one-tenth) and use this to plot x, y, and z vertices to be included as part of a mesh that will then be graphically displayed.
implement mouse controls such that the function that is mapped can be rotated and scaled using the mouse.
include a plane and an axis helper to provide a point of reference for the function that indicates the origin, which is assumed to be x=0, y=0, and z=0.
<script type="module">
import * as THREE from "https://cdn.skypack.dev/three#0.133.1";
import {OrbitControls}
// declare global variables
let scene, camera, cameraControls, renderer, axisHelper;
function init() {
scene = new THREE.Scene;
camera = new THREE.PerspectiveCamera(25, window.innerWidth/window.innerHeight, 1, 1000);
camera.position.set(-30, 50, -7);
const canvas = document.querySelector('#canvasElem');
renderer = new THREE.WebGLRenderer({canvas: canvas, antialias: true,
opacity: 0.5, transparent: true, alpha: true
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
cameraControls = new THREE.OrbitControls(camera, renderer.domElement);
cameraControls.addEventListener("mousemove", renderer);
cameraControls.enablePan = false;
axisHelper = new THREE.AxesHelper;
scene.add(axisHelper);
window.addEventListener("resize", ()=>{
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
camera.aspect = window.innerWidth/window.innerHeight;
camera.updateProjectionMatrix()
});
//Constants for use with hyperbolic paraboloid.
//If these are changed, the shape will change! Don't use zero.
const a = 1.0;
const b = 1.0;
/**
* This is the function that will compute z-position from X and Y positions
* to produce a hyperbolic parabaloid.
* #param {*} u
* #param {*} v
* #param {*} w
*/
function ComputeZ(u,v,w) {
x = u * 2 - 1;
y = v * 2 - 1;
w.set(x,y,(x*x)/(a*a) - (y*y)/(b*b));
}
/**
* These is a getter function used to set vertex colors.
* In the version where I code the geometry myself,
* they were set in the geometry generation function.
* #param {*} pointX
* #param {*} pointY
* #param {*} pointZ
* #returns
*/
function getVertexColors(pointX, pointY, pointZ) {
return new THREE.Color(pointX.x*0.5+0.5, pointY.y*0.5+0.5, pointZ.z*0.5+0.5);
}
/**
* The getter function (getVertexColors) one is to make
* the setter function (setVertexColors) more compact.
* #param {*} geometry
*/
//for (var i = 0; i < geometry.faces.length; i++)
//for (i in geometry.faces)
function setVertexColors(geometry) {
for (var i = 0; i < geometry.faces; i++) {
var face = geometry.faces[i];
face.vertexColors = [getVertexColors(geometry.vertices[face.a]),
getVertexColors(geometry.vertices[face.b]),
getVertexColors(geometry.vertices[face.c])];
// face.vertexColors[0] = new THREE.Color("rgb(255,0,0)");
// face.vertexColors[1] = new THREE.Color("rgb(0,255,0)");
// face.vertexColors[2] = new THREE.Color("rgb(0, 0,25)");
// face.vertexColors[3] = new THREE.Color("rgb(0, 0, 0)")
}
}
/**
* This creates a plane grid in the x-y direction, using the specified size and steps.
* #param {number} size
* #param {number} steps
*/
function createPlaneGrid(size, steps) {
// create a grouping for the lines
var group = new THREE.Group();
// create the material for the grid
var material = new THREE.LineBasicMaterial({color:0x000000, transparent:true, opacity:0.25
});
// create the x-axis lines for the grid
for (var i = 0; i <= steps; i+=1) {
var f = (i/steps)-0.5;
// create a base class for geometries and add a customed geometry to it
var geometry = new THREE.BufferGeometry();
// create the x-axis custom lines of the grid
const points = [
new THREE.Vector3( f*size, -size*0.5, 0 ),
new THREE.Vector3( f*size, size*0.5, 0 )
]
geometry.setFromPoints(points);
geometry.computeVertexNormals();
//create the x-axis line mesh
var axisX = new THREE.Line( geometry, material );
//add the line to the group
group.add( axisX );
}
// create the y-axis lines (geometry) for the grid
for (var i = 0; i <= steps; i+=1) {
var f = (i/steps)-0.5;
// create a base class for geometries and add a customed geometry to it
var geometry = new THREE.BufferGeometry();
// create the y-axis custom lines (geometry) of the grid
const points = [
new THREE.Vector3( -size*0.5, f*size, 0 ),
new THREE.Vector3( size*0.5, f*size, 0 ),
]
geometry.setFromPoints(points);
geometry.computeVertexNormals();
//create the y-axis line mesh
var axisY = new THREE.Line( geometry, material );
//add the line to the group
group.add( axisY );
}
// return the group when all these calculations are done
return group;
}
// create a parametric geometry
var ParamGeometry = new THREE.ParametricGeometry(ComputeZ, 20, 20);
setVertexColors(ParamGeometry);
// create a material
var ParaMaterial = new THREE.MeshBasicMaterial({color:0xffffff, side:THREE.DoubleSide,
vertexColors: THREE.VertexColors});
//Create models and groups
var ParaMesh = new THREE.Mesh( ParamGeometry, ParaMaterial ); //Create the actual object
scene.add( ParaMesh );
var plane = createPlaneGrid(4,12);
scene.add( plane );
// add directional lighting
var directionalLight = new THREE.DirectionalLight( 0xffffff, 1.0 );
directionalLight.position.set(100,100,100);
directionalLight.castShadow = true;
directionalLight.shadow.camera.left = -100;
directionalLight.shadow.camera.bottom = -100;
directionalLight.shadow.camera.right = 100;
directionalLight.shadow.camera.top = 100;
directionalLight.shadow.camera.far = 1000;
scene.add( directionalLight );
}}
function animate(){
requestAnimationFrame(animate);
render();
}
function render(){
// cameraControls.update();
renderer.render(scene, camera);
}
init();
animate();
</script>
The graph is drawn but the colours are not applied making the shape black and white.
I tried different ways of applying colour to the faces but non is working.
I need help on that.
A very rough example of a "plotter" with a bended/distorted PlaneGeometry:
body{
overflow: hidden;
margin: 0;
}
<script type="module">
import * as THREE from "https://cdn.skypack.dev/three#0.133.1";
import {
OrbitControls
} from "https://cdn.skypack.dev/three#0.133.1/examples/jsm/controls/OrbitControls.js";
let scene = new THREE.Scene();
let camera = new THREE.PerspectiveCamera(60, innerWidth / innerHeight, 0.1, 10);
camera.position.set(1, 1.5, 1).setLength(2.5);
camera.lookAt(scene.position);
let renderer = new THREE.WebGLRenderer({
antialias: true
});
renderer.setSize(innerWidth, innerHeight);
renderer.setClearColor(0x161616);
document.body.appendChild(renderer.domElement);
let controls = new OrbitControls(camera, renderer.domElement);
let light = new THREE.DirectionalLight(0xffffff, 1);
light.position.setScalar(1);
scene.add(light, new THREE.AmbientLight(0xffffff, 0.5));
let grid = new THREE.GridHelper(2, 20, 0xffff00, 0xffff00);
grid.position.y = -0.001;
scene.add(grid, new THREE.AxesHelper(1));
let graphGeom = new THREE.PlaneGeometry(2, 2, 20, 20);
graphGeom.rotateX(Math.PI * -0.5);
let graphMat = new THREE.MeshNormalMaterial({side: THREE.DoubleSide, wireframe: false});
let graph = new THREE.Mesh(graphGeom, graphMat);
// f(x,z)
let pos = graphGeom.attributes.position;
for(let i = 0; i < pos.count; i++){
let x = pos.getX(i);
let z = pos.getZ(i);
pos.setY(i, Math.sin(x * z * Math.PI) * Math.cos(z * z * Math.PI * 0.5) * 0.75);
}
graphGeom.computeVertexNormals();
scene.add(graph);
window.addEventListener("resize", onResize);
renderer.setAnimationLoop(_ => {
renderer.render(scene, camera);
})
function onResize(event) {
camera.aspect = innerWidth / innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
}
</script>
Related
I have a piece of code from 3 years ago that appears to not be working anymore from Three.js.
I have updated all of the other issues of the migration to ES6 for Three.js, but when I try to add as I did in the past more than 1 place marker on the planet surface, only 1 place marker is displayed, with only the last added place marker being successfully displayed in the desired position... with all other place markers being erased.
Problem example -> https://jsfiddle.net/jhL6s579/6/
Complete code:
HTML
<script src="https://threejs.org/build/three.js"></script>
<script src="https://threejs.org/examples/js/controls/OrbitControls.js"></script>
<script src="https://s3-eu-west-2.amazonaws.com/bckld/lab/loading.js"></script>
JS
// ------ Marker object ------------------------------------------------
function Marker() {
var radius = 0.005;
var sphereRadius = 0.02;
var height = 0.05;
var material = new THREE.MeshPhongMaterial({ color: 0xbab68f });
var cone = new THREE.Mesh(new THREE.ConeBufferGeometry(radius, height, 8, 1, true), material);
cone.position.y = height * 0.5;
cone.rotation.x = Math.PI;
var sphere = new THREE.Mesh(new THREE.SphereBufferGeometry(sphereRadius, 16, 8), material);
sphere.position.y = height * 0.95 + sphereRadius;
this.add(cone, sphere);
}
Marker.prototype = Object.create(new THREE.Object3D());
// ------ Earth object -------------------------------------------------
function Earth(radius, texture) {
this.userData.radius = radius;
var earth = new THREE.Mesh(
new THREE.SphereBufferGeometry(radius, 64.0, 48.0),
new THREE.MeshPhongMaterial({
map: texture
})
);
this.add(earth);
}
Earth.prototype = Object.create(new THREE.Object3D());
Earth.prototype.createMarker = function (lat,lon ) {
var marker = new Marker();
var lonRad = -lon * (Math.PI / 180);
var latRad = lat * (Math.PI / 180);
var r = this.userData.radius;
marker.position.set(Math.cos(latRad) * Math.cos(lonRad) * r, Math.sin(latRad) * r, Math.cos(latRad) * Math.sin(lonRad) * r);
marker.rotation.set(0.0, -lonRad, latRad - Math.PI * 0.5);
this.attach(marker);
};
// ------ Three.js code ------------------------------------------------
var scene, camera, renderer;
var controls;
init();
function init() {
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(45, 4 / 3, 0.1, 100);
camera.position.set(0.0, 1.5, 3.0);
renderer = new THREE.WebGLRenderer({ antialias: true });
controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.autoRotate = true;
controls.autoRotateSpeed = -1.0;
controls.enablePan = false;
var ambient = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambient);
var direcitonal = new THREE.DirectionalLight(0xffffff, 0.5);
direcitonal.position.set(5.0, 2.0, 5.0).normalize();
scene.add(direcitonal);
// just some code for the loading
var manager = createLoader(renderer.domElement, animate);
var texLoader = new THREE.TextureLoader(manager).setCrossOrigin(true);
var texture = texLoader.load('https://s3-eu-west-2.amazonaws.com/bckld/lab/textures/earth_latlon.jpg');
texture.anisotropy = renderer.capabilities.getMaxAnisotropy();
var earth = new Earth(1.0, texture);
earth.createMarker(48.856700, 2.350800); // Paris
earth.createMarker(28.524167, -80.650833); // Paris
scene.add(earth);
window.addEventListener('resize', onResize);
onResize();
document.body.appendChild(renderer.domElement);
}
function onResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}
CSS
body {
background: #000;
margin: 0;
overflow: hidden;
}
I have also tried doing this.attach(marker) but still not working...
You have to derive your custom classes Marker and Earth in ES6 now. Meaning you have to use the class syntax like below since Object3D is now a class, too.
// ------ Marker object ------------------------------------------------
class Marker extends THREE.Object3D {
constructor() {
super();
var radius = 0.005;
var sphereRadius = 0.02;
var height = 0.05;
var material = new THREE.MeshPhongMaterial({
color: 0xbab68f
});
var cone = new THREE.Mesh(new THREE.ConeBufferGeometry(radius, height, 8, 1, true), material);
cone.position.y = height * 0.5;
cone.rotation.x = Math.PI;
var sphere = new THREE.Mesh(new THREE.SphereBufferGeometry(sphereRadius, 16, 8), material);
sphere.position.y = height * 0.95 + sphereRadius;
this.add(cone, sphere);
}
}
// ------ Earth object -------------------------------------------------
class Earth extends THREE.Object3D {
constructor(radius, texture) {
super();
this.userData.radius = radius;
var earth = new THREE.Mesh(
new THREE.SphereBufferGeometry(radius, 64.0, 48.0),
new THREE.MeshPhongMaterial({
map: texture
})
);
this.add(earth);
}
createMarker(lat, lon) {
var marker = new Marker();
var lonRad = -lon * (Math.PI / 180);
var latRad = lat * (Math.PI / 180);
var r = this.userData.radius;
marker.position.set(Math.cos(latRad) * Math.cos(lonRad) * r, Math.sin(latRad) * r, Math.cos(latRad) * Math.sin(lonRad) * r);
marker.rotation.set(0.0, -lonRad, latRad - Math.PI * 0.5);
this.attach(marker);
}
}
// ------ Three.js code ------------------------------------------------
var scene, camera, renderer;
var controls;
init();
function init() {
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(45, 4 / 3, 0.1, 100);
camera.position.set(0.0, 1.5, 3.0);
renderer = new THREE.WebGLRenderer({
antialias: true
});
controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.autoRotate = true;
controls.autoRotateSpeed = -1.0;
controls.enablePan = false;
var ambient = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambient);
var direcitonal = new THREE.DirectionalLight(0xffffff, 0.5);
direcitonal.position.set(5.0, 2.0, 5.0).normalize();
scene.add(direcitonal);
// just some code for the loading
var manager = createLoader(renderer.domElement, animate);
var texLoader = new THREE.TextureLoader(manager).setCrossOrigin(true);
var texture = texLoader.load('https://s3-eu-west-2.amazonaws.com/bckld/lab/textures/earth_latlon.jpg');
texture.anisotropy = renderer.capabilities.getMaxAnisotropy();
var earth = new Earth(1.0, texture);
earth.createMarker(48.856700, 2.350800); // Paris
earth.createMarker(28.524167, -80.650833); // Paris
scene.add(earth);
window.addEventListener('resize', onResize);
onResize();
document.body.appendChild(renderer.domElement);
}
function onResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}
body {
margin: 0;
}
<script src="https://cdn.jsdelivr.net/npm/three#0.137.5/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three#0.137.5/examples/js/controls/OrbitControls.js"></script>
<script src="https://s3-eu-west-2.amazonaws.com/bckld/lab/loading.js"></script>
I have a simple scene in Three with some planes. Right now on click the planes move to random positions.
After a click, I'd like instead to move the planes into a new grid perpendicular to the camera, such that the projected grid's x and y axes are parallel with the screen's x and y axes.
Here's the scene:
// generate a scene object
var scene = new THREE.Scene();
scene.background = new THREE.Color(0xffffff);
// generate a camera
var aspectRatio = window.innerWidth / window.innerHeight;
var camera = new THREE.PerspectiveCamera(75, aspectRatio, 0.01, 10000);
// position the camera
camera.position.set(51.389, -451.056, 839.455);
var rotObjectMatrix = new THREE.Matrix4();
var q = {_x: 0.0184, _y: -0.2122, _z: 0.9770, _w: -0.0081};
rotObjectMatrix.makeRotationFromQuaternion(q);
camera.quaternion.setFromRotationMatrix(rotObjectMatrix);
camera.up.set(0.00806, -0.91008, -0.41432);
// generate a renderer
var renderer = new THREE.WebGLRenderer({antialias: true});
renderer.setPixelRatio(window.devicePixelRatio); // <3 retina
renderer.setSize(window.innerWidth, window.innerHeight); // canvas size
document.body.appendChild(renderer.domElement);
// generate controls
var controls = new THREE.TrackballControls(camera, renderer.domElement);
controls.target.set(19.053, -111.316, 93.996);
// generate some lights
var ambientLight = new THREE.AmbientLight(0xeeeeee);
scene.add(ambientLight);
// render loop
function render() {
requestAnimationFrame(render);
renderer.render(scene, camera);
controls.update();
};
// draw a grid
var grid = new THREE.GridHelper(2000, 20, 0x000000, 0x000000);
grid.material.opacity = 0.2;
grid.material.transparent = true;
grid.rotation.x = -Math.PI;
scene.add(grid);
// draw some
planes = [];
for (var i=0; i<2**10; i++) {
var geometry = new THREE.PlaneGeometry( 20, 20, 32 );
var material = new THREE.MeshBasicMaterial(
{color: 0xff0000, side: THREE.DoubleSide} );
var plane = new THREE.Mesh( geometry, material );
var x = ((i % 2**5) * 40) - (2**5 * 40)/2;
var z = (Math.floor(i/2**5) * 40) - (2**5 * 40)/2;
plane.position.set(x, 0, z);
scene.add(plane);
planes.push(plane);
}
// transition the planes on body click
document.querySelector('body').addEventListener('click', function() {
planes.forEach(function(plane) {
// placeholder
plane.position.set(
Math.random() * 500 - (Math.random() * 500)/2,
0,
Math.random() * 500 - (Math.random() * 500)/2,
)
})
})
render();
html, body { width: 100%; height: 100%; background: #000; }
body { margin: 0; overflow: hidden; }
canvas { width: 100%; height: 100%; }
<script src='https://cdnjs.cloudflare.com/ajax/libs/three.js/97/three.min.js'></script>
<script src='https://threejs.org/examples/js/controls/TrackballControls.js'></script>
Using the following comes close, but doesn't tilt the planes such that they're perpendicular to the camera:
planes.forEach(function(plane) {
// close to projection discussed above...
plane.position.set(
plane.position.x,
plane.position.z,
0,
)
})
Does anyone know how to achieve the projection described above? Any suggestions others can offer would be greatly appreciated!
This is a bit of a shortcut, but it'll help you avoid a lot of math calculations: Just group your planes into a Group and then use Group.lookAt(camera.position) to point them all in unison towards the camera.
// generate a scene object
var scene = new THREE.Scene();
scene.background = new THREE.Color(0xffffff);
// generate a camera
var aspectRatio = window.innerWidth / window.innerHeight;
var camera = new THREE.PerspectiveCamera(75, aspectRatio, 0.01, 10000);
// position the camera
camera.position.set(51.389, -451.056, 839.455);
var rotObjectMatrix = new THREE.Matrix4();
var q = {_x: 0.0184, _y: -0.2122, _z: 0.9770, _w: -0.0081};
rotObjectMatrix.makeRotationFromQuaternion(q);
camera.quaternion.setFromRotationMatrix(rotObjectMatrix);
// Not sure why you're changing camera's up axis, but this will
// need to be duplicated on planeGroup;
camera.up.set(0.00806, -0.91008, -0.41432);
// generate a renderer
var renderer = new THREE.WebGLRenderer({antialias: true});
renderer.setPixelRatio(window.devicePixelRatio); // <3 retina
renderer.setSize(window.innerWidth, window.innerHeight); // canvas size
document.body.appendChild(renderer.domElement);
// generate controls
var controls = new THREE.TrackballControls(camera, renderer.domElement);
// If you change the pos where the camera is looking at,
// you'll need to place planeGroup at this position.
controls.target.set(19.053, -111.316, 93.996);
// generate some lights
var ambientLight = new THREE.AmbientLight(0xeeeeee);
scene.add(ambientLight);
// render loop
function render() {
requestAnimationFrame(render);
renderer.render(scene, camera);
controls.update();
};
// draw a grid
var grid = new THREE.GridHelper(2000, 20, 0x000000, 0x000000);
grid.material.opacity = 0.2;
grid.material.transparent = true;
grid.rotation.x = -Math.PI;
scene.add(grid);
// draw some
planes = [];
var planeGroup = new THREE.Group();
for (var i=0; i<2**10; i++) {
var geometry = new THREE.PlaneGeometry( 20, 20, 32 );
var material = new THREE.MeshBasicMaterial(
{color: 0xff0000, side: THREE.DoubleSide} );
var plane = new THREE.Mesh( geometry, material );
var x = ((i % 2**5) * 40) - (2**5 * 40)/2;
var y = (Math.floor(i/2**5) * 40) - (2**5 * 40)/2;
// Setting x,y instead of x,z
plane.position.set(x, y, 0);
planeGroup.add(plane);
planes.push(plane);
}
scene.add(planeGroup);
// Copying camera.up and controls.target from above
planeGroup.up.copy(camera.up);
planeGroup.position.copy(controls.target);
planeGroup.lookAt(camera.position);
// transition the planes on body click
document.querySelector('body').addEventListener('click', function() {
planes.forEach(function(plane) {
// placeholder
plane.position.set(
Math.random() * 500 - (Math.random() * 500)/2,
0,
Math.random() * 500 - (Math.random() * 500)/2,
)
})
})
render();
html, body { width: 100%; height: 100%; background: #000; }
body { margin: 0; overflow: hidden; }
canvas { width: 100%; height: 100%; }
<script src='https://cdnjs.cloudflare.com/ajax/libs/three.js/97/three.min.js'></script>
<script src='https://threejs.org/examples/js/controls/TrackballControls.js'></script>
Then, if you want to randomize the plane's positions, you can still do that, but all you have to worry about afterward is to return them to their original x,y,0 positions, and they'll automatically line up "looking at" the camera.
EDIT: I had to copy the changes you made to camera.up and controls.target for this to work. If you change these 2 attributes, you'll need to copy them to planeGroup for this approach to work.
Here's the doc on the .lookAt() method
In threejs webgl I want to highlight the nearest edge from my mouse pointer and select it to get the length of the edge.
I am using Edgesgeometry to create the edges.
var edgeGeometry = new THREE.EdgesGeometry(meshes[meshIndex]); // or WireframeGeometry
var edgeMaterial = new THREE.LineBasicMaterial({ color: 0x00000, linewidth: 2 });
var wireframe = new THREE.LineSegments(edgeGeometry, edgeMaterial);
var object = new THREE.Mesh(meshes[meshIndex], material);
object.add(wireframe);
Just an idea.
You know which face you're intesecting and you have its edges, you can use THREE.Line3(), built from vertices of each edge (thus you'll use THREE.Line3() three times). Then, you know the point of intersection and have three lines of THREE.Line3(), now you can find the nearest edge by choosing the closest line to the point of intersection, and .closestPointToPoint() method will help. This method returns the closes point on an edge relatively to the point of intersection.
Means, that you just find three distances from the point of intersection to three edges of the face. And you'll highlight the edge with the minimal distance from it to the point of intersection.
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 1, 1000);
camera.position.set(0, 0, 10);
var renderer = new THREE.WebGLRenderer({
antialias: true
});
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
var geom = new THREE.PlaneBufferGeometry(8, 8).toNonIndexed();
var mat = new THREE.MeshBasicMaterial({
color: "blue"
});
var mesh = new THREE.Mesh(geom, mat);
scene.add(mesh);
renderer.domElement.addEventListener("mousemove", onMouseMove, false);
var raycaster = new THREE.Raycaster();
var mouse = new THREE.Vector2();
var intersects = [];
var localPoint = new THREE.Vector3();
let closestPoint = new THREE.Vector3();
var edgeGeom = new THREE.BufferGeometry().setFromPoints([
new THREE.Vector3(),
new THREE.Vector3()
]);
var edge = new THREE.Line(edgeGeom, new THREE.LineBasicMaterial({
color: "aqua"
}));
scene.add(edge);
var pos = mesh.geometry.attributes.position;
function onMouseMove(event) {
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
intersects = raycaster.intersectObject(mesh);
if (intersects.length === 0) return;
let faceIdx = intersects[0].faceIndex;
let lines = [
new THREE.Line3(
new THREE.Vector3().fromBufferAttribute(pos, faceIdx * 3 + 0),
new THREE.Vector3().fromBufferAttribute(pos, faceIdx * 3 + 1)
),
new THREE.Line3(
new THREE.Vector3().fromBufferAttribute(pos, faceIdx * 3 + 1),
new THREE.Vector3().fromBufferAttribute(pos, faceIdx * 3 + 2)
),
new THREE.Line3(
new THREE.Vector3().fromBufferAttribute(pos, faceIdx * 3 + 2),
new THREE.Vector3().fromBufferAttribute(pos, faceIdx * 3 + 0)
)
];
let edgeIdx = 0;
mesh.worldToLocal(localPoint.copy(intersects[0].point));
let minDistance = 1000;
for (let i = 0; i < 3; i++) {
lines[i].closestPointToPoint(localPoint, true, closestPoint);
let dist = localPoint.distanceTo(closestPoint);
if (dist < minDistance) {
minDistance = dist;
edgeIdx = i;
}
}
let pStart = mesh.localToWorld(lines[edgeIdx].start);
let pEnd = mesh.localToWorld(lines[edgeIdx].end);
edgeGeom.attributes.position.setXYZ(0, pStart.x, pStart.y, pStart.z);
edgeGeom.attributes.position.setXYZ(1, pEnd.x, pEnd.y, pEnd.z);
edgeGeom.attributes.position.needsUpdate = true;
}
render();
function render() {
requestAnimationFrame(render);
renderer.render(scene, camera);
}
body{
overflow: hidden;
margin: 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/95/three.min.js"></script>
The short version: How can one make a camera follow an object controlled by physics within a Three.js scene?
The long version: I'm working on a Three.js scene in which the W,A,S,D keys move a sphere along a plane. So far, however, I haven't figured out how to make the camera follow behind the sphere.
In the example below, the camera follows the sphere perfectly if one only presses the W key. However, if one presses A or D, the sphere starts to turn, and the camera is no longer behind the ball. If the sphere starts to turn, I want the camera to turn with it, so the camera is always following just behind the sphere, and is always a constant distance from the sphere. As users continue to press W, the ball will continue rolling forward relative to the camera.
In a previous scene [demo], I was able to implement this behavior by creating the sphere, adding that sphere to a group, then using the following bit of code each frame:
var relativeCameraOffset = new THREE.Vector3(0,50,200);
var cameraOffset = relativeCameraOffset.applyMatrix4(sphereGroup.matrixWorld);
camera.position.x = cameraOffset.x;
camera.position.y = cameraOffset.y;
camera.position.z = cameraOffset.z;
camera.lookAt(sphereGroup.position);
The key in the demo above was to rotate the sphere while keeping the sphereGroup unrotated, so I could compute the cameraOffset on the un-rotated sphereGroup.
In the demo below, the sphere's position is controlled by the Cannon.js physics library, which translates and rotates the sphere as forces are applied to the body. Does anyone know how I can make the camera follow behind the sphere in the scene below?
/**
* Generate a scene object with a background color
**/
function getScene() {
var scene = new THREE.Scene();
scene.background = new THREE.Color(0x111111);
return scene;
}
/**
* Generate the camera to be used in the scene. Camera args:
* [0] field of view: identifies the portion of the scene
* visible at any time (in degrees)
* [1] aspect ratio: identifies the aspect ratio of the
* scene in width/height
* [2] near clipping plane: objects closer than the near
* clipping plane are culled from the scene
* [3] far clipping plane: objects farther than the far
* clipping plane are culled from the scene
**/
function getCamera() {
var aspectRatio = window.innerWidth / window.innerHeight;
var camera = new THREE.PerspectiveCamera(75, aspectRatio, 0.1, 10000);
camera.position.set(0, 2000, -5000);
camera.lookAt(scene.position);
return camera;
}
/**
* Generate the light to be used in the scene. Light args:
* [0]: Hexadecimal color of the light
* [1]: Numeric value of the light's strength/intensity
* [2]: The distance from the light where the intensity is 0
* #param {obj} scene: the current scene object
**/
function getLight(scene) {
var light = new THREE.PointLight( 0xffffff, 0.6, 0, 0 )
light.position.set( -2000, 1000, -2100 );
scene.add( light );
var light = new THREE.PointLight( 0xffffff, 0.15, 0, 0 )
light.position.set( -190, 275, -1801 );
light.castShadow = true;
scene.add( light );
// create some ambient light for the scene
var ambientLight = new THREE.AmbientLight(0xffffff, 0.8);
scene.add(ambientLight);
return light;
}
/**
* Generate the renderer to be used in the scene
**/
function getRenderer() {
// Create the canvas with a renderer
var renderer = new THREE.WebGLRenderer({antialias: true});
// Add support for retina displays
renderer.setPixelRatio(window.devicePixelRatio);
// Specify the size of the canvas
renderer.setSize(window.innerWidth, window.innerHeight);
// Enable shadows
renderer.shadowMap.enabled = true;
// Specify the shadow type; default = THREE.PCFShadowMap
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
// Add the canvas to the DOM
document.body.appendChild(renderer.domElement);
return renderer;
}
/**
* Generate the controls to be used in the scene
* #param {obj} camera: the three.js camera for the scene
* #param {obj} renderer: the three.js renderer for the scene
**/
function getControls(camera, renderer) {
var controls = new THREE.TrackballControls(camera, renderer.domElement);
controls.zoomSpeed = 0.4;
controls.panSpeed = 0.4;
return controls;
}
/**
* Get stats
**/
function getStats() {
stats = new Stats();
stats.domElement.style.position = 'absolute';
stats.domElement.style.top = '0px';
stats.domElement.style.right = '0px';
document.body.appendChild( stats.domElement );
return stats;
}
/**
* Get grass
**/
function getGrass() {
var texture = loader.load('http://4.bp.blogspot.com/-JiJEc7lH1Is/UHJs3kn261I/AAAAAAAADYA/gQRAxHK2q_w/s1600/tileable_old_school_video_game_grass.jpg');
texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
texture.repeat.set(10, 10);
var material = new THREE.MeshLambertMaterial({
map: texture,
side: THREE.DoubleSide,
});
return material;
}
function getPlanes(scene, loader) {
var planes = [];
var material = getGrass();
[ [4000, 2000, 0, 0, -1000, 0] ].map(function(p) {
var geometry = new THREE.PlaneGeometry(p[0], p[1]);
var plane = new THREE.Mesh(geometry, material);
plane.position.x = p[2];
plane.position.y = p[3];
plane.position.z = p[4];
plane.rotation.y = p[5];
plane.rotation.x = Math.PI / 2;
plane.receiveShadow = true;
planes.push(plane);
scene.add(plane);
})
return planes;
}
/**
* Add background
**/
function getBackground(scene, loader) {
var imagePrefix = 'sky-parts/';
var directions = ['right', 'left', 'top', 'bottom', 'front', 'back'];
var imageSuffix = '.bmp';
var geometry = new THREE.BoxGeometry( 4000, 4000, 4000 );
// Add each of the images for the background cube
var materialArray = [];
for (var i = 0; i < 6; i++)
materialArray.push( new THREE.MeshBasicMaterial({
//map: loader.load(imagePrefix + directions[i] + imageSuffix),
color: 0xff0000,
side: THREE.BackSide
}));
var sky = new THREE.Mesh( geometry, materialArray );
scene.add(sky);
return sky;
}
/**
* Add a character
**/
function getSphere(scene) {
var geometry = new THREE.SphereGeometry( 30, 12, 9 );
var material = new THREE.MeshPhongMaterial({
color: 0xd0901d,
emissive: 0xaa0000,
side: THREE.DoubleSide,
flatShading: true
});
var sphere = new THREE.Mesh( geometry, material );
// allow the sphere to cast a shadow
sphere.castShadow = true;
sphere.receiveShadow = false;
// create a group for translations and rotations
var sphereGroup = new THREE.Group();
sphereGroup.add(sphere)
sphereGroup.castShadow = true;
sphereGroup.receiveShadow = false;
scene.add(sphereGroup);
return [sphere, sphereGroup];
}
/**
* Initialize physics engine
**/
function getPhysics() {
world = new CANNON.World();
world.gravity.set(0, -400, 0); // earth = -9.82 m/s
world.broadphase = new CANNON.NaiveBroadphase();
world.broadphase.useBoundingBoxes = true;
var solver = new CANNON.GSSolver();
solver.iterations = 7;
solver.tolerance = 0.1;
world.solver = solver;
world.quatNormalizeSkip = 0;
world.quatNormalizeFast = false;
world.defaultContactMaterial.contactEquationStiffness = 1e9;
world.defaultContactMaterial.contactEquationRelaxation = 4;
return world;
}
/**
* Generate the materials to be used for contacts
**/
function getPhysicsMaterial() {
var physicsMaterial = new CANNON.Material('slipperyMaterial');
var physicsContactMaterial = new CANNON.ContactMaterial(
physicsMaterial, physicsMaterial, 0.0, 0.3)
world.addContactMaterial(physicsContactMaterial);
return physicsMaterial;
}
/**
* Add objects to the world
**/
function addObjectPhysics() {
addFloorPhysics()
addSpherePhysics()
}
function addFloorPhysics() {
floors.map(function(floor) {
var q = floor.quaternion;
floorBody = new CANNON.Body({
mass: 0, // mass = 0 makes the body static
material: physicsMaterial,
shape: new CANNON.Plane(),
quaternion: new CANNON.Quaternion(-q._x, q._y, q._z, q._w)
});
world.addBody(floorBody);
})
}
function addSpherePhysics() {
sphereBody = new CANNON.Body({
mass: 1,
material: physicsMaterial,
shape: new CANNON.Sphere(30),
linearDamping: 0.5,
position: new CANNON.Vec3(1000, 500, -2000)
});
world.addBody(sphereBody);
}
/**
* Store all currently pressed keys & handle window resize
**/
function addListeners() {
window.addEventListener('keydown', function(e) {
pressed[e.key.toUpperCase()] = true;
})
window.addEventListener('keyup', function(e) {
pressed[e.key.toUpperCase()] = false;
})
window.addEventListener('resize', function(e) {
windowHalfX = window.innerWidth / 2;
windowHalfY = window.innerHeight / 2;
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
if (typeof(controls) != 'undefined') controls.handleResize();
})
}
/**
* Update the sphere's position
**/
function moveSphere() {
var delta = clock.getDelta(); // seconds
var moveDistance = 500 * delta; // n pixels per second
var rotateAngle = Math.PI / 2 * delta; // 90 deg per second
// move forwards, backwards, left, or right
if (pressed['W'] || pressed['ARROWUP']) {
sphereBody.velocity.z += moveDistance;
}
if (pressed['S'] || pressed['ARROWDOWN']) {
sphereBody.velocity.z -= moveDistance;
}
if (pressed['A'] || pressed['ARROWLEFT']) {
sphereBody.velocity.x += moveDistance;
}
if (pressed['D'] || pressed['ARROWRIGHT']) {
sphereBody.velocity.x -= moveDistance;
}
}
/**
* Follow the sphere
**/
function moveCamera() {
camera.position.x = sphereBody.position.x + 0;
camera.position.y = sphereBody.position.y + 50;
camera.position.z = sphereBody.position.z + -200;
camera.lookAt(sphereGroup.position);
}
function updatePhysics() {
world.step(1/60);
sphereGroup.position.copy(sphereBody.position);
sphereGroup.quaternion.copy(sphereBody.quaternion);
}
// Render loop
function render() {
requestAnimationFrame(render);
renderer.render(scene, camera);
moveSphere();
updatePhysics();
if (typeof(controls) === 'undefined') moveCamera();
if (typeof(controls) !== 'undefined') controls.update();
if (typeof(stats) !== 'undefined') stats.update();
};
// state
var pressed = {};
var clock = new THREE.Clock();
// globals
var scene = getScene();
var camera = getCamera();
var light = getLight(scene);
var renderer = getRenderer();
var world = getPhysics();
var physicsMaterial = getPhysicsMaterial();
//var stats = getStats();
//var controls = getControls(camera, renderer);
// global body references
var sphereBody, floorBody;
// add meshes
var loader = new THREE.TextureLoader();
var floors = getPlanes(scene, loader);
var background = getBackground(scene, loader);
var sphereData = getSphere(scene);
var sphere = sphereData[0];
var sphereGroup = sphereData[1];
addObjectPhysics();
addListeners();
render();
body { margin: 0; overflow: hidden; }
canvas { width: 100%; height: 100%; }
<script src='https://cdnjs.cloudflare.com/ajax/libs/three.js/88/three.min.js'></script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/cannon.js/0.6.2/cannon.js'></script>
Answers to comment questions
#jparimaa I think the most intuitive implementation would make W add forward momentum, S add backward momentum, and A and D rotate the camera around the ball. Is that possible?
#HariV The controls you link to are the ones I used in the demo without physics above. Is it possible to get that logic working with physics?
I think it's most intuitive for users if the W key always moves the ball "forward" relative to the camera
One option would be to calculate the direction between the ball and the camera and add velocity to that direction. In this case if you push the ball forward then you could rotate the camera without it affecting the velocity of the ball. Only after you press W/S after the rotation it would change the direction. I'm not sure if that is what you want but maybe this will give you some ideas.
I tried the following code (rotation is global variable initialized to 0)
function moveSphere() {
var delta = clock.getDelta(); // seconds
var moveDistance = 500 * delta; // n pixels per second
var dir = new THREE.Vector3(sphereBody.position.x, sphereBody.position.y, sphereBody.position.z);
dir.sub(camera.position).normalize(); // direction vector between the camera and the ball
if (pressed['W'] || pressed['ARROWUP']) {
sphereBody.velocity.x += moveDistance * dir.x;
sphereBody.velocity.z += moveDistance * dir.z;
}
if (pressed['S'] || pressed['ARROWDOWN']) {
sphereBody.velocity.x -= moveDistance * dir.x;
sphereBody.velocity.z -= moveDistance * dir.z;
}
}
function moveCamera() {
var delta = clock.getDelta();
var sensitivity = 150;
var rotateAngle = Math.PI / 2 * delta * sensitivity;
if (pressed['A'] || pressed['ARROWLEFT']) {
rotation -= rotateAngle;
}
if (pressed['D'] || pressed['ARROWRIGHT']) {
rotation += rotateAngle;
}
var rotZ = Math.cos(rotation)
var rotX = Math.sin(rotation)
var distance = 200;
camera.position.x = sphereBody.position.x - (distance * rotX);
camera.position.y = sphereBody.position.y + 50;
camera.position.z = sphereBody.position.z - (distance * rotZ);
camera.lookAt(sphereGroup.position);
}
I am trying to add a number of spheres in the following example. Initially it had only three cubes, but I need to add some 10 spheres that would be equidistant from each other and would be rotating in different speeds.
My Try
var parent, renderer, scene, camera, controls;
init();
animate();
function init()
{
// renderer
renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// scene
scene = new THREE.Scene();
// camera
camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 1, 100);
camera.position.set(20, 20, 20);
// controls
controls = new THREE.OrbitControls(camera);
controls.minDistance = 10;
controls.maxDistance = 50;
// axes
scene.add(new THREE.AxisHelper(20));
// geometry
var geometry = new THREE.SphereGeometry(0.3, 50, 50, 0, Math.PI * 2, 0, Math.PI * 2)
// material
var material = new THREE.MeshBasicMaterial({
color: 0xffffff,
wireframe: true
});
// parent
parent = new THREE.Object3D();
scene.add(parent);
// pivots
var pivot1 = new THREE.Object3D();
var pivot2 = new THREE.Object3D();
var pivot3 = new THREE.Object3D();
var pivot4 = new THREE.Object3D();
pivot1.rotation.z = 0;
pivot2.rotation.z = 2 * Math.PI / 3;
pivot3.rotation.z = 4 * Math.PI / 3;
pivot4.rotation.z = 6 * Math.PI / 3;
parent.add(pivot1);
parent.add(pivot2);
parent.add(pivot3);
parent.add(pivot4);
// mesh
var mesh1 = new THREE.Mesh(geometry, material);
var mesh2 = new THREE.Mesh(geometry, material);
var mesh3 = new THREE.Mesh(geometry, material);
var mesh4 = new THREE.Mesh(geometry, material);
mesh1.position.y = 5;
mesh2.position.y = 5;
mesh3.position.y = 5;
mesh4.position.y = 5;
pivot1.add(mesh1);
pivot2.add(mesh2);
pivot3.add(mesh3);
pivot4.add(mesh4);
}
function animate()
{
requestAnimationFrame(animate);
parent.rotation.z += 0.01;
controls.update();
renderer.render(scene, camera);
}
Why am I not able to add more than 3 spheres into the scene? I tried to add the fourth sphere but it did not work. How can speed be accounted for here? That is: can I specify different speeds for some spheres?
Missing 4th Sphere
You specify:
pivot1.rotation.z = 0;
pivot2.rotation.z = 2 * Math.PI / 3;
pivot3.rotation.z = 4 * Math.PI / 3;
pivot4.rotation.z = 6 * Math.PI / 3;
6 * Math.PI / 3 = 2 * Math.PI
Note, three.js uses radians, therefore 2 * PI is 0 (a full revolution is the same place as no rotation.
So pivot1 and pivot4 have the same effective rotation and your 2 sphere end up in the same place in space.
Speed
You currently handle speed by mutating the z rotation on every frame.
parent.rotation.z += 0.01;
This obviously works just fine for a demo. You can speed it up by moving more per frame (or getting more frames, ie better machine or other upgrades)
parent.rotation.z += 0.04;
Now it rotates at 4 times the speed!
More Spheres
Once you get past working with counts larger than your number of fingers on a hand, I recommend getting generic with arrays. Instead of listing out pivot1, pivot2, pivot3, . . . pivot0451, generate this with a loop. (Functionally you could use ranges if you prefer).
First, we declare how many spheres to make. Then divide up the circle (2 * Math.PI radians to go around). Then for ever sphere, make a pivot. Then, for every pivot, add a mesh. And you're done.
var numberOfSpheres = 10;
var radiansPerSphere = 2 * Math.PI / numberOfSpheres;
// pivots
var pivots = [];
for (var i = 0; i < numberOfSpheres; i++) {
var pivot = new THREE.Object3D();
pivot.rotation.z = i * radiansPerSphere;
parent.add(pivot);
pivots.push(pivot);
}
var meshes = pivots.map((pivot) => {
var mesh = new THREE.Mesh(geometry, material);
mesh.position.y = 5;
pivot.add(mesh)
return mesh;
});
I implemented this at this codepen.io
Happy coding.