dimanche 26 juin 2016

How to remove jitter/shaking in my special application of OrbitControl and PerspectiveCamera?

In the live snippet below (also at https://jsfiddle.net/gpolyn/bpo7t7f6), I offer optional dynamic updating of PerspectiveCamera's lookAt parameter and fov in the three.js render cycle. (My goal is to fill as much of the viewport as possible with the subject cube, through all orbit positions.)

I suspect that lack of precision in the matrix math code used to calculate my dynamic fov and lookAt paramters (three.js uses Float32 arrays) causes the jitter/shaking I notice in the cube when orbiting with the dynamic options selected from the controls. (The matrix operations can be found in the snippet addExtrema function.)

In my demo, my highest goal is to remove jitter/shaking in case 1, described here:

  1. the "dynamicFovAndLookAt" control option uses dynamic fov and lookAt updating causing quite a bit of jitter in the cube, no matter the orbit position; the fov and lookAt parameters can be seen fluctuating in the demo's lower right status box;
  2. "dynamicFov" uses dynamic fov updating causing some jitter in the cube, depending on orbiting; the fov parameter in the lower right status box will vary due to the dynamic recalculation;
  3. the "boundingSphere" option uses no dynamic fov, lookAt updating and the cube exhibits no jitter/shake through orbiting – the fov and lookAt parameters are constant in the lower right status box.

(Orbit position is reported in the lower left corner of the demo, while one of the box corners has a green dot to aid any discussion of the jitter effect.)

	var renderer, scene, camera, controls;
	var object;
	var vertices3;
	var cloud;
	var boxToBufferAlphaMapping = {
	  0: 0,
	  2: 1,
	  1: 2,
	  3: 4,
	  6: 7,
	  7: 10,
	  5: 8,
	  4: 6
	}
	var lastAlphas = [];
	var canvasWidth, canvasHeight;
	var windowMatrix;
	var boundingSphere;
	var figure;
	var fovWidth, fovDistance, fovHeight;
	var newFov, newLookAt;
	var dist, height, fov, lookAt;
	var aspect;
	var CONSTANT_FOR_FOV_CALC = 180 / Math.PI;
	var mat3;
	var CORNERS = 8;
	var ndc = new Array(CORNERS);
	var USE_GREEN_DOTS = false;
	var stats, orbitPosition, cameraFacts;
	var useDynamicFov;
	var datGuiData = {};


	init();
	render();
	afterInit();
	animate();

	function init() {

	  mat3 = new THREE.Matrix4();

	  canvasWidth = window.innerWidth;
	  canvasHeight = window.innerHeight;
	  aspect = canvasWidth / canvasHeight;
	  // renderer
	  <!-- renderer = new THREE.WebGLRenderer({antialias:true, logarithmicDepthBuffer:true}); -->
	  renderer = new THREE.WebGLRenderer({
	    antialias: true
	  });
	  renderer.setSize(canvasWidth, canvasHeight);
	  document.body.appendChild(renderer.domElement);

	  // scene
	  scene = new THREE.Scene();

	  // object
	  var geometry = new THREE.BoxGeometry(4, 4, 6);

	  // too lazy to add edges without EdgesHelper...
	  var material = new THREE.MeshBasicMaterial({
	    transparent: true,
	    opacity: 0
	  });
	  var cube = new THREE.Mesh(geometry, material);
	  object = cube;

	  // bounding sphere used for orbiting control in render
	  object.geometry.computeBoundingSphere();
	  boundingSphere = object.geometry.boundingSphere;

	  cube.position.set(2, 2, 3)
	    // awkward, but couldn't transfer cube position to sphere...
	  boundingSphere.translate(new THREE.Vector3(2, 2, 3));

	  // save vertices for subsequent use
	  vertices = cube.geometry.vertices;

	  var edges = new THREE.EdgesHelper(cube)
	  scene.add(edges);
	  scene.add(cube);

	  <!-- if (USE_GREEN_DOTS) addGreenDotsToScene(geometry); -->
	  addGreenDotsToScene(geometry);

	  // camera
	  <!-- camera = new THREE.PerspectiveCamera( 17, window.innerWidth / window.innerHeight, 20, 40 ); -->
	  camera = new THREE.PerspectiveCamera(17, window.innerWidth / window.innerHeight);
	  camera.position.set(20, 20, 20);

	  // controls
	  controls = new THREE.OrbitControls(camera);
	  controls.maxPolarAngle = 0.5 * Math.PI;
	  controls.minAzimuthAngle = 0;
	  controls.maxAzimuthAngle = 0.5 * Math.PI;
	  controls.enableZoom = false;

	  // performance monitor
	  stats = new Stats();
	  document.body.appendChild(stats.dom);

	  // orbitposition tracker
	  orbitPosition = new THREEg.OrbitReporter()
	  orbitPosition.domElement.style.position = 'absolute'
	  orbitPosition.domElement.style.left = '0px'
	  orbitPosition.domElement.style.bottom = '0px'
	  document.body.appendChild(orbitPosition.domElement)

	  // camera facts
	  cameraFacts = new THREEg.CameraReporter()
	  cameraFacts.domElement.style.position = 'absolute'
	  cameraFacts.domElement.style.right = '0px'
	  cameraFacts.domElement.style.bottom = '0px'
	  document.body.appendChild(cameraFacts.domElement)

	  // ambient
	  scene.add(new THREE.AmbientLight(0x222222));

	  // axes
	  scene.add(new THREE.AxisHelper(20));

	  // initial settings
	  dist = boundingSphere.distanceToPoint(camera.position);
	  height = boundingSphere.radius * 2;
	  fov = 2 * Math.atan(height / (2 * dist)) * (CONSTANT_FOR_FOV_CALC);
	  newFov = fov;
	  lookAt = new THREE.Vector3(2, 2, 3); // center of box
	  newLookAt = lookAt;

	  // dat.gui
	  window.onload = function() {
	    var view = datGuiData;
	    view.boundingSphere = true;
	    view.dynamicFov = false;
	    view.dynamicFovAndLookAt = false;

	    var gui = new dat.GUI();

	    var CB1Controller = gui.add(view, 'boundingSphere').listen();
	    CB1Controller.onChange(function(value) {
	      view.boundingSphere = true;
	      view.dynamicFov = false;
	      view.dynamicFovAndLookAt = false;
	    });

	    var CB2Controller = gui.add(view, 'dynamicFov').listen();
	    CB2Controller.onChange(function(value) {
	      view.boundingSphere = false;
	      view.dynamicFov = true;
	      view.dynamicFovAndLookAt = false;
	    });

	    var CB3Controller = gui.add(view, 'dynamicFovAndLookAt').listen();
	    CB3Controller.onChange(function(value) {
	      view.boundingSphere = false;
	      view.dynamicFov = true;
	      view.dynamicFovAndLookAt = true;
	    });
	  };

	}

	function addExtrema() {

	  // thread A		
	  mat3.multiplyMatrices(camera.matrixWorld, mat3.getInverse(camera.projectionMatrix));

	  // thread B	
	  var scratchVar;

	  var topIdx, bottomIdx, leftIdx, rightIdx;
	  var top = Number.NEGATIVE_INFINITY;
	  var bottom = Number.POSITIVE_INFINITY;
	  var right = Number.NEGATIVE_INFINITY;
	  var left = Number.POSITIVE_INFINITY;
	  var closestVertex, closestVertexDistance = Number.POSITIVE_INFINITY;
	  var vtx;

	  for (var i = 0; i < CORNERS; i++) {

	    scratchVar = vertices3[i].clone().applyMatrix4(camera.matrixWorldInverse);
	    scratchVar.applyMatrix4(camera.projectionMatrix);

	    scratchVar.divideScalar(scratchVar.w)
	    ndc[i] = scratchVar;

	    vtx = ndc[i];

	    if (vtx.x < left) {
	      left = vtx.x;
	      leftIdx = i;
	    } else if (vtx.x > right) {
	      right = vtx.x;
	      rightIdx = i;
	    }

	    if (vtx.y < bottom) {
	      bottom = vtx.y;
	      bottomIdx = i;
	    } else if (vtx.y > top) {
	      top = vtx.y;
	      topIdx = i;
	    }

	    if (vtx.z < closestVertexDistance) {
	      closestVertex = i;
	      closestVertexDistance = vtx.z;
	    }

	  }

	  var yNDCPercentCoverage = (Math.abs(ndc[topIdx].y) + Math.abs(ndc[bottomIdx].y)) / 2;
	  yNDCPercentCoverage = Math.min(1, yNDCPercentCoverage);

	  var xNDCPercentCoverage = (Math.abs(ndc[leftIdx].x) + Math.abs(ndc[rightIdx].x)) / 2;
	  xNDCPercentCoverage = Math.min(1, xNDCPercentCoverage);

	  var ulCoords = [ndc[leftIdx].x, ndc[topIdx].y, closestVertexDistance, 1]
	  var blCoords = [ndc[leftIdx].x, ndc[bottomIdx].y, closestVertexDistance, 1]
	  var urCoords = [ndc[rightIdx].x, ndc[topIdx].y, closestVertexDistance, 1]

	  var ul = new THREE.Vector4().fromArray(ulCoords);
	  ul.applyMatrix4(mat3).divideScalar(ul.w);

	  var bl = new THREE.Vector4().fromArray(blCoords);
	  bl.applyMatrix4(mat3).divideScalar(bl.w);

	  var ur = new THREE.Vector4().fromArray(urCoords);
	  ur.applyMatrix4(mat3).divideScalar(ur.w);

	  var center = new THREE.Vector3();
	  center.addVectors(ur, bl);
	  center.divideScalar(2);

	  var dist = camera.position.distanceTo(center);


	  var upperLeft = new THREE.Vector3().fromArray(ul.toArray().slice(0, 3));
	  var p;

	  if ((1 - yNDCPercentCoverage) < (1 - xNDCPercentCoverage)) { // height case
	    var bottomLeft = new THREE.Vector3().fromArray(bl.toArray().slice(0, 3));
	    var height = upperLeft.distanceTo(bottomLeft);
	    p = 2 * Math.atan(height / (2 * dist)) * (CONSTANT_FOR_FOV_CALC);
	  } else { // width case
	    var upperRight = new THREE.Vector3().fromArray(ur.toArray().slice(0, 3));
	    var width = upperRight.distanceTo(upperLeft);
	    p = 2 * Math.atan((width / aspect) / (2 * dist)) * (CONSTANT_FOR_FOV_CALC);
	  }

	  if (datGuiData.dynamicFovAndLookAt || datGuiData.dynamicFov) {
	    newFov = p;
	  } else {
	    dist = boundingSphere.distanceToPoint(camera.position);
	    height = boundingSphere.radius * 2;
	    newFov = 2 * Math.atan(height / (2 * dist)) * (CONSTANT_FOR_FOV_CALC);
	  }

	  if (datGuiData.dynamicFovAndLookAt == true) {
	    newLookAt = center;
	  } else {
	    newLookAt = lookAt;
	  }

	  if (USE_GREEN_DOTS) {
	    var alphas = cloud.geometry.attributes.alpha;

	    // make last points invisible
	    lastAlphas.forEach(function(alphaIndex) {
	      alphas.array[alphaIndex] = 0.0;
	    });
	    // now, make new points visible...
	    // (boxToBufferAlphaMapping is a BufferGeometry-Object3D geometry
	    // map between the object and green dots)
	    alphas.array[boxToBufferAlphaMapping[rightIdx]] = 1.0;
	    alphas.array[boxToBufferAlphaMapping[bottomIdx]] = 1.0;
	    alphas.array[boxToBufferAlphaMapping[topIdx]] = 1.0;
	    alphas.array[boxToBufferAlphaMapping[leftIdx]] = 1.0;

	    // store visible points for next cycle
	    lastAlphas = [boxToBufferAlphaMapping[rightIdx]];
	    lastAlphas.push(boxToBufferAlphaMapping[bottomIdx])
	    lastAlphas.push(boxToBufferAlphaMapping[topIdx])
	    lastAlphas.push(boxToBufferAlphaMapping[leftIdx])

	    alphas.needsUpdate = true;
	  }

	}

	function addGreenDotsToScene(geometry) {

	  var bg = new THREE.BufferGeometry();
	  bg.fromGeometry(geometry);
	  bg.translate(2, 2, 3); // yucky, and quick

	  var numVertices = bg.attributes.position.count;
	  var alphas = new Float32Array(numVertices * 1); // 1 values per vertex

	  <!-- for( var i = 0; i < numVertices; i ++ ) { -->
	  <!--     alphas[ i ] = 1; -->
	  <!-- } -->

	  alphas[2] = 1;

	  bg.addAttribute('alpha', new THREE.BufferAttribute(alphas, 1));

	  var uniforms = {
	    color: {
	      type: "c",
	      value: new THREE.Color(0x00ff00)
	    },
	  };

	  var shaderMaterial = new THREE.ShaderMaterial({
	    uniforms: uniforms,
	    vertexShader: document.getElementById('vertexshader').textContent,
	    fragmentShader: document.getElementById('fragmentshader').textContent,
	    transparent: true
	  });

	  cloud = new THREE.Points(bg, shaderMaterial);
	  scene.add(cloud);

	}

	function afterInit() {

	  windowMatrix = new THREE.Matrix4();
	  windowMatrix.set(canvasWidth / 2, 0, 0, canvasWidth / 2, 0, canvasHeight / 2, 0, canvasHeight / 2, 0, 0, 0.5, 0.5, 0, 0, 0, 1);

	  var vertices2 = object.geometry.vertices.map(function(vtx) {
	    return (new THREE.Vector4(vtx.x, vtx.y, vtx.z));
	  });

	  // create 'world-space' geometry points, using
	  // model ('world') matrix
	  vertices3 = vertices2.map(function(vt) {
	    return vt.applyMatrix4(object.matrixWorld);
	  })

	}

	function render() {

	  <!-- console.log({far: camera.far, camera_near: camera.near}) -->
	  camera.lookAt(newLookAt);
	  camera.fov = newFov;
	  camera.updateProjectionMatrix();
	  renderer.render(scene, camera);

	}

	function animate() {

	  requestAnimationFrame(animate);
	  render();
	  addExtrema()
	  stats.update();
	  orbitPosition.update(controls);
	  cameraFacts.update(camera, newLookAt);

	}
			body {
			  background-color: #000;
			  margin: 0px;
			  overflow: hidden;
			}
			.dg .c {
			  width: 40%
			}
			.dg .property-name {
			  width: 60%
			}
			
<script src="https://rawgit.com/mrdoob/three.js/dev/build/three.min.js"></script>
<script src="https://rawgit.com/mrdoob/three.js/master/examples/js/libs/stats.min.js"></script>
<script src="https://rawgit.com/mrdoob/three.js/master/examples/js/controls/OrbitControls.js"></script>
<script src="https://rawgit.com/gpolyn/789d63a662c1768320756f68a6099f15/raw/3a0f323bb284b09e624a11f93ff4055e23adea80/OrbitReporter.js"></script>
<script src="https://rawgit.com/gpolyn/70352cb34c7900ed2489400d4ecc45f7/raw/7b6e7e6bb3e175d4145879ef1afdeb38c31cf785/camera_reporter.js"></script>
<script src="https://rawgit.com/mrdoob/three.js/master/examples/js/libs/dat.gui.min.js"></script>
<script type="x-shader/x-vertex" id="vertexshader">

  attribute float alpha; varying float vAlpha; void main() { vAlpha = alpha; vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 ); gl_PointSize = 8.0; gl_Position = projectionMatrix * mvPosition; }

</script>

<script type="x-shader/x-fragment" id="fragmentshader">

  uniform vec3 color; varying float vAlpha; void main() { gl_FragColor = vec4( color, vAlpha ); }

</script>

Aucun commentaire:

Enregistrer un commentaire