Skip to content

Commit

Permalink
fix(linked objects): allow linked objects to use different materials
Browse files Browse the repository at this point in the history
Allow linked objects to use different materials as Blender apparently as an option to select between
the material being tied to the object or the mesh. Before the latter was always assumed to be the
case
  • Loading branch information
StjerneIdioten committed Feb 10, 2024
1 parent 4ed9d82 commit 79095ba
Show file tree
Hide file tree
Showing 3 changed files with 42 additions and 58 deletions.
4 changes: 2 additions & 2 deletions addon/i3dio/i3d.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ def add_camera_node(self, camera_object: bpy.types.Object, parent: SceneGraphNod
return self._add_node(CameraNode, camera_object, parent)

def add_shape(self, evaluated_mesh: EvaluatedMesh, shape_name: Optional[str] = None, is_merge_group=None,
bone_mapping: ChainMap = None) -> int:
bone_mapping: ChainMap = None, tangent = False) -> int:
if shape_name is None:
name = evaluated_mesh.name
else:
Expand All @@ -154,7 +154,7 @@ def add_shape(self, evaluated_mesh: EvaluatedMesh, shape_name: Optional[str] = N
if name not in self.shapes:
shape_id = self._next_available_id('shape')
indexed_triangle_set = IndexedTriangleSet(shape_id, self, evaluated_mesh, shape_name, is_merge_group,
bone_mapping)
bone_mapping, tangent)
# Store a reference to the shape from both it's name and its shape id
self.shapes.update(dict.fromkeys([shape_id, name], indexed_triangle_set))
self.xml_elements['Shapes'].append(indexed_triangle_set.element)
Expand Down
5 changes: 2 additions & 3 deletions addon/i3dio/node_classes/material.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,8 @@ def element(self):
def element(self, value):
self.xml_elements['node'] = value

@property
def tangent(self):
return self.xml_elements.get('Normalmap', None)
def is_normalmapped(self):
return 'Normalmap' in self.xml_elements

def populate_xml_element(self):
if self.blender_material.use_nodes:
Expand Down
91 changes: 38 additions & 53 deletions addon/i3dio/node_classes/shape.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import collections
import logging
from typing import (OrderedDict, Optional, List, Dict, ChainMap, Union)
from itertools import zip_longest
import bpy

from .node import (Node, SceneGraphNode)
Expand All @@ -12,13 +13,12 @@


class SubSet:
def __init__(self, material_id: int):
def __init__(self):
self.first_index = 0
self.first_vertex = 0
self.number_of_indices = 0
self.number_of_vertices = 0
self.triangles = []
self.material_id = material_id

