From 0d7e5fec75415961b09ecadd733415fef4497696 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Thu, 9 Nov 2017 16:14:40 +0100 Subject: [PATCH] save polyhedrons to LevelRenderData and render OpenGL in 3D --- src/c3nav/mapdata/render/data.py | 62 +++++++++----- src/c3nav/mapdata/render/engines/base.py | 6 +- src/c3nav/mapdata/render/engines/base3d.py | 32 +++++++ src/c3nav/mapdata/render/engines/opengl.py | 97 +++++++--------------- src/c3nav/mapdata/render/engines/svg.py | 2 +- src/c3nav/mapdata/render/renderer.py | 2 - 6 files changed, 107 insertions(+), 94 deletions(-) create mode 100644 src/c3nav/mapdata/render/engines/base3d.py diff --git a/src/c3nav/mapdata/render/data.py b/src/c3nav/mapdata/render/data.py index 645e8a4b..3deec143 100644 --- a/src/c3nav/mapdata/render/data.py +++ b/src/c3nav/mapdata/render/data.py @@ -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 diff --git a/src/c3nav/mapdata/render/engines/base.py b/src/c3nav/mapdata/render/engines/base.py index 657a208f..546eb594 100644 --- a/src/c3nav/mapdata/render/engines/base.py +++ b/src/c3nav/mapdata/render/engines/base.py @@ -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 diff --git a/src/c3nav/mapdata/render/engines/base3d.py b/src/c3nav/mapdata/render/engines/base3d.py new file mode 100644 index 00000000..1819fb45 --- /dev/null +++ b/src/c3nav/mapdata/render/engines/base3d.py @@ -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() diff --git a/src/c3nav/mapdata/render/engines/opengl.py b/src/c3nav/mapdata/render/engines/opengl.py index c722e9a9..5d1e6510 100644 --- a/src/c3nav/mapdata/render/engines/opengl.py +++ b/src/c3nav/mapdata/render/engines/opengl.py @@ -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() diff --git a/src/c3nav/mapdata/render/engines/svg.py b/src/c3nav/mapdata/render/engines/svg.py index 0dc292f6..9e9a3230 100644 --- a/src/c3nav/mapdata/render/engines/svg.py +++ b/src/c3nav/mapdata/render/engines/svg.py @@ -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) diff --git a/src/c3nav/mapdata/render/renderer.py b/src/c3nav/mapdata/render/renderer.py index 9e54eb04..90df1319 100644 --- a/src/c3nav/mapdata/render/renderer.py +++ b/src/c3nav/mapdata/render/renderer.py @@ -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))