opengl faces cache

This commit is contained in:
Laura Klünder 2017-11-08 17:52:27 +01:00
parent 3e41ac9e14
commit ef29f48873
5 changed files with 70 additions and 36 deletions

View file

@ -1,10 +1,14 @@
import operator
import pickle import pickle
from collections import namedtuple
from functools import reduce
from itertools import chain from itertools import chain
import numpy as np import numpy as np
from django.core.cache import cache from django.core.cache import cache
from django.db import transaction from django.db import transaction
from shapely.geometry import LineString, MultiLineString from django.utils.functional import cached_property
from shapely.geometry import GeometryCollection, LineString, MultiLineString
from shapely.ops import unary_union from shapely.ops import unary_union
from c3nav.mapdata.cache import MapHistory from c3nav.mapdata.cache import MapHistory
@ -14,17 +18,32 @@ from c3nav.mapdata.utils.mesh import triangulate_rings
from c3nav.mapdata.utils.mpl import shapely_to_mpl from c3nav.mapdata.utils.mpl import shapely_to_mpl
class HybridGeometry: def hybrid_union(geoms):
def __init__(self, geom, faces): if not geoms:
self._geom = geom return HybridGeometry(GeometryCollection(), set())
self._faces = faces 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()))
class HybridGeometry(namedtuple('HybridGeometry', ('geom', 'faces'))):
@classmethod @classmethod
def create(cls, geom, face_centers): def create(cls, geom, face_centers):
if isinstance(geom, (LineString, MultiLineString)): if isinstance(geom, (LineString, MultiLineString)):
return HybridGeometry(geom, set()) return HybridGeometry(geom, set())
return HybridGeometry(geom, set(np.argwhere(shapely_to_mpl(geom).contains_points(face_centers)).flatten())) return HybridGeometry(geom, set(np.argwhere(shapely_to_mpl(geom).contains_points(face_centers)).flatten()))
def union(self, geom):
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)
@cached_property
def is_empty(self):
return not self.faces
class AltitudeAreaGeometries: class AltitudeAreaGeometries:
def __init__(self, altitudearea=None, colors=None): def __init__(self, altitudearea=None, colors=None):
@ -37,7 +56,7 @@ class AltitudeAreaGeometries:
self.colors = colors self.colors = colors
def get_geometries(self): def get_geometries(self):
return chain((self.geometry, ), chain(*(areas.values() for areas in self.colors.values()))) return chain((self.geometry,), chain(*(areas.values() for areas in self.colors.values())))
def create_hybrid_geometries(self, face_centers): def create_hybrid_geometries(self, face_centers):
self.geometry = HybridGeometry.create(self.geometry, face_centers) self.geometry = HybridGeometry.create(self.geometry, face_centers)
@ -271,7 +290,7 @@ class LevelGeometries:
return geoms return geoms
def get_geometries(self): def get_geometries(self):
return chain(chain(*(area.get_geometries() for area in self.altitudeareas)), (self.walls, self.doors, ), return chain(chain(*(area.get_geometries() for area in self.altitudeareas)), (self.walls, self.doors,),
self.restricted_spaces_indoors.values(), self.restricted_spaces_outdoors.values()) self.restricted_spaces_indoors.values(), self.restricted_spaces_outdoors.values())
def create_hybrid_geometries(self, face_centers): def create_hybrid_geometries(self, face_centers):
@ -287,7 +306,7 @@ class LevelGeometries:
def build_mesh(self): def build_mesh(self):
rings = tuple(chain(*(get_rings(geom) for geom in self.get_geometries()))) rings = tuple(chain(*(get_rings(geom) for geom in self.get_geometries())))
self.vertices, self.faces = triangulate_rings(rings) self.vertices, self.faces = triangulate_rings(rings)
self.create_hybrid_geometries(face_centers=self.vertices[self.faces].sum(axis=1)/3) self.create_hybrid_geometries(face_centers=self.vertices[self.faces].sum(axis=1) / 3)
def get_level_render_data(level): def get_level_render_data(level):

View file

@ -3,6 +3,7 @@ from abc import ABC, abstractmethod
from functools import lru_cache from functools import lru_cache
from typing import Optional from typing import Optional
from shapely.geometry import JOIN_STYLE, box
from shapely.ops import unary_union from shapely.ops import unary_union
@ -32,6 +33,7 @@ class RenderEngine(ABC):
self.minx = xoff self.minx = xoff
self.miny = yoff self.miny = yoff
self.scale = scale self.scale = scale
self.orig_buffer = buffer
self.buffer = int(math.ceil(buffer*self.scale)) self.buffer = int(math.ceil(buffer*self.scale))
self.background = background self.background = background
@ -42,6 +44,7 @@ class RenderEngine(ABC):
self.buffer = int(math.ceil(buffer*self.scale)) self.buffer = int(math.ceil(buffer*self.scale))
self.buffered_width = self.width + 2 * self.buffer self.buffered_width = self.width + 2 * self.buffer
self.buffered_height = self.height + 2 * self.buffer self.buffered_height = self.height + 2 * self.buffer
self.buffered_bbox = box(self.minx, self.miny, self.maxx, self.maxy).buffer(buffer, join_style=JOIN_STYLE.mitre)
self.background_rgb = tuple(int(background[i:i + 2], 16)/255 for i in range(1, 6, 2)) self.background_rgb = tuple(int(background[i:i + 2], 16)/255 for i in range(1, 6, 2))
@ -83,14 +86,6 @@ class RenderEngine(ABC):
# if altitude is not set but height is, the altitude will depend on the geometries below # if altitude is not set but height is, the altitude will depend on the geometries below
# if fill_color is set, filter out geometries that cannot be filled # if fill_color is set, filter out geometries that cannot be filled
if fill is not None:
try:
geometry.geoms
except AttributeError:
if not hasattr(geometry, 'exterior'):
return
else:
geometry = type(geometry)(tuple(geom for geom in geometry.geoms if hasattr(geom, 'exterior')))
if geometry.is_empty: if geometry.is_empty:
return return
@ -101,3 +96,6 @@ class RenderEngine(ABC):
def _add_geometry(self, geometry, fill: Optional[FillAttribs] = None, stroke: Optional[StrokeAttribs] = None, 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, shape_cache_key=None):
pass pass
def set_mesh_lookup_data(self, vertices, faces):
pass

View file

@ -1,6 +1,6 @@
import io import io
import threading import threading
from collections import deque, namedtuple from collections import namedtuple
from itertools import chain from itertools import chain
from queue import Queue from queue import Queue
from typing import Optional, Tuple, Union from typing import Optional, Tuple, Union
@ -11,6 +11,7 @@ from PIL import Image
from shapely.geometry import CAP_STYLE, JOIN_STYLE, MultiPolygon, Polygon from shapely.geometry import CAP_STYLE, JOIN_STYLE, MultiPolygon, Polygon
from shapely.ops import unary_union 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, RenderEngine, StrokeAttribs
from c3nav.mapdata.utils.mesh import triangulate_polygon from c3nav.mapdata.utils.mesh import triangulate_polygon
@ -143,13 +144,24 @@ class OpenGLEngine(RenderEngine):
self.np_scale = np.array((scale_x, -scale_y)) self.np_scale = np.array((scale_x, -scale_y))
self.np_offset = np.array((-self.minx * scale_x - 1, self.maxy * scale_y - 1)) self.np_offset = np.array((-self.minx * scale_x - 1, self.maxy * scale_y - 1))
def _create_geometry(self, geometry: Union[Polygon, MultiPolygon], append=None): # mesh data
triangles = deque() self.vertices_lookup = None
self.faces_lookup = None
def set_mesh_lookup_data(self, vertices, faces):
self.vertices_lookup = vertices
self.faces_lookup = faces
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) vertices, faces = triangulate_polygon(geometry)
triangles = vertices[faces.flatten()] triangles = vertices[faces.flatten()]
vertices = np.vstack(triangles).astype(np.float32) vertices = np.vstack(triangles).astype(np.float32)
vertices = vertices * self.np_scale + self.np_offset vertices = vertices * self.np_scale + self.np_offset
if append is not None: if append is not None:
append = np.array(append, dtype=np.float32).flatten() append = np.array(append, dtype=np.float32).flatten()
@ -162,18 +174,22 @@ class OpenGLEngine(RenderEngine):
def _add_geometry(self, geometry, fill: Optional[FillAttribs] = None, stroke: Optional[StrokeAttribs] = None, 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, shape_cache_key=None):
if fill is not None: if fill is not None:
if stroke is not None and fill.color == stroke.color: 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), geometry = geometry.buffer(max(stroke.width, (stroke.min_px or 0) / self.scale),
cap_style=CAP_STYLE.flat, join_style=JOIN_STYLE.mitre) cap_style=CAP_STYLE.flat, join_style=JOIN_STYLE.mitre)
stroke = None stroke = None
self.vertices.append(self._create_geometry(geometry, self.color_to_rgb(fill.color))) self.vertices.append(self._create_geometry(geometry, self.color_to_rgb(fill.color)))
if stroke is not None: if stroke is not None:
geometry = self.buffered_bbox.intersection(geometry.geom)
lines = tuple(chain(*( lines = tuple(chain(*(
((geom.exterior, *geom.interiors) if isinstance(geom, Polygon) else (geom, )) ((geom.exterior, *geom.interiors) if isinstance(geom, Polygon) else (geom, ))
for geom in getattr(geometry, 'geoms', (geometry, )) for geom in getattr(geometry, 'geoms', (geometry, ))
))) )))
if not lines:
return
width = max(stroke.width, (stroke.min_px or 0) / self.scale) / 2 width = max(stroke.width, (stroke.min_px or 0) / self.scale) / 2
# if width would be <1px in the upsampled rendering, emulate it thorugh opacity on a 1px width # if width would be <1px in the upsampled rendering, emulate it thorugh opacity on a 1px width
@ -193,7 +209,7 @@ class OpenGLEngine(RenderEngine):
def get_png(self) -> bytes: def get_png(self) -> bytes:
return self.worker.render(self.width, self.height, self.samples, self.background_rgb, return self.worker.render(self.width, self.height, self.samples, self.background_rgb,
np.hstack(self.vertices).astype(np.float32).tobytes()) np.hstack(self.vertices).astype(np.float32).tobytes() if self.vertices else b'')
OpenGLEngine.worker.start() OpenGLEngine.worker.start()

View file

@ -198,6 +198,7 @@ class SVGEngine(RenderEngine):
def _add_geometry(self, geometry, fill: Optional[FillAttribs] = None, stroke: Optional[StrokeAttribs] = None, 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, shape_cache_key=None):
geometry = self.buffered_bbox.intersection(geometry.geom)
if fill: if fill:
attribs = ' fill="'+(fill.color)+'"' attribs = ' fill="'+(fill.color)+'"'

View file

@ -2,11 +2,10 @@ from django.core.cache import cache
from django.utils.functional import cached_property from django.utils.functional import cached_property
from shapely import prepared from shapely import prepared
from shapely.geometry import box from shapely.geometry import box
from shapely.ops import unary_union
from c3nav.mapdata.cache import MapHistory from c3nav.mapdata.cache import MapHistory
from c3nav.mapdata.models import MapUpdate from c3nav.mapdata.models import MapUpdate
from c3nav.mapdata.render.data import get_level_render_data from c3nav.mapdata.render.data import get_level_render_data, hybrid_union
from c3nav.mapdata.render.engines.base import FillAttribs, StrokeAttribs from c3nav.mapdata.render.engines.base import FillAttribs, StrokeAttribs
@ -70,17 +69,18 @@ class MapRenderer:
# add no access restriction to “unlocked“ access restrictions so lookup gets easier # add no access restriction to “unlocked“ access restrictions so lookup gets easier
unlocked_access_restrictions = self.unlocked_access_restrictions | set([None]) unlocked_access_restrictions = self.unlocked_access_restrictions | set([None])
bbox = self.bbox bbox = prepared.prep(self.bbox)
bbox_prep = prepared.prep(bbox)
for geoms, default_height in self.level_render_data.levels: for geoms, default_height in self.level_render_data.levels:
if not bbox_prep.intersects(geoms.affected_area): if not bbox.intersects(geoms.affected_area):
continue continue
engine.set_mesh_lookup_data(geoms.vertices, geoms.faces)
# hide indoor and outdoor rooms if their access restriction was not unlocked # hide indoor and outdoor rooms if their access restriction was not unlocked
add_walls = unary_union(tuple(area for access_restriction, area in geoms.restricted_spaces_indoors.items() add_walls = hybrid_union(tuple(area for access_restriction, area in geoms.restricted_spaces_indoors.items()
if access_restriction not in unlocked_access_restrictions)) if access_restriction not in unlocked_access_restrictions))
crop_areas = unary_union( crop_areas = hybrid_union(
tuple(area for access_restriction, area in geoms.restricted_spaces_outdoors.items() tuple(area for access_restriction, area in geoms.restricted_spaces_outdoors.items()
if access_restriction not in unlocked_access_restrictions) if access_restriction not in unlocked_access_restrictions)
).union(add_walls) ).union(add_walls)
@ -88,7 +88,7 @@ class MapRenderer:
# render altitude areas in default ground color and add ground colors to each one afterwards # render altitude areas in default ground color and add ground colors to each one afterwards
# shadows are directly calculated and added by the engine # shadows are directly calculated and added by the engine
for altitudearea in geoms.altitudeareas: for altitudearea in geoms.altitudeareas:
engine.add_geometry(bbox.intersection(altitudearea.geometry.difference(crop_areas)), engine.add_geometry(altitudearea.geometry.difference(crop_areas),
altitude=altitudearea.altitude, fill=FillAttribs('#eeeeee')) altitude=altitudearea.altitude, fill=FillAttribs('#eeeeee'))
for color, areas in altitudearea.colors.items(): for color, areas in altitudearea.colors.items():
@ -96,18 +96,18 @@ class MapRenderer:
areas = tuple(area for access_restriction, area in areas.items() areas = tuple(area for access_restriction, area in areas.items()
if access_restriction in unlocked_access_restrictions) if access_restriction in unlocked_access_restrictions)
if areas: if areas:
engine.add_geometry(bbox.intersection(unary_union(areas)), fill=FillAttribs(color)) engine.add_geometry(hybrid_union(areas), fill=FillAttribs(color))
# add walls, stroke_px makes sure that all walls are at least 1px thick on all zoom levels, # add walls, stroke_px makes sure that all walls are at least 1px thick on all zoom levels,
walls = None walls = None
if not add_walls.is_empty or not geoms.walls.is_empty: if not add_walls.is_empty or not geoms.walls.is_empty:
walls = bbox.intersection(geoms.walls.union(add_walls)) walls = geoms.walls.union(add_walls)
if walls is not None: if walls is not None:
engine.add_geometry(walls, height=default_height, fill=FillAttribs('#aaaaaa')) engine.add_geometry(walls, height=default_height, fill=FillAttribs('#aaaaaa'))
if not geoms.doors.is_empty: if not geoms.doors.is_empty:
engine.add_geometry(bbox.intersection(geoms.doors.difference(add_walls)), fill=FillAttribs('#ffffff'), engine.add_geometry(geoms.doors.difference(add_walls), fill=FillAttribs('#ffffff'),
stroke=StrokeAttribs('#ffffff', 0.05, min_px=0.2)) stroke=StrokeAttribs('#ffffff', 0.05, min_px=0.2))
if walls is not None: if walls is not None: