blender render engine: render buildings… in theory

This commit is contained in:
Laura Klünder 2018-12-05 19:22:20 +01:00
parent 1d40e3f02a
commit 5b4ee0b4d0
6 changed files with 142 additions and 9 deletions

View file

@ -43,12 +43,13 @@ class RenderEngine(ABC):
self.maxx = self.minx + width / scale self.maxx = self.minx + width / scale
self.maxy = self.miny + height / scale self.maxy = self.miny + height / scale
self.bbox = box(self.minx, self.miny, self.maxx, self.maxy)
# how many pixels around the image should be added and later cropped (otherwise rsvg does not blur correctly) # how many pixels around the image should be added and later cropped (otherwise rsvg does not blur correctly)
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.buffered_bbox = self.bbox.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))

View file

@ -1,5 +1,8 @@
import re import re
from itertools import chain
import numpy as np
from shapely import prepared
from shapely.affinity import scale from shapely.affinity import scale
from shapely.geometry import LineString, Point from shapely.geometry import LineString, Point
from shapely.ops import unary_union from shapely.ops import unary_union
@ -8,6 +11,7 @@ from c3nav.mapdata.render.engines import register_engine
from c3nav.mapdata.render.engines.base3d import Base3DEngine from c3nav.mapdata.render.engines.base3d import Base3DEngine
from c3nav.mapdata.render.utils import get_full_levels, get_min_altitude from c3nav.mapdata.render.utils import get_full_levels, get_min_altitude
from c3nav.mapdata.utils.geometry import assert_multipolygon from c3nav.mapdata.utils.geometry import assert_multipolygon
from c3nav.mapdata.utils.mesh import triangulate_gapless_mesh_from_polygons
@register_engine @register_engine
@ -35,6 +39,13 @@ class BlenderEngine(Base3DEngine):
bmesh.ops.triangulate(bm, faces=bm.faces[:], quad_method=0, ngon_method=0) bmesh.ops.triangulate(bm, faces=bm.faces[:], quad_method=0, ngon_method=0)
bmesh.update_edit_mesh(me, True) bmesh.update_edit_mesh(me, True)
def clone_object(obj):
new_obj = obj.copy()
new_obj.data = obj.data.copy()
scene = bpy.context.scene
scene.objects.link(new_obj)
return new_obj
def extrude_object(obj, height): def extrude_object(obj, height):
select_object(obj) select_object(obj)
bpy.ops.object.mode_set(mode='EDIT') bpy.ops.object.mode_set(mode='EDIT')
@ -46,7 +57,7 @@ class BlenderEngine(Base3DEngine):
) )
triangulate_object(obj) triangulate_object(obj)
bpy.ops.mesh.select_all(action='SELECT') bpy.ops.mesh.select_all(action='SELECT')
bpy.ops.mesh.normals_make_consistent() bpy.ops.mesh.normals_make_consistent(inside=False)
bpy.ops.object.mode_set(mode='OBJECT') bpy.ops.object.mode_set(mode='OBJECT')
def subtract_object(obj, other_obj, delete_after=False): def subtract_object(obj, other_obj, delete_after=False):
@ -152,7 +163,37 @@ class BlenderEngine(Base3DEngine):
extrude_object(obj, extrude) extrude_object(obj, extrude)
return obj return obj
def add_mesh(name, vertices, faces):
edges = set()
for face in faces:
for edge in ((face[0], face[1]), (face[1], face[2]), (face[2], face[0])):
edges.add(tuple(sorted(edge)))
# create mesh
mesh = bpy.data.meshes.new(name=name)
mesh.from_pydata(
vertices,
tuple(edges),
faces,
)
# add mesh to scene
obj = bpy.data.objects.new(name, mesh)
scene = bpy.context.scene
scene.objects.link(obj)
return obj
def cut_using_mesh_planes(obj, bottom_mesh_plane, top_mesh_plane, height):
height = abs(height)
bottom_obj = clone_object(bottom_mesh_plane)
extrude_object(bottom_obj, -height)
subtract_object(obj, bottom_obj, delete_after=False)
top_obj = clone_object(top_mesh_plane)
extrude_object(top_obj, height)
subtract_object(obj, top_obj, delete_after=False)
polygons_for_join = [] polygons_for_join = []
current_mesh_plane = None
''') ''')
def _clean_python(self, code): def _clean_python(self, code):
@ -168,10 +209,16 @@ class BlenderEngine(Base3DEngine):
def _add_python(self, code): def _add_python(self, code):
self.result += self._clean_python(code)+'\n' self.result += self._clean_python(code)+'\n'
def custom_render(self, level_render_data, bbox, access_permissions): def custom_render(self, level_render_data, access_permissions):
levels = get_full_levels(level_render_data) levels = get_full_levels(level_render_data)
min_altitude = get_min_altitude(levels, default=level_render_data.base_altitude) min_altitude = get_min_altitude(levels, default=level_render_data.base_altitude)
vertices, faces = triangulate_gapless_mesh_from_polygons([self.buffered_bbox])
current_min_z = min_altitude-700
current_max_z = min_altitude-700
vertices = np.hstack((vertices, np.full((vertices.shape[0], 1), current_min_z)))
self._add_mesh_plane('Bottom mesh', vertices / 1000, faces)
for geoms in levels: for geoms in levels:
# hide indoor and outdoor rooms if their access restriction was not unlocked # hide indoor and outdoor rooms if their access restriction was not unlocked
restricted_spaces_indoors = unary_union( restricted_spaces_indoors = unary_union(
@ -184,22 +231,79 @@ class BlenderEngine(Base3DEngine):
) )
restricted_spaces = unary_union((restricted_spaces_indoors, restricted_spaces_outdoors)) # noqa restricted_spaces = unary_union((restricted_spaces_indoors, restricted_spaces_outdoors)) # noqa
# crop altitudeareas
for altitudearea in geoms.altitudeareas: for altitudearea in geoms.altitudeareas:
altitudearea.geometry = altitudearea.geometry.geom.difference(restricted_spaces)
altitudearea.geometry_prep = prepared.prep(altitudearea.geometry)
# crop heightareas
new_heightareas = []
for geometry, height in geoms.heightareas:
geometry = geometry.geom.difference(restricted_spaces)
geometry_prep = prepared.prep(geometry)
new_heightareas.append((geometry, geometry_prep, height))
geoms.heightareas = new_heightareas
# create upper bounds for this level's walls (next mesh plane)
vertices, faces = triangulate_gapless_mesh_from_polygons(
[self.buffered_bbox] + assert_multipolygon(geoms.buildings) +
list(chain(*(assert_multipolygon(altitudearea.geometry) for altitudearea in geoms.altitudeareas)))
)
altitudes = []
for x, y in vertices:
point = Point(x/1000, y/1000)
xy = np.array((x, y))
matching_altitudeareas = [altitudearea for altitudearea in geoms.altitudeareas
if altitudearea.geometry_prep.intersects(point)]
if not matching_altitudeareas:
altitudearea_distances = tuple((altitudearea.geometry.distance(point), altitudearea)
for altitudearea in geoms.altitudeareas)
min_distance = min(distance for distance, altitudearea in altitudearea_distances)
matching_altitudeareas = [altitudearea for distance, altitudearea in altitudearea_distances
if distance == min_distance]
altitude = max(altitudearea.get_altitudes(xy)[0] for altitudearea in matching_altitudeareas)
matching_heights = [height for geom, geom_prep, height in geoms.heightareas
if geom_prep.intersects(point)]
if not matching_heights:
heightarea_distances = tuple((geom.distance(point), i)
for i, (geom, geom_prep, height) in enumerate(geoms.heightareas))
min_distance = min(distance for distance, i in heightarea_distances)
matching_heights = [geoms.heightareas[i][2] for distance, i in heightarea_distances
if distance == min_distance]
height = max(matching_heights)
altitudes.append(altitude+height)
last_min_z = current_min_z
last_max_z = current_max_z # noqa
current_min_z = min(altitudes) # noqa
current_max_z = max(altitudes)
vertices = np.hstack((vertices, np.array(altitudes).reshape((vertices.shape[0], 1))))
self._add_mesh_plane('Level %s top mesh plane' % geoms.short_label, vertices / 1000, faces)
self._add_polygon('Level %s buildings' % geoms.short_label, geoms.buildings,
last_min_z-1, current_max_z+1)
self._cut_last_poly_with_mesh_planes(last_min_z-1, current_max_z+1)
for altitudearea in geoms.altitudeareas:
break
name = 'Level %s Altitudearea %s' % (geoms.short_label, altitudearea.altitude) name = 'Level %s Altitudearea %s' % (geoms.short_label, altitudearea.altitude)
if altitudearea.altitude2 is not None: if altitudearea.altitude2 is not None:
min_slope_altitude = min(altitudearea.altitude, altitudearea.altitude2) min_slope_altitude = min(altitudearea.altitude, altitudearea.altitude2)
max_slope_altitude = max(altitudearea.altitude, altitudearea.altitude2) max_slope_altitude = max(altitudearea.altitude, altitudearea.altitude2)
self._add_polygon(name, altitudearea.geometry.geom, min_slope_altitude, max_slope_altitude) self._add_polygon(name, altitudearea.geometry, min_slope_altitude, max_slope_altitude)
bounds = altitudearea.geometry.geom.bounds bounds = altitudearea.geometry.bounds
self._add_slope(bounds, altitudearea.altitude, altitudearea.altitude2, self._add_slope(bounds, altitudearea.altitude, altitudearea.altitude2,
altitudearea.point1, altitudearea.point2) altitudearea.point1, altitudearea.point2)
self._subtract_slope() self._subtract_slope()
self._collect_last_polygon_for_join() self._collect_last_polygon_for_join()
self._add_polygon(name, altitudearea.geometry.geom, min_altitude-700, min_slope_altitude) self._add_polygon(name, altitudearea.geometry, min_altitude-700, min_slope_altitude)
self._collect_last_polygon_for_join() self._collect_last_polygon_for_join()
self._join_polygons() self._join_polygons()
else: else:
self._add_polygon(name, altitudearea.geometry.geom, min_altitude-700, altitudearea.altitude) self._add_polygon(name, altitudearea.geometry, min_altitude-700, altitudearea.altitude)
break break
@ -216,6 +320,8 @@ class BlenderEngine(Base3DEngine):
'maxz': maxz/1000, 'maxz': maxz/1000,
} }
) )
self._collect_last_polygon_for_join()
self._join_polygons()
def _add_slope(self, bounds, altitude1, altitude2, point1, point2): def _add_slope(self, bounds, altitude1, altitude2, point1, point2):
altitude_diff = altitude2-altitude1 altitude_diff = altitude2-altitude1
@ -247,6 +353,20 @@ class BlenderEngine(Base3DEngine):
} }
) )
def _add_mesh_plane(self, name, vertices, faces):
self._add_python('last_mesh_plane = current_mesh_plane')
self._add_python(
'current_mesh_plane = add_mesh(name=%(name)r, vertices=%(vertices)r, faces=%(faces)r)' % {
'name': name,
'vertices': vertices.tolist(),
'faces': faces.tolist(),
}
)
def _cut_last_poly_with_mesh_planes(self, minz, maxz):
height = maxz-minz
self._add_python('cut_using_mesh_planes(last_polygon, last_mesh_plane, current_mesh_plane, %f)' % (height/1000))
def _subtract_slope(self): def _subtract_slope(self):
self._add_python('subtract_object(last_polygon, last_slope, delete_after=True)') self._add_python('subtract_object(last_polygon, last_slope, delete_after=True)')

