【问题标题】:Three.js keep camera behind objectThree.js 将相机保持在对象后面
【发布时间】:2018-01-05 03:57:03
【问题描述】:

简短版:如何让相机跟随 Three.js 场景中物理控制的对象?

长版本:我正在处理 Three.js 场景,其中 W、A、S、D 键沿平面移动球体。然而,到目前为止,我还没有弄清楚如何让相机跟随球体后面。

在下面的示例中,只要按下 W 键,相机就会完美地跟随球体。但是,如果按下 A 或 D,球体开始转动,摄像机不再位于球的后面。如果球体开始转动,我希望相机随之转动,因此相机始终紧跟在球体后面,并且始终与球体保持恒定距离。当用户继续按下 W 时,球将继续相对于相机向前滚动。

在之前的场景 [demo] 中,我能够通过创建球体、将该球体添加到组中来实现此行为,然后每帧使用以下代码:

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);

上面演示中的关键是旋转sphere,同时保持sphereGroup 不旋转,这样我就可以在未旋转的sphereGroup 上计算cameraOffset

在下面的演示中,球体的位置由 Cannon.js 物理库控制,当力施加到身体上时,它会平移和旋转球体。有谁知道我怎样才能让相机跟随在下面场景中的球体后面?

/**
* 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>

评论问题的答案

@jparimaa 我认为最直观的实现是让 W 添加向前的动量,S 添加向后的动量,A 和 D 围绕球旋转相机。这可能吗?

@HariV 您链接到的控件是我在演示中使用的控件,上面没有物理。是否有可能使该逻辑与物理学一起使用?

【问题讨论】:

  • 在第一个演示中,A & D 键围绕球移动相机,而 W & S 键实际移动球。在第二个演示中,A & D 键也移动(扫射)球。因此它们的工作方式不同,这是故意的吗?还是应该只使用 W & S 键添加速度?
  • @duhaime 你检查过这个Chase Camera。这是你打算做的吗?
  • 我喜欢你编码的方式。
  • 现在你让我脸红了……

标签: javascript three.js cannon.js


【解决方案1】:

我认为如果 W 键总是将球相对于相机“向前”移动,这对用户来说是最直观的

一种选择是计算球和相机之间的方向,并在该方向上添加速度。在这种情况下,如果您将球向前推,那么您可以旋转相机而不会影响球的速度。只有在旋转后按 W/S 后它才会改变方向。我不确定这是否是你想要的,但也许这会给你一些想法。

我试过下面的代码(rotation是全局变量初始化为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);
}

【讨论】:

  • 这真是太棒了,非常感谢!我有一个简单的问题:当我按住 W 和 S 并绕圈行驶时,相机偶尔会向前移动一点,就好像场景跳过了一些帧一样。你知道可能是什么原因造成的,或者我可以做些什么来解决这个问题?
  • 我没有立刻想到什么。也许您可以尝试缩小范围:仅在旋转或移动和旋转或其他情况下才会发生。此外,您可以检查这些函数的调用方式是否有任何奇怪之处。
猜你喜欢
  • 2020-04-25
  • 2013-06-17
  • 2014-10-10
  • 1970-01-01
  • 2013-11-12
  • 2019-04-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多