用three.js在场景中创建一个网格。

14

我寻找的是什么

在three.js场景中显示一个填充整个场景的网格。在这种情况下,场景占据整个窗口。

该网格在3D中表示一个表面,并可以使用THREE.TrackballControls鼠标进行移动。这个网格朝向摄像机,因此初始时看起来像一个平坦的(2D)表面,直到用鼠标拖动轨迹球。

网格线的宽度应等于渲染器的宽度。

我做了什么

我已经为我所做的工作设置了一个可工作的jsFiddle

首先,我找到了场景的边界(所有这些都在jsFiddle中),

App = function(sceneContainerName) {
   this.sceneContainerName = sceneContainerName;

   this.SCREEN_WIDTH = window.innerWidth;
   this.SCREEN_HEIGHT = window.innerHeight;

   this.MAX_X = this.SCREEN_WIDTH / 2;
   this.MIN_X = 0 - (this.SCREEN_WIDTH / 2);
   this.MAX_Y = this.SCREEN_HEIGHT / 2;
   this.MIN_Y = 0 - (this.SCREEN_HEIGHT / 2);

   this.NUM_HORIZONTAL_LINES = 50;

   this.init();

};

安装并配置three.js。

init: function() {
   // init scene
   this.scene = new THREE.Scene();

   // init camera
   // View Angle, Aspect, Near, Far
   this.camera = new THREE.PerspectiveCamera(45, this.SCREEN_WIDTH / this.SCREEN_HEIGHT, 1, 10000);
   // set camera position
   this.camera.position.z = 1000;
   this.camera.position.y = 0;

   // add the camera to the scene
   this.scene.add(this.camera);

   this.projector = new THREE.Projector();

   // init renderer
   this.renderer = new THREE.CanvasRenderer();
   // start the renderer
   this.renderer.setSize(this.SCREEN_WIDTH, this.SCREEN_HEIGHT);

   this.drawGrid(this.NUM_HORIZONTAL_LINES);

   this.trackball = new THREE.TrackballControls(this.camera, this.renderer.domElement);
   this.trackball.staticMoving = true;

   var me = this;

   this.trackball.addEventListener('change', function() {
       me.render();
   });

   // attach the render-supplied DOM element
   var container = document.getElementById(this.sceneContainerName);
   container.appendChild(this.renderer.domElement);

   this.animate();
},

这些函数为每个屏幕角提供了一个向量。

getNWScreenVector: function() {
    return new THREE.Vector3(this.MIN_X, this.MAX_Y, 0);
},

getNEScreenVector: function() {
    return new THREE.Vector3(this.MAX_X, this.MAX_Y, 0);
},

getSWScreenVector: function() {
    return new THREE.Vector3(this.MIN_X, this.MIN_Y, 0);
},

getSEScreenVector: function() {
    return new THREE.Vector3(this.MAX_X, this.MIN_Y, 0);
},

我创建了一些代表屏幕顶部的线条几何图形,尝试从顶部开始向下绘制直线,一直绘制到屏幕底部。

// drawGrid will determine blocksize based on the 
// amount of horizontal gridlines to draw
drawGrid: function(numHorizontalGridLines) {

    // Determine the size of a grid block (square)
    this.gridBlockSize = this.SCREEN_HEIGHT / numHorizontalGridLines;

    var geometry = new THREE.Geometry();
    geometry.vertices.push(this.getNWScreenVector());
    geometry.vertices.push(this.getNEScreenVector());

    var material = new THREE.LineBasicMaterial({
        color: 0x000000,
        opacity: 0.2
    });

    for (var c = 0; c <= numHorizontalGridLines; c++) {
        var line = new THREE.Line(geometry, material);
        line.position.y = this.MAX_Y - (c * this.gridBlockSize);
        this.scene.add(line);
    }
}

问题

这个方法不起作用,在 jsFiddle 上第一行从屏幕外开始,而行的宽度没有填满屏幕宽度。

4个回答

31
自ThreeJS r57版本以来,有一种名为GridHelper的帮助器可用,使用它可以轻松绘制一个漂亮的网格,就像绘制任何其他几何对象一样。
GridHelper接受两个参数,第一个是网格的大小,第二个是两条线之间的步长大小。
以下是在场景上绘制网格的代码,大小为100,步长为10。
var grid = new THREE.GridHelper(100, 10);
scene.add(grid);

在您的情况下,您可以避免使用名为drawGrid的方法,并直接替换为上述两行代码,或者您可以将这两行代码添加到drawgrid方法中。
以下链接提供了一个实时示例: Grid Helper Example

“size”参数的度量单位是什么? - Jnr
据我所知,Three.JS API 的数字并不是单位,而是某种尺寸,这是通过各种参数(如屏幕尺寸和分辨率等)自动计算出来的。因此,举个例子,假设您的屏幕尺寸为1280 x 720 PPI(每英寸像素数),那么您的数字显示将按比例显示。 - Aravind.HU

