tangram
Version:
WebGL Maps for Vector Tiles
204 lines (168 loc) • 7.61 kB
JavaScript
// Polygon builders
import Geo from '../utils/geo';
import Vector from '../utils/vector';
import { default_uvs, outsideTile } from './common';
import earcut from 'earcut';
import quickselect from 'quickselect';
const up_vec3 = [0, 0, 1];
/**
* To tesselate flat 2D polygons.
* The x & y coordinates will be set as first two elements of provided vertex_template.
* @param {Array.<Array.<Array.<Array.<number>>>>} polygons The polygons to tesselate.
* @param {VertexData} vertex_data The VertexData were to store the results
* @param {Array.<number>} vertex_template The vertex template to use
* @param {Object} options The texture coordinate options to apply
* @return {number} the number of the resulting geometries (triangles)
*/
export function buildPolygons (
polygons, vertex_data, vertex_template, { texcoord_index, texcoord_scale, texcoord_normalize }) {
let vertex_elements = vertex_data.vertex_elements,
num_polygons = polygons.length,
geom_count = 0,
min_u, min_v, max_u, max_v,
min_x, min_y, max_x, max_y,
span_x, span_y, scale_u, scale_v;
if (texcoord_index) {
texcoord_normalize = texcoord_normalize || 1;
[min_u, min_v, max_u, max_v] = texcoord_scale || default_uvs;
}
for (let p = 0; p < num_polygons; p++) {
const max_rings = 500;
let polygon = polygons[p];
if (polygon.length > max_rings) {
polygon = [...polygon]; // copy to avoid modifying original
quickselect(polygon, max_rings, 1, polygon.length - 1, (a, b) => b.area - a.area);
polygon = polygon.slice(0, max_rings);
}
const indices = triangulatePolygon(earcut.flatten(polygon));
const num_indices = indices.length;
const element_offset = vertex_data.vertex_count;
// The vertices and vertex-elements must not be added if earcut returns no indices:
if (num_indices) {
// Find polygon extents to calculate UVs, fit them to the axis-aligned bounding box:
if (texcoord_index) {
[min_x, min_y, max_x, max_y] = Geo.findBoundingBox(polygon),
span_x = max_x - min_x,
span_y = max_y - min_y,
scale_u = (max_u - min_u) / span_x,
scale_v = (max_v - min_v) / span_y;
}
for (let ring_index = 0; ring_index < polygon.length; ring_index++) {
// Add vertex data:
let polygon_ring = polygon[ring_index];
for (let i = 0; i < polygon_ring.length; i++) {
let vertex = polygon_ring[i];
vertex_template[0] = vertex[0];
vertex_template[1] = vertex[1];
// Add UVs:
if (texcoord_index) {
vertex_template[texcoord_index + 0] = ((vertex[0] - min_x) * scale_u + min_u) * texcoord_normalize;
vertex_template[texcoord_index + 1] = ((vertex[1] - min_y) * scale_v + min_v) * texcoord_normalize;
}
vertex_data.addVertex(vertex_template);
}
}
// Add element indices:
for (let i = 0; i < num_indices; i++) {
vertex_elements.push(element_offset + indices[i]);
}
geom_count += num_indices / 3;
}
}
return geom_count;
}
// Tesselate and extrude a flat 2D polygon into a simple 3D model with fixed height and add to GL vertex buffer
export function buildExtrudedPolygons (
polygons,
z, height, min_height,
vertex_data, vertex_template,
normal_index,
normal_normalize,
{
remove_tile_edges,
tile_edge_tolerance,
texcoord_index,
texcoord_scale,
texcoord_normalize,
winding
}) {
// Top
var min_z = z + (min_height || 0);
var max_z = z + height;
vertex_template[2] = max_z;
var geom_count = buildPolygons(polygons, vertex_data, vertex_template, { texcoord_index, texcoord_scale, texcoord_normalize });
var vertex_elements = vertex_data.vertex_elements;
var element_offset = vertex_data.vertex_count;
// Walls
// Fit UVs to wall quad
if (texcoord_index) {
texcoord_normalize = texcoord_normalize || 1;
var [min_u, min_v, max_u, max_v] = texcoord_scale || default_uvs;
var texcoords = [
[min_u, max_v],
[min_u, min_v],
[max_u, min_v],
[max_u, max_v]
];
}
var num_polygons = polygons.length;
for (var p=0; p < num_polygons; p++) {
var polygon = polygons[p];
for (var q=0; q < polygon.length; q++) {
var contour = polygon[q];
for (var w=0; w < contour.length - 1; w++) {
if (remove_tile_edges && outsideTile(contour[w], contour[w+1], tile_edge_tolerance)) {
continue; // don't extrude tile edges
}
// Wall order is dependent on winding order, so that normals face outward
let w0, w1;
if (winding === 'CCW') {
w0 = w;
w1 = w+1;
}
else {
w0 = w+1;
w1 = w;
}
// Two triangles for the quad formed by each vertex pair, going from bottom to top height
var wall_vertices = [
[contour[w1][0], contour[w1][1], max_z],
[contour[w1][0], contour[w1][1], min_z],
[contour[w0][0], contour[w0][1], min_z],
[contour[w0][0], contour[w0][1], max_z]
];
// Calc the normal of the wall from up vector and one segment of the wall triangles
let wall_vec = Vector.normalize([contour[w1][0] - contour[w0][0], contour[w1][1] - contour[w0][1], 0]);
let normal = Vector.cross(up_vec3, wall_vec);
// Update vertex template with current surface normal
vertex_template[normal_index + 0] = normal[0] * normal_normalize;
vertex_template[normal_index + 1] = normal[1] * normal_normalize;
vertex_template[normal_index + 2] = normal[2] * normal_normalize;
for (var wv=0; wv < wall_vertices.length; wv++) {
vertex_template[0] = wall_vertices[wv][0];
vertex_template[1] = wall_vertices[wv][1];
vertex_template[2] = wall_vertices[wv][2];
if (texcoord_index) {
vertex_template[texcoord_index + 0] = texcoords[wv][0] * texcoord_normalize;
vertex_template[texcoord_index + 1] = texcoords[wv][1] * texcoord_normalize;
}
vertex_data.addVertex(vertex_template);
}
vertex_elements.push(element_offset + 0);
vertex_elements.push(element_offset + 1);
vertex_elements.push(element_offset + 2);
vertex_elements.push(element_offset + 2);
vertex_elements.push(element_offset + 3);
vertex_elements.push(element_offset + 0);
element_offset += 4;
geom_count += 2;
}
}
}
return geom_count;
}
// Triangulation using earcut
// https://github.com/mapbox/earcut
export function triangulatePolygon (data) {
return earcut(data.vertices, data.holes, data.dimensions);
}