Рисование шестиугольной сетки с использованием текстуры

Ранее я провел пару экспериментов, чтобы найти лучший способ рисования крупномасштабных шестиугольных сеток.

Я пробовал рисовать гексы, используя THREE.Line , THREE.LineSegments. Это работало довольно хорошо для небольших сеток, но если у меня было более 1000+ ячеек в сетке, fps начинал очень сильно падать.

Поэтому мне пришла в голову идея применить текстуру (содержащую шестиугольную сетку) к простой плоскости. Мне просто нужно было установить функцию repeat() текстуры, чтобы указать, сколько повторений будет выполняться по вертикали и горизонтали.

введите здесь описание изображения

Общая производительность значительно увеличилась, однако я заметил, что текстура соответствует размерам ячеек моей сетки только вокруг центра плоскости.

введите здесь описание изображения

Точки, которые вы можете видеть в сетке выше, - ТРИ. Точки, они просто показывают все углы для каждого шестиугольника. Это помогает мне увидеть, соответствует ли текстура ячейкам сетки или нет.

введите здесь описание изображения

Чем дальше я смотрю от центра, тем больше смещение между реальным шестиугольником и соответствующим шестиугольником на текстуре.

Я считаю, что есть более умный способ предварительно рассчитать размер, чтобы не было различий между этими двумя сетками.

Это пример того, как выглядит эта сетка и как она генерируется...

