1

How can I preserve constant size of meshes regardless of perspective using Three.js?

Lets assume I have multiple meshes of the same size. I want them to always have the same on screen size. I also have to use perspective camera.

I have found some related answers, but seems that I miss something:

  • First approach is to use Euclidean distance to objects from camera and scale them respectively every animation frame. Three JS Keep Label Size On Zoom

    I have tried to modify jsfiddle given in the answer to apply scale to meshes instead of sprites, but that doesn't work for me. The result looks as follows https://jsfiddle.net/a2ogz9vx/258/

var camera, scene, renderer, controls;
var planets = [];
var timestamp = 0;
var scaleVector = new THREE.Vector3();

init();
animate();

function init() {

  scene = new THREE.Scene();
  camera = new THREE.PerspectiveCamera(90, window.innerWidth / window.innerHeight, 1, 10000);
  camera.position.set(0, 100, 100);

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

  controls = new THREE.OrbitControls(camera, renderer.domElement);

  var createPlanet = function(name, radius, orbit, speed) {
    var geom = new THREE.SphereGeometry(radius, 32, 16);
    var mat = new THREE.MeshBasicMaterial({
      color: Math.random() * 0xFFFFFF,
    });
    var planet = new THREE.Mesh(geom, mat);
    planet.userData.orbit = orbit;
    planet.userData.speed = speed;

    var canvas = document.createElement('canvas');
    canvas.width = 256;
    canvas.height = 256;

    var tex = new THREE.Texture(canvas);
    tex.needsUpdate = true;
    var spriteMat = new THREE.SpriteMaterial({
      map: tex
    });
    var sprite = new THREE.Sprite(spriteMat);

    planet.add(sprite);
    planets.push(planet);
    scene.add(planet);


  };

  createPlanet("One", 11, -10, 5);
  createPlanet("Two", 11, 5, 5);
  createPlanet("Three", 11, 10, 5);

}

function animate() {
  requestAnimationFrame(animate);
  planets.forEach(function(planet) {
    
    var scaleFactor = 100;
    var scale = scaleVector.subVectors(planet.position, camera.position).length() / scaleFactor;
    planet.scale.set(scale, scale, scale);
    
    var orbit = planet.userData.orbit;
    var speed = planet.userData.speed;
    
    planet.position.x = speed * orbit;
    planet.position.z = speed * orbit;
  });
  render();
}

function render() {
  renderer.render(scene, camera);
}
<script src="https://cdn.rawgit.com/mrdoob/three.js/master/build/three.min.js"></script>
<script src="https://cdn.rawgit.com/mrdoob/three.js/master/examples/js/controls/OrbitControls.js"></script>

Result 1

var camera, scene, renderer, controls;
var planets = [];
var timestamp = 0;
var scaleVector = new THREE.Vector3();

init();
animate();

function init() {

  scene = new THREE.Scene();
  camera = new THREE.PerspectiveCamera(90, window.innerWidth / window.innerHeight, 1, 10000);
  camera.position.set(0, 100, 100);

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

  controls = new THREE.OrbitControls(camera, renderer.domElement);

  var createPlanet = function(name, radius, orbit, speed) {
    var geom = new THREE.SphereGeometry(radius, 32, 16);
    var mat = new THREE.MeshBasicMaterial({
      color: Math.random() * 0xFFFFFF,
    });
    var planet = new THREE.Mesh(geom, mat);
    planet.userData.orbit = orbit;
    planet.userData.speed = speed;

    var canvas = document.createElement('canvas');
    canvas.width = 256;
    canvas.height = 256;

    var tex = new THREE.Texture(canvas);
    tex.needsUpdate = true;
    var spriteMat = new THREE.SpriteMaterial({
      map: tex
    });
    var sprite = new THREE.Sprite(spriteMat);

    planet.add(sprite);
    planets.push(planet);
    scene.add(planet);


  };

  createPlanet("One", 11, -10, 5);
  createPlanet("Two", 11, 0, 5);
  createPlanet("Three", 11, 10, 5);

}

function animate() {
  requestAnimationFrame(animate);
  planets.forEach(function(planet) {

    const dist = planet.position.distanceTo(camera.position);
    const vFOV = THREE.Math.degToRad(camera.fov);
    const size = 2 * Math.tan(vFOV / 2) * dist;
    const scaleFactor = 130;
    const scale = size / scaleFactor;

    planet.scale.set(scale, scale, scale);

    var orbit = planet.userData.orbit;
    var speed = planet.userData.speed;

    planet.position.x = speed * orbit;
    planet.position.z = speed * orbit;
  });
  render();
}

function render() {
  renderer.render(scene, camera);
}
<script src="https://cdn.rawgit.com/mrdoob/three.js/master/build/three.min.js"></script>
<script src="https://cdn.rawgit.com/mrdoob/three.js/master/examples/js/controls/OrbitControls.js"></script>

enter image description here

Any suggestions how to fix the code or other approaches are welcome!

  • Are you sure you need to use `PerspectiveCamera`? `OrthographicCamera` seems pretty much perfect for this scenario. – msbit Oct 12 '18 at 07:54
  • @msbit Yes, I should use perspective camera, because all other items in the scene need it – Alexander Solovyev Oct 12 '18 at 08:02
  • 1
    Ah so some meshes need perspective and others need something akin to orthographic. Tricky. – msbit Oct 12 '18 at 08:05
  • 1
    @AlexanderSolovyev If you want to get rid of the perspective distortion, then you can't use a perspective projection. You have to use an orthographic projection. Possiby you have to "blend" 2 scenes, one drawn with perspective projection and one draw with orthographic projection. – Rabbid76 Oct 12 '18 at 08:42
  • @Rabbid76 Thanks, I will not try to solve the issue with use of a perspective projection then. Mixing scenes also sounds good, I will try to research that – Alexander Solovyev Oct 12 '18 at 09:09

0 Answers0