save polyhedrons to LevelRenderData and render OpenGL in 3D
This commit is contained in:
parent
7d4df19adc
commit
0d7e5fec75
6 changed files with 107 additions and 94 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
32
src/c3nav/mapdata/render/engines/base3d.py
Normal file
32
src/c3nav/mapdata/render/engines/base3d.py
Normal 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()
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue