コード例 #1
0
def main():
    """
    Generate .dae file.
    """

    mesh = Collada()
    effect = material.Effect('effect0', [],
                             'phong',
                             diffuse=(1, 0, 0),
                             specular=(0, 1, 0))
    mat = material.Material('material0', 'mymaterial', effect)
    mesh.effects.append(effect)
    mesh.materials.append(mat)

    vert_floats = numpy.array([
        -50, 50, 50, 50, 50, 50, -50, -50, 50, 50, -50, 50, -50, 50, -50, 50,
        50, -50, -50, -50, -50, 50, -50, -50
    ])
    normal_floats = numpy.array([
        0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0,
        0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0,
        -1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, -1, 0, 0, -1, 0, 0,
        -1, 0, 0, -1
    ])
    componests = ('X', 'Y', 'Z')
    vert_src = source.FloatSource('cubeverts-array', vert_floats, componests)
    normal_src = source.FloatSource('cubenormals-array', normal_floats,
                                    componests)

    input_list = source.InputList()
    input_list.addInput(0, 'VERTEX', '#cubeverts-array')
    input_list.addInput(1, 'NORMAL', '#cubenormals-array')

    indices = numpy.array([
        0, 0, 2, 1, 3, 2, 0, 0, 3, 2, 1, 3, 0, 4, 1, 5, 5, 6, 0, 4, 5, 6, 4, 7,
        6, 8, 7, 9, 3, 10, 6, 8, 3, 10, 2, 11, 0, 12, 4, 13, 6, 14, 0, 12, 6,
        14, 2, 15, 3, 16, 7, 17, 5, 18, 3, 16, 5, 18, 1, 19, 5, 20, 7, 21, 6,
        22, 5, 20, 6, 22, 4, 23
    ])

    geom = geometry.Geometry(mesh, 'geometry0', 'mycube',
                             [vert_src, normal_src])
    triset = geom.createTriangleSet(indices, input_list, 'materialref')
    geom.primitives.append(triset)
    mesh.geometries.append(geom)

    matnode = scene.MaterialNode('materialref', mat, inputs=[])
    geomnode = scene.GeometryNode(geom, [matnode])
    node = scene.Node('node0', children=[geomnode])

    myscene = scene.Scene('myscene', [node])
    mesh.scenes.append(myscene)
    mesh.scene = myscene

    mesh.write('collada_sample.dae')
コード例 #2
0
def build_scene(env, outfilename):
    #architecture is :
    #-env.name
    #--exterior
    #----ingredients
    #--compartments
    #----surface
    #------ingredients
    #----interior
    #------ingredients
    #create the document and a node for rootenv
    collada_xml = Collada()
    collada_xml.assetInfo.unitname = "centimeter"
    collada_xml.assetInfo.unitmeter = 0.01
    collada_xml.assetInfo.upaxis = "Y_UP"

    root_env = scene.Node(env.name)
    myscene = scene.Scene(env.name + "_Scene", [root_env])
    collada_xml.scenes.append(myscene)
    collada_xml.scene = myscene
    #
    bbsurface = None
    r = env.exteriorRecipe
    if r: collada_xml, root_env = buildRecipe(r, r.name, collada_xml, root_env)
    for o in env.compartments:
        rs = o.surfaceRecipe
        if rs:
            #p,s,bb=up (767.0) #used for lipids
            #pp,ss,bbsurface = up (700.0)
            collada_xml, root_env = buildRecipe(rs,
                                                str(o.name) + "_surface",
                                                collada_xml,
                                                root_env,
                                                mask=bbsurface)
        ri = o.innerRecipe
        if ri:
            collada_xml, root_env = buildRecipe(ri,
                                                str(o.name) + "_interior",
                                                collada_xml,
                                                root_env,
                                                mask=bbsurface)
    collada_xml, root_env = buildCompartmentsGeom(env.compartments[0],
                                                  collada_xml, root_env)
    collada_xml.write(outfilename)
    return collada_xml
