3
$\begingroup$

I studied a couple of videos (it seems to be on this topic), but I failed to achieve something.

For example, I have a cloud in Metashape and I want to convert it into a blender. enter image description here

enter image description here

If there is no such functionality, which way is better?

Upd: I realized that I have a cloud without a Col, but I don't know how to get it. enter image description here

$\endgroup$

1 Answer 1

7
$\begingroup$

Okay, I got it, the plan is:

  1. Install addon: import-ply-as-verts by TombstoneTumbleweedArt

##### BEGIN GPL LICENSE BLOCK #####

#  This program is free software; you can redistribute it and/or
#  modify it under the terms of the GNU General Public License
#  as published by the Free Software Foundation; either version 2
#  of the License, or (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software Foundation,
#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####

# <pep8 compliant>

# ########## Import PLY as Verts ########
#
# This module is close to 90% the original code supplied with Blender as the stock PLY import addon.  I have attempted
# to change it as little as possible and still obtain the desired result. 
#
# All love and respect to the original programmers who did all the heavy lifting for me :) 
#
# ##########
'''
bl_info = {
    "name": "Import PLY as Verts",
    "author": "Michael A Prostka",
    "blender": (3, 1, 0),
    "location": "File > Import/Export",
    "description": "Import PLY mesh data as point cloud",
    "category": "Import-Export",
}
'''
class ElementSpec:
    __slots__ = (
        "name",
        "count",
        "properties",
    )

    def __init__(self, name, count):
        self.name = name
        self.count = count
        self.properties = []

    def load(self, format, stream):
        if format == b'ascii':
            stream = stream.readline().split()
        return [x.load(format, stream) for x in self.properties]

    def index(self, name):
        for i, p in enumerate(self.properties):
            if p.name == name:
                return i
        return -1


class PropertySpec:
    __slots__ = (
        "name",
        "list_type",
        "numeric_type",
    )

    def __init__(self, name, list_type, numeric_type):
        self.name = name
        self.list_type = list_type
        self.numeric_type = numeric_type

    def read_format(self, format, count, num_type, stream):
        import struct

        if format == b'ascii':
            if num_type == 's':
                ans = []
                for i in range(count):
                    s = stream[i]
                    if not (len(s) >= 2 and s.startswith(b'"') and s.endswith(b'"')):
                        print("Invalid string", s)
                        print("Note: ply_import.py does not handle whitespace in strings")
                        return None
                    ans.append(s[1:-1])
                stream[:count] = []
                return ans
            if num_type == 'f' or num_type == 'd':
                mapper = float
            else:
                mapper = int
            ans = [mapper(x) for x in stream[:count]]
            stream[:count] = []
            return ans
        else:
            if num_type == 's':
                ans = []
                for i in range(count):
                    fmt = format + 'i'
                    data = stream.read(struct.calcsize(fmt))
                    length = struct.unpack(fmt, data)[0]
                    fmt = '%s%is' % (format, length)
                    data = stream.read(struct.calcsize(fmt))
                    s = struct.unpack(fmt, data)[0]
                    ans.append(s[:-1])  # strip the NULL
                return ans
            else:
                fmt = '%s%i%s' % (format, count, num_type)
                data = stream.read(struct.calcsize(fmt))
                return struct.unpack(fmt, data)

    def load(self, format, stream):
        if self.list_type is not None:
            count = int(self.read_format(format, 1, self.list_type, stream)[0])
            return self.read_format(format, count, self.numeric_type, stream)
        else:
            return self.read_format(format, 1, self.numeric_type, stream)[0]


class ObjectSpec:
    __slots__ = ("specs",)

    def __init__(self):
        # A list of element_specs
        self.specs = []

    def load(self, format, stream):
        return {
            i.name: [
                i.load(format, stream) for j in range(i.count)
            ]
            for i in self.specs
        }


def read(filepath):
    import re

    format = b''
    texture = b''
    version = b'1.0'
    format_specs = {
        b'binary_little_endian': '<',
        b'binary_big_endian': '>',
        b'ascii': b'ascii',
    }
    type_specs = {
        b'char': 'b',
        b'uchar': 'B',
        b'int8': 'b',
        b'uint8': 'B',
        b'int16': 'h',
        b'uint16': 'H',
        b'short': 'h',
        b'ushort': 'H',
        b'int': 'i',
        b'int32': 'i',
        b'uint': 'I',
        b'uint32': 'I',
        b'float': 'f',
        b'float32': 'f',
        b'float64': 'd',
        b'double': 'd',
        b'string': 's',
    }
    obj_spec = ObjectSpec()
    invalid_ply = (None, None, None)

    with open(filepath, 'rb') as plyf:
        signature = plyf.peek(5)

        if not signature.startswith(b'ply') or not len(signature) >= 5:
            print("Signature line was invalid")
            return invalid_ply

        custom_line_sep = None
        if signature[3] != ord(b'\n'):
            if signature[3] != ord(b'\r'):
                print("Unknown line separator")
                return invalid_ply
            if signature[4] == ord(b'\n'):
                custom_line_sep = b"\r\n"
            else:
                custom_line_sep = b"\r"

        # Work around binary file reading only accepting "\n" as line separator.
        plyf_header_line_iterator = lambda plyf: plyf
        if custom_line_sep is not None:
            def _plyf_header_line_iterator(plyf):
                buff = plyf.peek(2**16)
                while len(buff) != 0:
                    read_bytes = 0
                    buff = buff.split(custom_line_sep)
                    for line in buff[:-1]:
                        read_bytes += len(line) + len(custom_line_sep)
                        if line.startswith(b'end_header'):
                            # Since reader code might (will) break iteration at this point,
                            # we have to ensure file is read up to here, yield, amd return...
                            plyf.read(read_bytes)
                            yield line
                            return
                        yield line
                    plyf.read(read_bytes)
                    buff = buff[-1] + plyf.peek(2**16)
            plyf_header_line_iterator = _plyf_header_line_iterator

        valid_header = False
        for line in plyf_header_line_iterator(plyf):
            tokens = re.split(br'[ \r\n]+', line)

            if len(tokens) == 0:
                continue
            if tokens[0] == b'end_header':
                valid_header = True
                break
            elif tokens[0] == b'comment':
                if len(tokens) < 2:
                    continue
                elif tokens[1] == b'TextureFile':
                    if len(tokens) < 4:
                        print("Invalid texture line")
                    else:
                        texture = tokens[2]
                continue

            elif tokens[0] == b'obj_info':
                continue
            elif tokens[0] == b'format':
                if len(tokens) < 3:
                    print("Invalid format line")
                    return invalid_ply
                if tokens[1] not in format_specs:
                    print("Unknown format", tokens[1])
                    return invalid_ply
                try:
                    version_test = float(tokens[2])
                except Exception as ex:
                    print("Unknown version", ex)
                    version_test = None
                if version_test != float(version):
                    print("Unknown version", tokens[2])
                    return invalid_ply
                del version_test
                format = tokens[1]
            elif tokens[0] == b'element':
                if len(tokens) < 3:
                    print("Invalid element line")
                    return invalid_ply
                obj_spec.specs.append(ElementSpec(tokens[1], int(tokens[2])))
            elif tokens[0] == b'property':
                if not len(obj_spec.specs):
                    print("Property without element")
                    return invalid_ply
                if tokens[1] == b'list':
                    obj_spec.specs[-1].properties.append(PropertySpec(tokens[4], type_specs[tokens[2]], type_specs[tokens[3]]))
                else:
                    obj_spec.specs[-1].properties.append(PropertySpec(tokens[2], None, type_specs[tokens[1]]))
        if not valid_header:
            print("Invalid header ('end_header' line not found!)")
            return invalid_ply

        obj = obj_spec.load(format_specs[format], plyf)

    return obj_spec, obj, texture


