From a05c7a5a3c1cdad7fb6a9972977d2f4c9ad312b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Mon, 6 Nov 2017 11:18:45 +0100 Subject: [PATCH] add base for OpenGL map render engine --- src/c3nav/mapdata/render/__init__.py | 2 +- src/c3nav/mapdata/render/engines/__init__.py | 24 ++++++++ src/c3nav/mapdata/render/engines/opengl.py | 61 ++++++++++++++++++++ src/c3nav/mapdata/render/engines/svg.py | 4 +- src/c3nav/mapdata/render/renderer.py | 27 +++++---- src/c3nav/mapdata/views.py | 7 ++- src/c3nav/settings.py | 3 + 7 files changed, 108 insertions(+), 20 deletions(-) create mode 100644 src/c3nav/mapdata/render/engines/opengl.py diff --git a/src/c3nav/mapdata/render/__init__.py b/src/c3nav/mapdata/render/__init__.py index e8f0e67a..f6aa05f0 100644 --- a/src/c3nav/mapdata/render/__init__.py +++ b/src/c3nav/mapdata/render/__init__.py @@ -1,2 +1,2 @@ -from c3nav.mapdata.render.renderer import ImageRenderer # noqa +from c3nav.mapdata.render.renderer import MapRenderer # noqa from c3nav.mapdata.render.utils import get_render_level_ids, set_tile_access_cookie, get_tile_access_cookie # noqa diff --git a/src/c3nav/mapdata/render/engines/__init__.py b/src/c3nav/mapdata/render/engines/__init__.py index e69de29b..36070ee1 100644 --- a/src/c3nav/mapdata/render/engines/__init__.py +++ b/src/c3nav/mapdata/render/engines/__init__.py @@ -0,0 +1,24 @@ +from django.conf import settings +from django.core import checks + +from c3nav.mapdata.render.engines.svg import SVGEngine # noqa + + +@checks.register() +def check_image_renderer(app_configs, **kwargs): + errors = [] + if settings.IMAGE_RENDERER not in ('svg', 'opengl'): + errors.append( + checks.Error( + 'Invalid image renderer: '+settings.IMAGE_RENDERER, + obj='settings.IMAGE_RENDERER', + id='c3nav.mapdata.E001', + ) + ) + return errors + + +if settings.IMAGE_RENDERER == 'opengl': + from c3nav.mapdata.render.engines.opengl import OpenGLEngine as ImageRenderEngine # noqa +else: + from c3nav.mapdata.render.engines.svg import SVGEngine as ImageRenderEngine # noqa diff --git a/src/c3nav/mapdata/render/engines/opengl.py b/src/c3nav/mapdata/render/engines/opengl.py new file mode 100644 index 00000000..d6253454 --- /dev/null +++ b/src/c3nav/mapdata/render/engines/opengl.py @@ -0,0 +1,61 @@ +import io + +import ModernGL +import numpy as np +from PIL import Image + +from c3nav.mapdata.render.engines.base import RenderEngine + + +class OpenGLEngine(RenderEngine): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.vertices = [] + self.ctx = ModernGL.create_standalone_context() + + self.color_rbo = self.ctx.renderbuffer((self.width, self.height)) + self.depth_rbo = self.ctx.depth_renderbuffer((self.width, self.height)) + self.fbo = self.ctx.framebuffer([self.color_rbo], self.depth_rbo) + self.fbo.use() + + self.ctx.clear(*(i/255 for i in self.background_rgb)) + + self.prog = self.ctx.program([ + self.ctx.vertex_shader(''' + #version 330 + in vec2 in_vert; + in vec3 in_color; + out vec3 v_color; + void main() { + gl_Position = vec4(in_vert, 0.0, 1.0); + v_color = in_color; + } + '''), + self.ctx.fragment_shader(''' + #version 330 + in vec3 v_color; + out vec4 f_color; + void main() { + f_color = vec4(v_color, 1.0); + } + '''), + ]) + + def _add_geometry(self, geometry, fill=None, stroke=None, altitude=None, height=None, shape_cache_key=None): + pass + + def get_png(self) -> bytes: + if self.vertices: + vbo = self.ctx.buffer(np.hstack(self.vertices).tobytes()) + + # We control the 'in_vert' and `in_color' variables + vao = self.ctx.simple_vertex_array(self.prog, vbo, ['in_vert', 'in_color']) + vao.render() + + img = Image.frombytes('RGB', (self.width, self.height), self.fbo.read(components=3)) + + f = io.BytesIO() + img.save(f, 'PNG') + f.seek(0) + return f.read() diff --git a/src/c3nav/mapdata/render/engines/svg.py b/src/c3nav/mapdata/render/engines/svg.py index 4f2ae427..6f519e0a 100644 --- a/src/c3nav/mapdata/render/engines/svg.py +++ b/src/c3nav/mapdata/render/engines/svg.py @@ -6,9 +6,9 @@ from itertools import chain from typing import Optional import numpy as np -from PIL import Image from django.conf import settings from django.core import checks +from PIL import Image from shapely.affinity import translate from shapely.geometry import LineString, Polygon @@ -30,7 +30,7 @@ def check_svg_renderer(app_configs, **kwargs): checks.Error( 'Invalid SVG renderer: '+settings.SVG_RENDERER, obj='settings.SVG_RENDERER', - id='c3nav.mapdata.E001', + id='c3nav.mapdata.E002', ) ) return errors diff --git a/src/c3nav/mapdata/render/renderer.py b/src/c3nav/mapdata/render/renderer.py index 10b9fee3..69fdea64 100644 --- a/src/c3nav/mapdata/render/renderer.py +++ b/src/c3nav/mapdata/render/renderer.py @@ -8,10 +8,9 @@ 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.engines.base import FillAttribs, StrokeAttribs -from c3nav.mapdata.render.engines.svg import SVGEngine -class ImageRenderer: +class MapRenderer: def __init__(self, level, minx, miny, maxx, maxy, scale=1, access_permissions=None): self.level = level self.minx = minx @@ -64,9 +63,9 @@ class ImageRenderer: def cache_key(self): return self.update_cache_key + ':' + self.access_cache_key - def render(self): - svg = SVGEngine(self.width, self.height, self.minx, self.miny, - scale=self.scale, buffer=1, background='#DCDCDC') + def render(self, engine_cls): + engine = engine_cls(self.width, self.height, self.minx, self.miny, + scale=self.scale, buffer=1, background='#DCDCDC') # add no access restriction to “unlocked“ access restrictions so lookup gets easier unlocked_access_restrictions = self.unlocked_access_restrictions | set([None]) @@ -89,16 +88,16 @@ class ImageRenderer: # render altitude areas in default ground color and add ground colors to each one afterwards # shadows are directly calculated and added by the SVGImage class for altitudearea in geoms.altitudeareas: - svg.add_geometry(bbox.intersection(altitudearea.geometry.difference(crop_areas)), - altitude=altitudearea.altitude, fill=FillAttribs('#eeeeee'), - stroke=StrokeAttribs('rgba(0, 0, 0, 0.15)', 0.05, min_px=0.2)) + engine.add_geometry(bbox.intersection(altitudearea.geometry.difference(crop_areas)), + altitude=altitudearea.altitude, fill=FillAttribs('#eeeeee'), + stroke=StrokeAttribs('rgba(0, 0, 0, 0.15)', 0.05, min_px=0.2)) for color, areas in altitudearea.colors.items(): # only select ground colors if their access restriction is unlocked areas = tuple(area for access_restriction, area in areas.items() if access_restriction in unlocked_access_restrictions) if areas: - svg.add_geometry(bbox.intersection(unary_union(areas)), fill=FillAttribs(color)) + engine.add_geometry(bbox.intersection(unary_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 @@ -106,13 +105,13 @@ class ImageRenderer: walls = bbox.intersection(geoms.walls.union(add_walls)) if walls is not None: - svg.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: - svg.add_geometry(bbox.intersection(geoms.doors.difference(add_walls)), fill=FillAttribs('#ffffff'), - stroke=StrokeAttribs('#ffffff', 0.05, min_px=0.2)) + engine.add_geometry(bbox.intersection(geoms.doors.difference(add_walls)), fill=FillAttribs('#ffffff'), + stroke=StrokeAttribs('#ffffff', 0.05, min_px=0.2)) if walls is not None: - svg.add_geometry(walls, stroke=StrokeAttribs('#666666', 0.05, min_px=0.2)) + engine.add_geometry(walls, stroke=StrokeAttribs('#666666', 0.05, min_px=0.2)) - return svg + return engine diff --git a/src/c3nav/mapdata/views.py b/src/c3nav/mapdata/views.py index 0d02f419..8e4bcd25 100644 --- a/src/c3nav/mapdata/views.py +++ b/src/c3nav/mapdata/views.py @@ -14,7 +14,8 @@ from shapely.geometry import box from c3nav.mapdata.cache import MapHistory from c3nav.mapdata.middleware import no_language from c3nav.mapdata.models import Level, MapUpdate, Source -from c3nav.mapdata.render import ImageRenderer, get_render_level_ids, get_tile_access_cookie, set_tile_access_cookie +from c3nav.mapdata.render import MapRenderer, get_render_level_ids, get_tile_access_cookie, set_tile_access_cookie +from c3nav.mapdata.render.engines import ImageRenderEngine @no_language() @@ -50,7 +51,7 @@ def tile(request, level, zoom, x, y, format): access_permissions = get_tile_access_cookie(request) # init renderer - renderer = ImageRenderer(level, minx, miny, maxx, maxy, scale=2**zoom, access_permissions=access_permissions) + renderer = MapRenderer(level, minx, miny, maxx, maxy, scale=2 ** zoom, access_permissions=access_permissions) tile_cache_key = renderer.cache_key update_cache_key = renderer.update_cache_key @@ -93,7 +94,7 @@ def tile(request, level, zoom, x, y, format): content_type = 'image/svg+xml' if format == 'svg' else 'image/png' if data is None: - svg = renderer.render() + svg = renderer.render(ImageRenderEngine) if format == 'svg': data = svg.get_xml() filemode = 'w' diff --git a/src/c3nav/settings.py b/src/c3nav/settings.py index 2852a514..70a0e2b6 100644 --- a/src/c3nav/settings.py +++ b/src/c3nav/settings.py @@ -73,8 +73,11 @@ else: debug_fallback = "runserver" in sys.argv DEBUG = config.getboolean('django', 'debug', fallback=debug_fallback) + RENDER_SCALE = float(config.get('c3nav', 'render_scale', fallback=20.0)) +IMAGE_RENDERER = config.get('c3nav', 'image_renderer', fallback='svg') SVG_RENDERER = config.get('c3nav', 'svg_renderer', fallback='rsvg-convert') + CACHE_TILES = config.get('c3nav', 'cache_tiles', fallback=not DEBUG) CACHE_RESOLUTION = config.get('c3nav', 'cache_resolution', fallback=4)