コード例 #3
0
ファイル: export.py プロジェクト: lynch829/xcite-precisionrt
def export_dae(vertices, indices, fname):
    mesh = Collada()
    effect = material.Effect("effect0", [], "phong", diffuse=(1, 0, 0), specular=(0, 1, 0))
    mat = material.Material("material0", "mymaterial", effect)
    mesh.effects.append(effect)
    mesh.materials.append(mat)
    vert_src = source.FloatSource('verts-array', numpy.array(vertices), ('X', 'Y', 'Z'))
    inlist = source.InputList()
    inlist.addInput(0, 'VERTEX', '#verts-array')
    indices = numpy.array(indices)
    geom = geometry.Geometry(mesh, 'geometry0', 'linac', [vert_src])
    triset = geom.createTriangleSet(indices, inlist, "materialref")
    geom.primitives.append(triset)
    mesh.geometries.append(geom)
    matnode = scene.MaterialNode("materialref", mat, inputs=[])
    geomnode = scene.GeometryNode(geom, [matnode])
    node = scene.Node("node0", children=[geomnode])
    myscene = scene.Scene("myscene", [node])
    mesh.scenes.append(myscene)
    mesh.scene = myscene
    mesh.write(fname)
コード例 #4
0
 def _save_dae(self, name):
     """\
     Save the model as a collada file.
     """
     mesh = Collada()
     effect = material.Effect("effect0", [], "phong", diffuse=(0.9,0.9,0.9), \
         specular=(0.1,0.1,0.1))
     mat = material.Material("material0", "mymaterial", effect)
     mesh.effects.append(effect)
     mesh.materials.append(mat)
     vert_floats = []
     norm_floats = []
     for vertex in self.vertices:
         vert_floats.append(vertex.x)
         vert_floats.append(vertex.y)
         vert_floats.append(vertex.z)
     for normal in self.normals:
         norm_floats.append(normal.x)
         norm_floats.append(normal.y)
         norm_floats.append(normal.z)
     indices = array(self.collada_indices)
     vert_src = source.FloatSource("vert-array", array(vert_floats), \
         ('X', 'Y', 'Z'))
     norm_src = source.FloatSource("norm-array", array(norm_floats), \
         ('X', 'Y', 'Z'))
     geom = geometry.Geometry(mesh, "geometry0", "solid", [vert_src, norm_src])
     input_list = source.InputList()
     input_list.addInput(0, 'VERTEX', "#vert-array")
     input_list.addInput(1, 'NORMAL', "#norm-array")
     triset = geom.createTriangleSet(indices, input_list, "materialref")
     geom.primitives.append(triset)
     mesh.geometries.append(geom)
     matnode = scene.MaterialNode("materialref", mat, inputs=[])
     geomnode = scene.GeometryNode(geom, [matnode])
     node = scene.Node("node0", children=[geomnode])
     myscene = scene.Scene("myscene", [node])
     mesh.scenes.append(myscene)
     mesh.scene = myscene
     mesh.write(name+'.dae')
コード例 #5
0
    #        print "pfff"
    isoc = isocontour.getContour3d(ndata, 0, 0, isovalue,
                                   isocontour.NO_COLOR_VARIABLE)
    vert = np.zeros((isoc.nvert, 3)).astype('f')
    norm = np.zeros((isoc.nvert, 3)).astype('f')
    col = np.zeros((isoc.nvert)).astype('f')
    tri = np.zeros((isoc.ntri, 3)).astype('i')
    isocontour.getContour3dData(isoc, vert, norm, col, tri, 0)
    #print vert
    if maskGrid.crystal:
        vert = maskGrid.crystal.toCartesian(vert)
    return vert, norm, tri


collada_xml = Collada()
collada_xml.assetInfo.unitname = "centimeter"
collada_xml.assetInfo.unitmeter = 0.01
collada_xml.assetInfo.upaxis = "Y_UP"

root_env = scene.Node(env.name)
myscene = scene.Scene(env.name + "_Scene", [root_env])
collada_xml.scenes.append(myscene)
collada_xml.scene = myscene

name = "myMolecule"
matnode = oneMaterial(name, collada_xml)
master_node = buildMeshGeom(name, v, f, collada_xml, matnode)
collada_xml.nodes.append(master_node)

