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.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)
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.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))

View file

@ -1,5 +1,8 @@
import re
from itertools import chain
import numpy as np
from shapely import prepared
from shapely.affinity import scale
from shapely.geometry import LineString, Point
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.utils import get_full_levels, get_min_altitude
from c3nav.mapdata.utils.geometry import assert_multipolygon
from c3nav.mapdata.utils.mesh import triangulate_gapless_mesh_from_polygons
@register_engine
@ -35,6 +39,13 @@ class BlenderEngine(Base3DEngine):
bmesh.ops.triangulate(bm, faces=bm.faces[:], quad_method=0, ngon_method=0)
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):
select_object(obj)
bpy.ops.object.mode_set(mode='EDIT')
@ -46,7 +57,7 @@ class BlenderEngine(Base3DEngine):
)
triangulate_object(obj)
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')
def subtract_object(obj, other_obj, delete_after=False):
@ -152,7 +163,37 @@ class BlenderEngine(Base3DEngine):
extrude_object(obj, extrude)
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 = []
current_mesh_plane = None
''')
def _clean_python(self, code):
@ -168,10 +209,16 @@ class BlenderEngine(Base3DEngine):
def _add_python(self, code):
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)
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:
# hide indoor and outdoor rooms if their access restriction was not unlocked
restricted_spaces_indoors = unary_union(
@ -184,22 +231,79 @@ class BlenderEngine(Base3DEngine):
)
restricted_spaces = unary_union((restricted_spaces_indoors, restricted_spaces_outdoors)) # noqa
# crop 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)
if altitudearea.altitude2 is not None:
min_slope_altitude = min(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)
bounds = altitudearea.geometry.geom.bounds
self._add_polygon(name, altitudearea.geometry, min_slope_altitude, max_slope_altitude)
bounds = altitudearea.geometry.bounds
self._add_slope(bounds, altitudearea.altitude, altitudearea.altitude2,
altitudearea.point1, altitudearea.point2)
self._subtract_slope()
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._join_polygons()
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
@ -216,6 +320,8 @@ class BlenderEngine(Base3DEngine):
'maxz': maxz/1000,
}
)
self._collect_last_polygon_for_join()
self._join_polygons()
def _add_slope(self, bounds, altitude1, altitude2, point1, point2):
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):
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.
"""
def __init__(self):
self.buildings = None
self.altitudeareas = []
self.heightareas = []
self.walls = None
@ -60,6 +61,7 @@ class LevelGeometries:
def build_for_level(cls, level, altitudeareas_above):
geoms = LevelGeometries()
buildings_geom = unary_union([b.geometry for b in level.buildings.all()])
geoms.buildings = buildings_geom
buildings_geom_prep = prepared.prep(buildings_geom)
# remove columns and holes from space areas
@ -460,6 +462,6 @@ class LevelGeometries:
# unset heightareas, they are no loinger needed
self.all_walls = None
self.ramps = None
self.heightareas = None
# self.heightareas = None
self.vertices = None
self.faces = None

View file

@ -158,6 +158,7 @@ class LevelRenderData:
map_history.composite(MapHistory.open_level(sublevel.pk, 'base'), None)
new_geoms = LevelGeometries()
new_geoms.buildings = crop_to.intersection(old_geoms.buildings)
new_geoms.doors = crop_to.intersection(old_geoms.doors)
new_geoms.walls = crop_to.intersection(old_geoms.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)
if hasattr(engine, 'custom_render'):
engine.custom_render(level_render_data, bbox, access_permissions)
engine.custom_render(level_render_data, access_permissions)
return engine
if self.full_levels:

View file

@ -82,3 +82,12 @@ def triangulate_polygon(geometry: Union[Polygon, MultiPolygon], keep_holes=False
offset += len(new_vertices)
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)