【问题标题】:Radius of projected sphere in screen space屏幕空间中投影球体的半径
【发布时间】:2014-02-08 16:32:10
【问题描述】:

我正在尝试在投影到屏幕空间后找到球体的可见大小(以像素为单位)。球体以原点为中心,相机正对着它。因此,投影球体应该是二维的正圆。我知道这个1 现有问题。但是,那里给出的公式似乎没有产生我想要的结果。它太小了几个百分点。我认为这是因为它没有正确考虑视角。投影到屏幕空间后,由于透视缩短,您看不到球体的一半,但明显减少(您只看到球体的顶盖,而不是整个半球 2)。

如何导出精确的 2D 边界圆?

【问题讨论】:

    标签: math 3d geometry projection


    【解决方案1】:

    确实,使用透视投影,您需要计算球体“视野”从眼睛/相机中心到的高度(这个“视野”由眼睛与球体相切的光线决定)。

    符号:

    d: 眼睛到球体中心的距离
    r: 球体半径
    l: 眼睛到球体“地平线”上一个点的距离, @ 987654327@
    h:球体“水平”的高度/半径
    theta:“水平”锥与眼睛的(半)角
    phi:θ的互补角

    h / l = cos(phi)
    

    但是:

    r / d = cos(phi)
    

    所以,最后:

    h = l * r / d = sqrt(d^2 - r^2) * r / d
    

    然后,一旦您拥有h,只需应用标准公式(您链接的问题中的公式)即可在标准化视口中获得投影半径pr

    pr = cot(fovy / 2) * h / z
    

    z 表示从眼睛到球体“地平线”平面的距离:

    z = l * cos(theta) = sqrt(d^2 - r^2) * h / r
    

    所以:

    pr = cot(fovy / 2) * r / sqrt(d^2 - r^2)
    

    最后,将pr 乘以height / 2 得到实际屏幕半径(以像素为单位)。

    接下来是一个使用three.js 完成的小演示。分别使用n/fm/ps/w 对键可以更改摄像机的球面距离、半径和垂直视野。屏幕空间中渲染的黄色线段显示了屏幕空间中球体半径的计算结果。此计算在函数computeProjectedRadius() 中完成。

    projected-sphere.js:

    "use strict";
    
    function computeProjectedRadius(fovy, d, r) {
      var fov;
    
      fov = fovy / 2 * Math.PI / 180.0;
    
    //return 1.0 / Math.tan(fov) * r / d; // Wrong
      return 1.0 / Math.tan(fov) * r / Math.sqrt(d * d - r * r); // Right
    }
    
    function Demo() {
      this.width = 0;
      this.height = 0;
    
      this.scene = null;
      this.mesh = null;
      this.camera = null;
    
      this.screenLine = null;
      this.screenScene = null;
      this.screenCamera = null;
    
      this.renderer = null;
    
      this.fovy = 60.0;
      this.d = 10.0;
      this.r = 1.0;
      this.pr = computeProjectedRadius(this.fovy, this.d, this.r);
    }
    
    Demo.prototype.init = function() {
      var aspect;
      var light;
      var container;
    
      this.width = window.innerWidth;
      this.height = window.innerHeight;
    
      // World scene
      aspect = this.width / this.height;
      this.camera = new THREE.PerspectiveCamera(this.fovy, aspect, 0.1, 100.0);
    
      this.scene = new THREE.Scene();
      this.scene.add(THREE.AmbientLight(0x1F1F1F));
    
      light = new THREE.DirectionalLight(0xFFFFFF);
      light.position.set(1.0, 1.0, 1.0).normalize();
      this.scene.add(light);
    
      // Screen scene
      this.screenCamera = new THREE.OrthographicCamera(-aspect, aspect,
                                                       -1.0, 1.0,
                                                       0.1, 100.0);
      this.screenScene = new THREE.Scene();
    
      this.updateScenes();
    
      this.renderer = new THREE.WebGLRenderer({
        antialias: true
      });
      this.renderer.setSize(this.width, this.height);
      this.renderer.domElement.style.position = "relative";
      this.renderer.autoClear = false;
    
      container = document.createElement('div');
      container.appendChild(this.renderer.domElement);
      document.body.appendChild(container);
    }
    
    Demo.prototype.render = function() {
      this.renderer.clear();
      this.renderer.setViewport(0, 0, this.width, this.height);
      this.renderer.render(this.scene, this.camera);
      this.renderer.render(this.screenScene, this.screenCamera);
    }
    
    Demo.prototype.updateScenes = function() {
      var geometry;
    
      this.camera.fov = this.fovy;
      this.camera.updateProjectionMatrix();
    
      if (this.mesh) {
        this.scene.remove(this.mesh);
      }
    
      this.mesh = new THREE.Mesh(
        new THREE.SphereGeometry(this.r, 16, 16),
        new THREE.MeshLambertMaterial({
          color: 0xFF0000
        })
      );
      this.mesh.position.z = -this.d;
      this.scene.add(this.mesh);
    
      this.pr = computeProjectedRadius(this.fovy, this.d, this.r);
    
      if (this.screenLine) {
        this.screenScene.remove(this.screenLine);
      }
    
      geometry = new THREE.Geometry();
      geometry.vertices.push(new THREE.Vector3(0.0, 0.0, -1.0));
      geometry.vertices.push(new THREE.Vector3(0.0, -this.pr, -1.0));
    
      this.screenLine = new THREE.Line(
        geometry,
        new THREE.LineBasicMaterial({
          color: 0xFFFF00
        })
      );
    
      this.screenScene = new THREE.Scene();
      this.screenScene.add(this.screenLine);
    }
    
    Demo.prototype.onKeyDown = function(event) {
      console.log(event.keyCode)
      switch (event.keyCode) {
        case 78: // 'n'
          this.d /= 1.1;
          this.updateScenes();
          break;
        case 70: // 'f'
          this.d *= 1.1;
          this.updateScenes();
          break;
        case 77: // 'm'
          this.r /= 1.1;
          this.updateScenes();
          break;
        case 80: // 'p'
          this.r *= 1.1;
          this.updateScenes();
          break;
        case 83: // 's'
          this.fovy /= 1.1;
          this.updateScenes();
          break;
        case 87: // 'w'
          this.fovy *= 1.1;
          this.updateScenes();
          break;
      }
    }
    
    Demo.prototype.onResize = function(event) {
      var aspect;
    
      this.width = window.innerWidth;
      this.height = window.innerHeight;
    
      this.renderer.setSize(this.width, this.height);
    
      aspect = this.width / this.height;
      this.camera.aspect = aspect;
      this.camera.updateProjectionMatrix();
    
      this.screenCamera.left = -aspect;
      this.screenCamera.right = aspect;
      this.screenCamera.updateProjectionMatrix();
    }
    
    function onLoad() {
      var demo;
    
      demo = new Demo();
      demo.init();
    
      function animationLoop() {
        demo.render();
        window.requestAnimationFrame(animationLoop);
      }
    
      function onResizeHandler(event) {
        demo.onResize(event);
      }
    
      function onKeyDownHandler(event) {
        demo.onKeyDown(event);
      }
    
      window.addEventListener('resize', onResizeHandler, false);
      window.addEventListener('keydown', onKeyDownHandler, false);
      window.requestAnimationFrame(animationLoop);
    }
    

    index.html:

    <!DOCTYPE html>
    <html>
      <head>
        <title>Projected sphere</title>
          <style>
            body {
                background-color: #000000;
            }
          </style>
          <script src="http://cdnjs.cloudflare.com/ajax/libs/three.js/r61/three.min.js"></script>
          <script src="projected-sphere.js"></script>
        </head>
        <body onLoad="onLoad()">
          <div id="container"></div>
        </body>
    </html>
    

    【讨论】:

    • 哇,非常感谢您非常详细的回答!
    • 如果您使用 GeoGebra 之类的工具来创建 2D 绘图,请告诉我它的名称吗?
    • @knivil 我只是使用了 Inkscape。
    • 演示现在好像已经死了,不是吗?
    【解决方案2】:

    让球体的半径为r,并在距离观察者d 处被看到。投影平面与观察者的距离为f

    球体在半角asin(r/d)下可见,所以视半径为f.tan(asin(r/d)),可写为f . r / sqrt(d^2 - r^2)。 [错误的公式是f . r / d。]

    【讨论】:

      【解决方案3】:

      上面图解的公认答案很好,但我需要一个不知道视野的解决方案,只是一个在世界和屏幕空间之间转换的矩阵,所以我不得不调整这个解决方案。

      1. 重用其他答案中的一些变量名,计算球冠的起点(h 线与d 线相交的点):

        capOffset = cos(asin(l / d)) * r
        capCenter = sphereCenter + ( sphereNormal * capOffset )
        

        其中capCentersphereCenter 是世界空间中的点,sphereNormal 是从球体中心指向相机的沿d 的归一化向量。

      2. 将点转换为屏幕空间:

        capCenter2 = matrix.transform(capCenter)
        
      3. 1(或任意数量)添加到x 像素坐标:

        capCenter2.x += 1
        
      4. 将其转换回世界空间:

        capCenter2 = matrix.inverse().transform(capCenter2)
        
      5. 测量世界空间中原始点和新点之间的距离,并除以您添加的数量以获得比例因子:

        scaleFactor = 1 / capCenter.distance(capCenter2)
        
      6. 将该比例因子乘以上限半径h 以获得以像素为单位的可见屏幕半径:

        screenRadius = h * scaleFactor
        

      【讨论】:

        猜你喜欢
        • 2011-04-12
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-04-10
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多