]> git.tdb.fi Git - libs/gl.git/commitdiff
Support exporting splat materials from Blender
authorMikko Rasa <tdb@tdb.fi>
Tue, 4 Oct 2022 22:13:24 +0000 (01:13 +0300)
committerMikko Rasa <tdb@tdb.fi>
Wed, 5 Oct 2022 22:32:39 +0000 (01:32 +0300)
blender/io_mspgl/export_material.py
blender/io_mspgl/material.py
blender/io_mspgl/mesh.py

index a0c0432e993e2b37f1077036bfbc8dbbdc3613d5..fc144b7ab86d163d71c688207dd242219c4d3fdf 100644 (file)
@@ -1,3 +1,5 @@
+import itertools
+
 class MaterialExporter:
        def export_technique_resources(self, ctx, material, resources):
                from .export_texture import SamplerExporter, TextureExporter
@@ -8,7 +10,7 @@ class MaterialExporter:
                if type(material)!=Material:
                        material = Material(material)
 
-               textured_props = [p for p in material.properties if p.texture]
+               textured_props = [p for p in itertools.chain(material.properties, *(s.properties for s in material.sub_materials)) if p.texture]
 
                ctx.set_slices(len(textured_props)+1)
                for p in textured_props:
@@ -166,7 +168,7 @@ class MaterialExporter:
                from .datafile import Resource, Statement, Token
                mat_res = Resource(material.name+".mat", "material")
 
-               if material.type!="pbr" and material.type!="unlit":
+               if material.type!="pbr" and material.type!="unlit" and material.type!="splat":
                        raise Exception("Can't export material {} of unknown type {}".format(material.name, material.type))
 
                mat_res.statements.append(Statement("type", Token(material.type)));
@@ -174,7 +176,19 @@ class MaterialExporter:
                        st = self.create_property_statement(mat_res, p, resources)
                        if st:
                                mat_res.statements.append(st)
-               textures = [p.texture for p in material.properties if p.texture]
+
+               if material.sub_materials:
+                       for k, s in material.array_storage.items():
+                               mat_res.statements.append(Statement(k.replace("_map", "_storage"), Token(s[0]), s[1], s[2]))
+                       for s in material.sub_materials:
+                               st = Statement("sub")
+                               for p in s.properties:
+                                       ss = self.create_property_statement(mat_res, p, resources, raw_texture=True)
+                                       if ss:
+                                               st.sub.append(ss)
+                               mat_res.statements.append(st)
+
+               textures = [p.texture for p in itertools.chain(material.properties, *(s.properties for s in material.sub_materials)) if p.texture]
                if textures:
                        from .export_texture import SamplerExporter
                        sampler_export = SamplerExporter()
@@ -184,12 +198,18 @@ class MaterialExporter:
 
                return mat_res
 
-       def create_property_statement(self, mat_res, prop, resources):
+       def create_property_statement(self, mat_res, prop, resources, *, raw_texture=False):
                from .datafile import Statement
                if prop.texture:
                        from .export_texture import TextureExporter
                        texture_export = TextureExporter()
                        tex_res = resources[texture_export.get_texture_name(prop.texture, prop.tex_channels)]
+                       if raw_texture:
+                               for s in tex_res.statements:
+                                       if s.keyword.startswith("external_data"):
+                                               return mat_res.create_reference_statement(prop.tex_keyword, s.args[0])
+                                       elif s.keyword.startswith("external_image"):
+                                               return Statement(prop.tex_keyword, s.args[0])
                        return mat_res.create_reference_statement(prop.tex_keyword, tex_res)
                elif not prop.keyword:
                        return
index 3e2bce001943d4c26090129dcd9650153abae9a1..b1d2756ebf7eb264ebc5e0412a307401b6d019f7 100644 (file)
@@ -156,6 +156,33 @@ def get_unlit_inputs(node_tree, node, additive):
                return (color_input, None)
        return (None, None)
 