collada_xml.write("test.dae")
コード例 #6
0
ファイル: export_collada.py プロジェクト: A1kmm/bpycollada
class ColladaExport(object):
    def __init__(self, directory, export_as='dae_only'):
        self._dir = directory
        self._export_as = export_as
        self._geometries = {}
        self._materials = {}
        self._collada = Collada()

        self._scene = Scene('main', [])
        self._collada.scenes.append(self._scene)
        self._collada.scene = self._scene

    def save(self, fp):
        self._collada.write(fp)

    def object(self, b_obj, parent=None, children=True):
        b_matrix = b_obj.matrix_world
        if parent:
            if children:
                b_matrix = b_obj.matrix_local
            else:
                b_matrix = Matrix()

        node = self.node(b_obj.name, b_matrix)
        if any(b_obj.children) and children:
            self.object(b_obj, parent=node, children=False)
            for child in b_obj.children:
                self.object(child, parent=node)

        if parent:
            parent.children.append(node)
        else:
            self._scene.nodes.append(node)

        inode_meth = getattr(self, 'obj_' + b_obj.type, None)
        if inode_meth:
            node.children.extend(inode_meth(b_obj))

    def node(self, b_name, b_matrix=None):
        tf = []
        if b_matrix:
            tf.append(self.matrix(b_matrix))
        node = Node(b_name, transforms=tf)
        node.save()
        return node

    def obj_MESH(self, b_obj):
        geom = self._geometries.get(b_obj.data.name, None)
        if not geom:
            geom = self.mesh(b_obj.data)
            self._geometries[b_obj.data.name] = geom
        matnodes = []
        for slot in b_obj.material_slots:
            sname = slot.material.name
            if sname not in self._materials:
                self._materials[sname] = self.material(slot.material)
            matnodes.append(MaterialNode('none', self._materials[sname],
                inputs=[]))
        return [GeometryNode(geom, matnodes)]

    def mesh(self, b_mesh):
        vert_srcid = b_mesh.name + '-vertary'
        vert_f = [c for v in b_mesh.vertices for c in v.co]
        vert_src = FloatSource(vert_srcid, np.array(vert_f), ('X', 'Y', 'Z'))

        sources = [vert_src]

        smooth = list(filter(lambda f: f.use_smooth, b_mesh.faces))
        if any(smooth):
            vnorm_srcid = b_mesh.name + '-vnormary'
            norm_f = [c for v in b_mesh.vertices for c in v.normal]
            norm_src = FloatSource(vnorm_srcid, np.array(norm_f), ('X', 'Y', 'Z'))
            sources.append(norm_src)
        flat = list(filter(lambda f: not f.use_smooth, b_mesh.faces))
        if any(flat):
            fnorm_srcid = b_mesh.name + '-fnormary'
            norm_f = [c for f in flat for c in f.normal]
            norm_src = FloatSource(fnorm_srcid, np.array(norm_f), ('X', 'Y', 'Z'))
            sources.append(norm_src)

        name = b_mesh.name + '-geom'
        geom = Geometry(self._collada, name, name, sources)

        if any(smooth):
            ilist = InputList()
            ilist.addInput(0, 'VERTEX', _url(vert_srcid))
            ilist.addInput(1, 'NORMAL', _url(vnorm_srcid))
            # per vertex normals
            indices = np.array([
                i for v in [
                    (v, v) for f in smooth for v in f.vertices
                ] for i in v])
            if _is_trimesh(smooth):
                p = geom.createTriangleSet(indices, ilist, 'none')
            else:
                vcount = [len(f.vertices) for f in smooth]
                p = geom.createPolylist(indices, vcount, ilist, 'none')
            geom.primitives.append(p)
        if any(flat):
            ilist = InputList()
            ilist.addInput(0, 'VERTEX', _url(vert_srcid))
            ilist.addInput(1, 'NORMAL', _url(fnorm_srcid))
            indices = []
            # per face normals
            for i, f in enumerate(flat):
                for v in f.vertices:
                    indices.extend([v, i])
            indices = np.array(indices)
            if _is_trimesh(flat):
                p = geom.createTriangleSet(indices, ilist, 'none')
            else:
                vcount = [len(f.vertices) for f in flat]
                p = geom.createPolylist(indices, vcount, ilist, 'none')
            geom.primitives.append(p)

        self._collada.geometries.append(geom)
        return geom

    def material(self, b_mat):
        shader = 'lambert'
        if b_mat.specular_shader == 'PHONG':
            shader = 'phong'
        elif b_mat.specular_shader == 'BLINN':
            shader = 'blinn'
        if b_mat.use_shadeless:
            shader = 'constant'
        child = {
            'ambient': (b_mat.ambient,) * 3,
            'emission': (b_mat.emit,) * 3,
            'diffuse': tuple(b_mat.diffuse_color),
        }
        if b_mat.use_transparency:
            child.update({
                'transparent': (0.,0.,0.),
                'transparency': b_mat.alpha,
                })
        if b_mat.raytrace_mirror.use:
            child.update({
                'reflective': tuple(b_mat.mirror_color),
                'reflectivity': b_mat.raytrace_mirror.reflect_factor,
                })
        effect = Effect(b_mat.name + '-fx', [], shader, **child)
        mat = Material(b_mat.name, b_mat.name, effect)
        self._collada.effects.append(effect)
        self._collada.materials.append(mat)
        return mat

    def matrix(self, b_matrix):
        f = tuple(map(tuple, b_matrix.transposed()))
        return MatrixTransform(np.array(
            [e for r in f for e in r], dtype=np.float32))
コード例 #7
0
class ColladaExport:
    def __init__(self, objects, filepath, directory, kwargs):
        self._is_zae = kwargs["export_as"] == "zae"
        self._export_textures = kwargs["export_textures"]
        self._add_blender_extensions = kwargs["add_blender_extensions"]
        self._filepath = filepath
        self._dir = directory
        self._ext_files_map = {}
        self._ext_files_revmap = {}
        if self._is_zae:
            self._zip = zipfile.ZipFile(self._filepath, "w")

            class ZipAttr:
                compress_type = zipfile.ZIP_DEFLATED
                file_attr = 0o100644 << 16
                date_time = time.gmtime()[:6]
                scene_name = "scene.dae"

                @classmethod
                def new_item(celf, filename):
                    item = zipfile.ZipInfo()
                    item.compress_type = celf.compress_type
                    item.external_attr = celf.file_attr
                    item.date_time = celf.date_time
                    item.filename = filename
                    return item

                #end new_item

            #end ZipAttr
            self._zipattr = ZipAttr
            # First item in archive is uncompressed and named “mimetype”, and
            # contents is MIME type for archive. This way it ends up at a fixed
            # offset (filename at 30 bytes from start, contents at 38 bytes) for
            # easy detection by format-sniffing tools. This convention is used
            # by ODF and other formats similarly based on Zip archives.
            mimetype = zipfile.ZipInfo()
            mimetype.filename = "mimetype"
            mimetype.compress_type = zipfile.ZIP_STORED
            mimetype.external_attr = ZipAttr.file_attr
            mimetype.date_time = (2020, 8, 23, 1, 33, 52)
            # about when I started getting .zae export working
            self._zip.writestr(mimetype, b"model/vnd.collada+xml+zip")
            # extrapolating from the fact that the official type for .dae files
            # is “model/vnd.collada+xml”
        else:
            self._zip = None
            self._zipattr = None
        #end if
        self._up_axis = kwargs["up_axis"]
        if self._up_axis == "Z_UP":
            self._orient = Matrix.Identity(4)
        elif self._up_axis == "X_UP":
            self._orient = Matrix.Rotation(-120 * DEG, 4, Vector(1, -1, 1))
        else:  # "Y_UP" or unspecified
            self._orient = Matrix.Rotation(-90 * DEG, 4, "X")
        #end if
        obj_children = {}
        for obj in objects:
            parent = obj.parent
            if parent == None:
                parentname = None
            else:
                parentname = parent.name
            #end if
            if parentname not in obj_children:
                obj_children[parentname] = set()
            #end if
            obj_children[parentname].add(obj.name)
        #end for
        self._obj_children = obj_children
        self._selected_only = kwargs["use_selection"]
        self._geometries = {}
        self._materials = {}
        self._collada = Collada()
        self._collada.xmlnode.getroot().set("version",
                                            kwargs["collada_version"])
        self._collada.assetInfo.unitmeter = 1
        self._collada.assetInfo.unitname = "metre"
        self._collada.assetInfo.upaxis = self._up_axis
        self._collada.assetInfo.save()
        root_technique = self.blender_technique(
            True, self._collada.xmlnode.getroot())
        if root_technique != None:
            prefixes = E.id_prefixes()
            for k in sorted(DATABLOCK.__members__.keys()):
                v = DATABLOCK[k]
                if not v.internal_only:
                    prefix = E.prefix(name=k, value=v.nameid(""))
                    prefixes.append(prefix)
                #end if
            #end for
            root_technique.append(prefixes)
        #end if

        self._scene = Scene(DATABLOCK.SCENE.nameid("main"), [])
        self._collada.scenes.append(self._scene)
        self._collada.scene = self._scene
        self._id_seq = 0

    #end __init__

    def write_ext_file(self, category, obj_name, filename, contents):
        if category not in self._ext_files_map:
            self._ext_files_map[category] = {}
            self._ext_files_revmap[category] = {}
        #end if
        ext_files_map = self._ext_files_map[category]
        ext_files_revmap = self._ext_files_revmap[category]
        if obj_name in ext_files_map:
            # already encountered this external file
            out_filename = ext_files_map[obj_name]
        else:
            if not self._is_zae:
                outdir = os.path.join(self._dir, category.subdir)
                os.makedirs(outdir, exist_ok=True)
            #end if
            base_out_filename = os.path.join(category.subdir, filename)
            out_filename = base_out_filename
            seq = 0
            while out_filename in ext_files_revmap:
                if seq == 0:
                    base_parts = os.path.splitext(base_out_filename)
                #end if
                seq += 1
                assert seq < 1000000  # impose some ridiculous but finite upper limit
                out_filename = "%s-%0.3d%s" % (base_parts[0], seq,
                                               base_parts[1])
            #end while
            ext_files_map[obj_name] = out_filename
            ext_files_revmap[out_filename] = obj_name
            if self._is_zae:
                item = self._zipattr.new_item(out_filename)
                self._zip.writestr(item, contents)
            else:
                out = open(os.path.join(self._dir, out_filename), "wb")
                out.write(contents)
                out.close()
            #end if
        #end if
        return out_filename

    #end write_ext_file

    def save(self):
        if self._is_zae:
            item = self._zipattr.new_item(self._zipattr.scene_name)
            dae = io.BytesIO()
            self._collada.write(dae)
            self._zip.writestr(item, dae.getvalue())
            manifest = ElementTree.Element("dae_root")
            manifest.text = self._zipattr.scene_name
            item = self._zipattr.new_item("manifest.xml")
            self._zip.writestr(item, ElementTree.tostring(manifest))
            # all done
            self._zip.close()
        else:
            self._collada.write(self._filepath)
        #end if

    #end save

    def blender_technique(self, as_extra, obj):
        # experimental: add Blender-specific attributes via a custom <technique>.
        if self._add_blender_extensions:
            if isinstance(obj, DaeObject):
                obj = obj.xmlnode
            #end if
            blendstuff = E.technique(profile="BLENDER028")
            if as_extra:
                parent = E.extra()
            else:
                parent = obj
            #end if
            parent.append(blendstuff)
            if as_extra:
                obj.append(parent)
            #end if
        else:
            blendstuff = None
        #end if
        return blendstuff

    #end blender_technique

    def obj_blender_technique(self, as_extra, obj, b_data, attribs):
        # save any custom technique settings for this object.
        blendstuff = self.blender_technique(as_extra, obj)
        if blendstuff != None:
            for tagname, attrname in attribs:
                if hasattr(b_data, attrname):
                    subtag = getattr(E, tagname)(str(getattr(b_data,
                                                             attrname)))
                    blendstuff.append(subtag)
                #end if
            #end for
        #end if

    #end obj_blender_technique

    def next_internal_id(self):
        self._id_seq += 1
        return DATABLOCK.INTERNAL_ID.nameid("%0.5d" % self._id_seq)

    #end next_internal_id

    def node(self, b_matrix=None):
        node = Node(id=None, xmlnode=E.node())
        # construct my own xmlnode to avoid setting an id or name
        # (should be optional according to Collada spec)
        if b_matrix != None:
            node.transforms.append(self.matrix(b_matrix))
        #end if
        node.save()
        return node

    #end node

    def obj_camera(self, b_obj):
        result = []
        b_cam = b_obj.data
        if b_cam.type == "PERSP":
            cam_class = PerspectiveCamera
            args = \
                {
                    "xfov" : b_cam.angle_x / DEG,
                    "yfov" : b_cam.angle_y / DEG,
                }
        elif b_cam.type == "ORTHO":
            cam_class = OrthographicCamera
            args = \
                {
                    "xmag" : b_cam.ortho_scale,
                    "ymag" : b_cam.ortho_scale,
                }
        else:
            cam_class = None
        #end if
        if cam_class != None:
            # todo: shared datablock
            cam = cam_class \
              (
                id = DATABLOCK.CAMERA.nameid(b_obj.name),
                znear = b_cam.clip_start,
                zfar = b_cam.clip_end,
                **args
              )
            self._collada.cameras.append(cam)
            result.append(CameraNode(cam))
        #end if
        return result

    #end obj_camera

    def obj_light(self, b_obj):
        result = []
        b_light = b_obj.data
        light_type = tuple \
          (
            t for t in
                (
                    ("POINT", PointLight),
                    ("SPOT", SpotLight),
                    ("SUN", DirectionalLight),
                )
            if b_light.type == t[0]
          )
        if len(light_type) != 0:
            light_type = light_type[0][1]
        else:
            light_type = None
        #end if
        if light_type != None:
            # todo: shared datablock
            light = light_type \
              (
                DATABLOCK.LAMP.nameid(b_obj.name),
                color = tuple(b_light.color) + (1,)
              )
            for attr, battr, conv in \
                (
                  # conversions are inverses of those done in importer
                    ("falloff_ang", "spot_size", lambda ang : ang / DEG),
                    ("falloff_exp", "spot_blend", lambda blend : 1 / max(blend, 0.00001) - 1),
                      # some very small-magnitude positive value to avoid division by zero
                ) \
            :
                if hasattr(b_light, battr) and hasattr(light, attr):
                    setattr(light, attr, conv(getattr(b_light, battr)))
                #end if
            #end for
            if b_light.use_nodes:
                node_graph = b_light.node_tree
                the_node = list(n for n in node_graph.nodes
                                if n.type == "OUTPUT_LIGHT")[0]
                trace_path = iter \
                  (
                    (
                        ("Surface", "EMISSION"),
                        ("Strength", "LIGHT_FALLOFF"),
                    )
                  )
                found = False
                while True:
                    trace = next(trace_path, None)
                    if trace == None:
                        if not the_node.inputs["Strength"].is_linked:
                            found = True
                        #end if
                        break
                    #end if
                    input_name, want_shader_type = trace
                    input = the_node.inputs[input_name]
                    if not input.is_linked:
                        break
                    links = input.links
                    # note docs say this takes O(N) in total nr links in node graph to compute
                    if len(links) == 0:
                        break
                    the_node = links[0].from_node
                    if the_node.type != want_shader_type:
                        break
                    output_name = links[0].from_socket.name
                #end while
                if found:
                    strength = the_node.inputs["Strength"].default_value
                    if strength != 0:
                        atten = \
                            {
                                "Constant" : "constant_att",
                                "Linear" : "linear_att",
                                "Quadratic" : "quad_att",
                            }.get(output_name)
                        if atten != None and hasattr(light, atten):
                            setattr(light, atten, 1 / strength)
                        #end if
                    #end if
                #end if
            #end if
            self.obj_blender_technique \
              (
                True,
                light,
                b_light,
                [
                    ("angle", "angle"),
                    ("power", "energy"),
                    ("shadow_soft_size", "shadow_soft_size"),
                    ("spot_blend", "spot_blend"),
                    ("spot_size", "spot_size"),
                ]
              )
            self._collada.lights.append(light)
            result.append(LightNode(light))
        #end if
        return result

    #end obj_light

    def obj_empty(self, b_obj):
        result = Node(id=DATABLOCK.EMPTY.nameid(b_obj.name))
        return [result]

    #end obj_empty

    def obj_mesh(self, b_obj):
        def make_slotname(slotindex):
            # Blender doesn’t name material slots, but Collada does
            return "slot%.3d" % slotindex

        #end make_slotname

        def encode_mesh(b_mesh, b_mesh_name, b_material_slots):
            def is_trimesh(faces):
                return all([len(f.verts) == 3 for f in faces])

            #end is_trimesh

        #begin encode_mesh
            mesh_name = DATABLOCK.MESH.nameid(b_mesh_name)
            sources = []
            vert_srcid = self.next_internal_id()
            sources.append \
              (
                FloatSource
                  (
                    id = vert_srcid,
                    data = np.array([c for v in b_mesh.verts for c in v.co]),
                    components = ("X", "Y", "Z")
                  )
              )
            vnorm_srcid = self.next_internal_id()
            sources.append \
              (
                FloatSource
                  (
                    id = vnorm_srcid,
                    data = np.array([c for v in b_mesh.verts for c in v.normal]),
                    components = ("X", "Y", "Z")
                  )
              ) # todo: face normal might be different for flat shading
            uv_ids = []
            if b_mesh.loops.layers.uv.active != None:
                active_uv_name = b_mesh.loops.layers.uv.active.name
            else:
                active_uv_name = None
            #end if
            for i, (b_uvname,
                    uvlayer) in enumerate(b_mesh.loops.layers.uv.items()):
                uv_name = self.next_internal_id()
                uv_ids.append((uv_name, b_uvname))
                sources.append \
                  (
                    FloatSource
                      (
                        id = uv_name,
                        data = np.array
                          (
                            [
                                x
                                for f in b_mesh.faces
                                for l in f.loops
                                for x in l[uvlayer].uv
                            ]
                          ),
                        components = ("S", "T")
                      )
                  )
            #end for
            geom = Geometry(self._collada, mesh_name, mesh_name, sources)
            blendstuff = self.blender_technique(True, geom)
            if blendstuff != None:
                names = E.layer_names()
                for u in uv_ids:
                    names.append(E.name(name=u[1], refid=u[0], type="UV"))
                #end for
                blendstuff.append(names)
            #end if

            for slotindex in range(max(len(b_material_slots), 1)):
                slotname = make_slotname(slotindex)
                assigned = \
                    [
                        f
                        for f in b_mesh.faces
                        if f.material_index == slotindex
                    ]
                if any(assigned):
                    ilist = InputList()
                    ilist.addInput(0, "VERTEX", idurl(vert_srcid))
                    ilist.addInput(0, "NORMAL", idurl(vnorm_srcid))
                    setnr = 0
                    for u in uv_ids:
                        setnr += 1
                        ilist.addInput(1, "TEXCOORD", idurl(u[0]),
                                       (setnr, 0)[u[1] == active_uv_name])
                        # always assign set 0 to active UV layer
                    #end for
                    indices = []
                    for face in b_mesh.faces:
                        for face_loop in face.loops:
                            this_face = [face_loop.vert.index, face_loop.index]
                            indices.extend(this_face)
                        #end for
                    #end for
                    indices = np.array(indices)
                    if is_trimesh(assigned):
                        p = geom.createTriangleSet(indices, ilist, slotname)
                    else:
                        vcounts = [len(f.verts) for f in assigned]
                        p = geom.createPolylist(indices, vcounts, ilist,
                                                slotname)
                    #end if
                    geom.primitives.append(p)
                #end if
            #end for

            self._collada.geometries.append(geom)
            return geom

        #end encode_mesh

    #begin obj_mesh
        b_mesh_name = b_obj.data.name
        b_material_slots = b_obj.material_slots
        b_mesh = bmesh.new()
        geom = self._geometries.get(b_mesh_name, None)
        if not geom:
            b_mesh.from_mesh(b_obj.data)
            geom = encode_mesh(b_mesh, b_mesh_name, b_material_slots)
            self._geometries[b_mesh_name] = geom
        #end if
        matnodes = []
        for slotindex, slot in enumerate(b_material_slots):
            sname = slot.material.name
            if sname not in self._materials:
                self._materials[sname] = self.material(slot.material)
            #end if
            matnodes.append \
              (
                MaterialNode
                  (
                    make_slotname(slotindex),
                    self._materials[sname],
                    inputs = [("ACTIVE_UV", "TEXCOORD", "0")]
                      # always assign set 0 to active UV layer
                  )
              )
        #end for
        b_mesh.free()
        return [GeometryNode(geom, matnodes)]

    #end obj_mesh

    obj_type_handlers = \
        {
            "CAMERA" : (obj_camera, DATABLOCK.CAMERA),
            "EMPTY" : (obj_empty, DATABLOCK.EMPTY),
            "LIGHT" : (obj_light, DATABLOCK.LAMP),
            "MESH" : (obj_mesh, DATABLOCK.MESH),
        }

    def object(self, b_obj, parent=None):
        handle_type = self.obj_type_handlers.get(b_obj.type)
        if handle_type != None:
            if parent != None:
                b_matrix = b_obj.matrix_local
            else:
                b_matrix = self._orient @ b_obj.matrix_world
            #end if

            obj = handle_type[0](self, b_obj)
            is_node = len(obj) == 1 and isinstance(obj[0], Node)
            if is_node:
                obj = obj[0]
                assert b_matrix != None
                obj.transforms.append(self.matrix(b_matrix))
                node = obj
            else:
                node = self.node(b_matrix)
            #end if
            children = self._obj_children.get(b_obj.name)
            if children != None:
                for childname in children:
                    self.object(bpy.data.objects[childname], parent=node)
                #end for
            #end if
            if parent != None:
                parent.children.append(node)
            else:
                self._scene.nodes.append(node)
            #end if
            if not is_node:
                node.children.extend(obj)
            #end if
        #end if

    #end object

    def material(self, b_mat):
        shader = "lambert"
        effect_kwargs = \
            {
                "diffuse" : tuple(b_mat.diffuse_color[:3]),
                "double_sided" : not b_mat.use_backface_culling,
            }
        effect_params = []
        if b_mat.diffuse_color[3] != 1.0:
            effect_kwargs["transparency"] = b_mat.diffuse_color[3]
        #end if
        if b_mat.use_nodes:
            b_shader = list(n for n in b_mat.node_tree.nodes
                            if n.type == "BSDF_PRINCIPLED")
            if len(b_shader) == 1:
                # assume node setup somewhat resembles what importer creates
                b_shader = b_shader[0]
            else:
                b_shader = None
            #end if
            if b_shader != None:

                def get_input(name):
                    input = b_shader.inputs[name]
                    if not input.is_linked:
                        value = input.default_value
                    else:
                        value = None
                    #end if
                    return value

                #end get_input

                def get_input_map(name):
                    input = b_shader.inputs[name]
                    map = None  # to begin with
                    if input.is_linked:
                        links = input.links
                        # note docs say this takes O(N) in total nr links in node graph to compute
                        teximage = list \
                          (
                            l.from_node for l in links
                            if isinstance(l.from_node, bpy.types.ShaderNodeTexImage) and l.from_socket.name == "Color"
                          )
                        if len(teximage) != 0:
                            teximage = teximage[0].image
                            if teximage.packed_file != None:
                                contents = teximage.packed_file.data
                            else:
                                contents = open(
                                    bpy.path.abspath(teximage.filepath),
                                    "rb").read()
                            #end if
                            out_filepath = self.write_ext_file \
                              (
                                category = EXT_FILE.TEXTURE,
                                obj_name = teximage.name,
                                filename = os.path.basename(teximage.filepath),
                                contents = contents
                              )
                            image = CImage(id=self.next_internal_id(),
                                           path=out_filepath)
                            surface = Surface(id=self.next_internal_id(),
                                              img=image)
                            sampler = Sampler2D(id=self.next_internal_id(),
                                                surface=surface)
                            map = Map(sampler=sampler, texcoord="ACTIVE_UV")
                            effect_params.extend([image, surface, sampler])
                        #end if
                    #end if
                    return map

                #end get_input_map

                value = get_input("Base Color")
                if value != None:
                    effect_kwargs["diffuse"] = value[:3]
                elif self._export_textures:
                    map = get_input_map("Base Color")
                    if map != None:
                        effect_kwargs["diffuse"] = map
                    #end if
                #end if
                # todo: support maps for more inputs
                value = get_input("Metallic")
                metallic = True
                if value == None or value == 0:
                    value = get_input("Specular")
                    metallic = False
                #end if
                if value != None and value != 0:
                    shader = "phong"  # do I care about “blinn”?
                    if metallic:
                        effect_kwargs["reflective"] = effect_kwargs["diffuse"]
                    else:
                        effect_kwargs["reflective"] = (1, 1, 1)
                    #end if
                    effect_kwargs["reflectivity"] = value
                #end if
                value = get_input("Alpha")
                if value != None and value != 1.0:
                    effect_kwargs["transparency"] = value
                    # overridden by Transmission (below) if any
                #end if
                value = get_input("Transmission")
                if value != None and value != 0:
                    effect_kwargs["transparency"] = value
                    effect_kwargs["transparent"] = effect_kwargs["diffuse"]
                    value = get_input("IOR")
                    if value != None:
                        effect_kwargs["index_of_refraction"] = value
                    #end if
                #end if
            else:
                pass  # give up for now
            #end if
        else:
            # quick fudge based only on Viewport Display settings
            if b_mat.metallic > 0 or b_mat.roughness < 1:
                shader = "phong"  # do I care about “blinn”?
                try:
                    shininess = 1 / b_mat.roughness - 1
                    # inverse of formula used in importer
                except ZeroDivisionError:
                    shininess = math.inf
                #end try
                shininess = min(
                    shininess, 10000)  # just some arbitrary finite upper limit
                effect_kwargs["reflectivity"] = b_mat.specular_intensity
                effect_kwargs["shininess"] = shininess
                if b_mat.metallic > 0:
                    # not paying attention to actual value of b_mat.metallic!
                    effect_kwargs["reflective"] = b_mat.specular_color[:3]
                else:
                    effect_kwargs["reflective"] = (1, 1, 1)
                #end if
            #end if
        #end if
        effect = Effect(self.next_internal_id(), effect_params, shader,
                        **effect_kwargs)
        mat = Material(DATABLOCK.MATERIAL.nameid(b_mat.name), b_mat.name,
                       effect)
        self._collada.effects.append(effect)
        self._collada.materials.append(mat)
        return mat

    #end material

    @staticmethod
    def matrix(b_matrix):
        return \
            MatrixTransform \
              (
                np.array
                  (
                    [e for r in tuple(map(tuple, b_matrix)) for e in r],
                    dtype = np.float32
                  )
              )