window.onload = function() {
  var renderer, camera, scene, controls, all_loaded = false, controls, tiles = {};

  var imgDataURI = "data:image/jpeg;base64,iVBORw0KGgoAAAANSUhEUgAAAGgAAABaCAYAAABUrhMHAAAFhklEQVR4Xu2dT2gcdRTH34tpLS2Ipx70VkFRmpLZnlRSK+6MIkIqmIvFYnfWqgc9FBQFq2C9WKHoyT/spFKplwgmIKI7q9YG9ZSdpSmWCvamh55ESaltuk9+kU12k2x2fr/f/N5OlrenkJ3ve2++n9mZ3/x+O28x9L0fojh5GAbg9VxQ+L5JhIOyP+Wg8BYKoPwemQIov2yWKhNAAojXAbkG8fqtnU0AaVvGKxBAvH5rZxNA2pbxCgQQr9/a2QSQtmW8AgHE67d2NgGkbRmvQADx+q2dbSABlQLv4mQ1uVfbjRwKyn7hVwKCKE7uy2F52iWFgTeNYeD9A0RXbtx6dd/pr377QztKDgSlB++5A7fvmEWgnaocArxCVxfGJn+69GcOytMu4dATd9+55d/t5wBxJyp16HsfAeIRbNJ3lVria0fsoyD0C98CkI8AH1fi5EVVStn3PiSA5wEwjuL6o30sTzt1uejFNISPANEnUZy8sASo9Qr90VlAfACaeDKq1V/Rjs4oKBcLJ2AIjhI1f4nixth6qdX+IA7dD004WanVX2UsTztVWCy8B0N0FIh+bt+fDkAq6qGx3Xu2btvyNQHsAKTDUbUxrZ3NoaBcHB0nxE8RYIHw5mNR9fyFjdKFwZ7dSLd8o/YHiZ6t1BozDsvTDh0GoweQ8BQALFy/duPx07MXzrcHWQOo9eaRwHv5JuG7QHT598Vk79mzcE07e4aCiQnYettfXgKAuwCar0Vx4wOd8KXAewkJTwDQ5b9vT7ypKbiuo8962/37Ydtdw94cIO5Car5eqTXeXy9HV0DLp73AO4OETwPSTKWaHMi60DTxwmLhS0AaR8Azlbj+TBpNt23KfuEzAjoIhDNRrf6kTSxTbTnwpoFwnJA+j6rJwY3i9ATUEpcCbw4JRwjo+GScHDctTkcXFr03EPFNQJqvVJO9Otpe25YDbw7U/hC9HdWSd3ptn8X7Jd87hoDHCGl+MuX+pAakCjzsjzw0hMNT6u8mLU6ciud/zKLw1THCYGQfwPAX//9/8amoOn9uM+ex8U0L0PKnyeBISGtwP45sl59U2zOPEaCW2Trn0l6A8nBtyPJaF2Z07bYCpExPOxrpBihvoyvb0WLWo19rQCujvY3H82uvM/m+P9G933J1/5gZoGVQXe6I2wFtpjv8tDMWrmZgMge0fH1aNaek/r+Z58jWm/PjmMN0BkgB6ZiVpc0/y9w+a07KOYZVAKeAVkZ7hYtERIOyTqPWnZpIyLGOxgJo0FY6OfdHAPW6QVvnfQFkYBqnRABxum2QSwAZmMYpEUCcbhvkEkAGpnFKBBCn2wa5BJCBaZwSAcTptkEuAWRgGqdEAHG6bZBLABmYxikRQJxuG+QSQAamcUoEEKfbBrkEkIFpnBIBxOm2QS4BZGAap0QAcbptkEsAGZjGKRFAnG4b5BJABqZxSgQQp9sGuQSQgWmcEgHE6bZBLgFkYBqnRABxum2Qa+AAlQP58rzBcbAkcfrdbHn8xBTLis4ZoNVNgVRKeYBLH1jmgLo1BWovTR6BTA8qM0C9mgKtLinvTY4G5iFieQy/89DL1WP40sii+6mqr40sTJoCpT3rSiuYTqe0rkE2TYHSAlLbSTMlg2G2bVMgHUCtbV02OerHJ9XkzNPzE5TVudQE0Aooaei3xr+sRyM2gJTWtslR3po2pR39SlNZ2yPHUt/r/lHaMlsanJV8w7bMHE2BstqRNTMSg97YXH4awNWhYx63YxVAflzD3EjXSrWOJj8V7dpli/hq5VYAWRjoWiqAXDtsGV8AWRroWi6AXDtsGV8AWRroWi6AXDtsGV8AWRroWi6AXDtsGV8AWRroWi6AXDtsGV8AWRroWi6AXDtsGV8AWRroWq4A/QeCYJEAFo/atwAAAABJRU5ErkJggg=="

  function initRenderer(){
    renderer = new THREE.WebGLRenderer({antialias:true});
    renderer.setSize( window.innerWidth, window.innerHeight);
    document.body.appendChild(renderer.domElement);
    renderer.setClearColor(0x264d73, 1);
  }

  function initScene(){
    scene = new THREE.Scene();
    //scene.fog = new THREE.Fog( 0x264d73, 500, 900 )
  }

  function initCamera(){
    camera = new THREE.PerspectiveCamera( 45, window.innerWidth/window.innerHeight, 1, 10000 );
    camera.position.set( 0.0, 211 ,40 );
    camera.lookAt(scene.position);
    scene.add(camera);
    controls = new THREE.OrbitControls( camera );
  }

  function initLights(){
    var aLight = new THREE.AmbientLight(0xD0D0D0, 0.5);
    scene.add(aLight);
  }

  ////// Initializers ////////////////////////

  var Cell = function( q, r, s ){
    this.q = q;
    this.r = r;
    this.s = s;
    this.h = 0;
    this._hashID = this.q+"."+this.r+"."+this.s;
  };
  Cell.prototype.constructor = Cell;

  function cellToPixel ( layout, cell ){ //// point ///
    var M = layout.orientation;
    var v = new THREE.Vector3();
    v.x = ( M.f0 * cell.q + M.f1 * cell.r ) * layout.size;
    v.y = cell.h ;
    v.z = ( M.f2 * cell.q + M.f3 * cell.r ) * layout.size;
    return v;
  };

  var Tile = function( coords, layout ){
    this.cell = new Cell( coords[0], coords[1], coords[2] );
    this.pos = cellToPixel( layout, this.cell );
  };

  Tile.prototype.constructor = Tile;

  function generateGrid( grid_size, cell_size , origin ){ 
    //// set layout type ( pointy ) ///
    var layout = {
      orientation: {
        //// forward matrix ////
        f0: Math.sqrt(3.0), 
        f1: Math.sqrt(3.0) / 2.0, 
        f2: 0.0, 
        f3: 3.0 / 2.0, 
        /// reverse matrix ////
        b0: Math.sqrt(3.0) / 3.0, 
        b1: -1.0 / 3.0, 
        b2: 0.0, 
        b3: 2.0 / 3.0, 
        start_angle: 0.5
      },
      size: cell_size, 
      origin: origin
    }

    ///generate tiles ///
    var r1, r2, t, q, r;
    for ( q = -grid_size; q <= grid_size; q++ ) {
      r1 = Math.max(-grid_size, -q - grid_size);
      r2 = Math.min(grid_size, -q + grid_size);
      for ( r = r1; r <= r2; r++ ) {
        t = new Tile( [ q, r,  -q-r ] , layout );
        tiles[ t.cell._hashID ] = t;
      }
    }

    /// hex-corners ///
    var hex_corners = [], offset_x , offset_y, angle = 0;
    for (var i = 0; i < 6; i++) {
      angle = 2.0 * Math.PI * ( i + layout.orientation.start_angle ) / 6;
      offset_x = layout.size * Math.cos( angle );
      offset_y = layout.size * Math.sin( angle );
      hex_corners.push( [parseFloat( offset_x.toFixed())  , parseFloat(offset_y.toFixed())] );
    }

    //// hex corner points ///
    var points_Geo = new THREE.Geometry();
    for( var t in tiles ){
      for( var c = 0, c_l = hex_corners.length; c < c_l; c++ ){
        points_Geo.vertices.push( new THREE.Vector3( tiles[t].pos.x + hex_corners[c][0],0, tiles[t].pos.z + hex_corners[c][1] ) );
      }
    }
    points_Geo.mergeVertices(); 

    var points_Mat = new THREE.PointsMaterial( { size: 3, color: 0xFFFFFF, sizeAttenuation: true, transparent: true, opacity: 1  } ); 
    var points_Mesh =  new THREE.Points( points_Geo, points_Mat );

    scene.add( points_Mesh  )
  }

  function add_plane(){
    var plane, material, image = new Image(), texture = new THREE.Texture();
    image.onload  = function(){
      texture.image = image;
      texture.needsUpdate = true;

      texture.wrapS = THREE.RepeatWrapping; 
      texture.wrapT = THREE.RepeatWrapping;
      texture.anisotropy = 16;
      texture.repeat.set( 9.587, 11 ); 

      console.log(texture)

      material = new THREE.MeshLambertMaterial({transparent: true, emissive:0xb3cce6, color: 0xFFFFFF, opacity: 0.7, map : texture });

      plane = new THREE.Mesh(new THREE.PlaneGeometry( 500, 500 ), material);
      plane.rotateX( -(Math.PI/2) )
      plane.position.set( -11.1,-0.1,0)

      scene.add(plane);
      camera.lookAt( plane );
      all_loaded = true;
    };

    image.src = imgDataURI;

  }

  ///// Mouse events ////////

  ///// Main /////////
  function main(){
    initRenderer(window.innerWidth, window.innerHeight );
    initScene();
    initCamera(window.innerWidth, window.innerHeight );
    initLights();
    add_plane();
    generateGrid( 8, 15, {x:0, y:0, z: 0});
    animate();
  }

  function animate(){
    window.requestAnimationFrame( animate );
    //if( all_loaded ){
    render_all();
    //}
    controls.update()
  }

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

  main();
}
body , canvas{
  width: 100%;
  height: 100%;
  margin:0;
  padding:0;
}
<script src="https://ajax.googleapis.com/ajax/libs/threejs/r76/three.min.js"></script>
<script src="https://dl.dropboxusercontent.com/u/3587259/Code/Threejs/OrbitControls.js"></script>