+def get_splat_layers(node_tree, node):
+       from .util import get_linked_node_and_socket
+
+       if node.type!='MIX_SHADER':
+               return
+
+       layers = []
+       while True:
+               factor_from, factor_sock = get_linked_node_and_socket(node_tree, node.inputs["Fac"])
+               if factor_from.type!='SEPRGB':
+                       return
+
+               factor_from, _ = get_linked_node_and_socket(node_tree, factor_from.inputs["Image"])
+               if factor_from.type!='VERTEX_COLOR':
+                       return
+
+               shader1, _ = get_linked_node_and_socket(node_tree, node.inputs[1])
+               shader2, _ = get_linked_node_and_socket(node_tree, node.inputs[2])
+               layers.append((shader2, factor_from.layer_name, factor_sock.name[0]))
+               if shader1.type=='MIX_SHADER':
+                       node = shader1
+               else:
+                       layers.append((shader1, None, None))
+                       break
+
+       return layers
+
 class MaterialProperty:
        def __init__(self, keyword, tex_keyword, value):
                self.keyword = keyword
@@ -224,11 +251,18 @@ class MaterialProperty:
                                else:
                                        raise Exception("Unsupported property input node type "+from_node.type)
 
+class SubMaterial:
+       def __init__(self):
+               self.properties = []
+               self.weight_source = (None, None)
+
 class Material:
        def __init__(self, material):
                self.name = material.name
                self.type = None
                self.properties = []
+               self.sub_materials = []
+               self.array_storage = {}
 
                self.render_mode = material.render_mode
                self.technique = material.technique
@@ -266,33 +300,45 @@ class Material:
 
                if from_node.type=='BSDF_PRINCIPLED':
                        self.type = "pbr"
-
-                       base_color = self.create_property("base_color", (0.8, 0.8, 0.8, 1.0))
-                       tint = self.create_property("tint", (1.0, 1.0, 1.0, 1.0))
-                       metalness = self.create_property("metalness", 0.0)
-                       roughness = self.create_property("roughness", 0.5)
-                       normal = self.create_property("normal_map")
-                       emission = self.create_property("emission", (0.0, 0.0, 0.0))
-
-                       base_color.set_from_input(material.node_tree, from_node.inputs["Base Color"], from_node.inputs["Alpha"])
-                       if base_color.tint:
-                               tint.value = base_color.tint
-                       metalness.set_from_input(material.node_tree, from_node.inputs["Metallic"])
-                       roughness.set_from_input(material.node_tree, from_node.inputs["Roughness"])
-                       normal.set_from_input(material.node_tree, from_node.inputs["Normal"])
-                       emission.set_from_input(material.node_tree, from_node.inputs["Emission"])
+                       self.init_pbr_properties(material.node_tree, from_node)
                elif from_node.type=='EMISSION' or from_node.type=='MIX_SHADER':
-                       color_input, alpha_input = get_unlit_inputs(material.node_tree, from_node, self.blend_type=='ADDITIVE')
-                       if not color_input:
-                               raise Exception("Unsupported configuration for unlit material {}".format(self.name))
+                       splat_layers = get_splat_layers(material.node_tree, from_node)
+                       if splat_layers:
+                               for s in splat_layers:
+                                       if s[0].type!='BSDF_PRINCIPLED':
+                                               raise Exception("Unsupported splat layer type {} on splat material {}".format(s[0].type, self.name))
+
+                               from .texture import Texture
+
+                               self.type = "splat"
+                               self.sub_materials = []
+                               for l in splat_layers:
+                                       self.init_pbr_properties(material.node_tree, l[0])
+                                       sub = SubMaterial()
+                                       sub.properties = self.properties
+                                       sub.weight_source = l[1:]
+                                       self.sub_materials.append(sub)
+                                       self.properties = []
+
+                                       for p in sub.properties:
+                                               if p.texture:
+                                                       texture = Texture(p.texture, p.tex_channels)
+                                                       storage = (texture.pixelformat, texture.width, texture.height)
+                                                       existing = self.array_storage.setdefault(p.tex_keyword, storage)
+                                                       if storage!=existing:
+                                                               raise Exception("Inconsistent storage for {} on splat material {}".format(p.tex_keyword, self.name))
+                       else:
+                               color_input, alpha_input = get_unlit_inputs(material.node_tree, from_node, self.blend_type=='ADDITIVE')
+                               if not color_input:
+                                       raise Exception("Unsupported configuration for unlit material {}".format(self.name))
 
