javascript - Three.js Zoom-for-å-passe med offset


Jeg prøver å lage en zoom-til-pass-funksjon som sikrer at en liste med punkter passer perfekt inn i tegneområdet, samtidig som det legges til konfigurerbare marginer på alle sider av bildet. Det vil si zoom-til-pass et område av rammen i stedet for hele visningsområdet:

(merk at forskyvningene i dette bildet ikke er nøyaktige)

Jeg bruker et perspektivkamera her. Funksjonen må oppdatere kameraposisjonen, men ikke dens parametere eller siktretning.

Jeg fant en velfungerende zoom-til-pass-funksjon*, men jeg sliter med å implementere forskyvningene.

Min første tilnærming med bare å forskyve punktkoordinatene (ved bruk av kameraets koordinatsystem) fungerte ikke. Mer av bildet vises, men de valgte punktene mine havner ikke på kantene av området. Dette gir mening i ettertid, siden perspektivforvrengningen vil flytte punktene bort fra deres tiltenkte posisjoner.

Kan noen hjelpe med en mulig løsning for hvordan man beregner kamerafjernhet og posisjon riktig?


* Three.js har ikke en funksjon for zoom-til-tilpass, men det finnes mange eksempler og spørsmål på nettet om hvordan man kan implementere denne logikken. Den fineste for denne typen brukstilfelle er sannsynligvis CameraViewBox. Jeg har tilpasset deres eksempel til mitt brukstilfelle i denne fiddle:

import * as THREE from 'https://cdn.skypack.dev/[email ';
import { OrbitControls } from 'https://cdn.skypack.dev/[email /examples/jsm/controls/OrbitControls.js';

let camera, controls, scene, renderer, material;
let isDragging = false;
let cameraViewBox;
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
const meshes = [];
const selection = new Set();
const selectedMaterial = new THREE.MeshPhongMaterial({ color: 0xff0000, flatShading: true });
const floorPlane = new THREE.Plane(new THREE.Vector3(0, 1, 0));

init();
animate();

function init() {
  scene = new THREE.Scene();
  scene.background = new THREE.Color(0xcccccc);
  scene.fog = new THREE.FogExp2(0xcccccc, 0.002);

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

  camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 1, 1000);
  camera.position.set(400, 200, 0);

  // Create the cameraViewBox
  cameraViewBox = new THREE.CameraViewBox();
  cameraViewBox.setViewFromCamera(camera);

  // controls
  controls = new OrbitControls(camera, renderer.domElement);
  controls.minDistance = 100;
  controls.maxDistance = 500;
  controls.maxPolarAngle = Math.PI / 2;

  // world
  const geometry = new THREE.BoxGeometry(1, 1, 1);
  geometry.translate(0, 0.5, 0);
  material = new THREE.MeshPhongMaterial({
    color: 0xffffff,
    flatShading: true
  });

  for (let i = 0; i < 500; i++) {
    const mesh = new THREE.Mesh(geometry, material);
    mesh.position.x = Math.random() * 1600 - 800;
    mesh.position.y = 0;
    mesh.position.z = Math.random() * 1600 - 800;
    mesh.scale.x = 20;
    mesh.scale.y = Math.random() * 80 + 10;
    mesh.scale.z = 20;
    mesh.updateMatrix();
    mesh.matrixAutoUpdate = false;
    scene.add(mesh);
    meshes.push(mesh);
  }

  // lights
  const dirLight1 = new THREE.DirectionalLight(0xffffff);
  dirLight1.position.set(1, 1, 1);
  scene.add(dirLight1);

  const dirLight2 = new THREE.DirectionalLight(0x002288);
  dirLight2.position.set(-1, -1, -1);
  scene.add(dirLight2);

  const ambientLight = new THREE.AmbientLight(0x222222);
  scene.add(ambientLight);

  window.addEventListener('resize', onWindowResize);

  // Add DOM events
  renderer.domElement.addEventListener('mousedown', onMouseDown, false);
  window.addEventListener('mousemove', onMouseMove, false);
  renderer.domElement.addEventListener('mouseup', onMouseUp, false);
}

function onWindowResize() {

  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();

  renderer.setSize(window.innerWidth, window.innerHeight);
}

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

// Add selection support
function onMouseDown() {
  isDragging = false;
}

function onMouseMove() {
  isDragging = true;
}

function onMouseUp(event) {
  if (isDragging) {
    isDragging = false;
    return;
  } else {
    isDragging = false;
  }

  mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
  mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
  raycaster.setFromCamera(mouse, camera);

  var intersects = raycaster.intersectObjects(meshes);
  if (intersects.length > 0) {
    var mesh = intersects[0].object;

    if (selection.has(mesh)) {
      mesh.material = material;
      selection.delete(mesh);
    } else {
      mesh.material = selectedMaterial;
      selection.add(mesh);
    }
  }
}

function centerOnSelection() {
  if (selection.size === 0) {
    return;
  }

  cameraViewBox.setViewFromCamera(camera);
  cameraViewBox.setFromObjects(Array.from(selection));
  cameraViewBox.getCameraPositionAndTarget(camera.position, controls.target, floorPlane);
  controls.update();
}

Svar #1

Jeg var nå i stand til å løse dette selv til en viss grad. Det er overraskende enkelt hvis vi starter med symmetriske forskyvninger:

Ved å bruke en smalere FOV-vinkel (grønn) for å beregne kameraposisjonen vil de projiserte punktene bli forskjøvet med en viss mengde i det endelige bildet. Hvis vi finner riktig vinkel, havner punktene på den eksakte forskyvningen vi ser etter.

Vi kan beregne denne vinkelen ved å bruke grunnleggende trigonometri. Vi beregner avstanden til Normalized Device Coordinate-planet (dvs. høyde/bredde fra -1 til 1; blå i bildet) og deretter bruker vi offseten (prosentverdi som varierer fra 0,0 til 1,0) og lager en ny vinkel:

tan(FOV / 2) = 1 / dist  =>  dist = 1 / tan(FOV / 2)
tan(FOVg / 2) = (1 - offset) / dist  =>  FOVg = atan((1 - offset) / dist) * 2

Gjenta dette for det horisontale synsfeltet (modifisert av aspektforholdet), ved å bruke samme eller en annen forskyvningsverdi. Deretter bruk den eksisterende zoom-til-pass-logikken med disse nye vinklene.


Denne tilnærmingen fungerer godt for symmetriske forskyvninger. Det samme vil sannsynligvis være mulig for asymmetriske forskyvninger ved å beregne 4 individuelle nye vinkler. Den vanskelige delen er å beregne riktig kameraposisjon og zoom ved bruk av disse...

Relaterte artikler