def as_dict(self):
subset_attributes = {'firstIndex': f"{self.first_index}",
Expand All @@ -28,7 +28,7 @@ def as_dict(self):
return subset_attributes

def __str__(self):
return f'materialId="{self.material_id}" numTriangles="{len(self.triangles)}" ' \
return f'numTriangles="{len(self.triangles)}" ' \
f'firstIndex="{self.first_index}" firstVertex="{self.first_vertex}" ' \
f'numIndices="{self.number_of_indices}" numVertices="{self.number_of_vertices}"'

Expand All @@ -37,8 +37,8 @@ def add_triangle(self, triangle):


class Vertex:
def __init__(self, material_name, position, normal, vertex_color, uvs, blend_ids=None, blend_weights=None):
self._material_name = material_name
def __init__(self, subset_idx: int, position, normal, vertex_color, uvs, blend_ids=None, blend_weights=None):
self._subset_idx = subset_idx
self._position = position
self._normal = normal
self._vertex_color = vertex_color
Expand All @@ -49,7 +49,7 @@ def __init__(self, material_name, position, normal, vertex_color, uvs, blend_ids
self._make_hash_string()

def _make_hash_string(self):
self._str = f"{self._material_name}{self._position}{self._normal}{self._vertex_color}"
self._str = f"{self._subset_idx}{self._position}{self._normal}{self._vertex_color}"

for uv in self._uvs:
self._str += f"{uv}"
Expand Down Expand Up @@ -146,19 +146,18 @@ class IndexedTriangleSet(Node):
ID_FIELD_NAME = 'shapeId'

def __init__(self, id_: int, i3d: I3D, evaluated_mesh: EvaluatedMesh, shape_name: Optional[str] = None,
is_merge_group: bool = False, bone_mapping: ChainMap = None):
is_merge_group: bool = False, bone_mapping: ChainMap = None, tangent = False):
self.id: int = id_
self.i3d: I3D = i3d
self.evaluated_mesh: EvaluatedMesh = evaluated_mesh
self.vertices: OrderedDict[Vertex, int] = collections.OrderedDict()
self.triangles: List[List[int]] = list() # List of lists of vertex indexes
self.subsets: OrderedDict[str, SubSet] = collections.OrderedDict()
self.material_indexes: str = ''
self.subsets: List[SubSet] = []
self.is_merge_group = is_merge_group
self.bone_mapping: ChainMap = bone_mapping
self.bind_index = 0
self.vertex_group_ids = {}
self.tangent = False
self.tangent = tangent
if shape_name is None:
self.shape_name = self.evaluated_mesh.name
else:
Expand All @@ -183,20 +182,16 @@ def element(self):
def element(self, value):
self.xml_elements['node'] = value

def process_subsets(self, mesh):
for idx, (material_name, subset) in enumerate(self.subsets.items()):
self.logger.debug(f"Subset with index [{idx}] based on material '{material_name}'")

if idx > 0:
self.logger.debug(f"Previous subset exists")
_, previous_subset = list(self.subsets.items())[idx-1]
subset.first_vertex = previous_subset.first_vertex + previous_subset.number_of_vertices
subset.first_index = previous_subset.first_index + previous_subset.number_of_indices

self.process_subset(mesh, material_name)
def process_subsets(self, mesh) -> None:
next_vertex = 0
next_index = 0
for idx, subset in enumerate(self.subsets):
self.logger.debug(f"Subset with index {idx}")
subset.first_vertex = next_vertex
subset.first_index = next_index
next_vertex, next_index = self.process_subset(mesh, subset)

def process_subset(self, mesh, material_name: str, triangle_offset: int = 0):
subset = self.subsets[material_name]
def process_subset(self, mesh, subset: SubSet, triangle_offset: int = 0) -> tuple[int, int]:
self.logger.debug(f"Processing subset: {subset}")
for triangle in subset.triangles[triangle_offset:]:

Expand Down Expand Up @@ -252,7 +247,7 @@ def process_subset(self, mesh, material_name: str, triangle_offset: int = 0):
blend_ids += padding
blend_weights += padding

vertex = Vertex(material_name,
vertex = Vertex(triangle.material_index,
blender_vertex.co.xyz,
mesh.loops[loop_index].normal,
vertex_color,
Expand All @@ -268,10 +263,9 @@ def process_subset(self, mesh, material_name: str, triangle_offset: int = 0):
vertex_index = self.vertices[vertex]

self.triangles[-1].append(vertex_index)

subset.number_of_indices += 3

self.logger.debug(f"Has subset '{material_name}' with '{len(subset.triangles)}' triangles and {subset}")
self.logger.debug(f"Subset {triangle.material_index} with '{len(subset.triangles)}' triangles and {subset}")
return (subset.number_of_vertices, subset.number_of_indices)

def populate_from_evaluated_mesh(self):
mesh = self.evaluated_mesh.mesh
Expand All @@ -280,6 +274,9 @@ def populate_from_evaluated_mesh(self):
self.logger.info(f"has no material assigned, assigning default material")
mesh.materials.append(self.i3d.get_default_material().blender_material)
self.logger.info(f"assigned default material i3d_default_material")

for _ in mesh.materials:
self.subsets.append(SubSet())

has_warned_for_empty_slot = False
for triangle in mesh.loop_triangles:
Expand All @@ -291,18 +288,8 @@ def populate_from_evaluated_mesh(self):
has_warned_for_empty_slot = True
triangle_material = self.i3d.get_default_material().blender_material

if triangle_material.name not in self.subsets:
self.logger.info(f"Has material {triangle_material.name!r}")
# TODO: Figure out why we have to supply the original material instead of the one on the evaluated
# object. The evaluated one still contains references to deleted nodes from the node_tree
# of the material. Although I thought it would be updated?
material_id = self.i3d.add_material(triangle_material.original)
if self.i3d.materials[material_id].tangent is not None:
self.tangent = True
self.subsets[triangle_material.name] = SubSet(material_id)

# Add triangle to subset
self.subsets[triangle_material.name].add_triangle(triangle)
self.subsets[triangle.material_index].add_triangle(triangle)

self.process_subsets(mesh)

Expand All @@ -322,23 +309,22 @@ def append_from_evaluated_mesh(self, mesh_to_append):
f"merge groups need to share the same subset!")
return
else:
if mesh.materials[0].name not in self.subsets:
if mesh.materials[0].name != self.evaluated_mesh.mesh.materials[0].name:
self.logger.warning(f"Mesh '{mesh.name}' has a different material from merge group root, "
f"which is not allowed!")
return

material_name = mesh.materials[0].name
triangle_offset = len(self.subsets[material_name].triangles)
vertex_offset = self.subsets[material_name].number_of_vertices
triangle_offset = len(self.subsets[-1].triangles)
vertex_offset = self.subsets[-1].number_of_vertices
for triangle in mesh.loop_triangles:
self.subsets[material_name].add_triangle(triangle)
self.subsets[-1].add_triangle(triangle)

self.bind_index += 1
self.process_subset(mesh, material_name, triangle_offset)
self.process_subset(mesh, self.subsets[-1], triangle_offset)
self.write_vertices(vertex_offset)
self.write_triangles(triangle_offset)
subset = list(self.xml_elements['subsets'])[0]
for key, value in self.subsets[material_name].as_dict().items():
for key, value in self.subsets[-1].as_dict().items():
subset.set(key, value)

def write_vertices(self, offset=0):
Expand Down Expand Up @@ -423,13 +409,9 @@ def populate_xml_element(self):
)

# Write subsets
for _, subset in self.subsets.items():
self.material_indexes += f"{subset.material_id} "
for subset in self.subsets:
xml_i3d.SubElement(self.xml_elements['subsets'], 'Subset', subset.as_dict())

# Removes the last whitespace from the string, since an extra will always be added
self.material_indexes = self.material_indexes.strip()


class ControlVertex:
def __init__(self, position):
Expand Down Expand Up @@ -567,6 +549,7 @@ class ShapeNode(SceneGraphNode):
def __init__(self, id_: int, shape_object: Optional[bpy.types.Object], i3d: I3D,
parent: Optional[SceneGraphNode] = None):
self.shape_id = None
self.tangent = False
super().__init__(id_=id_, blender_object=shape_object, i3d=i3d, parent=parent)

@property
Expand All @@ -578,13 +561,15 @@ def add_shape(self):
self.shape_id = self.i3d.add_curve(EvaluatedNurbsCurve(self.i3d, self.blender_object))
self.xml_elements['NurbsCurve'] = self.i3d.shapes[self.shape_id].element
else:
self.shape_id = self.i3d.add_shape(EvaluatedMesh(self.i3d, self.blender_object))
self.shape_id = self.i3d.add_shape(EvaluatedMesh(self.i3d, self.blender_object), tangent=self.tangent)
self.xml_elements['IndexedTriangleSet'] = self.i3d.shapes[self.shape_id].element

def populate_xml_element(self):
if self.blender_object.type == 'MESH':
m_ids = [self.i3d.add_material(m.material) for m in self.blender_object.material_slots]
self._write_attribute('materialIds', ' '.join(map(str, m_ids)) or str(self.i3d.add_material(self.i3d.get_default_material())))
self.tangent = any((self.i3d.materials[m_id].is_normalmapped() for m_id in m_ids))
self.add_shape()
self.logger.debug(f"has shape ID '{self.shape_id}'")
self._write_attribute('shapeId', self.shape_id)
if self.blender_object.type == 'MESH':
self._write_attribute('materialIds', self.i3d.shapes[self.shape_id].material_indexes)
super().populate_xml_element()

0 comments on commit 79095ba

Please sign in to comment.