-                       self.type = "unlit"
+                               self.type = "unlit"
 
-                       color = self.create_property("color", "texture", (1.0, 1.0, 1.0, 1.0))
+                               color = self.create_property("color", "texture", (1.0, 1.0, 1.0, 1.0))
 
-                       color.set_from_input(material.node_tree, color_input, alpha_input)
-                       if self.blend_type=='ADDITIVE' and alpha_input:
-                               self.blend_type = 'ADDITIVE_ALPHA'
+                               color.set_from_input(material.node_tree, color_input, alpha_input)
+                               if self.blend_type=='ADDITIVE' and alpha_input:
+                                       self.blend_type = 'ADDITIVE_ALPHA'
                else:
                        raise Exception("Unsupported surface node type {} on material {}".format(from_node.type, self.name))
 
@@ -315,3 +361,19 @@ class Material:
                        prop = MaterialProperty(*args)
                self.properties.append(prop)
                return prop
+
+       def init_pbr_properties(self, node_tree, from_node):
+               base_color = self.create_property("base_color", (0.8, 0.8, 0.8, 1.0))
+               tint = self.create_property("tint", (1.0, 1.0, 1.0, 1.0))
+               metalness = self.create_property("metalness", 0.0)
+               roughness = self.create_property("roughness", 0.5)
+               normal = self.create_property("normal_map")
+               emission = self.create_property("emission", (0.0, 0.0, 0.0))
+
+               base_color.set_from_input(node_tree, from_node.inputs["Base Color"], from_node.inputs["Alpha"])
+               if base_color.tint:
+                       tint.value = base_color.tint
+               metalness.set_from_input(node_tree, from_node.inputs["Metallic"])
+               roughness.set_from_input(node_tree, from_node.inputs["Roughness"])
+               normal.set_from_input(node_tree, from_node.inputs["Normal"])
+               emission.set_from_input(node_tree, from_node.inputs["Emission"])
index 3a24db96a07f29a8cd9aacff8e6ede446f7cc81a..e4db4afb2a2abae7262564828aa9cc821422961d 100644 (file)
@@ -66,8 +66,11 @@ class Vertex:
 
 
 class VertexGroup:
-       def __init__(self, group):
-               if group:
+       def __init__(self, *args):
+               if len(args)==2:
+                       self.group = args[0]
+                       self.weight = args[1]
+               elif len(args)==1 and args[0]:
                        self.group = group.group
                        self.weight = group.weight
                else:
@@ -92,6 +95,7 @@ class Face:
                self.normal = face.normal
                self.use_smooth = face.use_smooth
                self.material_index = face.material_index
+               self.splat_mask = 0
                self.flag = False
 
        def __cmp__(self, other):
@@ -184,6 +188,19 @@ class Mesh:
                self.auto_smooth_angle = mesh.auto_smooth_angle
                self.max_groups_per_vertex = mesh.max_groups_per_vertex
 
+               # Check some material properties
+               from .material import Material
+               has_normal_maps = False
+               splat_material = None
+               for m in self.materials:
+                       mat = Material(m)
+                       for p in itertools.chain(mat.properties, *(s.properties for s in mat.sub_materials)):
+                               if p.tex_keyword=="normal_map" and p.texture:
+                                       has_normal_maps = True
+                                       break
+                       if mat.type=="splat":
+                               splat_material = mat
+
                # Clone only the desired UV layers
                if mesh.use_uv=='NONE' or not mesh.uv_layers:
                        self.uv_layers = []
@@ -206,7 +223,7 @@ class Mesh:
                                        self.uv_layers = []
 
                self.colors = None
-               if mesh.vertex_colors:
+               if mesh.vertex_colors and not splat_material:
                        self.colors = ColorLayer(mesh.vertex_colors[0])
 
                # Rewrite links between elements to point to cloned data, or create links
@@ -242,14 +259,24 @@ class Mesh:
                elif mesh.tangent_vecs=='YES':
                        self.tangent_vecs = True
                elif mesh.tangent_vecs=='AUTO':
