0
votes

I'm trying to "shoot" bullets/projectiles using three.js:

let renderer, camera, scene, light, plane, cube, spheres;

initialize();
animate();

function initialize() {
  renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true });
  renderer.setSize(window.innerWidth, window.innerHeight);
  document.body.appendChild(renderer.domElement);

  camera = new THREE.PerspectiveCamera(55, window.innerWidth / window.innerHeight);

  scene = new THREE.Scene();
  
  light = new THREE.SpotLight("#ffffff");
  light.position.y = 700;
  scene.add(light);

  plane = new THREE.Mesh();
  plane.material = new THREE.MeshToonMaterial({ color: "#0000ff" });
  plane.geometry = new THREE.PlaneGeometry(60, 30);
  plane.position.z = -50;
  scene.add(plane);

  cube = new THREE.Mesh();
  cube.material = new THREE.MeshToonMaterial({ color: "#ff0000", transparent: true, opacity: 0.85 });
  cube.geometry = new THREE.BoxGeometry(0.5, 0.5, 0.5);
  cube.position.y = -1;
  cube.position.z = -3;
  scene.add(cube);
  
  spheres = [];

  window.addEventListener("click", event => {
    let mouse2d = getMouse2D(event.clientX, event.clientY);
    let mouse3d = getMouse3D(mouse2d, plane.position.z);
    shoot(mouse3d);
  }, false);
}

function animate() {
  spheres.forEach(sphere => {
    // TODO: Update sphere position based on sphere.userData.target
    sphere.position.z -= 1;
  });

  requestAnimationFrame(animate);
  renderer.render(scene, camera);
}

function shoot(target) {
  let sphere = new THREE.Mesh();
  sphere.material = new THREE.MeshToonMaterial({ color: "#00ff00" });
  sphere.geometry = new THREE.SphereGeometry(0.25);
  sphere.position.x = cube.position.x;
  sphere.position.y = cube.position.y;
  sphere.position.z = cube.position.z;
  sphere.userData.target = target;
  scene.add(sphere);
  spheres.push(sphere);
}

function getMouse3D(mouse2d, z) {
  let vector = new THREE.Vector3(mouse2d.x, mouse2d.y);
  vector.unproject(camera);
  let dir = vector.sub(camera.position).normalize();
  let distance = (z - camera.position.z) / dir.z;
  return camera.position.clone().add(dir.multiplyScalar(distance));
}

function getMouse2D(x, y) {
  return new THREE.Vector2(
    (x / renderer.domElement.clientWidth) * 2 - 1,
    -(y / renderer.domElement.clientHeight) * 2 + 1
  );
}
body {
  width: 100%;
  height: 100%;
  margin: 0;
  padding: 0;
  border: 0;
  background: #eeeeee;
}

canvas {
  display: block;
  cursor: crosshair;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/87/three.js">
</script>

As you can see, spheres are shot from the cube to the plane on mouse click.

I've written the getMouse3D() function to determine the position on the plane that the user clicked on, and when a sphere is spawned, I store it in sphere.userData.target. But I don't know how to update the position of the sphere each frame so that it moves towards it (see the TODO comment). In other words, by the time a sphere reaches the plane, it should intersect it at sphere.userData.target (where the user clicked).

How do I do this?

1
Take a look at the popular tweening library, tween.js. Many people use it in conjunction with three.js, and you'll find related questions across Stack Overflow.TheJim01

1 Answers

2
votes

You know position of the cube, you know final position (target), so you can calculate direction, and multiply it with the speed:

sphere.position.addScaledVector(sphere.userData.direction, sphere.userData.speed * delta);
if (sphere.position.z <= plane.position.z) sphere.userData.speed = 0; // stop, when we reached the plane

I re-worked your code with THREE.Raycaster() to get the position of target (it simplifies the code, so you can rid off getMouse3D() and getMouse2D() functions).

let renderer, camera, scene, light, plane, cube, spheres, raycaster = new THREE.Raycaster(), mouse = new THREE.Vector2(), intersects, clock = new THREE.Clock(), delta = 0;

initialize();
animate();

function initialize() {
  renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true });
  renderer.setSize(window.innerWidth, window.innerHeight);
  document.body.appendChild(renderer.domElement);

  camera = new THREE.PerspectiveCamera(55, window.innerWidth / window.innerHeight);

  scene = new THREE.Scene();
  
  light = new THREE.SpotLight("#ffffff");
  light.position.y = 700;
  scene.add(light);

  plane = new THREE.Mesh();
  plane.material = new THREE.MeshToonMaterial({ color: "#0000ff" });
  plane.geometry = new THREE.PlaneGeometry(60, 30);
  plane.position.z = -50;
  scene.add(plane);

  cube = new THREE.Mesh();
  cube.material = new THREE.MeshToonMaterial({ color: "#ff0000", transparent: true, opacity: 0.85 });
  cube.geometry = new THREE.BoxGeometry(0.5, 0.5, 0.5);
  cube.position.y = -1;
  cube.position.z = -3;
  scene.add(cube);
  
  spheres = [];

  window.addEventListener("click", event => {
    mouse.x = ( event.clientX / window.innerWidth ) * 2 - 1;
    mouse.y = - ( event.clientY / window.innerHeight ) * 2 + 1;
    raycaster.setFromCamera(mouse, camera);
    intersects = raycaster.intersectObjects([plane]);
    if (intersects.length == 0) return;
    shoot(intersects[0].point);
  }, false);
}

function animate() {
  delta = clock.getDelta();
  spheres.forEach(sphere => {
    sphere.position.addScaledVector(sphere.userData.direction, sphere.userData.speed * delta);
    if (sphere.position.z <= plane.position.z) sphere.userData.speed = 0; // stop, when we reached the plane
  });

  requestAnimationFrame(animate);
  renderer.render(scene, camera);
}

function shoot(target) {
  let sphere = new THREE.Mesh();
  sphere.material = new THREE.MeshToonMaterial({ color: "#00ff00" });
  sphere.geometry = new THREE.SphereGeometry(0.5);
  sphere.position.copy(cube.position);
  sphere.userData.direction = target.sub(cube.position).normalize();
  sphere.userData.speed = 20;
  scene.add(sphere);
  spheres.push(sphere);
}
body {
  width: 100%;
  height: 100%;
  margin: 0;
  padding: 0;
  border: 0;
  background: #eeeeee;
}

canvas {
  display: block;
  cursor: crosshair;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/87/three.js">
</script>