save polyhedrons to LevelRenderData and render OpenGL in 3D

This commit is contained in:
Laura Klünder 2017-11-09 16:14:40 +01:00
parent 7d4df19adc
commit 0d7e5fec75
6 changed files with 107 additions and 94 deletions

View file

@ -1,13 +1,11 @@
import operator
import pickle
import threading
from collections import Counter, deque, namedtuple
from functools import reduce
from collections import Counter, deque
from itertools import chain
import numpy as np
from django.db import transaction
from django.utils.functional import cached_property
from scipy.interpolate import NearestNDInterpolator
from shapely.geometry import GeometryCollection, LineString, MultiLineString
from shapely.ops import unary_union
@ -21,14 +19,20 @@ from c3nav.mapdata.utils.mpl import shapely_to_mpl
def hybrid_union(geoms):
if not geoms:
return HybridGeometry(GeometryCollection(), set())
return HybridGeometry(GeometryCollection(), ())
if len(geoms) == 1:
return geoms[0]
return HybridGeometry(unary_union(tuple(geom.geom for geom in geoms)),
reduce(operator.or_, (geom.faces for geom in geoms), set()))
tuple(chain(*(geom.faces for geom in geoms))))
class HybridGeometry(namedtuple('HybridGeometry', ('geom', 'faces'))):
class HybridGeometry:
__slots__ = ('geom', 'faces')
def __init__(self, geom, faces):
self.geom = geom
self.faces = faces
@classmethod
def create(cls, geom, face_centers):
if isinstance(geom, (LineString, MultiLineString)):
@ -40,12 +44,12 @@ class HybridGeometry(namedtuple('HybridGeometry', ('geom', 'faces'))):
return HybridGeometry(geom, tuple(f for f in faces if f))
def union(self, geom):
return HybridGeometry(self.geom.union(geom.geom), self.faces | geom.faces)
return HybridGeometry(self.geom.union(geom.geom), self.faces+geom.faces)
def difference(self, geom):
return HybridGeometry(self.geom.difference(geom.geom), self.faces - geom.faces)
return HybridGeometry(self.geom.difference(geom.geom), self.faces)
@cached_property
@property
def is_empty(self):
return not self.faces
@ -68,6 +72,10 @@ class AltitudeAreaGeometries:
self.colors = {color: {key: HybridGeometry.create(geom, face_centers) for key, geom in areas.items()}
for color, areas in self.colors.items()}
def create_polyhedrons(self):
for geometry in self.get_geometries():
geometry.faces = None
class FakeCropper:
@staticmethod
@ -240,11 +248,6 @@ class LevelGeometries:
self.restricted_spaces_outdoors = None
self.affected_area = None
self.vertices = None
self.faces = None
self.vertex_altitudes = None
self.vertex_heights = None
@staticmethod
def build_for_level(level):
geoms = LevelGeometries()
@ -414,7 +417,7 @@ class LevelGeometries:
# lower faces
faces.append(np.dstack((self.vertices[np.flip(geom_faces, axis=1)], bottom[geom_faces])))
return np.vstack(faces)
return (np.vstack(faces), )
def build_mesh(self):
rings = tuple(chain(*(get_rings(geom) for geom in self.get_geometries())))
@ -422,14 +425,31 @@ class LevelGeometries:
self.create_hybrid_geometries(face_centers=self.vertices[self.faces].sum(axis=1) / 3)
# calculate altitudes
self.vertex_altitudes = self._build_vertex_values((area.geometry, int(area.altitude*100))
for area in reversed(self.altitudeareas))/100
self.vertex_heights = self._build_vertex_values((area, int(height*100))
for area, height in self.heightareas)/100
self.vertex_wall_heights = self.vertex_altitudes+self.vertex_heights
vertex_altitudes = self._build_vertex_values((area.geometry, int(area.altitude*100))
for area in reversed(self.altitudeareas))/100
vertex_heights = self._build_vertex_values((area, int(height*100))
for area, height in self.heightareas)/100
vertex_wall_heights = vertex_altitudes + vertex_heights
# create polyhedrons
self._create_polyhedron(self.walls, bottom=self.vertex_altitudes, top=self.vertex_wall_heights)
self.walls.faces = self._create_polyhedron(self.walls, bottom=vertex_altitudes, top=vertex_wall_heights)
self.doors.faces = self._create_polyhedron(self.doors, bottom=vertex_wall_heights-1, top=vertex_wall_heights)
for key, geometry in self.restricted_spaces_indoors.items():
geometry.faces = self._create_polyhedron(geometry, bottom=vertex_altitudes, top=vertex_wall_heights)
for key, geometry in self.restricted_spaces_outdoors.items():
geometry.faces = None
for area in self.altitudeareas:
area.create_polyhedrons()
"""
for area in self.altitudeareas:
area.create_hybrid_geometries(face_centers)
self.restricted_spaces_outdoors = {key: HybridGeometry.create(geom, face_centers)
for key, geom in self.restricted_spaces_outdoors.items()}
"""
# unset heightareas, they are no loinger needed
self.heightareas = None
self.vertices = None
self.faces = None

View file

@ -63,7 +63,7 @@ class RenderEngine(ABC):
raise ValueError('invalid color string!')
def add_geometry(self, geometry, fill: Optional[FillAttribs] = None, stroke: Optional[StrokeAttribs] = None,
altitude=None, height=None, shape_cache_key=None):
altitude=None, height=None, auto_height=False, shape_cache_key=None):
# draw a shapely geometry with a given style
# altitude is the absolute altitude of the upper bound of the element
# height is the height of the element
@ -74,10 +74,10 @@ class RenderEngine(ABC):
return
self._add_geometry(geometry=geometry, fill=fill, stroke=stroke,
altitude=altitude, height=height, shape_cache_key=shape_cache_key)
altitude=altitude, height=height, auto_height=auto_height, shape_cache_key=shape_cache_key)
@abstractmethod
def _add_geometry(self, geometry, fill: Optional[FillAttribs] = None, stroke: Optional[StrokeAttribs] = None,
def _add_geometry(self, geometry, fill: Optional[FillAttribs], stroke: Optional[StrokeAttribs],
altitude=None, height=None, shape_cache_key=None):
pass

View file

@ -0,0 +1,32 @@
import numpy as np
from c3nav.mapdata.render.data import HybridGeometry
from c3nav.mapdata.render.engines.base import RenderEngine
class Base3DEngine(RenderEngine):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.vertices = []
scale_x = self.scale / self.width * 2
scale_y = self.scale / self.height * 2
scale_z = (scale_x+scale_y)/2
self.np_scale = np.array((scale_x, -scale_y, scale_z))
self.np_offset = np.array((-self.minx * scale_x - 1, self.maxy * scale_y - 1, 0))
def _append_to_vertices(self, vertices, append=None):
if append is not None:
append = np.array(append, dtype=np.float32).flatten()
vertices = np.hstack((
vertices,
append.reshape(1, append.size).repeat(vertices.shape[0], 0)
))
return vertices
def _place_geometry(self, geometry: HybridGeometry, append=None):
faces = geometry.faces[0] if len(geometry.faces) == 1 else np.vstack(geometry.faces)
vertices = faces.reshape(-1, 3) * self.np_scale + self.np_offset
return self._append_to_vertices(vertices, append).flatten()

View file

@ -3,16 +3,17 @@ import threading
from collections import namedtuple
from itertools import chain
from queue import Queue
from typing import Optional, Tuple, Union
from typing import Optional, Tuple
import ModernGL
import numpy as np
from PIL import Image
from shapely.geometry import CAP_STYLE, JOIN_STYLE, MultiPolygon, Polygon
from shapely.geometry import CAP_STYLE, JOIN_STYLE, Polygon
from shapely.ops import unary_union
from c3nav.mapdata.render.data import HybridGeometry
from c3nav.mapdata.render.engines.base import FillAttribs, RenderEngine, StrokeAttribs
from c3nav.mapdata.render.engines.base import FillAttribs, StrokeAttribs
from c3nav.mapdata.render.engines.base3d import Base3DEngine
from c3nav.mapdata.utils.mesh import triangulate_polygon
@ -31,11 +32,11 @@ class RenderContext(namedtuple('RenderContext', ('width', 'height', 'ctx', 'prog
prog = ctx.program([
ctx.vertex_shader('''
#version 330
in vec2 in_vert;
in vec3 in_vert;
in vec4 in_color;
out vec4 v_color;
void main() {
gl_Position = vec4(in_vert, 0.0, 1.0);
gl_Position = vec4(in_vert, 1.0);
v_color = in_color;
}
'''),
@ -131,68 +132,13 @@ class OpenGLWorker(threading.Thread):
return task.get_result()
class OpenGLEngine(RenderEngine):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class OpenGLEngine(Base3DEngine):
def _add_geometry(self, geometry, fill: Optional[FillAttribs], stroke: Optional[StrokeAttribs], **kwargs):
self.vertices = []
scale_x = self.scale / self.width * 2
scale_y = self.scale / self.height * 2
self.np_scale = np.array((scale_x, -scale_y))
self.np_offset = np.array((-self.minx * scale_x - 1, self.maxy * scale_y - 1))
# mesh data
self.vertices_lookup = None
self.faces_lookup = None
self.vertices_altitudes = None
self.vertices_heightss = None
def set_mesh_lookup_data(self, data):
self.vertices_lookup = data.vertices
self.faces_lookup = data.faces
self.vertices_altitudes = data.vertices_altitudes
self.vertices_heights = data.vertices_heights
def _create_geometry(self, geometry: Union[Polygon, MultiPolygon, HybridGeometry], append=None):
if isinstance(geometry, HybridGeometry):
vertices = self.vertices_lookup[
self.faces_lookup[np.array(tuple(geometry.faces))].flatten()
].astype(np.float32)
else:
vertices, faces = triangulate_polygon(geometry)
triangles = vertices[faces.flatten()]
vertices = np.vstack(triangles).astype(np.float32)
vertices = vertices * self.np_scale + self.np_offset
if append is not None:
append = np.array(append, dtype=np.float32).flatten()
vertices = np.hstack((
vertices,
append.reshape(1, append.size).repeat(vertices.shape[0], 0)
))
return vertices.flatten()
def _add_geometry(self, geometry, fill: Optional[FillAttribs] = None, stroke: Optional[StrokeAttribs] = None,
altitude=None, height=None, shape_cache_key=None):
if fill is not None:
if stroke is not None and fill.color == stroke.color and 0:
geometry = geometry.buffer(max(stroke.width, (stroke.min_px or 0) / self.scale),
cap_style=CAP_STYLE.flat, join_style=JOIN_STYLE.mitre)
stroke = None
self.vertices.append(self._create_geometry(geometry, self.color_to_rgb(fill.color)))
self.vertices.append(self._place_geometry(geometry, self.color_to_rgb(fill.color)))
if stroke is not None:
geometry = self.buffered_bbox.intersection(geometry.geom)
lines = tuple(chain(*(
((geom.exterior, *geom.interiors) if isinstance(geom, Polygon) else (geom, ))
for geom in getattr(geometry, 'geoms', (geometry, ))
)))
if not lines:
return
width = max(stroke.width, (stroke.min_px or 0) / self.scale) / 2
# if width would be <1px, emulate it through opacity on a 1px width
@ -203,10 +149,27 @@ class OpenGLEngine(RenderEngine):
else:
alpha = 1
self.vertices.append(self._create_geometry(
unary_union(lines).buffer(width, cap_style=CAP_STYLE.flat, join_style=JOIN_STYLE.mitre),
self.color_to_rgb(stroke.color, alpha=alpha)
))
self.vertices.append(self._create_border(geometry, width, self.color_to_rgb(stroke.color, alpha=alpha)))
def _create_border(self, geometry: HybridGeometry, width, append=None):
altitude = np.vstack(geometry.faces)[:, :, 2].max()+0.001
geometry = self.buffered_bbox.intersection(geometry.geom)
lines = tuple(chain(*(
((geom.exterior, *geom.interiors) if isinstance(geom, Polygon) else (geom,))
for geom in getattr(geometry, 'geoms', (geometry,))
)))
if not lines:
return np.empty((0, ))
lines = unary_union(lines).buffer(width, cap_style=CAP_STYLE.flat, join_style=JOIN_STYLE.mitre)
vertices, faces = triangulate_polygon(lines)
triangles = np.hstack((vertices[faces.flatten()], np.full((faces.size, 1), fill_value=altitude)))
vertices = np.vstack(triangles).astype(np.float32) * self.np_scale + self.np_offset
return self._append_to_vertices(vertices, append).flatten()
worker = OpenGLWorker()

View file

@ -212,7 +212,7 @@ class SVGEngine(RenderEngine):
else:
self.altitudes[new_altitude] = new_geometry
def _add_geometry(self, geometry, fill: Optional[FillAttribs] = None, stroke: Optional[StrokeAttribs] = None,
def _add_geometry(self, geometry, fill: Optional[FillAttribs], stroke: Optional[StrokeAttribs],
altitude=None, height=None, shape_cache_key=None):
geometry = self.buffered_bbox.intersection(geometry.geom)

View file

@ -75,8 +75,6 @@ class MapRenderer:
if not bbox.intersects(geoms.affected_area):
continue
engine.set_mesh_lookup_data(geoms)
# hide indoor and outdoor rooms if their access restriction was not unlocked
add_walls = hybrid_union(tuple(area for access_restriction, area in geoms.restricted_spaces_indoors.items()
if access_restriction not in unlocked_access_restrictions))