View file

@ -24,6 +24,7 @@ class LevelGeometries:
Store geometries for a Level. Store geometries for a Level.
""" """
def __init__(self): def __init__(self):
self.buildings = None
self.altitudeareas = [] self.altitudeareas = []
self.heightareas = [] self.heightareas = []
self.walls = None self.walls = None
@ -60,6 +61,7 @@ class LevelGeometries:
def build_for_level(cls, level, altitudeareas_above): def build_for_level(cls, level, altitudeareas_above):
geoms = LevelGeometries() geoms = LevelGeometries()
buildings_geom = unary_union([b.geometry for b in level.buildings.all()]) buildings_geom = unary_union([b.geometry for b in level.buildings.all()])
geoms.buildings = buildings_geom
buildings_geom_prep = prepared.prep(buildings_geom) buildings_geom_prep = prepared.prep(buildings_geom)
# remove columns and holes from space areas # remove columns and holes from space areas
@ -460,6 +462,6 @@ class LevelGeometries:
# unset heightareas, they are no loinger needed # unset heightareas, they are no loinger needed
self.all_walls = None self.all_walls = None
self.ramps = None self.ramps = None
self.heightareas = None # self.heightareas = None
self.vertices = None self.vertices = None
self.faces = None self.faces = None

View file

@ -158,6 +158,7 @@ class LevelRenderData:
map_history.composite(MapHistory.open_level(sublevel.pk, 'base'), None) map_history.composite(MapHistory.open_level(sublevel.pk, 'base'), None)
new_geoms = LevelGeometries() new_geoms = LevelGeometries()
new_geoms.buildings = crop_to.intersection(old_geoms.buildings)
new_geoms.doors = crop_to.intersection(old_geoms.doors) new_geoms.doors = crop_to.intersection(old_geoms.doors)
new_geoms.walls = crop_to.intersection(old_geoms.walls) new_geoms.walls = crop_to.intersection(old_geoms.walls)
new_geoms.all_walls = crop_to.intersection(old_geoms.all_walls) new_geoms.all_walls = crop_to.intersection(old_geoms.all_walls)

View file

@ -39,7 +39,7 @@ class MapRenderer:
scale=self.scale, buffer=1, background='#DCDCDC', center=center) scale=self.scale, buffer=1, background='#DCDCDC', center=center)
if hasattr(engine, 'custom_render'): if hasattr(engine, 'custom_render'):
engine.custom_render(level_render_data, bbox, access_permissions) engine.custom_render(level_render_data, access_permissions)
return engine return engine
if self.full_levels: if self.full_levels:

View file

@ -82,3 +82,12 @@ def triangulate_polygon(geometry: Union[Polygon, MultiPolygon], keep_holes=False
offset += len(new_vertices) offset += len(new_vertices)
return np.vstack(vertices), np.vstack(faces) return np.vstack(vertices), np.vstack(faces)
def triangulate_gapless_mesh_from_polygons(geometries):
rings = []
for polygon in geometries:
polygon = polygon.buffer(0)
rings.append(polygon.exterior)
rings.extend(polygon.interiors)
return triangulate_rings(rings)