import re
from math import pi
from xml.dom import minidom
from xml.etree import ElementTree as ET
import openalea.plantgl.all as pgl
from .. import geometry
[docs]
def write_opf(g, filename, features=None):
"""Write the opf file from a given MTG object.
:param g: an mtg object, either read from a file or generated by a simulator
:type g: MTG.mtg
:param filename: the file to write the .opf file.
:type filename: str
:param features: additional attributes to write in the "attributeBDD"
section of the .opf file, at the root node level.
:type features: dict, optional
"""
if hasattr(g.node(g.root), "opf_info"):
opf_info = g[0]["opf_info"]
else:
opf_info = {"version": "2.0", "editable": "true"}
root = ET.Element("opf", opf_info)
if hasattr(g.node(g.root), "ref_meshes"):
meshBDD = ET.SubElement(root, "meshBDD")
_write_meshBDD(g, meshBDD)
if hasattr(g.node(g.root), "materials"):
materialBDD = ET.SubElement(root, "materialBDD")
_write_materialBDD(g, materialBDD)
if hasattr(g.node(g.root), "shapes"):
shapeBDD = ET.SubElement(root, "shapeBDD")
_write_shapeBDD(g, shapeBDD)
if features is None: # there is no given features list
if hasattr(g.node(g.root), "user_attributes"): # try if feature list is given internally
user_attributes = dict(
[(key, g[0]["user_attributes"][key][2]) for key in g[0]["user_attributes"]]
)
attributeBDD = ET.SubElement(root, "attributeBDD")
_write_attributeBDD(attributeBDD, user_attributes=user_attributes)
else: # don't write any features
user_attributes = dict()
else:
if len(features) > 0:
user_attributes = features
attributeBDD = ET.SubElement(root, "attributeBDD")
_write_attributeBDD(attributeBDD, user_attributes=user_attributes)
else:
user_attributes = dict()
topology = ET.SubElement(root, "topology", attrib=_get_xml_attribute(g, g.root))
_write_topology(g, topology, features=user_attributes)
text = ET.tostring(root, encoding="UTF-8")
dom = minidom.parseString(text)
with open(filename, "wb") as f:
f.write(dom.toprettyxml(indent="\t", encoding="UTF-8"))
def _vector_array_to_string(vec, level=0):
"""Convert an array to a raw string for later writing it in the .opf file. Internal function.
Note: Add "level" times identation in the beggining
:param vec: vector / matrix to write as text format with correct indentations and separators
:type vec: numpy array
:param level: number of indentations at the beginning of the string
:type level: int
:return: Conversion of "vec" in string format.
:rtype: str
"""
text = ""
for v in vec:
text += "\t".join([str(value) for value in v]) + "\t"
return "\n" + "\t" * (level + 1) + text + "\n" + "\t" * level
def _write_mesh(g, tr, mesh):
"""Write the mesh. Called by `_write_meshBDD`. Internal function.
:param g: MTG object to write the mesh information.
:type g: MTG.mtg
:param tr: TriangleSet object to write the mesh information.
:type tr: openalea.plantgl.scenegraph._pglsg
:param mesh: xml section to write the mesh information.
:type mesh: xml.ElementTree
"""
# write the points list
points = ET.SubElement(mesh, "points")
points.text = _vector_array_to_string(tr.pointList, 3)
# write the the normal vectors
normals = ET.SubElement(mesh, "normals")
normals.text = _vector_array_to_string(tr.normalList, 3)
# write the texture coordinates if exist
if tr.texCoordList is not None:
texture = ET.SubElement(mesh, "textureCoords")
texture.text = _vector_array_to_string(g.texCoordList, 3)
# write the connectivity table
faces = ET.SubElement(mesh, "faces")
for id, f in enumerate(tr.indexList):
face = ET.SubElement(faces, "face", {"Id": str(id)})
face.text = "\n" + "\t" * 5 + "%s %s %s" % (f[0], f[1], f[2]) + "\n" + "\t" * 4
def _color3_to_RGBA_opf(color, alpha):
"""Convert a Color3 object to an array with 4 elements (color + alpha). Internal function.
:param color: list of red / green / blue values (between 0 and 255)
:type color: Color3
:param alpha: alpha coefficent for RGBA definition (between 0 and 1)
:type alpha: float
:return: RGBA 4 elements vector of floats with values between 0 and 1.
:rtype: list
"""
return [
str(color.red / 255.0),
str(color.green / 255.0),
str(color.blue / 255.0),
] + [str(alpha)]
def _write_materialBDD(g, materialBDD):
"""Write the reference meshes in the materialBDD part of the .opf file. Internal function.
:param g: MTG object to write the material information.
:type g: MTG.mtg
:param materialBDD: xml section to write the material information.
:type materialBDD: xml.ElementTree
"""
materials = g[0]["materials"]
for material_id in materials:
material = materials[material_id]
alpha = 1 - material.transparency
# Define a material
mat = ET.SubElement(materialBDD, "material", {"Id": str(material_id)})
# Emission
emi = ET.SubElement(mat, "emission")
emi.text = "\t".join(_color3_to_RGBA_opf(material.emission, alpha))
# Ambient
amb = ET.SubElement(mat, "ambient")
amb.text = "\t".join(_color3_to_RGBA_opf(material.ambient, alpha))
# Diffuse
dif = ET.SubElement(mat, "diffuse")
dif_vec = _color3_to_RGBA_opf(material.ambient, alpha)
for i in range(3):
dif_vec[i] = str(material.diffuse * float(dif_vec[i]))
dif.text = "\t".join(dif_vec)
# Specular
spec = ET.SubElement(mat, "specular")
spec.text = "\t".join(_color3_to_RGBA_opf(material.specular, alpha))
# Shininess
shininess = ET.SubElement(mat, "shininess")
shininess.text = str(128.0 * material.shininess)
def _write_meshBDD(g, meshBDD):
"""Write the reference meshes in the meshBDD part of the .opf file. Internal function.
:param g: MTG object to write the mesh information.
:type g: MTG.mtg
:param meshBDD: xml section to write the mesh information.
:type meshBDD: xml.ElementTree
"""
ref_meshes = g[0]["ref_meshes"]
nb_meshes = len(ref_meshes)
if isinstance(ref_meshes, list):
# make ref_meshes into a dictionary
ref_meshes = dict(zip(range(nb_meshes), zip(ref_meshes, [True] * nb_meshes)))
for id in ref_meshes:
tr, enableScale = ref_meshes[id]
attribute = {
"name": "",
"shape": "",
"Id": str(id),
"enableScale": str(enableScale).lower(),
}
mesh = ET.SubElement(meshBDD, "mesh", attribute)
_write_mesh(g, tr, mesh)
def _write_shapeBDD(g, shapeBDD):
"""Write the reference shapes in the shapeBDD part of the .opf file. Internal function.
:param g: MTG object to write the shape information.
:type g: MTG.mtg
:param shapeBDD: xml section to write the shape information.
:type shapeBDD: xml.ElementTree
"""
shapes = g[0]["shapes"]
if isinstance(shapes, list):
shapes = dict(zip(range(len(shapes)), shapes))
for id in shapes:
sh = ET.SubElement(shapeBDD, "shape", {"Id": str(id)})
if not shapes[id].get("name"):
node = ET.SubElement(sh, "name")
node.text = " Mesh%d" % (id)
for key in shapes[id]:
node = ET.SubElement(sh, key)
node.text = str(shapes[id][key])
def _write_attributeBDD(attributeBDD, user_attributes):
"""Write the attributes in the attributeBDD part of the .opf file. Internal function.
:param attributeBDD: xml section to write the attributes information.
:type attributeBDD: xml.ElementTree
:param user_attributes: Disctionnary of attributes given by the user.
:type user_attributes: dict
"""
for key in user_attributes:
ET.SubElement(attributeBDD, "attribute", {"name": key, "class": user_attributes[key]})
def _write_info(g, node, v, features=None):
"""Write for each node of the MTG additional features / attributes.
Called by `_write_topology`. Internal function.
:param g: MTG object to write the additional features.
:type g: MTG.mtg
:param node: xml id of mtg
:type node: int
:param v: node id
:type v: int
:param features: Disctionnary of attributes given by the user.
:type features: dict
"""
if features is None:
try:
user_attributes = g[0]["user_attributes"]
except KeyError:
user_attributes = []
else:
user_attributes = features
for k in user_attributes:
try:
text = str(g[v][k])
info_node = ET.SubElement(node, k)
info_node.text = text
except KeyError:
pass
try:
# try if the current node is associated with a geometry
# then a shape index should be given
shapeIndex = g[v]["shapeIndex"]
except KeyError: # no such key, do nothing
pass
else: # no exception then add the geometry
geometry_node = ET.SubElement(node, "geometry", attrib={"class": "Mesh"})
shape_node = ET.SubElement(geometry_node, "shapeIndex")
shape_node.text = str(shapeIndex)
mat = geometry.mat_from_transformed(g[v]["geometry"])
mat_node = ET.SubElement(geometry_node, "mat")
mat_str = "\n" + "".join(["\t".join([str(mat[i, j]) for j in range(4)]) + "\n" for i in range(3)])
mat_node.text = mat_str
up, down = geometry.tapering_radius_from_transformed(g[v]["geometry"])
up_node = ET.SubElement(geometry_node, "dUp")
up_node.text = str(up)
down_node = ET.SubElement(geometry_node, "dDwn")
down_node.text = str(down)
def _get_xml_attribute(g, v):
label = g[v].get("label")
if label is None:
if v == g.root:
label = "Scene"
else:
raise ValueError("No label defined for the mtg node %d" % (v))
return {"class": re.sub(r"\d+", "", label), "scale": str(g.scale(v)), "id": str(v)}
def _write_topology(g, parent_in, child_in=None, v=None, features=None, visited=None):
"""Write the topology of the MTG in the .opf file in a recursive way. Internal function.
Note:
----
If mtg node w is v's child with edge type follow,
w's xml node is in fact a child node of parent_in otherwise, if w is v's branch child,
w's xml node is a child node of child_in. The mtg nodes should be added in the opf
such that : parent before child, complex before component
:param g: MTG object to write the topology.
:type g: MTG.mtg
:param parent_in: xml node of mtg node v's parent
:type parent_in: xml.ElementTree
:param child_in: xml node of mtg node v
:type child_in: xml.ElementTree
:param v: node id
:type v: int
:param features: Disctionnary of attributes given by the user.
:type features: dict
:param visited: dict of all visited nodes (associate a (useless ?) boolean as value).
:type visited: dict
"""
# At initialization
if v is None:
v = g.root
if child_in is None:
child = parent_in
else:
child = child_in
if visited is None:
visited = {}
parent = parent_in
_write_info(g, child, v, features)
for w in g.component_roots(v): # root of disjoint component trees
visited[w] = True
attrib = _get_xml_attribute(g, w)
child_next = ET.SubElement(child, "decomp", attrib)
_write_topology(
g,
parent_in=child,
child_in=child_next,
v=w,
features=features,
visited=visited,
)
# I should now add all the children of v into the xml object
for w in g.children(v):
if not visited.get(w):
complex_w = w
complex_v = v
# go to the first complex of w that is component to v and w's first common complex
while g.complex(complex_w) != g.complex(complex_v):
complex_w = g.complex(complex_w)
complex_v = g.complex(complex_v)
visited[complex_w] = True
attrib = _get_xml_attribute(g, complex_w)
if g.edge_type(w) == "+":
# add sub node to v's xml node
parent_next = child # v's xml node <=> w's parent's xml node
child_next = ET.SubElement(child, "branch", attrib) # w's xml node
_write_topology(
g,
parent_next,
child_next,
complex_w,
features=features,
visited=visited,
)
else:
# add sub node to v's parent's xml node
child = ET.SubElement(parent, "follow", attrib)
_write_topology(g, parent, child, complex_w, features, visited=visited)
# ----------------------------------------------------------------------------
# This is a fail attempt to extract reference meshes from a list of shapes
# the two functions _extract_ref_.. and _extract_mate... are infact generator (yield key world)
# So you can use next(_extract_...) to get the next value of the function
# while conserving the internal state of the function
# the idea is, when receiving a shape object, we look into the deepest geometric primitive
# (a shape is a chained list of transformation and end to a geometric primitive)
# and compute the overall transformatoin
def _deep_primitive(geo):
if isinstance(geo, pgl.Transformed) or isinstance(geo, pgl.Shape):
return _deep_primitive(geo.geometry)
else:
return geo.__class__.__name__, geo
def _extract_ref_meshes(shapes):
ref_meshes = {}
meshIndex = {}
enableScale = {}
for s in shapes:
geo_name, geo = _deep_primitive(s)
if not ref_meshes.get(geo_name):
if geo_name == "Frustum":
ref_meshes[geo_name] = pgl.tesselate(
pgl.EulerRotated(0.0, pi / 2, 0.0, pgl.Cylinder(radius=1.0))
)
# the plantgl object furstum is oriented along the z axis while in opf
# we should save the reference mesh that is oriented along the x axis.
# we apply a rotation around y of angle pi/2, the reference mesh is
# thus oriented along x axis.
# The radius is set to be 1 to facilitate the tapering and the scaling
# The default height of the cylinder is 1.
meshIndex[geo_name] = len(ref_meshes) - 1
enableScale[geo_name] = True
ref_meshes[geo_name].computeNormalList()
key = geo_name
elif geo_name == "Cylinder":
ref_meshes[geo_name] = pgl.tesselate(
pgl.EulerRotated(0.0, pi / 2, 0.0, pgl.Cylinder(radius=1.0))
)
meshIndex[geo_name] = len(ref_meshes) - 1
enableScale[geo_name] = False
ref_meshes[geo_name].computeNormalList()
key = geo_name
elif geo_name == "TriangleSet":
pgl_id = geo.getPglId()
ref_meshes[pgl_id] = geo
meshIndex[pgl_id] = len(ref_meshes) - 1
enableScale[pgl_id] = False
ref_meshes[pgl_id].computeNormalList()
key = pgl_id
yield ref_meshes[key], meshIndex[key], enableScale[key]
def _extract_materials(shapes):
materials = {}
materialIndex = {}
for s in shapes:
ap = s.appearance
color_name = ap.name
if not materials.get(color_name):
materials[color_name] = ap
materialIndex[color_name] = len(materials) - 1
yield materials[color_name], materialIndex[color_name]
# self.Mtg.add_property('geometry')
# self.Mtg.add_property('shapeIndex')
# self.Mtg.add_property('meshIndex')
# self.Mtg.add_property('materialIndex')
# self.Mtg.add_property('ref_meshes')
# self.Mtg.add_property('materials')
# self.Mtg.add_property('shapes')
[docs]
def apply_scene(g, scene):
"""Add a scene to the mtg.
The scene should support the method : scene.todict() which will return a dictionary with
vid as keys and the vertex's geometry as value. In order to allow this behavior, when you
define your scene from a mtg, for each shape of the scene, you should set id to be vid :
shape.id = vid and then you combine the shapes to create your scene.
This function will also try to extract reference meshes and materials from the given scene
(implementation is not complete yet !). But it works when each mtg node has exactly one
geometric object associated if it has.
:param g: an mtg object
:type g: MTG.mtg
:param scene: a plantgl object
:type scene: plantgl.Scene
"""
property_names = [
"geometry",
"shapeIndex",
"meshIndex",
"materialIndex",
"ref_meshes",
"materials",
"shapes",
]
for name in property_names:
g.add_property(name)
# create a dictionnary that associate each vertex with this shape
# sd = {vid : shapelist}, a vertex could be associated with more than 1 shapes
if isinstance(scene, pgl.Scene):
sd = scene.todict()
shapes = [s[0] for s in sd.values()]
elif isinstance(scene, dict):
sd = scene
shapes = list(scene.values())
else:
raise TypeError("This type is not recognized !")
# create a generator of shapes, a vertex can have more than 1 shape
# for instance let's just take the first one
# get the root id of the mtg
root = g.root
g.node(root).ref_meshes = {}
g.node(root).materials = {}
g.node(root).shapes = {}
def get_key(d, x):
return list(d.keys())[list(d.values()).index(x)]
ref_meshes = _extract_ref_meshes(shapes)
materials = _extract_materials(shapes)
# get tapered_x object :
Tapered_opf = geometry.taper_along_x()
for vid, sh in zip(sd.keys(), shapes):
try:
mesh, meshIndex, enableScale = next(ref_meshes)
except ValueError:
material, materialIndex = next(materials)
continue
except StopIteration:
print(vid)
raise StopIteration
material, materialIndex = next(materials)
# ----------------- update the global properties -----------------
# if discover a new mesh :
if meshIndex not in g.node(root).ref_meshes.keys():
g.node(root).ref_meshes[meshIndex] = (mesh, enableScale)
# if discover a new material :
if materialIndex not in g.node(root).materials.keys():
g.node(root).materials[materialIndex] = material
# if discover a new shape ( an association of a material index and a mesh index)
d = {"meshIndex": meshIndex, "materialIndex": materialIndex}
try:
shapeIndex = get_key(g.node(root).shapes, d)
except ValueError: # d is not in the shape list
g.node(root).shapes[len(g.node(root).shapes)] = d
shapeIndex = len(g.node(root).shapes) - 1
# ------------------ update the geometry information of the vertex
geo_name, geo = _deep_primitive(sh)
if geo_name == "Frustum":
base = geo.radius
top = geo.radius * geo.taper
height = geo.height
tap = Tapered_opf(top, base, mesh)
mat = pgl.Matrix4(pgl.Matrix3(0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0)) * pgl.Matrix4(
pgl.Matrix3(height, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0)
)
# the transformation matrix of sh is made with assumption that the
# reference geometry object is oriented along the z axis.
# Before applying it we should orient the Tapered object
# along z axis (it's by default in opf that is oriented along x axis)
# Similarly the scaling is applied along x axis first !
mat = geometry.mat_from_transformed(sh) * mat
final_geo = geometry.transformed_from_mat(mat, tap, is_mesh=False)
elif geo_name == "Cylinder":
radius = geo.radius
height = geo.height
mat = pgl.Matrix4(pgl.Matrix3(0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0)) * pgl.Matrix4(
pgl.Matrix3(height, 0.0, 0.0, 0.0, radius, 0.0, 0.0, 0.0, radius)
)
mat = geometry.mat_from_transformed(sh) * mat
final_geo = geometry.transformed_from_mat(mat, mesh, is_mesh=False)
elif geo_name == "TriangleSet":
final_geo = geo
else:
raise NotImplementedError("This geometry is not supported yet : %s" % (geo_name))
# add geometry to vid :
g.node(vid).shapeIndex = shapeIndex
g.node(vid).geometry = final_geo
g.node(vid).meshIndex = meshIndex
g.node(vid).materialIndex = materialIndex