Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve vertex color exporting significantly. #396

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
214 changes: 153 additions & 61 deletions korman/exporter/mesh.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,18 @@
# You should have received a copy of the GNU General Public License
# along with Korman. If not, see <http://www.gnu.org/licenses/>.

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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -382,10 +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...
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)
# 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):
Expand All @@ -395,74 +453,47 @@ 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]

# 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 = []

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)
dPosDv += self._get_bump_gradient(bumpmap[1], gradUVWs[p], mesh, vids, bumpmap[0], 1)
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:
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
# 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()
Expand All @@ -473,13 +504,22 @@ 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

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)
Expand Down Expand Up @@ -711,20 +751,72 @@ 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_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
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)]
Expand Down
5 changes: 5 additions & 0 deletions korman/properties/modifiers/water.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down