From fc77f67ecc7a786ab48bc0223c16c7fcfe414e30 Mon Sep 17 00:00:00 2001 From: Jrius <2261279+Jrius@users.noreply.github.com> Date: Tue, 3 Aug 2021 13:45:20 +0200 Subject: [PATCH 1/3] Vertex color smoothing, preserving sharp edges --- korman/exporter/etlight.py | 80 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 76 insertions(+), 4 deletions(-) diff --git a/korman/exporter/etlight.py b/korman/exporter/etlight.py index fd6072c..1654b41 100644 --- a/korman/exporter/etlight.py +++ b/korman/exporter/etlight.py @@ -51,6 +51,7 @@ class LightBaker: self.force = False self._lightmap_images = {} self._uvtexs = {} + self._active_vcols = {} def __del__(self): if self._own_report: @@ -120,6 +121,7 @@ class LightBaker: # this stuff has been observed to be problematic with GoodNeighbor self._pop_lightgroups() self._restore_uvtexs() + self._restore_vcols() if not self.retain_lightmap_uvtex: self._remove_stale_uvtexes(bake) return result @@ -179,6 +181,7 @@ class LightBaker: elif key[0] == "vcol": self._report.msg("{} Vertex Color(s) [H:{:X}]", len(value), hash(key[1:]), indent=1) self._bake_vcols(value, key[1:]) + self._fix_vertex_colors(value) else: raise RuntimeError(key[0]) inc_progress() @@ -186,6 +189,67 @@ class LightBaker: # Return how many thingos we baked return sum(map(len, bake.values())) + def _fix_vertex_colors(self, blender_objects): + # Blender's lightmapper has a bug which allows vertices to "self-occlude" when shared between + # two faces. See here https://forum.guildofwriters.org/viewtopic.php?f=9&t=6576&p=68939 + # What we're doing here is an improved version of the algorithm in the previous link. + # For each loop, we find all other loops in the mesh sharing the same vertex, which aren't + # separated by a sharp edge. We then take the brightest color out of all those loops, and + # assign it back to the base loop. + # "Sharp edges" include edges manually tagged as sharp by the user, or part of a non-smooth + # face, or edges for which the face angle is superior to the mesh's auto-smooth threshold. + # (If the object has an edge split modifier, well, screw you!) + for bo in blender_objects: + mesh = bo.data + bm = bmesh.new() + bm.from_mesh(mesh) + + light_vcol = bm.loops.layers.color.get(self.vcol_layer_name) + + # If no vertex color is found, then baking either failed (error raised by oven) + # or is turned off. Either way, bail out. + if light_vcol is None: + bm.free() + del bm + continue + + bm.faces.ensure_lookup_table() + + for face in bm.faces: + for loop in face.loops: + vert = loop.vert + max_color = loop[light_vcol] + if not face.smooth: + # Face is sharp, so we can't smooth anything. + continue + # Now that we have a loop and its vertex, find all edges the vertex connects to. + for edge in vert.link_edges: + if len(edge.link_faces) != 2: + # Either a border edge, or an abomination. + continue + if not edge.smooth or (mesh.use_auto_smooth and + edge.calc_face_angle() > mesh.auto_smooth_angle): + # Sharp edge. Don't care. + continue + if face in edge.link_faces: + # Alright, this edge is connected to our loop AND our face. + # Now for the Fun Stuff(c)... First, actually get ahold of the other + # face (the one we're connected to via this edge). + other_face = next(f for f in edge.link_faces if f != face) + # Now get ahold of the loop sharing our vertex on the OTHER SIDE + # of that damnable edge... + other_loop = next(loop for loop in other_face.loops if loop.vert == vert) + other_color = other_loop[light_vcol] + # Phew ! Good, now just pick whichever color has the highest average value + if sum(max_color) / 3 < sum(other_color) / 3: + max_color = other_color + # Assign our hard-earned color back + loop[light_vcol] = max_color + + bm.to_mesh(mesh) + bm.free() + del bm + def _generate_lightgroup(self, bo, user_lg=None): """Makes a new light group for the baking process that excludes all Plasma RT lamps""" shouldibake = (user_lg is not None and bool(user_lg.objects)) @@ -448,13 +512,16 @@ class LightBaker: needs_vcol_layer = autocolor is None if needs_vcol_layer: autocolor = vcols.new(vcol_layer_name) - toggle.track(vcols, "active", autocolor) + self._active_vcols[mesh] = ( + next(i for i, vc in enumerate(mesh.vertex_colors) if vc.active), + next(i for i, vc in enumerate(mesh.vertex_colors) if vc.active_render), + ) # Mark "autocolor" as our active render layer for vcol_layer in mesh.vertex_colors: autocol = vcol_layer.name == vcol_layer_name - toggle.track(vcol_layer, "active_render", autocol) - toggle.track(vcol_layer, "active", autocol) + vcol_layer.active_render = autocol + vcol_layer.active = autocol mesh.update() # Vertex colors are sort of ephemeral, so if we have an exit stack, we want to @@ -463,7 +530,7 @@ class LightBaker: # future exports as an optimization. We won't reach this point if there is already an # autocolor layer (gulp). if not self.force and needs_vcol_layer: - self._mesh.context_stack.enter_context(TemporaryObject(vcol_layer, vcols.remove)) + self._mesh.context_stack.enter_context(TemporaryObject(vcol_layer.name, lambda layer_name: vcols.remove(vcols[layer_name]))) # Indicate we should bake return True @@ -483,6 +550,11 @@ class LightBaker: i.active = uvtex_name == i.name mesh.uv_textures.active = mesh.uv_textures[uvtex_name] + def _restore_vcols(self): + for mesh, (vcol_index, vcol_render_index) in self._active_vcols.items(): + mesh.vertex_colors[vcol_index].active = True + mesh.vertex_colors[vcol_render_index].active_render = True + def _select_only(self, objs, toggle): if isinstance(objs, bpy.types.Object): toggle.track(objs, "hide_render", False) From 8c6e49ea50ae44842a39b8448c8ad2150cedf051 Mon Sep 17 00:00:00 2001 From: Jrius <2261279+Jrius@users.noreply.github.com> Date: Fri, 6 Aug 2021 15:22:16 +0200 Subject: [PATCH 2/3] Smarter handling of BMesh. Sharp edges are ignored only if auto-smooth is on, just like Blender does. --- korman/exporter/etlight.py | 81 +++++++++++++++++--------------------- 1 file changed, 37 insertions(+), 44 deletions(-) diff --git a/korman/exporter/etlight.py b/korman/exporter/etlight.py index 1654b41..04f7ff0 100644 --- a/korman/exporter/etlight.py +++ b/korman/exporter/etlight.py @@ -201,54 +201,47 @@ class LightBaker: # (If the object has an edge split modifier, well, screw you!) for bo in blender_objects: mesh = bo.data - bm = bmesh.new() - bm.from_mesh(mesh) - - light_vcol = bm.loops.layers.color.get(self.vcol_layer_name) - - # If no vertex color is found, then baking either failed (error raised by oven) - # or is turned off. Either way, bail out. - if light_vcol is None: - bm.free() - del bm + if self.vcol_layer_name not in mesh.vertex_colors: + # No vertex color. Baking either failed or is turned off. continue - bm.faces.ensure_lookup_table() + with bmesh_from_object(bo) as bm: + bm.faces.ensure_lookup_table() + light_vcol = bm.loops.layers.color.get(self.vcol_layer_name) - for face in bm.faces: - for loop in face.loops: - vert = loop.vert - max_color = loop[light_vcol] - if not face.smooth: - # Face is sharp, so we can't smooth anything. - continue - # Now that we have a loop and its vertex, find all edges the vertex connects to. - for edge in vert.link_edges: - if len(edge.link_faces) != 2: - # Either a border edge, or an abomination. - continue - if not edge.smooth or (mesh.use_auto_smooth and - edge.calc_face_angle() > mesh.auto_smooth_angle): - # Sharp edge. Don't care. + for face in bm.faces: + for loop in face.loops: + vert = loop.vert + max_color = loop[light_vcol] + if not face.smooth: + # Face is sharp, so we can't smooth anything. continue - if face in edge.link_faces: - # Alright, this edge is connected to our loop AND our face. - # Now for the Fun Stuff(c)... First, actually get ahold of the other - # face (the one we're connected to via this edge). - other_face = next(f for f in edge.link_faces if f != face) - # Now get ahold of the loop sharing our vertex on the OTHER SIDE - # of that damnable edge... - other_loop = next(loop for loop in other_face.loops if loop.vert == vert) - other_color = other_loop[light_vcol] - # Phew ! Good, now just pick whichever color has the highest average value - if sum(max_color) / 3 < sum(other_color) / 3: - max_color = other_color - # Assign our hard-earned color back - loop[light_vcol] = max_color - - bm.to_mesh(mesh) - bm.free() - del bm + # Now that we have a loop and its vertex, find all edges the vertex connects to. + for edge in vert.link_edges: + if len(edge.link_faces) != 2: + # Either a border edge, or an abomination. + continue + if mesh.use_auto_smooth and (not edge.smooth + or edge.calc_face_angle() > mesh.auto_smooth_angle): + # Normals are split for edges marked as sharp by the user, and edges + # whose angle is above the theshold. Auto smooth must be on in both cases. + continue + if face in edge.link_faces: + # Alright, this edge is connected to our loop AND our face. + # Now for the Fun Stuff(c)... First, actually get ahold of the other + # face (the one we're connected to via this edge). + other_face = next(f for f in edge.link_faces if f != face) + # Now get ahold of the loop sharing our vertex on the OTHER SIDE + # of that damnable edge... + other_loop = next(loop for loop in other_face.loops if loop.vert == vert) + other_color = other_loop[light_vcol] + # Phew ! Good, now just pick whichever color has the highest average value + if sum(max_color) / 3 < sum(other_color) / 3: + max_color = other_color + # Assign our hard-earned color back + loop[light_vcol] = max_color + + bm.to_mesh(mesh) def _generate_lightgroup(self, bo, user_lg=None): """Makes a new light group for the baking process that excludes all Plasma RT lamps""" From a193e6064405993577bd09ba15caa087e6cd7da2 Mon Sep 17 00:00:00 2001 From: Jrius <2261279+Jrius@users.noreply.github.com> Date: Fri, 6 Aug 2021 23:28:45 +0200 Subject: [PATCH 3/3] Avoid BMesh Blender crash --- korman/exporter/etlight.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/korman/exporter/etlight.py b/korman/exporter/etlight.py index 04f7ff0..8b8a67d 100644 --- a/korman/exporter/etlight.py +++ b/korman/exporter/etlight.py @@ -189,6 +189,17 @@ class LightBaker: # Return how many thingos we baked return sum(map(len, bake.values())) + @contextmanager + def _bmesh_from_mesh(self, mesh): + bm = bmesh.new() + try: + # from_object would likely cause Blender to crash further in the export process, + # so use the safer from_mesh instead. + bm.from_mesh(mesh) + yield bm + finally: + bm.free() + def _fix_vertex_colors(self, blender_objects): # Blender's lightmapper has a bug which allows vertices to "self-occlude" when shared between # two faces. See here https://forum.guildofwriters.org/viewtopic.php?f=9&t=6576&p=68939 @@ -205,7 +216,7 @@ class LightBaker: # No vertex color. Baking either failed or is turned off. continue - with bmesh_from_object(bo) as bm: + with self._bmesh_from_mesh(mesh) as bm: bm.faces.ensure_lookup_table() light_vcol = bm.loops.layers.color.get(self.vcol_layer_name)