First person shooter controls with three.js - javascript

I'm completely new to three.js and 3D. I'm trying to make a really simple first person shooter. I found heaps of examples but they all look really complicated. I want to understand the code before I use it. What I am having trouble with is the camera rotation. Everything else is fine. My approach doesn't quite work. It seems to be rotating on the z axis even though I'm setting that to 0. Here's all my code (125 lines)
var width = window.innerWidth, height = window.innerHeight,
scene = new THREE.Scene(),
camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000),
renderer = new THREE.WebGLRenderer(),
mouse = {
x: 0,
y: 0,
movedThisFrame: false
};
renderer.setSize(width, height);
document.body.appendChild(renderer.domElement);
var geometry = new THREE.BoxGeometry(1, 1, 1);
var material = new THREE.MeshBasicMaterial({
color: 0x00ff00
});
var cube = new THREE.Mesh(geometry, material);
scene.add(cube);
var floorgeometry = new THREE.BoxGeometry(100,0.1,100);
var floormaterial = new THREE.MeshBasicMaterial({
color: 0xff0000
});
var floor = new THREE.Mesh(floorgeometry, floormaterial);
floor.position.y = -1;
scene.add(floor);
var keys = {
w: false,
a: false,
s: false,
d: false
};
camera.position.z = 5;
function radDeg(radians) {
return radians * 180 / Math.PI;
}
function degRad(degrees) {
return degrees * Math.PI / 180;
}
function rotateCam() {
if (!mouse.movedThisFrame) {
mouse.x = 0;
mouse.y = 0;
}
/*
What am I doing wrong here?
*/
camera.rotation.x -= mouse.y * 0.001;
camera.rotation.y -= mouse.x * 0.001;
camera.rotation.z = 0;
mouse.movedThisFrame = false;
}
function moveCam() {
var rotation = camera.rotation.y % (Math.PI * 2), motion = [0,0];
if (keys.w) {
motion[0] += 0.1 * Math.cos(rotation);
motion[1] += 0.1 * Math.sin(rotation);
}
if (keys.a) {
motion[0] += 0.1 * Math.cos(rotation + degRad(90));
motion[1] += 0.1 * Math.sin(rotation + degRad(90));
}
if (keys.s) {
motion[0] += 0.1 * Math.cos(rotation - degRad(180));
motion[1] += 0.1 * Math.sin(rotation - degRad(180));
}
if (keys.d) {
motion[0] += 0.1 * Math.cos(rotation - degRad(90));
motion[1] += 0.1 * Math.sin(rotation - degRad(90));
}
camera.position.z -= motion[0];
camera.position.x -= motion[1];
}
window.onload = function() {
renderer.domElement.onclick = function() {
console.log('requested pointer lock');
renderer.domElement.requestPointerLock();
};
renderer.domElement.onmousemove = function(e) {
if (!mouse.movedThisFrame) {
mouse.x = e.movementX;
mouse.y = e.movementY;
mouse.movedThisFrame = true;
}
};
document.onkeydown = function(e) {
var char = String.fromCharCode(e.keyCode);
if (char == 'W')
keys.w = true;
else if (char == 'A')
keys.a = true;
else if (char == 'S')
keys.s = true;
else if (char == 'D')
keys.d = true;
};
document.onkeyup = function(e) {
var char = String.fromCharCode(e.keyCode);
if (char == 'W')
keys.w = false;
else if (char == 'A')
keys.a = false;
else if (char == 'S')
keys.s = false;
else if (char == 'D')
keys.d = false;
};
function animate() {
requestAnimationFrame(animate);
rotateCam();
moveCam();
renderer.render(scene, camera);
}
animate();
};
The problem is in the rotateCam function. It doesn't quite work and I don't really know why.
I also tried using the code on this question but it didn't work.

First person controls are more complicated than you may think. Even if you figure out your angle math, when the pointer is not locked, the mouse hits the window edge and turning stops.
I suggest you start with the pointer lock example (http://threejs.org/examples/#misc_controls_pointerlock) which is an example of first person controls for 3js.

Related

Cannot read properties of undefined (reading 'getHex’) when trying to combine webgl_interactive_cubes with pointer lock three.js

I’m trying to create scene with walk-navigation, with interactive objects, for educational purpose. I’m using Pointer Lock Control example for a walk navigation and interactive cubes example from three.js. It’s beginning so project is far from being perfect, although “walk” works. Unfortunately cursor part bugs out showing
Uncaught TypeError: Cannot read properties of undefined (reading 'getHex')
at render (three-fps.js:200:62)
at animate (three-fps.js:182:3)
this is my code:
import * as THREE from "three";
import { PointerLockControls } from "three/examples/jsm/controls/PointerLockControls.js";
let canvas;
let camera, scene, raycaster, renderer;
let INTERSECTED;
let controls;
let moveForward = false;
let moveBackward = false;
let moveLeft = false;
let moveRight = false;
let canJump = false;
let prevTime = performance.now();
const velocity = new THREE.Vector3();
const direction = new THREE.Vector3();
const pointer = new THREE.Vector2();
const radius = 100;
init();
animate();
function init() {
canvas = document.getElementById("canvas");
camera = new THREE.PerspectiveCamera(
70,
window.innerWidth / window.innerHeight,
1,
10000
);
camera.position.y = 10;
//controls
controls = new PointerLockControls(camera, canvas);
const onKeyDown = function (event) {
switch (event.code) {
case "ArrowUp":
case "KeyW":
moveForward = true;
break;
case "ArrowLeft":
case "KeyA":
moveLeft = true;
break;
case "ArrowDown":
case "KeyS":
moveBackward = true;
break;
case "ArrowRight":
case "KeyD":
moveRight = true;
break;
case "Space":
if (canJump === true) velocity.y += 350;
canJump = false;
break;
}
};
const onKeyUp = function (event) {
switch (event.code) {
case "ArrowUp":
case "KeyW":
moveForward = false;
break;
case "ArrowLeft":
case "KeyA":
moveLeft = false;
break;
case "ArrowDown":
case "KeyS":
moveBackward = false;
break;
case "ArrowRight":
case "KeyD":
moveRight = false;
break;
}
};
document.addEventListener("keydown", onKeyDown);
document.addEventListener("keyup", onKeyUp);
scene = new THREE.Scene();
scene.background = new THREE.Color(0xf0f0f0);
const light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(1, 1, 1).normalize();
scene.add(light);
const geometry = new THREE.BoxGeometry(20, 20, 20);
for (let i = 0; i < 2000; i++) {
const object = new THREE.Mesh(
geometry,
new THREE.MeshLambertMaterial({ color: Math.random() * 0xffffff })
);
object.position.x = Math.random() * 800 - 400;
object.position.y = Math.random() * 800 - 400;
object.position.z = Math.random() * 800 - 400;
object.rotation.x = Math.random() * 2 * Math.PI;
object.rotation.y = Math.random() * 2 * Math.PI;
object.rotation.z = Math.random() * 2 * Math.PI;
object.scale.x = Math.random() + 0.5;
object.scale.y = Math.random() + 0.5;
object.scale.z = Math.random() + 0.5;
scene.add(object);
}
raycaster = new THREE.Raycaster();
// floor
let floorGeometry = new THREE.PlaneGeometry(2000, 2000, 100, 100);
floorGeometry.rotateX(-Math.PI / 2);
const floorMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff });
const floor = new THREE.Mesh(floorGeometry, floorMaterial);
scene.add(floor);
// Create a WebGL renderer
renderer = new THREE.WebGLRenderer({ canvas });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(canvas.clientWidth, canvas.clientHeight);
document.addEventListener("mousemove", onPointerMove);
//
window.addEventListener("resize", onWindowResize);
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
function onPointerMove(event) {
pointer.x = (event.clientX / window.innerWidth) * 2 - 1;
pointer.y = -(event.clientY / window.innerHeight) * 2 + 1;
}
//
function animate() {
requestAnimationFrame(animate);
const time = performance.now();
const delta = (time - prevTime) / 1000;
velocity.x -= velocity.x * 10.0 * delta;
velocity.z -= velocity.z * 10.0 * delta;
direction.z = Number(moveForward) - Number(moveBackward);
direction.x = Number(moveRight) - Number(moveLeft);
direction.normalize(); // this ensures consistent movements in all directions
if (moveForward || moveBackward) velocity.z -= direction.z * 400.0 * delta;
if (moveLeft || moveRight) velocity.x -= direction.x * 400.0 * delta;
controls.moveRight(-velocity.x * delta);
controls.moveForward(-velocity.z * delta);
prevTime = time;
camera.updateMatrixWorld();
render();
}
function render() {
camera.updateMatrixWorld();
// find intersections
raycaster.setFromCamera(pointer, camera);
const intersects = raycaster.intersectObjects(scene.children, false);
if (intersects.length > 0) {
if (INTERSECTED != intersects[0].object) {
if (INTERSECTED)
INTERSECTED.material.emissive.setHex(INTERSECTED.currentHex);
INTERSECTED = intersects[0].object;
INTERSECTED.currentHex = INTERSECTED.material.emissive.getHex();
INTERSECTED.material.emissive.setHex(0xff0000);
}
} else {
if (INTERSECTED)
INTERSECTED.material.emissive.setHex(INTERSECTED.currentHex);
INTERSECTED = null;
}
renderer.render(scene, camera);
}
// check for undefined 'emissive' on material
if (INTERSECTED.material.emissive != undefined) {
INTERSECTED.currentHex = INTERSECTED.material.emissive.getHex();
INTERSECTED.material.emissive.setHex(0xff0000);
}

Raycaster malfunctioning in three.js

I am creating a game where players can move around from first-person perspective, where the ground is generated with Perlin noise and therefore uneven. I would like to simulate gravity in the game. Hence, a raycasting thing has been implemented, which is supposed to find the player's distance from the ground and stop them from falling when they hit the ground. Here is my code (if the snipper is unclear visit https://3d.211368e.repl.co):
const scene = new THREE.Scene(), camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 1, 10000000000000), renderer = new THREE.WebGLRenderer(), canvas = renderer.domElement;
camera.rotation.order = "YXZ";
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMapType = THREE.PCFSoftShadowMap;
document.body.appendChild(canvas);
const light = new THREE.DirectionalLight( 0xffffff, 1);
light.position.set(0, 10000, 0);
light.castShadow = true;
light.shadow.camera.top = 10000;
light.shadow.camera.right = 10000;
light.shadow.camera.bottom = -10000;
light.shadow.camera.left = -10000;
light.shadow.camera.far = 100000;
wwwww
scene.add(light);
var sky = new THREE.Mesh(new THREE.SphereGeometry(100000, 3, 3, 0, Math.PI, 0, Math.PI), new THREE.MeshBasicMaterial({color: 0x579ebb}));
sky.material.side = THREE.BackSide;
sky.rotateX(-Math.PI / 2);
scene.add(sky);
class Vector2{
constructor(x, y){
this.x = x;
this.y = y;
}
dot(other){
return this.x * other.x + this.y * other.y;
}
}
function Shuffle(tab){
for(let e = tab.length-1; e > 0; e--){
let index = Math.round(Math.random() * (e-1)),
temp = tab[e];
tab[e] = tab[index];
tab[index] = temp;
}
}
function MakePermutation(){
let P = [];
for(let i = 0; i < 256; i++){
P.push(i);
}
Shuffle(P);
for(let i = 0; i < 256; i++){
P.push(P[i]);
}
return P;
}
let P = MakePermutation();
function GetConstantVector(v){
let h = v & 3;
if(h == 0) return new Vector2(1.0, 1.0);
if(h == 1) return new Vector2(-1.0, 1.0);
if(h == 2) return new Vector2(-1.0, -1.0);
return new Vector2(1.0, -1.0);
}
function Fade(t){
return ((6 * t - 15) * t + 10) * t ** 3;
}
function Lerp(t, a1, a2){
return a1 + t*(a2-a1);
}
function Noise2D(x, y){
let X = Math.floor(x) & 255;
let Y = Math.floor(y) & 255;
let xf = x - Math.floor(x);
let yf = y - Math.floor(y);
let topRight = new Vector2(xf - 1, yf - 1);
let topLeft = new Vector2(xf, yf - 1);
let bottomRight = new Vector2(xf - 1, yf);
let bottomLeft = new Vector2(xf, yf);
let valueTopRight = P[P[X+1]+Y+1];
let valueTopLeft = P[P[X]+Y+1];
let valueBottomRight = P[P[X+1]+Y];
let valueBottomLeft = P[P[X]+Y];
let dotTopRight = topRight.dot(GetConstantVector(valueTopRight));
let dotTopLeft = topLeft.dot(GetConstantVector(valueTopLeft));
let dotBottomRight = bottomRight.dot(GetConstantVector(valueBottomRight));
let dotBottomLeft = bottomLeft.dot(GetConstantVector(valueBottomLeft));
let u = Fade(xf);
let v = Fade(yf);
return Lerp(u, Lerp(v, dotBottomLeft, dotTopLeft), Lerp(v, dotBottomRight, dotTopRight));
}
const plane = new THREE.Mesh(new THREE.PlaneGeometry(10000, 10000, 500, 500), new THREE.MeshPhongMaterial({color: 0x00aa00}));
plane.rotateX(-Math.PI / 2 + 0.00001);
plane.receiveShadow = true;
for (let y = 0, i = 0; y < 501; y++){
for(let x = 0; x < 501; x++, i++){
let n = 0.0, a = 1.0, f = 0.005;
for (let o = 0; o < 3; o++){
let v = a*Noise2D(x*f, y*f);
n += v;
a *= 0.5;
f *= 2.0;
}
n += 1;
n /= 2;
plane.geometry.vertices[i].z = n * 1000;
}
}
scene.add(plane);
const point = plane.geometry.vertices[Math.floor(Math.random() * 1000)];
camera.position.set(point.x, point.z + 2, point.y);
const geo = new THREE.Mesh(new THREE.BoxGeometry(10, 10, 10), new THREE.MeshBasicMaterial({color: 0xff0000}));
geo.castShadow = true;
scene.add(geo);
const render = () => {
requestAnimationFrame(render);
const below = new THREE.Vector3(camera.position.x, -1000000, camera.position.y), cast = new THREE.Raycaster(camera.position, below), intersect = cast.intersectObject(plane);
if (intersect.length > 0){
if (intersect[0].distance < 3) camera.translateY(-1);
}else{
camera.translateY(-1);
}
renderer.render(scene, camera);
}
render();
onmousemove = () => {
if (camera.rotation._x > -0.8 || camera.rotation._y > -0.8){
camera.rotateX(-Math.atan(event.movementY / 300));
camera.rotateY(-Math.atan(event.movementX / 300));
}else{
if (Math.atan(event.movementY / 300) < 0) camera.rotateX(-Math.atan(event.movementY / 300));
if (Math.atan(event.movementX / 300) < 0) camera.rotateY(-Math.atan(event.movementX / 300));
}
camera.rotation.z = 0;
}
onresize = () => {
renderer.setSize(window.innerWidth, window.innerHeight);
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
}
onkeydown = (event) => {
if (event.key == "w") camera.translateZ(-10);
if (event.key == "a") camera.translateX(-1);
if (event.key == "s") camera.translateZ(1);
if (event.key == "d") camera.translateX(1);
if (event.key == "ArrowUp") camera.translateY(1);
if (event.key == "ArrowDown") camera.translateY(-1);
}
body{
margin: 0;
background-color: black;
overflow: hidden;
}
canvas{
border: none;
}
<meta name="viewport" content="width=device-width">
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/94/three.min.js"></script>
<script src="https://cdn.rawgit.com/mrdoob/three.js/0949e59f/examples/js/controls/OrbitControls.js"></script>
<script src="https://cdn.rawgit.com/mrdoob/three.js/0949e59f/examples/js/utils/SceneUtils.js"></script>
<script src="https://cdn.rawgit.com/mrdoob/three.js/0949e59f/examples/js/libs/dat.gui.min.js"></script>
If the ground is not detected at least 3 units below the camera, the player will continue falling. However, sometimes nothing is spotted below the camera, while the player is clearly hovering over the ground. This is extremely frustrating. Is there any reliable alternative method to solve this problem, such as using something other than raycasting? Or is there a bug in the code? TIA
See the Raycaster documentation. The constructor takes the origin, the direction and near and far parameters. So you could do:
const gravityDirection = new THREE.Vector3(0, -1, 0);
cast = new THREE.Raycaster(camera.position, gravityDirection, 0, 3);
and this also makes the distance check redundant, as the far parameter already filters out hits further away than 3 units.

Rotating an object with spherifical coordinates Three js

Im trying to roatate this space ship that i created, the rotation itself works the problem is that whenever i rotate it with the left and right arrow keys i want the spaceship to rotate just like with cartesian coordinates the problem is that whenever the objects rotates around the planet, the spaceship inclination doesnt change at all, like it would with cartesian coordinates and i dont get why really because the rotation around the planet itself works.
Sorry if the code is too long most of it is just to create the spaceship itself, i think the problem itself is in the update function itself.
Program:
/*global THREE*/
var camera = [];
var scene, renderer, currentCamera = 0;
var viewSize = 40;
var aspectRatio;
var geometry, material, mesh;
var wiredObjects = [];
var leftArrow, rightArrow, upArrow, downArrow;
var clock = new THREE.Clock();
//!!var controls;
var defaultScale = 1;
var planetRadius = 12;
var rocketHeight = planetRadius/12;
var rocketPartHeight = rocketHeight/2;
var rocketInfRadius = rocketPartHeight;
var rocketMidRadius = rocketPartHeight/2;
var rocketSupRadius = 0;
var boosterRadius = rocketInfRadius/5;
var boosterHeight = rocketInfRadius/4;
var rocketTrashDistance = 1.2 * planetRadius;
var objPositions = [];
var objAngles = [];
var nrTrash = 20;
var floatingTrash = [];
var trashSizes = [];
var minTrashSize = planetRadius/24;
var maxTrashSize = planetRadius/20;
var trashGeometries = [];
var copyVideo;
var universe;
var planet;
var rocket;
var loader = new THREE.TextureLoader();
var space_texture = new THREE.TextureLoader().load(
"https://wallpaperaccess.com/full/1268183.jpg"
);
'use strict';
function addObjPart(obj, geometry, mater, hex, x, y, z, rotX, rotY, rotZ) {
material = (mater != null)? mater : new THREE.MeshBasicMaterial({color: hex, wireframe: wires});
mesh = new THREE.Mesh(geometry, material);
mesh.rotateX(rotX);
mesh.rotateY(rotY);
mesh.rotateZ(rotZ);
mesh.position.set(x, y, z);
obj.add(mesh);
wiredObjects.push(mesh);
return mesh;
}
function getObjPositions() {
var i;
var nrObj = nrTrash+1;
var angleTheta, anglePhi;
var objX, objY, objZ;
var posVector = new THREE.Vector3(0,0,0); // spherical coordinates vector
var angleVector = new THREE.Vector2(0,0); // angles Theta and Phi for spherical coordinates
for (i = 0; i < nrObj; i++) {
angleTheta = Math.random() * 2*Math.PI;
anglePhi = Math.random() * 2*Math.PI;
angleVector.set(angleTheta, anglePhi);
objAngles.push(angleVector);
objX = rocketTrashDistance * Math.sin(angleTheta) * Math.sin(anglePhi);
objY = rocketTrashDistance * Math.cos(angleTheta);
objZ = rocketTrashDistance * Math.sin(angleTheta) * Math.cos(anglePhi);
posVector.set(objX, objY, objZ);
objPositions.push(posVector);
}
}
function createUniverse(x, y, z, scale) {
wires = true;
universe = new THREE.Object3D();
universe.scale.set(scale, scale, scale);
var rocketPos = objPositions[0];
addPlanet(universe, 0, 0, 0);
addRocket(universe, rocketPos.x, rocketPos.y, rocketPos.z);
addAux(universe);
universe.position.set(x, y, z);
scene.add(universe);
return universe;
}
function addPlanet(obj, x, y, z) {
planet = new THREE.Object3D();
geometry = new THREE.SphereGeometry(planetRadius);
var planetTexture = new THREE.TextureLoader().load(
"https://st2.depositphotos.com/5171687/44380/i/450/depositphotos_443805316-stock-photo-equirectangular-map-clouds-storms-earth.jpg"
);
var planetMaterial = new THREE.MeshBasicMaterial( {
map: planetTexture,
transparent:true,
side:THREE.DoubleSide,
} );
addObjPart(obj, geometry, planetMaterial, 0x0000ff, x, y, z, 0, 0, 0);
}
function addRocket(obj, x, y, z) {
rocket = new THREE.Group();
var n_rocket = new THREE.Object3D();
addRocketTop(n_rocket, 0, 0, -rocketPartHeight/2);
addRocketBottom(n_rocket, 0, 0, rocketPartHeight/2);
addRocketBooster(n_rocket, 0, rocketInfRadius-boosterRadius, rocketPartHeight+0.5*boosterHeight);
addRocketBooster(n_rocket, 0, -rocketInfRadius+boosterRadius,rocketPartHeight+0.5*boosterHeight);
addRocketBooster(n_rocket, rocketInfRadius-boosterRadius, 0, rocketPartHeight+0.5*boosterHeight);
addRocketBooster(n_rocket, -rocketInfRadius+boosterRadius, 0, rocketPartHeight+0.5*boosterHeight);
rocket.add(n_rocket);
rocket.position.set(x, y, z);
obj.add(rocket);
return rocket;
}
function addRocketTop(obj, x, y, z) {
geometry = new THREE.CylinderGeometry(rocketMidRadius, rocketSupRadius, rocketPartHeight, 41,1);
addObjPart(obj, geometry, null, 0xff0000, x, y, z, Math.PI/180*90, 0, 0);
}
function addRocketBottom(obj, x, y, z) {
geometry = new THREE.CylinderGeometry(rocketInfRadius, rocketMidRadius, rocketPartHeight, 41,1);
addObjPart(obj, geometry, null, 0x000fff, x, y, z, Math.PI/180*90, 0, 0);
}
function addRocketBooster(obj, x, y, z) {
geometry = new THREE.CapsuleGeometry(boosterRadius, boosterHeight, 0.5, 20);
addObjPart(obj, geometry, null, 0xff0000, x, y, z, Math.PI/180*90, 0, 0);
}
function addTrash(x, y, z) {
}
function render() {
renderer.render(scene, camera[currentCamera]); // tells 3js renderer to draw scene visualization based on camera
}
function onResize() {
if (window.innerWidth > 0 && window.innerHeight > 0){
var i;
var val = 2;
aspectRatio = window.innerWidth / window.innerHeight;
renderer.setSize(window.innerWidth, window.innerHeight);
var nrCameras = camera.length;
for (i = 0; i < 1; i++) { // Ortographic Cameras
camera[i].left = -viewSize * aspectRatio / val;
camera[i].right = viewSize * aspectRatio / val;
camera[i].top = viewSize / val;
camera[i].bottom = viewSize / -val;
camera[i].updateProjectionMatrix();
}
for (i=1; i < nrCameras; i++) { // Perspective cameras
camera[i].aspect = aspectRatio;
camera[i].updateProjectionMatrix();
}
}
}
function update()
{
var timeOccurred = clock.getDelta();
var rocketSpeed = Math.PI/180 * 40;
if (rightArrow || leftArrow || upArrow || downArrow) { // rocket movement flags
var rocketTheta = objAngles[0].x;
var rocketPhi = objAngles[0].y;
var rocketX, rocketY, rocketZ;
if (leftArrow){
rocketPhi += rocketSpeed * timeOccurred;
}
if (rightArrow){
//n_rocket.rotation.x += - rocketSpeed * timeOccurred;
rocketPhi += - rocketSpeed * timeOccurred;
}
if (upArrow){
//n_rocket.rotation.z += - rocketSpeed * timeOccurred;
rocketTheta += -rocketSpeed * timeOccurred;
}
if (downArrow){
//n_rocket.rotation.z += rocketSpeed * timeOccurred;
rocketTheta += rocketSpeed * timeOccurred;
}
rocketX = rocketTrashDistance * Math.sin(rocketTheta) * Math.sin(rocketPhi);
rocketY = rocketTrashDistance * Math.cos(rocketTheta);
rocketZ = rocketTrashDistance * Math.sin(rocketTheta) * Math.cos(rocketPhi);
rocket.position.set(rocketX, rocketY, rocketZ);
objAngles[0].set(rocketTheta, rocketPhi);
objPositions[0].set(rocketX, rocketY, rocketZ);
}
}
function animate() {
update();
requestAnimationFrame(animate);
// controls.update();
render();
}
function addAux(obj) {
geometry = new THREE.SphereGeometry(5);
addObjPart(obj, geometry, null, 0xffc0cb, 15, 0, 0);
addObjPart(obj, geometry, null, 0xffff00, 15, 0, 0);
addObjPart(obj, geometry, null, 0x0000ff, 15, 0, 0);
}
function createScene() {
scene = new THREE.Scene();
scene.add(new THREE.AxesHelper(100));
scene.background = space_texture;
getObjPositions();
universe = createUniverse(0, 0, 0, defaultScale);
}
function createOrtographicCamera(x, y, z) {
var val = 2;
aspectRatio = window.innerWidth / window.innerHeight;
var camera = new THREE.OrthographicCamera( viewSize * aspectRatio/-val,
viewSize * aspectRatio / val,
viewSize / val,
viewSize / -val,
1,
1000);
camera.position.x = x;
camera.position.y = y;
camera.position.z = z;
camera.lookAt(scene.position);
return camera;
}
function onKeyDown(e) {
var keyName = e.keyCode;
switch (keyName) {
case 49://1
currentCamera = 0;
break;
case 37 : // left arrow key
leftArrow = true;
break;
case 38: // up arrow key
upArrow = true;
break;
case 39: // right arrow key
rightArrow = true;
break;
case 40: // down arrow key
downArrow = true;
break;
default:
break;
}
}
function onKeyUp(e) {
var keyName = e.keyCode;
switch (keyName) {
case 37 : // left arrow key
leftArrow = false;
break;
case 38: // up arrow key
upArrow = false;
break;
case 39: // right arrow key
rightArrow = false;
break;
case 40: // down arrow key
downArrow = false;
break;
default:
break;
}
}
function init() {
renderer = new THREE.WebGLRenderer({antialias: true});
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
createScene();
camera[0] = createOrtographicCamera(viewSize, 0, 0);
//!! controls = new THREE.OrbitControls(camera[0], renderer.domElement);
animate();
window.addEventListener("resize", onResize);
window.addEventListener("keydown", onKeyDown);
window.addEventListener("keyup", onKeyUp);
}