person Alexus    schedule 23.08.2016    source источник
comment
Это связано с ошибками точности с плавающей запятой. Текстура имеет жесткие целые числа. точки используют поплавки/двойники. чем дальше вы идете, тем больше несоответствие. возможно, используйте прогнозирующий целочисленный набор для создания ваших точек на основе текстурных координат возможных точек.   -  person Tschallacka    schedule 23.08.2016
comment
ты имеешь в виду повторение текстур в расчетах?   -  person Alexus    schedule 23.08.2016
comment
нет, у вашей текстуры фиксированные номера. точка всегда будет, например, на x = 14, y = 6. Если текстура повторяется, она будет на x = retpeatnumside * texturewidth + 14 и y = repeatnumtop * textureheight + 14, однако ваши динамические вычисления гексаконов выполняют некоторые математические вычисления для решить, где находятся идеальные точки шестиугольника. и если какое-либо из этих чисел окажется числом с плавающей запятой, вы получите ошибки точности, то, что было 14, может закончиться как 13,48377213 из-за математики. три рендера оба на своих местах. что проявляется как разница для вас. потому что один является чистым целым числом, а другой плавающим   -  person Tschallacka    schedule 23.08.2016
comment
ну, я сгенерировал текстуру, используя ту же технику в JS, поэтому я полагаю, что она должна быть одинаковой. Я надеялся, что значения повторения можно рассчитать, чтобы они соответствовали сгенерированным размерам шестиугольника.   -  person Alexus    schedule 23.08.2016
comment
Можете ли вы показать нам код, в котором вы генерируете точки для рисования шестиугольника?   -  person Tschallacka    schedule 23.08.2016
comment
Дело в том, что шестиугольник вычисляется от центральной точки наружу. Таким образом, расстояние от центральной точки может быть 0,54 2,23 для левого верхнего угла, 1 3 для верхнего угла и т. д. Однако шестиугольники на вашей текстуре НЕ являются динамически генерируемым набором данных. точки всегда живут в своих статических координатах, а не относительно центральной точки. Таким образом, чем ближе вы к смещению 0,0, тем больше они совпадают, чем больше вы отклоняетесь от 0,0, тем больше разница в расчетах от центральных точек.   -  person Tschallacka    schedule 23.08.2016
comment
обновил свой пост, ваши аргументы действительно имеют смысл. Я попытаюсь реализовать ваше предложение и посмотреть, решит ли оно мою проблему.   -  person Alexus    schedule 23.08.2016
comment
Давайте продолжим обсуждение в чате.   -  person Tschallacka    schedule 23.08.2016