def load_ply_mesh(filepath, ply_name):
    import bpy

    obj_spec, obj, texture = read(filepath)
    # XXX28: use texture
    if obj is None:
        print("Invalid file")
        return

    uvindices = colindices = None
    colmultiply = None
    normals = False

    # Read the file
    for el in obj_spec.specs:
        if el.name == b'vertex':
            vindices_x, vindices_y, vindices_z = el.index(b'x'), el.index(b'y'), el.index(b'z')
            uvindices = (el.index(b's'), el.index(b't'))
            if -1 in uvindices:
                uvindices = None
            # ignore alpha if not present
            if el.index(b'alpha') == -1:
                colindices = el.index(b'red'), el.index(b'green'), el.index(b'blue')
            else:
                colindices = el.index(b'red'), el.index(b'green'), el.index(b'blue'), el.index(b'alpha')
            if -1 in colindices:
                if any(idx > -1 for idx in colindices):
                    print("Warning: At least one obligatory color channel is missing, ignoring vertex colors.")
                colindices = None
            else:  # if not a float assume uchar
                colmultiply = [1.0 if el.properties[i].numeric_type in {'f', 'd'} else (1.0 / 255.0) for i in colindices]

        #elif el.name == b'face':
            #findex = el.index(b'vertex_indices')
        #elif el.name == b'tristrips':
          #  trindex = el.index(b'vertex_indices')
        #elif el.name == b'edge':
            #eindex1, eindex2 = el.index(b'vertex1'), el.index(b'vertex2')

    #mesh_faces = []
    mesh_uvs = []
    mesh_colors = []
    
    verts = obj[b'vertex']
    
    ################## ITS ALL IN THE verts OBJECT 
    # [0] = x pos
    # [1] = y pos
    # [2] = z pos
    # [3] = x norm      * If present
    # [4] = y norm
    # [5] = z norm
    # [6] = r color     * Will start at [3] if no normals found
    # [7] = g color
    # [8] = b color
    # [9] = a color
    
    # If len(verts[0]) is greater than 7, we have normals
    vertinfo = len(verts[0])
    if vertinfo > 7:
        normals = True
    
    # Copy the positions
    mesh = bpy.data.meshes.new(name=ply_name)
    mesh.vertices.add(len(obj[b'vertex']))
    mesh.vertices.foreach_set("co", [a for v in obj[b'vertex'] for a in (v[vindices_x], v[vindices_y], v[vindices_z])])

    # Create our new object here
    for ob in bpy.context.selected_objects:
        ob.select_set(False)
    obj = bpy.data.objects.new(ply_name, mesh)
    bpy.context.collection.objects.link(obj)
    bpy.context.view_layer.objects.active = obj
    obj.select_set(True)
  
    # If colors are found, create a new Attribute 'Col' to hold them (NOT the Vertex_Color block!)
    
    if colindices: 
        # Create new Attribute 'Col' to hold the color data
        bpy.context.active_object.data.attributes.new(name="Col", type='FLOAT_COLOR', domain='POINT')
        newcolor = bpy.context.active_object.data
        # If there are no normals, the color data will start at [3], otherwise [6]
        for i, col in enumerate(verts):
            if normals == False:
                if len(colindices) == 3:
                    newcolor.attributes['Col'].data[i].color[0] = (verts[i][3]) / 255.0
                    newcolor.attributes['Col'].data[i].color[1] = (verts[i][4]) / 255.0
                    newcolor.attributes['Col'].data[i].color[2] = (verts[i][5]) / 255.0
                else: 
                    newcolor.attributes['Col'].data[i].color[0] = (verts[i][3]) / 255.0
                    newcolor.attributes['Col'].data[i].color[1] = (verts[i][4]) / 255.0
                    newcolor.attributes['Col'].data[i].color[2] = (verts[i][5]) / 255.0
                    newcolor.attributes['Col'].data[i].color[3] = (verts[i][6]) / 255.0
            elif normals == True:        
                if len(colindices) == 3:
                    newcolor.attributes['Col'].data[i].color[0] = (verts[i][6]) / 255.0
                    newcolor.attributes['Col'].data[i].color[1] = (verts[i][7]) / 255.0
                    newcolor.attributes['Col'].data[i].color[2] = (verts[i][8]) / 255.0
                else: 
                    newcolor.attributes['Col'].data[i].color[0] = (verts[i][6]) / 255.0
                    newcolor.attributes['Col'].data[i].color[1] = (verts[i][7]) / 255.0
                    newcolor.attributes['Col'].data[i].color[2] = (verts[i][8]) / 255.0
                    newcolor.attributes['Col'].data[i].color[3] = (verts[i][9]) / 255.0
            
    mesh.update()
    mesh.validate()

    # Left from stock importer
    if texture and uvindices:
        pass
        # TODO add support for using texture.

        # import os
        # import sys
        # from bpy_extras.image_utils import load_image

        # encoding = sys.getfilesystemencoding()
        # encoded_texture = texture.decode(encoding=encoding)
        # name = bpy.path.display_name_from_filepath(texture)
        # image = load_image(encoded_texture, os.path.dirname(filepath), recursive=True, place_holder=True)

        # if image:
        #     texture = bpy.data.textures.new(name=name, type='IMAGE')
        #     texture.image = image

        #     material = bpy.data.materials.new(name=name)
        #     material.use_shadeless = True

        #     mtex = material.texture_slots.add()
        #     mtex.texture = texture
        #     mtex.texture_coords = 'UV'
        #     mtex.use_map_color_diffuse = True

        #     mesh.materials.append(material)
        #     for face in mesh.uv_textures[0].data:
        #         face.image = image

    return mesh


def load_ply(filepath):
    import time
    import bpy
    import numpy

    t = time.time()
    ply_name = bpy.path.display_name_from_filepath(filepath)

    mesh = load_ply_mesh(filepath, ply_name)
    if not mesh:
        return {'CANCELLED'}

    print("\nSuccessfully imported %r in %.3f sec" % (filepath, time.time() - t))

    return {'FINISHED'}


def load(operator, context, filepath=""):
    return load_ply(filepath)
  1. Import the cloud: File > Import > Stanford.ply

Create a shader with the Col attribute and add geometry nodes with this shader ...

enter image description here

$\endgroup$
6
  • $\begingroup$ Hey there, we discourage links as answers. Please take the tour $\endgroup$ Mar 19, 2022 at 12:52
  • $\begingroup$ @josephhansen, The tour says about the links "...encouraged, but..." hm. $\endgroup$ Mar 19, 2022 at 14:45
  • $\begingroup$ @vklidu, I do not know how to improve the answer anymore, thanks. $\endgroup$ Mar 19, 2022 at 14:45
  • 1
    $\begingroup$ @DeoneaseDistortions Thanks ... that is fine ... alternatively, but not necesarilly you can paste also the code (in this case) ... $\endgroup$
    – vklidu
    Mar 19, 2022 at 15:28
  • 1
    $\begingroup$ @vklidu from the edit history I see your point. I'll delete all of my comments prior to this one. $\endgroup$ Mar 19, 2022 at 16:41

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .