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
from collections import namedtuple
from functools import reduce
from itertools import chain
import numpy as np
from django.core.cache import cache
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 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
class HybridGeometry:
def __init__(self, geom, faces):
self._geom = geom
self._faces = faces
def hybrid_union(geoms):
if not geoms:
return HybridGeometry(GeometryCollection(), set())
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
def create(cls, geom, face_centers):
if isinstance(geom, (LineString, MultiLineString)):
return HybridGeometry(geom, set())
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:
def __init__(self, altitudearea=None, colors=None):
@ -37,7 +56,7 @@ class AltitudeAreaGeometries:
self.colors = colors
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):
self.geometry = HybridGeometry.create(self.geometry, face_centers)
@ -271,7 +290,7 @@ class LevelGeometries:
return geoms
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())
def create_hybrid_geometries(self, face_centers):
@ -287,7 +306,7 @@ class LevelGeometries:
def build_mesh(self):
rings = tuple(chain(*(get_rings(geom) for geom in self.get_geometries())))
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):

View file

@ -3,6 +3,7 @@ from abc import ABC, abstractmethod
from functools import lru_cache
from typing import Optional
from shapely.geometry import JOIN_STYLE, box
from shapely.ops import unary_union
@ -32,6 +33,7 @@ class RenderEngine(ABC):
self.minx = xoff
self.miny = yoff
self.scale = scale
self.orig_buffer = buffer
self.buffer = int(math.ceil(buffer*self.scale))
self.background = background
@ -42,6 +44,7 @@ class RenderEngine(ABC):
self.buffer = int(math.ceil(buffer*self.scale))
self.buffered_width = self.width + 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))
@ -83,14 +86,6 @@ class RenderEngine(ABC):
# 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 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:
return
@ -101,3 +96,6 @@ class RenderEngine(ABC):
def _add_geometry(self, geometry, fill: Optional[FillAttribs] = None, stroke: Optional[StrokeAttribs] = None,
altitude=None, height=None, shape_cache_key=None):
pass
def set_mesh_lookup_data(self, vertices, faces):
pass

View file

@ -1,6 +1,6 @@
import io
import threading
from collections import deque, namedtuple
from collections import namedtuple
from itertools import chain
from queue import Queue
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.ops import unary_union
from c3nav.mapdata.render.data import HybridGeometry
from c3nav.mapdata.render.engines.base import FillAttribs, RenderEngine, StrokeAttribs
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_offset = np.array((-self.minx * scale_x - 1, self.maxy * scale_y - 1))
def _create_geometry(self, geometry: Union[Polygon, MultiPolygon], append=None):
triangles = deque()
# mesh data
self.vertices_lookup = None
self.faces_lookup = None
vertices, faces = triangulate_polygon(geometry)
triangles = vertices[faces.flatten()]
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)
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
if append is not None:
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,
altitude=None, height=None, shape_cache_key=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),
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)))
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 in the upsampled rendering, emulate it thorugh opacity on a 1px width
@ -193,7 +209,7 @@ class OpenGLEngine(RenderEngine):
def get_png(self) -> bytes:
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()

View file

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

View file

@ -2,11 +2,10 @@ from django.core.cache import cache
from django.utils.functional import cached_property
from shapely import prepared
from shapely.geometry import box
from shapely.ops import unary_union
from c3nav.mapdata.cache import MapHistory
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
@ -70,17 +69,18 @@ class MapRenderer:
# add no access restriction to “unlocked“ access restrictions so lookup gets easier
unlocked_access_restrictions = self.unlocked_access_restrictions | set([None])
bbox = self.bbox
bbox_prep = prepared.prep(bbox)
bbox = prepared.prep(self.bbox)
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
engine.set_mesh_lookup_data(geoms.vertices, geoms.faces)
# 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()
if access_restriction not in unlocked_access_restrictions))
crop_areas = unary_union(
add_walls = hybrid_union(tuple(area for access_restriction, area in geoms.restricted_spaces_indoors.items()
if access_restriction not in unlocked_access_restrictions))
crop_areas = hybrid_union(
tuple(area for access_restriction, area in geoms.restricted_spaces_outdoors.items()
if access_restriction not in unlocked_access_restrictions)
).union(add_walls)
@ -88,7 +88,7 @@ class MapRenderer:
# render altitude areas in default ground color and add ground colors to each one afterwards
# shadows are directly calculated and added by the engine
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'))
for color, areas in altitudearea.colors.items():
@ -96,18 +96,18 @@ class MapRenderer:
areas = tuple(area for access_restriction, area in areas.items()
if access_restriction in unlocked_access_restrictions)
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,
walls = None
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:
engine.add_geometry(walls, height=default_height, fill=FillAttribs('#aaaaaa'))
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))
if walls is not None: