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);
}
Related
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>
I'm trying to raycast skinning mesh (of knowing issue) after some skeleton changes (without animation on it, so performance isn't a priority).
The tricky thing i imagine in this attempt is:
Load skinned mesh add to scene
Make some changes in positions of specific bones at loaded mesh
Copy geometries of transformed loaded mesh (maybe from buffer?)
Create new mesh (some kind of imitation ghost mesh) from copied geometries and apply to it
set raycast on ghost mesh with opacity material= 0.0
Above list should work, but I'm stuck third day on point 3 cause I can't get transformed vertices after skinning.
var scene, camera, renderer, mesh, ghostMesh;
var raycaster = new THREE.Raycaster();
var raycasterMeshHelper;
initScene();
render();
function initScene() {
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 200);
camera.position.set(20, 7, 3);
renderer = new THREE.WebGLRenderer({
antialias: true
});
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
window.addEventListener('resize', onWindowResize, false);
var orbit = new THREE.OrbitControls(camera, renderer.domElement);
//lights stuff
var ambientLight = new THREE.AmbientLight(0xffffff, 0.3);
scene.add(ambientLight);
var lights = [];
lights[0] = new THREE.PointLight(0xffffff, 1, 0);
lights[1] = new THREE.PointLight(0xffffff, 1, 0);
lights[2] = new THREE.PointLight(0xffffff, 1, 0);
lights[0].position.set(0, 200, 0);
lights[1].position.set(100, 200, 100);
lights[2].position.set(-100, -200, -100);
scene.add(lights[0]);
scene.add(lights[1]);
scene.add(lights[2]);
//raycaster mesh
var raycasterMaterial = new THREE.MeshBasicMaterial({
color: 0xdddddd,
opacity: 0.7,
transparent: true
});
var geometrySphere = new THREE.SphereGeometry(0.5, 16, 16);
raycasterMeshHelper = new THREE.Mesh(geometrySphere, raycasterMaterial);
raycasterMeshHelper.visible = false;
scene.add(raycasterMeshHelper);
renderer.domElement.addEventListener('mousemove', onMouseMove, false);
//model Loading
var loader = new THREE.JSONLoader();
loader.load("https://raw.githubusercontent.com/visus100/skinnedTests/master/js_fiddle/skinned_mesh.json", function(geometry) {
var meshMaterial = new THREE.MeshStandardMaterial({
color: 0x00df15,
skinning: true
});
mesh = new THREE.SkinnedMesh(geometry, meshMaterial);
scene.add(mesh);
var skeleton = new THREE.SkeletonHelper(mesh);
scene.add(skeleton);
//some experimental skeletonal changes
mesh.skeleton.bones[1].rotation.z += 0.10;
mesh.skeleton.bones[2].rotation.x += -0.65;
mesh.skeleton.bones[3].rotation.y += -0.45;
mesh.skeleton.bones[3].position.x += 0.11;
//updates matrix
mesh.updateMatrix();
mesh.geometry.applyMatrix(mesh.matrix);
mesh.updateMatrixWorld(true);
//crate ghost mesh geometry
createGhostMesh();
//crate point cloud helper from buffergeometry
var bufferGeometry = new THREE.BufferGeometry().fromGeometry(mesh.geometry);
var particesMaterial = new THREE.PointsMaterial({
color: 0xff00ea,
size: 0.07,
sizeAttenuation: false
});
particles = new THREE.Points(bufferGeometry, particesMaterial);
particles.sortParticles = true;
scene.add(particles);
});
}
function createGhostMesh() {
var geometryForGhostMesh = new THREE.Geometry();
//push vertices and other stuff to geometry
for (i = 0; i < mesh.geometry.vertices.length; i++) {
var temp = new THREE.Vector3(mesh.geometry.vertices[i].x, mesh.geometry.vertices[i].y, mesh.geometry.vertices[i].z);
geometryForGhostMesh.vertices.push(temp);
//////
//here should be the code for calc translation vertices of skinned mesh and added to geometryForGhostMesh
//////
geometryForGhostMesh.skinIndices.push(mesh.geometry.skinIndices[i]);
geometryForGhostMesh.skinWeights.push(mesh.geometry.skinWeights[i]);
}
for (i = 0; i < mesh.geometry.faces.length; i++) {
geometryForGhostMesh.faces.push(mesh.geometry.faces[i]);
}
//create material and add to scene
var ghostMaterial = new THREE.MeshBasicMaterial({
color: 0xff0000,
opacity: 0.1,
transparent: true,
skinning: true
});
ghostMesh = new THREE.Mesh(geometryForGhostMesh, ghostMaterial);
scene.add(ghostMesh);
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
function render() {
requestAnimationFrame(render);
renderer.render(scene, camera);
};
function onMouseMove(event) {
//raycaster for ghostMesh
if (ghostMesh) {
var rect = renderer.domElement.getBoundingClientRect();
var mouseX = ((event.clientX - rect.left) / rect.width) * 2 - 1;
var mouseY = -((event.clientY - rect.top) / rect.height) * 2 + 1;
raycaster.setFromCamera(new THREE.Vector2(mouseX, mouseY), camera);
var intersects = raycaster.intersectObject(ghostMesh);
if (intersects.length > 0) {
raycasterMeshHelper.visible = true;
raycasterMeshHelper.position.set(0, 0, 0);
raycasterMeshHelper.lookAt(intersects[0].face.normal);
raycasterMeshHelper.position.copy(intersects[0].point);
} else {
raycasterMeshHelper.visible = false;
}
}
}
body {
margin: 0px;
background-color: #000000;
overflow: hidden;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/98/three.min.js"></script>
<script src="https://threejs.org/examples/js/controls/OrbitControls.js"></script>
Please note that I need this in thre.js build r98 or less, because the rest of my code (not included here) and without morph tangents only skinning bones.
I tried to write it clearly and please if anyone want help do it so because I'm not a pro.
I not including my approach of calculating transformed geometries because I failed too hard.
I dug a lot about this problem here e.g. issue6440 and for today it's still not fixed.
But there existing methods to work with it e.g https://jsfiddle.net/fnjkeg9x/1/ but after several of attempts I failed and my conclusion is that the stormtrooper works on morph tanges and this could be the reason I failed.
EDIT:
I created next codepen based on this topics get-the-global-position-of-a-vertex-of-a-skinned-mesh and Stormtrooper.
Decided to start with simple box to make bounding around skinned transformed mesh.
Result is fail because it giving 0 at line:
boneMatrix.fromArray(skeleton.boneMatrices, si * 16);
Here i comparing stormtrooper with my example output from console: Screen shot image
Codpen with new progress: https://codepen.io/donkeyLuck0/pen/XQbBMQ
My other idea is to apply this bones form loaded model and rig as a morph tangent programmatically (but i don't even know if it is possible and how to figure it out)
Founded example of animated model
Sketchfab animation with points tracking
This is super late to the game, but here's an example of GPU picking that works with skinned meshes and doesn't require a separate picking scene to keep in sync with your main scene, nor does it require the user to manage custom materials:
https://github.com/bzztbomb/three_js_gpu_picking
The trick that allows for easy material overriding and scene re-use is here:
https://github.com/bzztbomb/three_js_gpu_picking/blob/master/gpupicker.js#L58
A proper support for raycasting for skinned meshes was added in https://github.com/mrdoob/three.js/pull/19178 in revision 116.
You can use GPU picking to "pick" skinned object. It won't give you a position though
Note: GPU picking requires rendering every pickable object with a custom material. How you implement that is up to you. This article does it by making 2 scenes. That might not be as useful for skinned objects.
Unfortunately three.js provides no way to override materials AFAICT. Here's an example that replaces the materials on the pickable objects before rendering for picking and then restores them after. You would also need to hide any objects you don't want picked.
const renderer = new THREE.WebGLRenderer({
antialias: true,
canvas: document.querySelector('canvas'),
});
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, 2, 0.1, 200);
camera.position.set(20, 7, 3);
const orbit = new THREE.OrbitControls(camera, renderer.domElement);
//lights stuff
const ambientLight = new THREE.AmbientLight(0xffffff, 0.3);
scene.add(ambientLight);
const lights = [];
lights[0] = new THREE.PointLight(0xffffff, 1, 0);
lights[1] = new THREE.PointLight(0xffffff, 1, 0);
lights[2] = new THREE.PointLight(0xffffff, 1, 0);
lights[0].position.set(0, 200, 0);
lights[1].position.set(100, 200, 100);
lights[2].position.set(-100, -200, -100);
scene.add(lights[0]);
scene.add(lights[1]);
scene.add(lights[2]);
//raycaster mesh
const raycasterMaterial = new THREE.MeshBasicMaterial({
color: 0xdddddd,
opacity: 0.7,
transparent: true
});
const geometrySphere = new THREE.SphereGeometry(0.5, 16, 16);
raycasterMeshHelper = new THREE.Mesh(geometrySphere, raycasterMaterial);
raycasterMeshHelper.visible = false;
scene.add(raycasterMeshHelper);
//model Loading
const pickableObjects = [];
const loader = new THREE.JSONLoader();
loader.load("https://raw.githubusercontent.com/visus100/skinnedTests/master/js_fiddle/skinned_mesh.json", (geometry) => {
const meshMaterial = new THREE.MeshStandardMaterial({
color: 0x00df15,
skinning: true
});
const mesh = new THREE.SkinnedMesh(geometry, meshMaterial);
scene.add(mesh);
const id = pickableObjects.length + 1;
pickableObjects.push({
mesh,
renderingMaterial: meshMaterial,
pickingMaterial: new THREE.MeshPhongMaterial({
skinning: true,
emissive: new THREE.Color(id),
color: new THREE.Color(0, 0, 0),
specular: new THREE.Color(0, 0, 0),
//map: texture,
//transparent: true,
//side: THREE.DoubleSide,
//alphaTest: 0.5,
blending: THREE.NoBlending,
}),
});
//some experimental skeletonal changes
mesh.skeleton.bones[1].rotation.z += 0.10;
mesh.skeleton.bones[2].rotation.x += -0.65;
mesh.skeleton.bones[3].rotation.y += -0.45;
mesh.skeleton.bones[3].position.x += 0.11;
//updates matrix
mesh.updateMatrix();
mesh.geometry.applyMatrix(mesh.matrix);
mesh.updateMatrixWorld(true);
});
class GPUPickHelper {
constructor() {
// create a 1x1 pixel render target
this.pickingTexture = new THREE.WebGLRenderTarget(1, 1);
this.pixelBuffer = new Uint8Array(4);
}
pick(cssPosition, scene, camera) {
const {
pickingTexture,
pixelBuffer
} = this;
// set the view offset to represent just a single pixel under the mouse
const pixelRatio = renderer.getPixelRatio();
camera.setViewOffset(
renderer.context.drawingBufferWidth, // full width
renderer.context.drawingBufferHeight, // full top
cssPosition.x * pixelRatio | 0, // rect x
cssPosition.y * pixelRatio | 0, // rect y
1, // rect width
1, // rect height
);
// render the scene
// r102
//renderer.setRenderTarget(pickingTexture);
//renderer.render(scene, camera);
//renderer.setRenderTarget(null);
// r98
renderer.render(scene, camera, pickingTexture);
// clear the view offset so rendering returns to normal
camera.clearViewOffset();
//read the pixel
renderer.readRenderTargetPixels(
pickingTexture,
0, // x
0, // y
1, // width
1, // height
pixelBuffer);
const id =
(pixelBuffer[0] << 16) |
(pixelBuffer[1] << 8) |
(pixelBuffer[2]);
return id;
}
}
function resizeRendererToDisplaySize(renderer) {
const canvas = renderer.domElement;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const needResize = canvas.width !== width || canvas.height !== height;
if (needResize) {
renderer.setSize(width, height, false);
}
return needResize;
}
const pickPosition = {
x: 0,
y: 0,
};
const pickHelper = new GPUPickHelper();
let lastPickedId = 0;
let lastPickedObjectSavedEmissive;
function render(time) {
time *= 0.001; // convert to seconds;
if (resizeRendererToDisplaySize(renderer)) {
const canvas = renderer.domElement;
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
}
if (lastPickedId) {
pickableObjects[lastPickedId - 1].renderingMaterial.emissive.setHex(lastPickedObjectSavedEmissive);
lastPickedId = 0;
}
for (pickableObject of pickableObjects) {
pickableObject.mesh.material = pickableObject.pickingMaterial;
}
const id = pickHelper.pick(pickPosition, scene, camera, time);
for (pickableObject of pickableObjects) {
pickableObject.mesh.material = pickableObject.renderingMaterial;
}
const pickedObject = pickableObjects[id - 1];
if (pickedObject) {
lastPickedId = id;
lastPickedObjectSavedEmissive = pickedObject.renderingMaterial.emissive.getHex();
pickedObject.renderingMaterial.emissive.setHex((time * 8) % 2 > 1 ? 0xFFFF00 : 0xFF0000);
}
renderer.render(scene, camera);
requestAnimationFrame(render);
};
requestAnimationFrame(render);
function setPickPosition(event) {
pickPosition.x = event.clientX;
pickPosition.y = event.clientY;
}
function clearPickPosition() {
// unlike the mouse which always has a position
// if the user stops touching the screen we want
// to stop picking. For now we just pick a value
// unlikely to pick something
pickPosition.x = -100000;
pickPosition.y = -100000;
}
window.addEventListener('mousemove', setPickPosition);
window.addEventListener('mouseout', clearPickPosition);
window.addEventListener('mouseleave', clearPickPosition);
window.addEventListener('touchstart', (event) => {
// prevent the window from scrolling
event.preventDefault();
setPickPosition(event.touches[0]);
}, {
passive: false
});
window.addEventListener('touchmove', (event) => {
setPickPosition(event.touches[0]);
});
window.addEventListener('touchend', clearPickPosition);
window.addEventListener('mousemove', setPickPosition);
window.addEventListener('mouseout', clearPickPosition);
window.addEventListener('mouseleave', clearPickPosition);
body {
margin: 0;
}
canvas {
width: 100vw;
height: 100vh;
display: block;
}
<script src="https://threejsfundamentals.org/threejs/resources/threejs/r98/three.min.js"></script>
<script src="https://threejsfundamentals.org/threejs/resources/threejs/r98/js/controls/OrbitControls.js"></script>
<canvas></canvas>
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
I'm trying to rotate multiple objects around the same Vector3 point but in different directions - so they effectively 'swarm' around the point.
I'm using the trig approach rather simply wrapping each object in a Container and applying some random rotations as I'm projecting their 3D vector to 2D positions to attach label DIVs above the canvas, and the container approach messes with with project class.
Here's my current code which makes all objects rotate around the point along the same orbit path:
for(var i = 0; i<objectsArr.length; i++){
var obj = objectsArr[i];
var radius = obj.angle * (Math.PI / 180);
obj.position.x = obj.radius * Math.cos(radius);
obj.position.y = obj.radius * Math.sin(radius);
obj.angle += obj.orbitSpeed;
}
Does anyone know how I can make them orbit in random directions along X, Y, Z axis?
First, see this answer about rotating objects about a point.
From your code, it looks like you're storing each object's orbit speed, and their current angle of rotation (which represents the vector to the position). Rather than storing a 2D angle, store it as a Vector3 which represents the normal of (perpendicular to) the object's orbital plane. (This will let you really get a "swarm" going later, when you can define different orbital planes.) I also recommend storing the orbit speed in radians per step, so you don't have to perform the conversion every pass.
The rest of problem actually becomes very simple with the Vector3.applyAxisAngle method.
Some pseudo-code:
Subtract the rotation point position from the object's position.
Use the object's orbit speed, and angle to update the temp position.
Add the rotation point position back to the object's position.
To see it in your code:
var obj;
for(var i = 0; i< objectsArr.length; i++){
obj = objectsArr[i];
obj.position.sub(rotationPoint); // rotationPoint is a Vector3
obj.position.applyAxisAngle(obj.angle, obj.orbitSpeed);
obj.add(rotationPoint);
}
And here's a live demo of a few objects orbiting randomly about a "nucleus" at (10, 10, 10).
var renderer, scene, camera, controls, stats, nucleus;
var WIDTH = window.innerWidth,
HEIGHT = window.innerHeight,
FOV = 60,
NEAR = 1,
FAR = 1000;
var electrons = [],
numElectrons = 100; // more electrons = slower updating
function populateScene() {
var geo = new THREE.SphereBufferGeometry(10, 16, 16);
var mat = new THREE.MeshPhongMaterial({color:"blue"});
nucleus = new THREE.Mesh(geo, mat);
nucleus.position.set(10, 10, 10); // you can change these values
scene.add(nucleus);
var electron = null,
plane = new THREE.Plane(),
point = new THREE.Vector3();
geo = new THREE.SphereBufferGeometry(1, 16, 16);
mat = new THREE.MeshPhongMaterial({color:"red"});
for(var i = 0; i < numElectrons; ++i){
electron = new THREE.Mesh(geo, mat);
electrons.push(electron);
electron.angle = new THREE.Vector3(
Math.random(),
Math.random(),
Math.random()
).normalize();
electron.orbitSpeed = (Math.random() * 0.05) + 0.05;
if(Math.random() > 0.5) electron.orbitSpeed *= -1;
plane.normal.copy(electron.angle);
point.set(Math.random(), Math.random(), Math.random());
plane.projectPoint(point, electron.position);
electron.position.setLength(Math.floor(Math.random() * 20) + 15);
electron.position.applyAxisAngle(electron.angle, Math.random() / 10);
electron.position.add(nucleus.position);
scene.add(electron);
}
}
function updateElectrons(){
var obj = null;
for(var i = 0; i < numElectrons; ++i){
obj = electrons[i]
obj.position.sub(nucleus.position);
obj.position.applyAxisAngle(obj.angle, obj.orbitSpeed);
obj.position.add(nucleus.position);
}
}
function init() {
document.body.style.backgroundColor = "slateGray";
renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true
});
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.body.appendChild(renderer.domElement);
document.body.style.overflow = "hidden";
document.body.style.margin = "0";
document.body.style.padding = "0";
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(FOV, WIDTH / HEIGHT, NEAR, FAR);
camera.position.z = 100;
scene.add(camera);
controls = new THREE.TrackballControls(camera, renderer.domElement);
controls.dynamicDampingFactor = 0.5;
controls.rotateSpeed = 3;
var light = new THREE.PointLight(0xffffff, 1, Infinity);
camera.add(light);
stats = new Stats();
stats.domElement.style.position = 'absolute';
stats.domElement.style.top = '0';
document.body.appendChild(stats.domElement);
resize();
window.onresize = resize;
populateScene();
animate();
}
function resize() {
WIDTH = window.innerWidth;
HEIGHT = window.innerHeight;
if (renderer && camera && controls) {
renderer.setSize(WIDTH, HEIGHT);
camera.aspect = WIDTH / HEIGHT;
camera.updateProjectionMatrix();
controls.handleResize();
}
}
function render() {
renderer.render(scene, camera);
}
function animate() {
requestAnimationFrame(animate);
updateElectrons();
render();
controls.update();
stats.update();
}
function threeReady() {
init();
}
(function() {
function addScript(url, callback) {
callback = callback || function() {};
var script = document.createElement("script");
script.addEventListener("load", callback);
script.setAttribute("src", url);
document.head.appendChild(script);
}
addScript("https://threejs.org/build/three.js", function() {
addScript("https://threejs.org/examples/js/controls/TrackballControls.js", function() {
addScript("https://threejs.org/examples/js/libs/stats.min.js", function() {
threeReady();
})
})
})
})();
three.js r86
I have created an inset trackball camera controller for a 3D scene, using three.js. Currently, this uses a tiny cube, a circle and an orthographic camera placed at the origin of the world. However, these three objects are still visible in the scene itself, as viewed through the main camera. (In my demo code below, I have deliberately made the cube 10x10x10 so that it is clearly visible, but it could be made much smaller.)
Also, elements that are part of the main scene that pass through the origin are visible in the inset. For example: the AxisHelper that belongs to the main scene can be seen in the inset.
Is it possible in three.js/webgl to make certain objects visible to only certain cameras?
If not, then a workaround would be to place the objects required for the trackball feature way off out into deep space, where the main camera cannot see them, but I would prefer a purer solution if possible.
Demo: http://codepen.io/anon/pen/MKWrOr
<!DOCTYPE html>
<html>
<head>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r73/three.js"></script>
<style>
body {
margin: 0;
overflow: hidden;
}
</style>
</head>
<body>
<div id="WebGL-output"></div>
<script>
function init() {
var scene = new THREE.Scene()
var renderer = new THREE.WebGLRenderer()
var camera
var cameras = []
var WIDTH = window.innerWidth
var HEIGHT = window.innerHeight
;(function createPerspectiveCamera(){
var FOV = 45
var ASPECT = WIDTH / HEIGHT
var NEAR = 1
var FAR = 360
camera = new THREE.PerspectiveCamera(FOV, ASPECT, NEAR, FAR)
camera.position.x = 100
camera.position.y = 100
camera.position.z = 100
camera.viewport = { x: 0, y: 0, width: WIDTH, height: HEIGHT }
camera.lookAt(scene.position)
cameras.push(camera)
})()
;(function initializeRenderer(){
renderer.setClearColor(new THREE.Color(0xEEEEFF))
renderer.setSize(WIDTH, HEIGHT)
renderer.autoClear = false;
document.getElementById("WebGL-output").appendChild(renderer.domElement)
;(function render() {
var viewport
renderer.setViewport( 0, 0, WIDTH, HEIGHT );
renderer.clear();
cameras.forEach(function (camera) {
viewport = camera.viewport // custom property
renderer.setViewport(
viewport.x
, viewport.y
, viewport.width
, viewport.height
)
renderer.render(scene, camera)
})
requestAnimationFrame(render)
})()
})()
;(function createCameraController(){
var viewport = {
x: WIDTH - 100
, y: HEIGHT - 100
, width: 100
, height: 100
}
var circle = {
x: WIDTH - 50
, y: 50
, radius: 50
}
var settings = {
viewport: viewport
, circle: circle
}
addCameraController(scene, camera, cameras, settings)
})()
// Something to look at
scene.add(new THREE.AxisHelper(70))
}
function addCameraController(scene, camera, cameras, settings) {
var controlCamera
var viewport = settings.viewport
// For mouse interactions
var centreX = settings.circle.x
var centreY = settings.circle.y
var radius = settings.circle.radius
var radius2 = radius * radius
var rotationMatrix = new THREE.Matrix4()
var pivotMatrix = new THREE.Matrix4()
var startMatrix = new THREE.Matrix4()
var start = new THREE.Vector3()
var end = new THREE.Vector3()
var angle
camera.matrixAutoUpdate = false /** takes control of main camera **/
;(function createControlCameraCubeAndCircle(){
var side = 10
var radius = Math.sqrt(side/2 * side/2 * 3)
;(function createCamera(){
controlCamera = new THREE.OrthographicCamera(
-radius, radius
, radius, -radius
, -radius, radius
);
controlCamera.viewport = viewport
controlCamera.rotation.copy(camera.rotation)
// If matrixAutoUpdate is set immediately, the camera rotation is
// not applied
setTimeout(function () {
controlCamera.matrixAutoUpdate = false
}, 1)
scene.add(controlCamera)
cameras.push( controlCamera )
})()
;(function createCompanionCube(){
var cube = new THREE.Object3D()
var cubeGeometry = new THREE.BoxGeometry( side, side, side )
var lineMaterial = new THREE.LineBasicMaterial({
color: 0xffffff
, transparent: true
, opacity: 0.5
})
var faceMaterial = new THREE.MeshPhongMaterial({
color: 0x006699
, emissive: 0x006699
, shading: THREE.FlatShading
, transparent: true
, opacity: 0.2
})
cube.add(
new THREE.LineSegments(
new THREE.WireframeGeometry( cubeGeometry )
, lineMaterial
)
)
cube.add(
new THREE.Mesh(
cubeGeometry
, faceMaterial
)
)
// cube.add(new THREE.AxisHelper(radius))
scene.add(cube);
})()
;(function createCircle(){
var circleGeometry = new THREE.CircleGeometry( radius, 36 );
var material = new THREE.MeshBasicMaterial( {
color: 0xccccff
} );
var circle = new THREE.Mesh( circleGeometry, material );
controlCamera.add( circle );
circle.translateZ(-radius)
})()
})()
window.addEventListener("mousedown", startDrag, false)
function startDrag(event) {
var x = event.clientX - centreX
var y = centreY - event.clientY
var delta2 = x * x + y * y
if (delta2 > radius2) {
return
}
var z = Math.sqrt(radius2 - delta2)
start.set(x, y, z)
window.addEventListener("mousemove", drag, false)
window.addEventListener("mouseup", stopDrag, false)
function drag(event) {
var delta
x = event.clientX - centreX
y = centreY - event.clientY
delta2 = x * x + y * y
if (delta2 > radius2) {
// constrain to adge of sphere
delta = Math.sqrt(delta2)
x = x / delta * radius
y = y / delta * radius
z = 0
} else {
z = Math.sqrt(radius2 - delta2)
}
end.set(x, y, z)
angle = start.angleTo(end)
start.cross(end).normalize()
rotationMatrix.makeRotationAxis(start, -angle)
controlCamera.matrix.multiply(rotationMatrix)
controlCamera.matrixWorldNeedsUpdate = true
rotationMatrix.extractRotation(camera.matrixWorld)
start.applyMatrix4(rotationMatrix).normalize()
rotationMatrix.makeRotationAxis(start, -angle)
camera.applyMatrix(rotationMatrix)
camera.matrixWorldNeedsUpdate = true
start.copy(end)
}
function stopDrag(event) {
window.removeEventListener("mousemove", drag, false)
window.removeEventListener("mouseup", stopDrag, false)
}
}
}
window.onload = init
</script>
</body>
</html>
three.js supports layers.
An object is visible to a camera if the object and the camera share a common layer. The camera and and all objects are by default in layer 0.
For example,
camera.layers.enable( 1 ); // camera now sees default layer 0 and layer 1
camera.layers.set( 1 ); // camera now sees only layer 1
mesh.layers.set( 1 ); // mesh is in layer 1
three.js r.75