How can I achieve an even distribution of sprites across the surface of a sphere in THREE.js?

I'm trying to make a database of words where the most important words are closer to the top of the sphere and the less important are further away. So I created a sphere with enough vertices for each word, created a list of those vertices in order of distance from the top of the sphere, and placed the text sprites at the positions of the vertices in order of that sorted list.
Video version: https://i.gyazo.com/aabaf0b4a26f4413dc6a0ebafab2b4bd.mp4
Sounded like a good plan in my head, but clearly the geometry of a sphere causes the words to be further spread out the further away from the top they are. I need a result that looks like a somewhat even distribution across the surface. It doesn't have to be perfect, just visually closer than this.
How can I achieve the desired effect?
Here are the relevant methods:
positionDb(db) {
console.log("mostRelated", db.mostRelated);
console.log("depthList", this.depthList);
let mostRelated = db.mostRelated;
let depthList = this.depthList;
for (let i = 0; i < mostRelated.length; i++) {
this.addTextNode(mostRelated[i].data, this.depthList[i].vertice, this.depthList[i].depth);
}
}
addTextNode(text, vert, distance) {
let fontSize = 0.5 * (600 / distance);
let sprite = new THREE.TextSprite({
fillStyle: '#000000',
fontFamily: '"Arial", san-serif',
fontSize: fontSize,
fontWeight: 'bold',
text: text
});
this.scene.add(sprite);
sprite.position.set(vert.x, vert.y, vert.z);
setTimeout(() => {
sprite.fontFamily = '"Roboto", san-serif';
}, 1000)
}
this.scene = scene;
this.geometry = new THREE.SphereGeometry(420, 50, 550);
var material = new THREE.MeshBasicMaterial({
color: 0x0011ff
});
var sphere = new THREE.Mesh(this.geometry, wireframe);
var wireframe = new THREE.WireframeGeometry(this.geometry);
let frontVert = {
x: 0,
y: 100,
z: 0
}
let depthList = [];
this.geometry.vertices.forEach(vertice => {
let depth = getDistance(frontVert, vertice);
if (depthList.length === 0) {
depthList.push({
depth,
vertice
});
} else {
let flag = false;
for (let i = 0; i < depthList.length; i++) {
let item = depthList[i];
if (depth < item.depth) {
flag = true;
depthList.splice(i, 0, {
depth,
vertice
});
break;
}
}
if (!flag) depthList.push({
depth,
vertice
});
}
});
Maybe a fibonacci sphere
function fibonacciSphere(numPoints, point) {
const rnd = 1;
const offset = 2 / numPoints;
const increment = Math.PI * (3 - Math.sqrt(5));
const y = ((point * offset) - 1) + (offset / 2);
const r = Math.sqrt(1 - Math.pow(y, 2));
const phi = (point + rnd) % numPoints * increment;
const x = Math.cos(phi) * r;
const z = Math.sin(phi) * r;
return new THREE.Vector3(x, y, z);
}
Example:
function fibonacciSphere(numPoints, point) {
const rnd = 1;
const offset = 2 / numPoints;
const increment = Math.PI * (3 - Math.sqrt(5));
const y = ((point * offset) - 1) + (offset / 2);
const r = Math.sqrt(1 - Math.pow(y, 2));
const phi = (point + rnd) % numPoints * increment;
const x = Math.cos(phi) * r;
const z = Math.sin(phi) * r;
return new THREE.Vector3(x, y, z);
}
function main() {
const fov = 75;
const aspect = 2; // the canvas default
const near = 0.1;
const far = 5;
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
camera.position.z = 2;
const scene = new THREE.Scene();
function addTextNode(text, vert) {
const div = document.createElement('div');
div.className = 'label';
div.textContent = text;
div.style.marginTop = '-1em';
const label = new THREE.CSS2DObject(div);
label.position.copy(vert);
scene.add(label);
}
const renderer = new THREE.CSS2DRenderer();
const container = document.querySelector('#c');
container.appendChild(renderer.domElement);
const controls = new THREE.OrbitControls(camera, renderer.domElement);
const numPoints = 50;
for (let i = 0; i < numPoints; ++i) {
addTextNode(`p${i}`, fibonacciSphere(numPoints, i));
}
function render(time) {
time *= 0.001;
// three's poor choice of how to hanlde size strikes again :(
renderer.setSize(container.clientWidth, container.clientHeight);
const canvas = renderer.domElement;
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
renderer.render(scene, camera);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
}
main();
body {
margin: 0;
overflow: hidden;
}
#c {
width: 100vw;
height: 100vh;
display: block;
}
.label {
color: red;
}
<script src="https://threejsfundamentals.org/threejs/resources/threejs/r113/build/three.min.js"></script>
<script src="https://threejsfundamentals.org/threejs/resources/threejs/r113/examples/js/controls/OrbitControls.js"></script>
<script src="https://threejsfundamentals.org/threejs/resources/threejs/r113/examples/js/renderers/CSS2DRenderer.js"></script>
<div id="c"></div>