-                       from .material import Material
-                       self.tangent_vecs = False
-                       for m in self.materials:
-                               mat = Material(m)
-                               if mat.type=="pbr":
-                                       normal_prop = next((p for p in mat.properties if p.tex_keyword=="normal_map"), None)
-                                       if normal_prop and normal_prop.texture:
-                                               self.tangent_vecs = True
+                       self.tangent_vecs = has_normal_maps
+
+               # Collect splat weight sources if needed
+               self.splat_layers = []
+               self.splat_sources = []
+               if splat_material:
+                       names = {s.weight_source[0] for s in splat_material.sub_materials}
+                       self.splat_layers = [ColorLayer(l) for l in mesh.vertex_colors if l.name in names]
+
+                       layers_by_name = {l.name:l for l in self.splat_layers}
+                       for s in splat_material.sub_materials:
+                               if s.weight_source[0] is None:
+                                       self.splat_sources.append((None, None))
+                               else:
+                                       self.splat_sources.append((layers_by_name[s.weight_source[0]], "RGBA".index(s.weight_source[1])))
+
+                       self.vertex_groups = True
+                       self.max_groups_per_vertex = 3
 
                self.batches = []
 
@@ -497,6 +524,43 @@ class Mesh:
                        else:
                                v.color = (1.0, 1.0, 1.0, 1.0)
 
+       def prepare_splat_weights(self, task):
+               if not self.splat_layers:
+                       return
+
+               splat_weights = []
+               remainder = None
+               for s in self.splat_sources:
+                       if s[0] is None:
+                               splat_weights.append(remainder)
+                       else:
+                               index = s[1]
+                               layer_values = [c[index] for c in s[0].colors]
+                               if remainder:
+                                       splat_weights.append([v*r for v, r in zip(layer_values, remainder)])
+                                       remainder = [(1-v)*r for v, r in zip(layer_values, remainder)]
+                               else:
+                                       splat_weights.append(layer_values)
+                                       remainder = [1-v for v in layer_values]
+
+               splat_weights = list(zip(*splat_weights))
+
+               for f in self.faces:
+                       for i in f.loop_indices:
+                               f.splat_mask |= sum(1<<j for j, w in enumerate(splat_weights[i]) if w>0)
+
+               self.split_vertices(self.find_splat_group, task)
+
+               for v in self.vertices:
+                       if v.faces:
+                               f = v.faces[0]
+                               weights = splat_weights[f.get_loop_index(v)]
+                               v.groups = [VertexGroup(i, w) for i, w in enumerate(weights) if (f.splat_mask>>i)&1]
+                       else:
+                               v.groups = []
+                       while len(v.groups)<self.max_groups_per_vertex:
+                               v.groups.append(VertexGroup(None))
+
        def split_vertices(self, find_group_func, task, *args):
                vertex_count = len(self.vertices)
                for i in range(vertex_count):
@@ -595,6 +659,17 @@ class Mesh:
 
                return group
 
+       def find_splat_group(self, vertex, face):
+               face.flag = True
+
+               group = [face]
+               for f in vertex.faces:
+                       if not f.flag and f.splat_mask==face.splat_mask:
+                               f.flag = True
+                               group.append(f)
+
+               return group
+
        def compute_normals(self, task):
                for i, v in enumerate(self.vertices):
                        v.normal = mathutils.Vector()
@@ -899,8 +974,12 @@ def create_mesh_from_object(ctx, obj):
        mesh.prepare_triangles(task)
        task = ctx.task("Smoothing", 0.5)
        mesh.prepare_smoothing(task)
-       task = ctx.task("Vertex groups", 0.6)
-       mesh.prepare_vertex_groups(obj)
+       if mesh.splat_sources:
+               task = ctx.task("Splat weights", 0.6)
+               mesh.prepare_splat_weights(task)
+       else:
+               task = ctx.task("Vertex groups", 0.6)
+               mesh.prepare_vertex_groups(obj)
        task = ctx.task("Preparing UVs", 0.75)
        mesh.prepare_uv(task)
        task = ctx.task("Preparing vertex colors", 0.85)