From ec633db858ff40b640e2e323cc2ac4be6a7ef9fa Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Sun, 14 Jan 2024 23:15:44 -0500 Subject: [PATCH 1/3] Improve vertex color exporting significantly. This moves the piece-by-piece assembly of vertex colors from `_export_geometry()` into a one-stop-shop for getting the near final vertex colors as Blender knows them. Included in this is separating out the adjustment channels for wavesets - which are stuffed inside of vertex colors. It is now an error for a waveset to have a "col", "color", or "colour" vertex color layer. This is to prevent confusion. Wavesets now accept alpha (red), specularity (green), fresnel (blue), and edgelength (alpha) vertex color layers. The color values in these layers is averaged and output to the respective channels. Further, a default value for edgelength is now computed similar (but not exactly like) PlasmaMax's `SetWaterColor()` function. Artist input to the edgelength vertex color layer will modulate Korman's calculation. --- korman/exporter/mesh.py | 157 ++++++++++++++++++++------- korman/properties/modifiers/water.py | 5 + 2 files changed, 125 insertions(+), 37 deletions(-) diff --git a/korman/exporter/mesh.py b/korman/exporter/mesh.py index 212386d5..0858f375 100644 --- a/korman/exporter/mesh.py +++ b/korman/exporter/mesh.py @@ -13,12 +13,18 @@ # You should have received a copy of the GNU General Public License # along with Korman. If not, see . +from __future__ import annotations + import bpy +import mathutils + +from PyHSPlasma import * + from contextlib import ExitStack +import functools import itertools -from PyHSPlasma import * -from math import fabs -from typing import Iterable +from math import fabs, sqrt +from typing import * import weakref from ..exporter.logger import ExportProgressLogger @@ -269,6 +275,56 @@ def _calc_num_uvchans(self, bo, mesh): return (num_user_texs, total_texs, max_user_texs) + def _calc_water_color(self, mesh: bpy.types.Mesh) -> Tuple[float]: + chain_iterable = itertools.chain.from_iterable + Vector = mathutils.Vector + vertices = mesh.vertices + num_vertices = len(vertices) + + lengths: List[float] = [0.0] * num_vertices + weights: List[float] = [0.0] * num_vertices + + # Calculate the length of each edge in the exported triangles. Remember that + # some tessfaces Blender hands us could be quads. + for tessface in mesh.tessfaces: + tessface_vertices = tessface.vertices + triangles = [[ + (tessface_vertices[0], tessface_vertices[1]), + (tessface_vertices[1], tessface_vertices[2]), + (tessface_vertices[2], tessface_vertices[0]), + ]] + if len(tessface.vertices) == 4: + triangles.append([ + (tessface_vertices[0], tessface_vertices[2]), + (tessface_vertices[2], tessface_vertices[3]), + (tessface_vertices[3], tessface_vertices[0]), + ]) + for edges in triangles: + edge_lengths_sq = ((Vector(vertices[i].co) - Vector(vertices[j].co)).length_squared for i, j in edges) + largest_edge = sqrt(max(edge_lengths_sq)) + for i in set(chain_iterable(edges)): + lengths[i] += largest_edge + weights[i] += 1.0 + + # Average everything out + for i in range(num_vertices): + if weights[i] > 0.0: + lengths[i] /= weights[i] + weights[i] = 0.0 + + ## TODO + # The max plugin's SetWaterColor() function runs through a smoothing pass + # that basically runs through each edge and multiplies the result by + # a constant and accumulates that same constant in the weights array, + # then averages everything out again. We could do that, certainly, but + # I don't see the point right now. Just having the edge length data being + # something other than 1.0 seems like a good enough win for now. + + # Return 1.0 / (kNumLens * length) + kNumLens = 4.0 + return tuple([1.0 / (i * kNumLens) if i > 0.0 else 1.0 for i in lengths]) + + def _check_vtx_alpha(self, mesh, material_idx): if material_idx is not None: polygons = (i for i in mesh.polygons if i.material_index == material_idx) @@ -297,6 +353,9 @@ def check(): return True if mods.lightmap.bake_lightmap: return True + + # NOTE: Wavesets will fire off at the RT Lights check, + # so there is no problem with a waveset mesh's fake alpha layer. if self._check_vtx_alpha(mesh, material_idx): return True @@ -383,9 +442,7 @@ def _export_geometry(self, bo, mesh, materials, geospans, mat2span_LUT): bumpmap = self.material.get_bump_layer(bo) # Locate relevant vertex color layers now... - lm = bo.plasma_modifiers.lightmap - color = None if lm.bake_lightmap else self._find_vtx_color_layer(mesh.tessface_vertex_colors) - alpha = self._find_vtx_alpha_layer(mesh.tessface_vertex_colors) + vertex_colors = self._get_vertex_colors(bo, mesh) # Convert Blender faces into things we can stuff into libHSPlasma for i, tessface in enumerate(mesh.tessfaces): @@ -402,24 +459,6 @@ def _export_geometry(self, bo, mesh, materials, geospans, mat2span_LUT): # NOTE: Blender has no third (W) coordinate tessface_uvws = [uvtex.data[i].uv for uvtex in mesh.tessface_uv_textures] - # Unpack colors - if color is None: - tessface_colors = ((1.0, 1.0, 1.0), (1.0, 1.0, 1.0), (1.0, 1.0, 1.0), (1.0, 1.0, 1.0)) - else: - src = color[i] - tessface_colors = (src.color1, src.color2, src.color3, src.color4) - - # Unpack alpha values - if alpha is None: - tessface_alphas = (1.0, 1.0, 1.0, 1.0) - else: - src = alpha[i] - # Some time between 2.79b and 2.80, vertex alpha colors appeared in Blender. However, - # there is no way to actually visually edit them. That means that we need to keep that - # fact in mind because we're just averaging the color to make alpha. - tessface_alphas = ((sum(src.color1[:3]) / 3), (sum(src.color2[:3]) / 3), - (sum(src.color3[:3]) / 3), (sum(src.color4[:3]) / 3)) - if bumpmap is not None: gradPass = [] gradUVWs = [] @@ -453,11 +492,11 @@ def _export_geometry(self, bo, mesh, materials, geospans, mat2span_LUT): mult_color = geospans[mat2span_LUT[tessface.material_index]].mult_color else: mult_color = (1.0, 1.0, 1.0, 1.0) - tessface_color, tessface_alpha = tessface_colors[j], tessface_alphas[j] - vertex_color = (int(tessface_color[0] * mult_color[0] * 255), - int(tessface_color[1] * mult_color[1] * 255), - int(tessface_color[2] * mult_color[2] * 255), - int(tessface_alpha * mult_color[0] * 255)) + src_vertex_color = vertex_colors[j] + vertex_color = (int(src_vertex_color[0] * mult_color[0] * 255), + int(src_vertex_color[1] * mult_color[1] * 255), + int(src_vertex_color[2] * mult_color[2] * 255), + int(src_vertex_color[3] * mult_color[3] * 255)) # Now, we'll index into the vertex dict using the per-face elements :( # We're using tuples because lists are not hashable. The many mathutils and PyHSPlasma @@ -711,20 +750,64 @@ def _find_create_dspan(self, bo, geospan, pass_index): else: return self._dspans[location][crit] - def _find_vtx_alpha_layer(self, color_collection): - alpha_layer = next((i for i in color_collection if i.name.lower() == "alpha"), None) - if alpha_layer is not None: - return alpha_layer.data + def _find_named_vtx_color_layer(self, color_collection, name: str): + color_layer = next((i for i in color_collection if i.name.lower() == name), None) + if color_layer is not None: + return color_layer.data return None + _find_vtx_alpha_layer = functools.partialmethod(_find_named_vtx_color_layer, name="alpha") + if TYPE_CHECKING: + def _find_vtx_alpha_layer(self, color_collection): + ... + def _find_vtx_color_layer(self, color_collection): manual_layer = next((i for i in color_collection if i.name.lower() in _VERTEX_COLOR_LAYERS), None) if manual_layer is not None: return manual_layer.data - baked_layer = color_collection.get("autocolor") - if baked_layer is not None: - return baked_layer.data - return None + return self._find_named_vtx_color_layer(color_collection, "autocolor") + + def _get_vertex_colors(self, bo: bpy.types.Object, mesh: bpy.types.Mesh) -> Tuple[Tuple[float, float, float, float]]: + num_vertices = len(mesh.vertices) + vertex_colors = [None] * num_vertices + if bo.plasma_modifiers.water_basic.enabled: + # Wavesets have a special meaning for vertex colors. So, we're going to + # separate each color channel out into separate layers for clarity. + # Here's how it looks: + # R = opacity/alpha + # G = specularity + # B = fresnel + # A = edge length + opacity_layer = self._find_named_vtx_color_layer(mesh.vertex_colors, "alpha") + specular_layer = self._find_named_vtx_color_layer(mesh.vertex_colors, "specularity") + fresnel_layer = self._find_named_vtx_color_layer(mesh.vertex_colors, "fresnel") + edge_layer = self._find_named_vtx_color_layer(mesh.vertex_colors, "edgelength") + water_color = self._calc_water_color(mesh) + for i in range(num_vertices): + r_channel = opacity_layer[i].color if opacity_layer is not None else (1.0, 1.0, 1.0) + b_channel = specular_layer[i].color if specular_layer is not None else (1.0, 1.0, 1.0) + g_channel = fresnel_layer[i].color if fresnel_layer is not None else (1.0, 1.0, 1.0) + a_channel = edge_layer[i].color if edge_layer is not None else (1.0, 1.0, 1.0) + vertex_colors[i] = ( + (r_channel[0] + r_channel[1] + r_channel[2]) / 3, + (b_channel[0] + b_channel[1] + b_channel[2]) / 3, + (g_channel[0] + g_channel[1] + g_channel[2]) / 3, + # Modulate our edge length calculation with what the artist thinks. + water_color[i] * (a_channel[0] + a_channel[1] + a_channel[2]) / 3, + ) + else: + color_layer = self._find_vtx_color_layer(mesh.vertex_colors) + alpha_layer = self._find_vtx_alpha_layer(mesh.vertex_colors) + for i in range(num_vertices): + color_channels = color_layer[i].color if color_layer is not None else (1.0, 1.0, 1.0) + a_channel = alpha_layer[i].color if alpha_layer is not None else (1.0, 1.0, 1.0) + vertex_colors[i] = ( + color_channels[0], + color_channels[1], + color_channels[2], + (a_channel[0] + a_channel[1] + a_channel[2]) / 3, + ) + return tuple(vertex_colors) def is_nonpreshaded(self, bo: bpy.types.Object, bm: bpy.types.Material) -> bool: return self._non_preshaded[(bo, bm)] diff --git a/korman/properties/modifiers/water.py b/korman/properties/modifiers/water.py index 9b395877..a99cb164 100644 --- a/korman/properties/modifiers/water.py +++ b/korman/properties/modifiers/water.py @@ -242,6 +242,11 @@ class PlasmaWaterModifier(idprops.IDPropMixin, PlasmaModifierProperties, bpy.typ def copy_material(self): return True + def sanity_check(self): + vertex_color_layers = frozenset((i.name.lower() for i in self.id_data.data.vertex_colors)) + if {"col", "color", "colour"} in vertex_color_layers: + raise ExportError(f"[{self.id_data.name}] Water modifiers cannot use vertex color lighting") + def export(self, exporter, bo, so): waveset = exporter.mgr.find_create_object(plWaveSet7, name=bo.name, so=so) if self.wind_object: From 24150bcb464a13d14a668afbe69953fe47c17da1 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Mon, 15 Jan 2024 13:11:53 -0500 Subject: [PATCH 2/3] Remove the per-face list comprehension. This means that we gather up the UVs for every vertex in a temporary tuple of tuples at the beginning of the export process. That's more memory intensive, but it removes quite a bit of fiddling in the tighter inner loops, so it's an overall win. --- korman/exporter/mesh.py | 48 ++++++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/korman/exporter/mesh.py b/korman/exporter/mesh.py index 0858f375..30ef75a5 100644 --- a/korman/exporter/mesh.py +++ b/korman/exporter/mesh.py @@ -441,8 +441,9 @@ def _export_geometry(self, bo, mesh, materials, geospans, mat2span_LUT): geodata = { idx: _GeoData(len(mesh.vertices)) for idx, _ in materials } bumpmap = self.material.get_bump_layer(bo) - # Locate relevant vertex color layers now... + # Locate relevant vertex color and UV layers now... vertex_colors = self._get_vertex_colors(bo, mesh) + uvws = self._get_uvs(mesh) # Convert Blender faces into things we can stuff into libHSPlasma for i, tessface in enumerate(mesh.tessfaces): @@ -452,31 +453,22 @@ def _export_geometry(self, bo, mesh, materials, geospans, mat2span_LUT): face_verts = [] use_smooth = tessface.use_smooth + vertices = tessface.vertices dPosDu = hsVector3(0.0, 0.0, 0.0) dPosDv = hsVector3(0.0, 0.0, 0.0) - # Unpack the UV coordinates from each UV Texture layer - # NOTE: Blender has no third (W) coordinate - tessface_uvws = [uvtex.data[i].uv for uvtex in mesh.tessface_uv_textures] - if bumpmap is not None: gradPass = [] gradUVWs = [] - if len(tessface.vertices) != 3: - gradPass.append([tessface.vertices[0], tessface.vertices[1], tessface.vertices[2]]) - gradPass.append([tessface.vertices[0], tessface.vertices[2], tessface.vertices[3]]) - gradUVWs.append((tuple((uvw[0] for uvw in tessface_uvws)), - tuple((uvw[1] for uvw in tessface_uvws)), - tuple((uvw[2] for uvw in tessface_uvws)))) - gradUVWs.append((tuple((uvw[0] for uvw in tessface_uvws)), - tuple((uvw[2] for uvw in tessface_uvws)), - tuple((uvw[3] for uvw in tessface_uvws)))) + if len(vertices) != 3: + gradPass.append([vertices[0], vertices[1], vertices[2]]) + gradPass.append([vertices[0], vertices[2], vertices[3]]) + gradUVWs.append((uvws[vertices[0]], uvws[vertices[1]], uvws[vertices[2]])) + gradUVWs.append((uvws[vertices[0]], uvws[vertices[2]], uvws[vertices[3]])) else: - gradPass.append(tessface.vertices) - gradUVWs.append((tuple((uvw[0] for uvw in tessface_uvws)), - tuple((uvw[1] for uvw in tessface_uvws)), - tuple((uvw[2] for uvw in tessface_uvws)))) + gradPass.append(vertices) + gradUVWs.append((uvws[vertices[0]], uvws[vertices[1]], uvws[vertices[2]])) for p, vids in enumerate(gradPass): dPosDu += self._get_bump_gradient(bumpmap[1], gradUVWs[p], mesh, vids, bumpmap[0], 0) @@ -484,8 +476,8 @@ def _export_geometry(self, bo, mesh, materials, geospans, mat2span_LUT): dPosDv = -dPosDv # Convert to per-material indices - for j, vertex in enumerate(tessface.vertices): - uvws = tuple([tuple(uvw[j]) for uvw in tessface_uvws]) + for j, vertex in enumerate(vertices): + vertex_uvs = uvws[vertex] # Calculate vertex colors. if mat2span_LUT: @@ -501,7 +493,7 @@ def _export_geometry(self, bo, mesh, materials, geospans, mat2span_LUT): # Now, we'll index into the vertex dict using the per-face elements :( # We're using tuples because lists are not hashable. The many mathutils and PyHSPlasma # types are not either, and it's entirely too much work to fool with all that. - coluv = (vertex_color, uvws) + coluv = (vertex_color, vertex_uvs) if coluv not in data.blender2gs[vertex]: source = mesh.vertices[vertex] geoVertex = plGeometrySpan.TempVertex() @@ -518,7 +510,11 @@ def _export_geometry(self, bo, mesh, materials, geospans, mat2span_LUT): geoVertex.normal = normal geoVertex.color = hsColor32(*vertex_color) - uvs = [hsVector3(uv[0], 1.0 - uv[1], 0.0) for uv in uvws] + + # Profiling indicates that unrolling this list comprehension is basically + # a no-op for performance. Probably because we're still looping but have + # to use a generator (ie `range`). + uvs = [hsVector3(uv[0], 1.0 - uv[1], 0.0) for uv in vertex_uvs] if bumpmap is not None: uvs.append(dPosDu) uvs.append(dPosDv) @@ -767,6 +763,14 @@ def _find_vtx_color_layer(self, color_collection): return manual_layer.data return self._find_named_vtx_color_layer(color_collection, "autocolor") + def _get_uvs(self, mesh: bpy.types.Mesh) -> Tuple[Sequence[mathutils.Vector]]: + num_vertices = len(mesh.vertices) + num_uv_textures = len(mesh.uv_textures) + uvws = [[None] * num_uv_textures] * num_vertices + for vertex_idx, (uv_idx, uv_layer) in itertools.product(range(num_vertices), enumerate(mesh.uv_layers)): + uvws[vertex_idx][uv_idx] = tuple(uv_layer.data[vertex_idx].uv) + return tuple((tuple(i) for i in uvws)) + def _get_vertex_colors(self, bo: bpy.types.Object, mesh: bpy.types.Mesh) -> Tuple[Tuple[float, float, float, float]]: num_vertices = len(mesh.vertices) vertex_colors = [None] * num_vertices From ccf7340123e0538ef4bab0694a0ed15ce9ed6dbc Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Mon, 15 Jan 2024 13:13:57 -0500 Subject: [PATCH 3/3] Unroll the DX9 normal correction. timeit indicates that this is a 43% improvement in performance. --- korman/exporter/mesh.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/korman/exporter/mesh.py b/korman/exporter/mesh.py index 30ef75a5..b5addcf2 100644 --- a/korman/exporter/mesh.py +++ b/korman/exporter/mesh.py @@ -504,8 +504,13 @@ def _export_geometry(self, bo, mesh, materials, geospans, mat2span_LUT): normal = source.normal if use_smooth else tessface.normal # MOUL/DX9 craps its pants if any element of the normal is exactly 0.0 - normal = map(lambda x: max(x, 0.01) if x >= 0.0 else min(x, -0.01), normal) - normal = hsVector3(*normal) + # Profiling indicates that unrolling this would give a performance penalty + # of roughly. Note: unrolling this calculation gives a 43% speedup. + normal = hsVector3( + max(normal[0], 0.01) if normal[0] >= 0.0 else min(normal[0], -0.01), + max(normal[1], 0.01) if normal[1] >= 0.0 else min(normal[1], -0.01), + max(normal[2], 0.01) if normal[2] >= 0.0 else min(normal[2], -0.01), + ) normal.normalize() geoVertex.normal = normal