Ответы (1)


Проблема, с которой вы столкнулись, связана с ошибками округления. Код, вычисляющий, где нарисовать шестиугольник, обычно делает это, исходя из смещения начала. Заимствуйте кодовую форму Рассчитать 6 вершин случайно сгенерированного шестиугольника Я могу предоставить вы вывод консоли того, как он вычисляет.

Поскольку это вычисляется динамически, возникают ошибки округления, и браузер должен дать указание дисплею нарисовать его где-то, что передает его графической карте, а графическая карта решает, какие пиксели будут фактически рисовать линии.

var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");

// draw your original hexagon
ctx.beginPath();
ctx.moveTo(0,0);
ctx.lineTo(30,-50);
ctx.lineTo(83,-50);
ctx.lineTo(113,0);
ctx.lineTo(83,50);
ctx.lineTo(30,50);
ctx.closePath();
ctx.lineWidth=3;
ctx.stroke();

// same hexagon using drawHexagon()
ctx.strokeStyle='red';   
ctx.lineWidth=1;
drawHexagon(0,0,113/2);

function drawHexagon(leftX,middleY,radius){
  var centerX=leftX+radius;
  var centerY=middleY;    
  ctx.beginPath();
  ctx.moveTo (centerX+radius*Math.cos(0), centerY+radius*Math.sin(0));          
  for (var i=1; i<=6;i++) {
    console.log("FLoaty coordinates, not absolute pixel: "+(centerX+radius*Math.cos(i*2*Math.PI/6)));
    ctx.lineTo(centerX+radius*Math.cos(i*2*Math.PI/6), centerY+radius*Math.sin(i*2*Math.PI/6));
  }
  ctx.closePath();
  ctx.stroke();
}
drawHexagon(0,100,113/2);
drawHexagon(0,200,113/2);
body{ background-color: ivory; padding:10px; }
canvas{border:1px solid red;}
<h4>Fn() to draw hexagon with specified radius and left-midpoint.</h4>
<canvas id="canvas" width=300 height=300></canvas>

Иногда они будут совпадать с полным пикселем изображения текстуры, иногда нет, потому что округление сместит их куда-то в сторону.

Чтобы решить эту проблему, вы должны убедиться, что шестиугольники, которые вы рисуете в three.js, идеально совпадают с координатными точками в вашей текстуре. Для этого вам нужно иметь карту смещения для всех точек в текстуре, а затем найти ближайший шестиугольник карты текстуры и соответственно.

псевдокод:

 center = hexagon.pos();
 left = center.posX % texturewidth;
 realcenter = null;
 for(c=0;c<textureHexagons.length;c++) {
     current = textureHexagon[c];
     if(left >= current.posX && left <= current.posX + width) {
        realcenter = textureHexagon[c];
     }
 }
 baseOffset = textureWidth * Math.round((center.posX / textureWidth))
 hexagon.topPoint = baseOffset + realcenter.topPoint;
 hexagon.leftTopPoint = baseOffset + realcenter.leftTopPoint;
 etc...
person Tschallacka    schedule 23.08.2016