import copy
from cis_interface import backwards
from cis_interface.serialize.PlySerialize import PlyDict, PlySerialize
[docs]class ObjDict(PlyDict):
r"""Class for storing obj information.
Args:
vertices (list, optional): 3D positions of vertices comprising the
3D object. Defaults to [].
faces (list, optional): Indices of 3 or more vertices making up faces
or a tuple containing the indices for the position, texture
coordinate, and normal for each vertex in the face. This
information can also be provided in their own lists, but
there must be an entry for every face. Defaults to [].
vertex_colors (list, optional): RGB values for each of the vertices.
If not provided, all vertices will be black. Defaults to [].
material (str, optional): Material to use for faces. Defaults to None.
normals (list, optional): 3D normals for vertices. Defaults to [].
texcoords (list, optional): 3D texture coordinates for vertices.
Defaults to [].
face_texcoords (list, optional): Indices of texture coordinates for each
vertex in the face. Entries of None are ignored. Defaults to [].
face_normals (list, optional): Indices of normals for each vertex in the
face. Entries of None are ignored. Defaults to [].
"""
def __init__(self, **kwargs):
list_keys = ['vertices', 'faces', 'vertex_colors',
'normals', 'texcoords', 'face_texcoords', 'face_normals']
for k in list_keys:
if kwargs.get(k, None) is None:
kwargs.setdefault(k, [])
kwargs.setdefault('material', None)
super(ObjDict, self).__init__(**kwargs)
self.update(**self.standardize())
[docs] def standardize(self, no_copy=False):
r"""Put the dictionary in the standard format with face information
split into separate fields.
Args:
no_copy (bool, optional): If True, the current dictionary will be
updated. Otherwise a copy will be returned. Defaults to False.
Returns:
ObjDict: Standardized obj information.
"""
if no_copy:
out = self
else:
out = copy.deepcopy(self)
# Convert face tuples to lists
face_keys = {'faces': 0, 'face_texcoords': 1, 'face_normals': 2}
for k in ['face_texcoords', 'face_normals']:
if not out.get(k, []):
out[k] = [[None for v in f] for f in self['faces']]
for i, f in enumerate(self['faces']):
for k in ['face_texcoords', 'face_normals']:
if out[k][i] is None:
out[k][i] = [None for v in f]
for j, v in enumerate(f):
if issubclass(v.__class__, (int, float)):
out['faces'][i][j] = v
else:
for k, kindex in face_keys.items():
out[k][i][j] = v[kindex]
return out
[docs] @classmethod
def from_shape(cls, shape, d, conversion=1.0):
r"""Create a ply dictionary from a PlantGL shape and descritizer.
Args:
scene (openalea.plantgl.scene): Scene that should be descritized.
d (openalea.plantgl.descritizer): Descritizer.
conversion (float, optional): Conversion factor that should be
applied to the vertex positions. Defaults to 1.0.
"""
iobj = super(ObjDict, cls).from_shape(shape, d, conversion=conversion)
if iobj is not None:
# Texcoords
if d.result.texCoordList:
for t in d.result.texCoordList:
# TODO: Should the coords be scaled?
iobj['texcoords'].append([t.x, t.y])
if d.result.texCoordIndexList:
for t in d.result.texCoordIndexList:
if t[0] < len(iobj['texcoords']):
iobj['face_texcoords'].append([t[0], t[1], t[2]])
else:
iobj['face_texcoords'].append([None, None, None])
# Normals
if d.result.normalList:
for n in d.result.normalList:
iobj['normals'].append([n.x, n.y, n.z])
if d.result.texCoordIndexList:
for n in d.result.texCoordIndexList:
if n[0] < len(iobj['normals']):
iobj['face_normals'].append([n[0], n[1], n[2]])
else:
iobj['face_normals'].append([None, None, None])
return iobj
[docs] def to_geom_args(self, conversion=1.0, name=None):
r"""Get arguments for creating a PlantGL geometry.
Args:
conversion (float, optional): Conversion factor that should be
applied to the vertices. Defaults to 1.0.
name (str, optional): Name that should be given to the created
PlantGL symbol. Defaults to None and is ignored.
Returns:
tuple: Class, arguments and keyword arguments for PlantGL geometry.
"""
import openalea.plantgl.all as pgl
smb_class, args, kwargs = super(ObjDict, self).to_geom_args(
conversion=conversion, name=name)
index_class = pgl.Index3
array_class = pgl.Index3Array
# Texture coords
if self.get('texcoords', []):
obj_texcoords = []
for t in self['texcoords']:
obj_texcoords.append(pgl.Vector2(t[0], t[1]))
kwargs['texCoordList'] = pgl.Point2Array(obj_texcoords)
if self.get('face_texcoords', []):
obj_ftexcoords = []
for t in self['face_texcoords']:
if (t is not None) and (t[0] is not None):
entry = [int(_t) for _t in t]
else:
entry = [len(self['texcoords']) for _ in range(3)]
obj_ftexcoords.append(index_class(*entry))
kwargs['texCoordIndexList'] = array_class(obj_ftexcoords)
# Normals
if self.get('normals', []):
obj_normals = []
for n in self['normals']:
obj_normals.append(pgl.Vector3(n[0], n[1], n[2]))
kwargs['normalList'] = pgl.Point3Array(obj_normals)
if self.get('face_normals', []):
obj_fnormals = []
for n in self['face_normals']:
if (n is not None) and (n[0] is not None):
entry = [int(_n) for _n in n]
else:
entry = [len(self['normals']) for _ in range(3)]
obj_fnormals.append(index_class(*entry))
kwargs['normalIndexList'] = array_class(obj_fnormals)
return smb_class, args, kwargs
[docs] def append(self, solf, default_rgb=None):
r"""Append new ply information to this dictionary.
Args:
solf (ObjDict): Another ply to append to this one.
default_rgb (list, optional): Default color in RGB that should be
used for missing colors. Defaults to [0, 0, 0].
"""
super(ObjDict, self).append(solf, default_rgb=default_rgb)
# Merge material using first in list
material = None
for x in [self, solf]:
if x['material'] is not None:
material = x['material']
break
self['material'] = material
# Merge vertex things
for k in ['normals', 'texcoords']:
fk = 'face_' + k
nprev = len(self[k])
self[k] += solf[k]
for f in solf[fk]:
fnew = []
for v in f:
if v is None:
fnew.append(v)
else:
fnew.append(v + nprev)
self[fk].append(fnew)
return self
[docs]class ObjSerialize(PlySerialize):
r"""Class for serializing/deserializing .obj file formats. Reader
adapted from https://www.pygame.org/wiki/OBJFileLoader."""
@property
def serializer_type(self):
r"""int: Type of serializer."""
return 9
[docs] def func_serialize(self, args, zero_indexed=True):
r"""Serialize a message.
Args:
args (ObjDict): Dictionary of obj information.
zero_indexed (bool, optional): If True, the input indices are assumed
to start at zero and they will be adjusted to start at one and
conform with .obj format. If False, the input indices are assumed
to start at one and they will not be adjusted. Defaults to True.
Returns:
bytes, str: Serialized message.
"""
lines = []
fkey_order = ['faces', 'face_texcoords', 'face_normals']
# Standardize
if isinstance(args, dict):
args = ObjDict(**args)
sargs = args.standardize()
# Header
if self.write_header:
lines += ['# Author cis_auto',
'# Generated by cis_interface', '']
if sargs.get('material', None) is not None:
lines.append('usemtl %s' % sargs['material'])
if 'vertices' in sargs:
if not sargs.get('vertex_colors', []):
for v in sargs['vertices']:
lines.append('v %6.4f %6.4f %6.4f' % tuple(v))
else:
for i in range(len(sargs['vertices'])):
line = 'v'
line += ' %6.4f %6.4f %6.4f' % tuple(sargs['vertices'][i])
line += ' %d %d %d' % tuple(sargs['vertex_colors'][i])
lines.append(line)
for v in sargs['normals']:
lines.append('vn %6.4f %6.4f %6.4f' % tuple(v))
for v in sargs['texcoords']:
lines.append('vt %6.4f %6.4f' % tuple(v))
# Faces
add_ind = 0
if zero_indexed:
add_ind = 1
for i in range(sargs.nface):
iline = 'f'
for j in range(len(sargs['faces'][i])):
v = [sargs[k][i][j] for k in fkey_order]
iline += ' %d/' % (v[0] + add_ind)
if v[1] is not None:
iline += '%d' % (v[1] + add_ind)
iline += '/'
if v[2] is not None:
iline += '%d' % (v[2] + add_ind)
lines.append(iline)
out = self.newline.join(lines) + self.newline
return backwards.unicode2bytes(out)
[docs] def func_deserialize(self, msg, zero_indexed=True):
r"""Deserialize a message.
Args:
msg (str, bytes): Message to be deserialized.
zero_indexed (bool, optional): If True, the parsed indices are adjusted
to start at zero. If False, the indices will not be adjusted and
will start at one as per .obj format. Defaults to True.
Returns:
ObjDict: Deserialized .obj information. The faces are zero indexed.
"""
if len(msg) == 0:
out = self.empty_msg
else:
lines = backwards.bytes2unicode(msg).split(self.newline)
out = ObjDict()
nvert = 0
for line in lines:
if line.startswith('#'):
continue
values = line.split()
if not values:
continue
if values[0] == 'v':
out['vertices'].append([x for x in map(float, values[1:4])])
iclr = self.default_rgb
if len(values) == 7:
if not out['vertex_colors']:
out['vertex_colors'] = [self.default_rgb for
_ in range(nvert)]
iclr = [x for x in map(int, values[4:7])]
if out['vertex_colors']:
out['vertex_colors'].append(iclr)
nvert += 1
elif values[0] == 'vn':
out['normals'].append([x for x in map(float, values[1:4])])
elif values[0] == 'vt':
out['texcoords'].append([x for x in map(float, values[1:3])])
elif values[0] in ('usemtl', 'usemat'):
out['material'] = values[1]
elif values[0] == 'f':
sub_ind = 0
if zero_indexed:
sub_ind = 1
face = []
texcoords = []
norms = []
for v in values[1:]:
w = v.split('/')
face.append(int(w[0]) - sub_ind)
itexc = None
inorm = None
if len(w) >= 2 and len(w[1]) > 0:
itexc = int(w[1]) - sub_ind
texcoords.append(itexc)
if len(w) >= 3 and len(w[2]) > 0:
inorm = int(w[2]) - sub_ind
norms.append(inorm)
out['faces'].append(face)
out['face_texcoords'].append(texcoords)
out['face_normals'].append(norms)
for x in out['faces'][-1]:
assert(x <= (len(out['vertices']) - sub_ind))
out.standardize(no_copy=True)
return out