ThreeJS: Find neighbor faces in PlaneBufferGeometry

I'm looking for a better/faster way to find the neighbor faces (that share the same edge) in my PlaneBufferGeometry. Currently my THREE.Raycaster intersects fine with my object (using intersectObject). But I need to find all surrounding faces.
At this moment I use the following dirty way to find the 'next door' faces. It works well in my scenario, but doesn't feel right:
let rc = new THREE.Raycaster();
let intersects = [];
for (let i = -1; i <= 1; i++) {
for (let j = -1; j <= 1; j++) {
let v = new THREE.Vector3(x + i, y, z + j);
rc.set(v, new THREE.Vector3(0, -1, 0));
rc.near = 0;
rc.far = 2;
let subIntersects = rc.intersectObject(mesh);
for (let n = 0; n < subIntersects.length; n++) {
intersects.push(subIntersects[n]);
}
}
}
Is there for instance a way to quickly find these faces in the mesh.geometry.attributes.position.array?
Any suggestions are welcome. Thanks in advance!
You mentioned 50k items in the position array. That doens't seem too slow to me. This example is only 10x10 so 100 items in the array so it's easy to see it's working, but I had it set to 200x200 which is 40k items and it seemed fast enough?
'use strict';
function main() {
const canvas = document.querySelector('#c');
const renderer = new THREE.WebGLRenderer({canvas});
const fov = 60;
const aspect = 2; // the canvas default
const near = 0.1;
const far = 200;
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
camera.position.z = 1;
const scene = new THREE.Scene();
scene.background = new THREE.Color('#444');
scene.add(camera);
const planeGeometry = new THREE.PlaneBufferGeometry(1, 1, 20, 20);
const material = new THREE.MeshBasicMaterial({color: 'blue'});
const plane = new THREE.Mesh(planeGeometry, material);
scene.add(plane);
const edgeGeometry = new THREE.BufferGeometry();
const positionNumComponents = 3;
edgeGeometry.setAttribute('position', planeGeometry.getAttribute('position'));
edgeGeometry.setIndex([]);
const edgeMaterial = new THREE.MeshBasicMaterial({
color: 'yellow',
wireframe: true,
depthTest: false,
});
const edges = new THREE.Mesh(edgeGeometry, edgeMaterial);
scene.add(edges);
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;
}
class PickHelper {
constructor() {
this.raycaster = new THREE.Raycaster();
}
pick(normalizedPosition, scene, camera, time) {
// cast a ray through the frustum
this.raycaster.setFromCamera(normalizedPosition, camera);
// get the list of objects the ray intersected
const intersectedObjects = this.raycaster.intersectObjects(scene.children, [plane]);
if (intersectedObjects.length) {
// pick the first object. It's the closest one
const intersection = intersectedObjects[0];
const faceIndex = intersection.faceIndex;
const indexAttribute = planeGeometry.getIndex();
const indices = indexAttribute.array;
const vertIds = indices.slice(faceIndex * 3, faceIndex * 3 + 3);
const neighbors = []; // note: self will be added to list
for (let i = 0; i < indices.length; i += 3) {
for (let j = 0; j < 3; ++j) {
const p0Ndx = indices[i + j];
const p1Ndx = indices[i + (j + 1) % 3];
if ((p0Ndx === vertIds[0] && p1Ndx === vertIds[1]) ||
(p0Ndx === vertIds[1] && p1Ndx === vertIds[0]) ||
(p0Ndx === vertIds[1] && p1Ndx === vertIds[2]) ||
(p0Ndx === vertIds[2] && p1Ndx === vertIds[1]) ||
(p0Ndx === vertIds[2] && p1Ndx === vertIds[0]) ||
(p0Ndx === vertIds[0] && p1Ndx === vertIds[2])) {
neighbors.push(...indices.slice(i, i + 3));
break;
}
}
}
const edgeIndices = edgeGeometry.getIndex();
edgeIndices.array = new Uint16Array(neighbors);
edgeIndices.count = neighbors.length;
edgeIndices.needsUpdate = true;
}
}
}
const pickPosition = {x: 0, y: 0};
const pickHelper = new PickHelper();
clearPickPosition();
function render(time) {
time *= 0.001; // convert to seconds;
if (resizeRendererToDisplaySize(renderer)) {
const canvas = renderer.domElement;
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
}
pickHelper.pick(pickPosition, scene, camera, time);
renderer.render(scene, camera);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
function getCanvasRelativePosition(event) {
const rect = canvas.getBoundingClientRect();
return {
x: event.clientX - rect.left,
y: event.clientY - rect.top,
};
}
function setPickPosition(event) {
const pos = getCanvasRelativePosition(event);
pickPosition.x = (pos.x / canvas.clientWidth ) * 2 - 1;
pickPosition.y = (pos.y / canvas.clientHeight) * -2 + 1; // note we flip Y
}
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);
}
main();
body { margin: 0; }
canvas { width: 100vw; height: 100vh; display: block; }
<script src="https://threejsfundamentals.org/threejs/resources/threejs/r112/build/three.js"></script>
<canvas id="c"></canvas>
Like I mentioned in the comment. If you know it's PlaneBufferGeometry then you can look in the three.js code and see the exact layout of faces so given a faceIndex you can just compute the neighbors directly. The code above is generic, at least for BufferGeometry with an index.
Looking at the code I'm pretty sure it's
// it looks like this is the grid order for PlaneBufferGeometry
//
// b --c
// |\\1|
// |0\\|
// a-- d
const facesAcrossRow = planeGeometry.parameters.widthSegments * 2;
const col = faceIndex % facesAcrossRow
const row = faceIndex / facesAcrossRow | 0;
const neighboringFaceIndices = [];
// check left face
if (col > 0) {
neighboringFaceIndices.push(row * facesAcrossRow + col - 1);
}
// check right face
if (col < facesAcrossRow - 1) {
neighboringFaceIndices.push(row * facesAcrossRow + col + 1);
}
// check up. there can only be one up if we're in an odd triangle (b,c,d)
if (col % 2 && row < planeGeometry.parameters.heightSegments) {
// add the even neighbor in the next row
neighboringFaceIndices.push((row + 1) * facesAcrossRow + col - 1);
}
// check down. there can only be one down if we're in an even triangle (a,b,d)
if (col % 2 === 0 && row > 0) {
// add the odd neighbor in the previous row
neighboringFaceIndices.push((row - 1) * facesAcrossRow + col + 1);
}
Trying that out
'use strict';
function main() {
const canvas = document.querySelector('#c');
const renderer = new THREE.WebGLRenderer({canvas});
const fov = 60;
const aspect = 2; // the canvas default
const near = 0.1;
const far = 200;
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
camera.position.z = 1;
const scene = new THREE.Scene();
scene.background = new THREE.Color('#444');
scene.add(camera);
const planeGeometry = new THREE.PlaneBufferGeometry(1, 1, 20, 20);
const material = new THREE.MeshBasicMaterial({color: 'blue'});
const plane = new THREE.Mesh(planeGeometry, material);
scene.add(plane);
const edgeGeometry = new THREE.BufferGeometry();
const positionNumComponents = 3;
edgeGeometry.setAttribute('position', planeGeometry.getAttribute('position'));
edgeGeometry.setIndex([]);
const edgeMaterial = new THREE.MeshBasicMaterial({
color: 'yellow',
wireframe: true,
depthTest: false,
});
const edges = new THREE.Mesh(edgeGeometry, edgeMaterial);
scene.add(edges);
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;
}
class PickHelper {
constructor() {
this.raycaster = new THREE.Raycaster();
}
pick(normalizedPosition, scene, camera, time) {
// cast a ray through the frustum
this.raycaster.setFromCamera(normalizedPosition, camera);
// get the list of objects the ray intersected
const intersectedObjects = this.raycaster.intersectObjects(scene.children, [plane]);
if (intersectedObjects.length) {
// pick the first object. It's the closest one
const intersection = intersectedObjects[0];
const faceIndex = intersection.faceIndex;
const indexAttribute = planeGeometry.getIndex();
const indices = indexAttribute.array;
// it looks like this is the grid order for PlaneBufferGeometry
//
// b --c
// |\\1|
// |0\\|
// a-- d
const facesAcrossRow = planeGeometry.parameters.widthSegments * 2;
const col = faceIndex % facesAcrossRow
const row = faceIndex / facesAcrossRow | 0;
const neighboringFaceIndices = [];
// check left face
if (col > 0) {
neighboringFaceIndices.push(row * facesAcrossRow + col - 1);
}
// check right face
if (col < facesAcrossRow - 1) {
neighboringFaceIndices.push(row * facesAcrossRow + col + 1);
}
// check up. there can only be one up if we're in an odd triangle (b,c,d)
if (col % 2 && row < planeGeometry.parameters.heightSegments) {
// add the even neighbor in the next row
neighboringFaceIndices.push((row + 1) * facesAcrossRow + col - 1);
}
// check down. there can only be one down if we're in an even triangle (a,b,d)
if (col % 2 === 0 && row > 0) {
// add the odd neighbor in the previous row
neighboringFaceIndices.push((row - 1) * facesAcrossRow + col + 1);
}
const neighbors = [];
for (const faceIndex of neighboringFaceIndices) {
neighbors.push(...indices.slice(faceIndex * 3, faceIndex * 3 + 3));
}
const edgeIndices = edgeGeometry.getIndex();
edgeIndices.array = new Uint16Array(neighbors);
edgeIndices.count = neighbors.length;
edgeIndices.needsUpdate = true;
}
}
}
const pickPosition = {x: 0, y: 0};
const pickHelper = new PickHelper();
clearPickPosition();
function render(time) {
time *= 0.001; // convert to seconds;
if (resizeRendererToDisplaySize(renderer)) {
const canvas = renderer.domElement;
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
}
pickHelper.pick(pickPosition, scene, camera, time);
renderer.render(scene, camera);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
function getCanvasRelativePosition(event) {
const rect = canvas.getBoundingClientRect();
return {
x: event.clientX - rect.left,
y: event.clientY - rect.top,
};
}
function setPickPosition(event) {
const pos = getCanvasRelativePosition(event);
pickPosition.x = (pos.x / canvas.clientWidth ) * 2 - 1;
pickPosition.y = (pos.y / canvas.clientHeight) * -2 + 1; // note we flip Y
}
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);
}
main();
body { margin: 0; }
canvas { width: 100vw; height: 100vh; display: block; }
<script src="https://threejsfundamentals.org/threejs/resources/threejs/r112/build/three.js"></script>
<canvas id="c"></canvas>
For something more complex than a PlaneBufferGeometry
you could also pre-generate a map of faceIndexs to neighbors if the code at the top is too slow.

Categories