12

你的代码中有两个bug。

首先,你从MAX_Y这一行开始,并将每一行放在上一行的固定距离下。相对较小的bug是,在getNWScreenVectorgetNEScreenVector中,你将线的顶点放置在高度为MAX_Y的位置,然后在

line.position.y = this.MAX_Y - (c * this.gridBlockSize);

您正在将翻译添加到MAX_Y -(c * this.gridBlockSize)行上,这会给出最终的y位置为MAX_Y + MAX_Y -(c * this.gridBlockSize),这多了一个MAX_Y。如果从getNWScreenVectorgetNEScreenVector开始绘制下降线条对您的程序有意义,则应将line.position.y行更改为:
line.position.y = -c * this.gridBlockSize;

您可以看到,在jsFiddle上,线已经居中对齐,但它们的尺寸仍不正确。这是因为您没有考虑到场景在透视图下的情况。您所有的线都将其z坐标设置为0,因此当您设置this.camera.position.z = 1000时,它们距离相机1000个单位。不能假设在透视图中绘制与像素宽度相同的东西具有相同的宽度。

相反,我们可以计算它们需要多大。我不会在这里进行完整的透视解释,但我们可以弄清楚覆盖屏幕所需线条覆盖的面积大小。在相机构造函数中指定了45度的垂直FOV和相机与线之间的距离为1000。如果您查看Three.js如何创建透视矩阵,您基本上可以找到答案:makePerspective

首先,我们需要屏幕上半部分的垂直距离,因为0位于屏幕中心。 Math.tan(45 / 2 * (Math.PI / 180))(一半角度的正切,转换为弧度)给出了垂直距离与距离相机的比值,因此您可以用以下公式计算高度:

halfDisplayHeight = 1000 * Math.tan(45 / 2 * (Math.PI / 180));

或将其加倍至

this.DISPLAY_HEIGHT = 2 * 1000 * Math.tan(45 / 2 * (Math.PI / 180));

水平视野(horizontal FOV)在画布不是正方形的情况下并不相同,但线条区域的宽高比将与屏幕的宽高比成比例。就像 three.js 一样,您可以通过乘以相机构造函数提供的纵横比来计算宽度:

this.DISPLAY_WIDTH = this.DISPLAY_HEIGHT * (this.SCREEN_WIDTH / this.SCREEN_HEIGHT);

以下是放置行的值。全部列举如下:

this.DISPLAY_HEIGHT = 2 * 1000 * Math.tan(45 / 2 * (Math.PI / 180));
this.DISPLAY_WIDTH = this.DISPLAY_HEIGHT * (this.SCREEN_WIDTH / this.SCREEN_HEIGHT);
this.MAX_X = this.DISPLAY_WIDTH / 2;
this.MIN_X = 0 - (this.DISPLAY_WIDTH / 2);
this.MAX_Y = this.DISPLAY_HEIGHT / 2;
this.MIN_Y = 0 - (this.DISPLAY_HEIGHT / 2);

最后,您需要将这些线条铺满整个区域,因此您应该设置
this.gridBlockSize = this.DISPLAY_HEIGHT / numHorizontalGridLines;

你可以在这里看到它的工作效果:http://jsfiddle.net/pa46hxwo/ 还有另一种方法,就是保持线条不变,但将相机移近到线条附近,使得线条的宽度恰好等于该距离下画布的像素宽度。公式只是对上述公式进行了重新排列,针对DISPLAY_HEIGHT解决高度问题,而是解决当高度等于屏幕高度时的距离:
this.camera.position.z = this.SCREEN_HEIGHT / (2 * Math.tan(45 / 2 * (Math.PI / 180)));

你可以在这里查看:http://jsfiddle.net/0jtykgrL/ 这是对你的代码做出的小改动,但这意味着相机位置将根据画布大小而改变,这可能会影响其他需要精确定位的内容,所以选择权在你手中。

Brendan,非常棒的答案!我很欣赏这个数学方法,这个答案是一个很好的参考。在看到你的答案之前,基于一些用于检测3D世界中鼠标位置的代码,我想出了一个解决方案,使用projector.unprojectVector来获取屏幕的角落。这是 jsFiddle 。网格完美匹配,但我在相对于该网格绘制在场景中的所有内容似乎都卡在轨迹球的外缘,使场景导航不太直观。你的解决方案将有所帮助。 - cs_brandt

12

您可以像这样绘制网格。

// each square
var planeW = 50; // pixels
var planeH = 50; // pixels 
var numW = 50; // how many wide (50*50 = 2500 pixels wide)
var numH = 50; // how many tall (50*50 = 2500 pixels tall)
var plane = new THREE.Mesh(
    new THREE.PlaneGeometry( planeW*numW, planeH*numH, planeW, planeH ),
    new THREE.MeshBasicMaterial( {
        color: 0x000000,
        wireframe: true
    } )
);

scene.add(plane);

2
注意,现在它会绘制对角线。 - Eric
2
我不相信这个程序现在还能按照计划正常工作。 - 8protons

0

这个线程帮助了一个应用于A-Frame的技术变化,并稍作改动; 动态创建和调整网格,使其与场景子元素的边界框成比例。

注意:为了获取最初为null的边界框坐标,必须调用以下方法,而不像预先为您计算的边界球一样:

'boundingBox.geometry.computeBoundingBox();'

这个“场景”相关的Codepen可以在这里找到:https://codepen.io/ubermario/pen/JvZMPg

    <!DOCTYPE html>
    <html lang="en" >

    <head>

      <meta charset="UTF-8">
      <link rel="shortcut icon" type="image/x-icon" href="https://static.codepen.io/assets/favicon/favicon-8ea04875e70c4b0bb41da869e81236e54394d63638a1ef12fa558a4a835f1164.ico" />
      <link rel="mask-icon" type="" href="https://static.codepen.io/assets/favicon/logo-pin-f2d2b6d2c61838f7e76325261b7195c27224080bc099486ddd6dccb469b8e8e6.svg" color="#111" />
      <title>CodePen -  A-Frame Dynamically Sized GridHelper</title>     
    </head>

    <body translate="no" >

      <script src="https://aframe.io/releases/0.8.0/aframe.min.js"></script>
      <a-scene>
        <a-entity>
          <a-sphere position="0 1.25 -5" radius="1.25" color="#EF2D5E"></a-sphere>
          <a-box position="-1 0.5 -3" rotation="0 45 0" width="1" height="1" depth="1" color="#4CC3D9"></a-box>
          <a-cylinder position="1 0.75 -3" radius="0.5" height="1.5" color="#FFC65D"></a-cylinder>
          <!-- replaced with gridhelper>
      <a-plane position="0 0 -4" rotation="-90 0 0" width="4" height="4" color="#7BC8A4"></a-plane> -->
          <!-- Remove sky; it distorts the dimensions of just the sphere, box, cylinder objects
          <a-sky color="#ECECEC"></a-sky>
          -->
          <a-entity>
      </a-scene>




        <script >
          AFRAME.registerComponent("cmpGridHelper", {
      schema: {
        size: { default: 5, type: "int" },
        divisions: { default: 10, type: "int" },
        colorCenterLine: { default: "red", type: "color" },
        colorGrid: { default: "black", type: "color" },
        x: { default: 0, type: "number" },
        y: { default: 0, type: "number" },
        z: { default: 0, type: "number" }
      },
      init: function() {
        var entity = this.el.object3D,
          schema = this.data,
          grid = new THREE.GridHelper(
            schema.size,
            schema.divisions,
            schema.colorCenterLine,
            schema.colorGrid
          );
        grid.name = "gridHelper";
        entity.add(grid);
      },
      update: function(oldData) {
        var entity = this.el.object3D,
          grid = entity.getObjectByName("gridHelper"),
          schema = this.data;
        grid.position.set(schema.x, schema.y, schema.z);
      },
      remove: function() {
        var entity = this.el.object3D;
        entity.remove(entity.getObjectByName("gridHelper"));
      }
    });

    var sceneObj3d = document.querySelector("a-scene");

    if (sceneObj3d.hasLoaded) {
      fnAddDyanmicGridHelper();
    } else {
      sceneObj3d.addEventListener("loaded", fnAddDyanmicGridHelper);
    }

function fnAddDyanmicGridHelper() {
  //var helper = new THREE.BoundingBoxHelper(sceneObj3d.querySelector('a-entity').object3D, 0xff0000);
  var childObjects = sceneObj3d.querySelector('a-entity').object3D;
  var boundingBox = new THREE.BoxHelper(childObjects, 0xff0000);
  //Uncomment the next line to display the bounding box
  //childObjects.add(boundingBox);
  //Need the following method call to get the geometry.boundingBox coordinates in addition to the sphere coordinates
  boundingBox.geometry.computeBoundingBox();                            

  var x = boundingBox.geometry.boundingSphere.center.x;
  var z = boundingBox.geometry.boundingSphere.center.z;
  var size = boundingBox.geometry.boundingSphere.radius * 2;
  sceneObj3d.setAttribute("cmpGridHelper", {
    x: x,
    z: z,
    y: boundingBox.geometry.boundingBox.min.y,
    size: size,
    divisions: size
  });
}
        </script>
    </body>

    </html>

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接