Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extended vox format not supported #6

Open
destrosvet opened this issue Apr 17, 2018 · 16 comments
Open

Extended vox format not supported #6

destrosvet opened this issue Apr 17, 2018 · 16 comments

Comments

@destrosvet
Copy link

After import *.vox nothing happend and got "Unknown Chunk id nTRN" in console.

@atomhax
Copy link

atomhax commented Nov 1, 2018

@destrosvet I ran into the same issue. It's because the file format of MagicaVoxel has undergone some revisions to allow for a world editor and this importer doesn't know how to handle that data. Additionally the old material data chunks were deprecated and a new MATL chunk is being used. A workaround I used was to google for an older version of MagicaVoxel with a compatible file version.

@RichysHub
Copy link
Owner

Apologies for the delayed response.
I didn't know about the format change, so thank you for bringing that to my attention.

Current solution would be as @atomhax suggests; using an old version of MagicaVoxel, until I get around to updating this importer.

@wizardgsz
Copy link
Contributor

@destrosvet, could you please attach a sample data that you can't load?
@atomhax, have you got a file using MATL chuck?

Thanks for your help

@wizardgsz
Copy link
Contributor

About new releases chunks, we have: nTRN, nGRP, nGRP, nSHP, rOBJ, LAYR, MATL

@destrosvet, @atomhax, just to "skip" them and keep loading new VOX files (here there is an example using them chr_sword NEW.zip):

            elif name == 'nTRN':
                vox.read(s_self)
            elif name == 'nGRP':
                vox.read(s_self)
            elif name == 'nGRP':
                vox.read(s_self)
            elif name == 'MATL':
                vox.read(s_self)
            elif name == 'LAYR':
                vox.read(s_self)
            elif name == 'rOBJ':
                vox.read(s_self)

Right before the "throw an error if unknow chunk" part:

            else:
                # Any other chunk, we don't know how to handle
                # This puts us out-of-step
                print('Unknown Chunk id {}'.format(name))
                return {'CANCELLED'}

See also: MagicaVoxel-file-format-vox-extension.txt

@wizardgsz
Copy link
Contributor

wizardgsz commented Oct 2, 2019

Sample data, for example: chr_sword NEW.zip

Parser snippet for new chunks (just some random thoughts to keep track of them):

def read_STRING(vox):
    size, = struct.unpack('<i', vox.read(4))
    str = vox.read(size)
    str = str.decode('utf-8')

    return str


def read_DICT(vox):
    num_of_key_value_pairs, = struct.unpack('<i', vox.read(4))

    if num_of_key_value_pairs > 0:
        print("\tkeys:", num_of_key_value_pairs)

        for i in range(num_of_key_value_pairs):
            str1 = read_STRING(vox)
            str2 = read_STRING(vox)

            print("\tkey-value: ('{}', '{}')".format(str1, str2))


def read_ROTATION(vox):
    # store a row-major rotation in the bits of a byte
    print("TODO")

As usual, in main loop:

            elif name == 'nTRN':
                node_id, = struct.unpack('<i', vox.read(4))
                print("nTRN({}):".format(node_id))
                read_DICT(vox)
                child_node_id, = struct.unpack('<i', vox.read(4))
                reserved_id, = struct.unpack('<i', vox.read(4))
                layer_id, = struct.unpack('<i', vox.read(4))
                num_of_frames, = struct.unpack('<i', vox.read(4))
                assert(reserved_id == -1)   # sanity check
                assert(num_of_frames == 1)  # sanity check

                for i in range(num_of_frames):
                    # for each frame
                    read_DICT(vox)

            elif name == 'nGRP':
                node_id, = struct.unpack('<i', vox.read(4))
                print("nGRP({}):".format(node_id))
                read_DICT(vox)
                children, = struct.unpack('<i', vox.read(4))

                for i in range(children):
                    # for each child
                    child_node_id, = struct.unpack('<i', vox.read(4))
                    print("\tchild_node_id:", str(child_node_id))

            elif name == 'nSHP':
                node_id, = struct.unpack('<i', vox.read(4))
                print("nSHP({}):".format(node_id))
                read_DICT(vox)
                children, = struct.unpack('<i', vox.read(4))
                assert(children== 1)  # sanity check

                for i in range(children):
                    # for each child
                    child_node_id, = struct.unpack('<i', vox.read(4))
                    print("\tchild_node_id:", str(child_node_id))
                    read_DICT(vox)

            elif name == 'MATL':
                # material
                matl_id, = struct.unpack('<i', vox.read(4))
                print("MATL({}):".format(matl_id))
                read_DICT(vox)
            elif name == 'LAYR':
                layr_id, = struct.unpack('<i', vox.read(4))
                print("LAYR({}):".format(layr_id))
                read_DICT(vox)
                reserved_id, = struct.unpack('<i', vox.read(4))
                assert(reserved_id == -1)  # sanity check
            elif name == 'rOBJ':
                #robj_id, = struct.unpack('<i', vox.read(4))
                print("rOBJ:")
                read_DICT(vox)
            else:
                ...................

p.s. is it possible to change the issue title?

@RichysHub RichysHub changed the title not working in 2.79b Extended vox format not supported Oct 2, 2019
@RichysHub
Copy link
Owner

Fantastic work so far 👏

I have updated issue title.

@wizardgsz
Copy link
Contributor

wizardgsz commented Oct 3, 2019

Always for the "random thoughts" series... about rOBJ chunk:

I think MagicaVoxel uses them to store some additional data we can skip in Blender, e.g.

View menu options?

Parsing chunk 'rOBJ':
rOBJ:
        keys: 3
        key-value: ('_type', '_ground')
        key-value: ('_color', '80 80 80')
        key-value: ('_enable', '1')
Reading at dec: 39677 hex: 9AFD
Parsing chunk 'rOBJ':
rOBJ:
        keys: 3
        key-value: ('_type', '_bg')
        key-value: ('_color', '0 0 0')
        key-value: ('_enable', '0')
Reading at dec: 39744 hex: 9B40
Parsing chunk 'rOBJ':
rOBJ:
        keys: 4
        key-value: ('_type', '_edge')
        key-value: ('_color', '0 0 0')
        key-value: ('_width', '0.2')
        key-value: ('_enable', '0')
Reading at dec: 39830 hex: 9B96
Parsing chunk 'rOBJ':
rOBJ:
        keys: 6
        key-value: ('_type', '_grid')
        key-value: ('_color', '0 0 0')
        key-value: ('_spacing', '1')
        key-value: ('_width', '0.05')
        key-value: ('_display', '0')
        key-value: ('_enable', '0')
Reading at dec: 39951 hex: 9C0F

Rendering mode: path tracking renderer, camera, bloom and light info?

Reading at dec: 38948 hex: 9824
Parsing chunk 'rOBJ':
rOBJ:
        keys: 5
        key-value: ('_type', '_inf')
        key-value: ('_i', '0.6')
        key-value: ('_k', '255 255 255')
        key-value: ('_angle', '50 50')
        key-value: ('_area', '0.07')
Reading at dec: 39051 hex: 988B
Parsing chunk 'rOBJ':
rOBJ:
        keys: 3
        key-value: ('_type', '_uni')
        key-value: ('_i', '0.7')
        key-value: ('_k', '255 255 255')
Reading at dec: 39118 hex: 98CE
Parsing chunk 'rOBJ':
rOBJ:
        keys: 3
        key-value: ('_type', '_rayleigh')
        key-value: ('_d', '0.4')
        key-value: ('_k', '77 153 255')
Reading at dec: 39189 hex: 9915
Parsing chunk 'rOBJ':
rOBJ:
        keys: 4
        key-value: ('_type', '_mie')
        key-value: ('_d', '0.4')
        key-value: ('_k', '255 255 255')
        key-value: ('_g', '0.78')
Reading at dec: 39270 hex: 9966
Parsing chunk 'rOBJ':
rOBJ:
        keys: 4
        key-value: ('_type', '_fog')
        key-value: ('_d', '0.2')
        key-value: ('_k', '255 255 255')
        key-value: ('_enable', '0')
Reading at dec: 39353 hex: 99B9
Parsing chunk 'rOBJ':
rOBJ:
        keys: 9
        key-value: ('_type', '_len')
        key-value: ('_fov', '45')
        key-value: ('_dof', '0.25')
        key-value: ('_exp', '0')
        key-value: ('_vig', '0')
        key-value: ('_sg', '0')
        key-value: ('_gam', '2.2')
        key-value: ('_blade_n', '0')
        key-value: ('_blade_r', '0')
Reading at dec: 39503 hex: 9A4F
Parsing chunk 'rOBJ':
rOBJ:
        keys: 5
        key-value: ('_type', '_bloom')
        key-value: ('_mix', '0.5')
        key-value: ('_scale', '0')
        key-value: ('_aspect', '0')
        key-value: ('_threshold', '1')
Reading at dec: 39603 hex: 9AB3

Credits go to Interface · MagicaVoxel User Reference Manual

@RichysHub
Copy link
Owner

Oh wow.
That's some great detective work.

I'm happy for all that detail to be skipped, though I suppose there is the possibility of including them in the future.

The ground color, lighting, and camera position all jump out to me as something potentially useful. These would be fairly easy to replicate in Blender too, I would imagine.

@wizardgsz
Copy link
Contributor

wizardgsz commented Oct 4, 2019

Yeah, but my doubt is:

Is it better to work with overall scene setup, animations, camera, lights, global parameters (like fov, fog, ...) directly in Blender, after importing objects I mean, or in MagicaVoxel editor importing those settings?

Anyhow, now we briefly know info about other chunks and we can handle them (or just skip) if needed. My personal opinion about new chunks (in some sort of "priority" order for next implementations):

  • parse MATT (you got it already, so we can start from your old Materials branch)
  • parse MATL (both "matter" chunks should share a common data-structure for later handling)
  • handle nTRN (i.e. roto-translations and maybe keyframing)
  • parse nGRP and nSHP for grouping
  • skip rOBJ

Thanks again!

@RichysHub
Copy link
Owner

RichysHub commented Oct 4, 2019

Absolutely, I don't think they should be a priority in any sense.
More that it's just cool to know that we could import them, should we ever deem that a nice feature.

@wizardgsz
Copy link
Contributor

wizardgsz commented Oct 4, 2019

Starting from your Materials branch, I'm going to implement MATT and MATL here.

Still immature for a pull request, but if you have time to review and give me some hints... especially, as I am not a Python programmer, I'm afraid of making mistakes.
I am using your "named tuple" structure for both chunks (the material_spec class).
My first goal: load and store Matter properties first. Use them and create Materials in Blender, then.

Have a nice weekend. Bye bye

@wizardgsz
Copy link
Contributor

To supply "default values" to namedtuples, I defined a new class:

# http://mmabrouk.github.io/python/2014/05/23/namedtuples-with-default-optional-arguments/
class material_spec(namedtuple('material_spec', [               # Material chunks, both MATT and MATL
                                            'type_',            # Matter type: _diffuse, _metal, _glass, _emit
                                            'weight',           #
                                            'Plastic',          #
                                            'Roughness',        #
                                            'Specular',         #
                                            'IOR',              # Index of refraction
                                            'Attenuation',      #
                                            'Power',            #
                                            'Glow',             #
                                            'isTotalPower'      # Unused?
                                            ])):
    def __new__(cls,
                # TODO: Default values?
                type_ = "_diffuse",
                weight = 1.0,
                Plastic = 0.0,
                Roughness = 0.5,
                Specular = 0.5,
                IOR = 1.45,
                Attenuation = 0.0,
                Power = 0.0,
                Glow = 0.0,
                isTotalPower = True
                ):
        return super(material_spec, cls).__new__(cls,
                type_,
                weight,
                Plastic,
                Roughness,
                Specular,
                IOR,
                Attenuation,
                Power,
                Glow,
                isTotalPower
                )

@RichysHub
Copy link
Owner

That linked page is quite dated.
Python 3.7 (which Blender 2.80 uses) added an additional argument to namedtuple, specifically to handle default arguments.
https://docs.python.org/3.7/library/collections.html#collections.namedtuple

material_spec = namedtuple('material_spec', [                   # Material chunks, both MATT and MATL
                                            'type_',            # Matter type: _diffuse, _metal, _glass, _emit
                                            'weight',           #
                                            'Plastic',          #
                                            'Roughness',        #
                                            'Specular',         #
                                            'IOR',              # Index of refraction
                                            'Attenuation',      #
                                            'Power',            #
                                            'Glow',             #
                                            'isTotalPower'      # Unused?
                                            ], 
                                            defaults = [    # TODO: Default values?
                                            "_diffuse",     # type_
                                            1.0,            # weight
                                            0.0,            # Plastic
                                            0.5,            # Roughness
                                            0.5,            # Specular
                                            1.45,           # IOR
                                            0.0,            # Attenuation
                                            0.0,            # Power
                                            0.0,            # Glow
                                            True            # isTotalPower
                                            ]
)

Expanded out here, for the sake of commenting.

@wizardgsz
Copy link
Contributor

wizardgsz commented Oct 5, 2019

Oh, thanks, very nice. But I read that tuples are "immutable", so I cannot modify values later.

Here it is the full updated script: https://github.com/wizardgsz/MagicaVoxel-VOX-importer/blob/MATT_MATL/io_scene_vox.py

Having spec as "named tuple", I have to use the _replace method.
I read about recordtype, a mutable type, but it seems Blender does not support it.

spec = material_spec(type_=material_types[matt_type], weight=weight)
..

# For each property, for example Power:
value = ...read it from file (for MATT), or from DICT structure (from MATL)
spec = spec._replace(Power = value)

That is:

def read_MATT(vox, material_specs):
    matt_id, matt_type, weight = struct.unpack('<iif', vox.read(12))
    print("MATT({}):".format(matt_id))

    material_types = ["_diffuse", "_metal", "_glass", "_emit"]

    prop_bits, = struct.unpack('<i', vox.read(4))
    binary = bin(prop_bits)

    spec = material_spec(type_=material_types[matt_type], weight=weight)

    if prop_bits & 1:
        value, = struct.unpack('<f', vox.read(4))
        spec = spec._replace(Plastic = value)
    if prop_bits & 2:
        value, = struct.unpack('<f', vox.read(4))
        spec = spec._replace(Roughness = value)
    if prop_bits & 4:
        value, = struct.unpack('<f', vox.read(4))
        spec = spec._replace(Specular = value)
    if prop_bits & 8:
        value, = struct.unpack('<f', vox.read(4))
        spec = spec._replace(IOR = value)
    if prop_bits & 16:
        value, = struct.unpack('<f', vox.read(4))
        spec = spec._replace(Attenuation = value)
    if prop_bits & 32:
        value, = struct.unpack('<f', vox.read(4))
        spec = spec._replace(Power = value)
    if prop_bits & 64:
        value, = struct.unpack('<f', vox.read(4))
        spec = spec._replace(Glow = value)
    if prop_bits & 128:
        value, struct.unpack('<f', vox.read(4))

    # isTotalPower is never supplied by this
    material_specs.update({matt_id: spec})

    return spec


def read_MATL(vox, material_specs):
    matl_id, = struct.unpack('<i', vox.read(4))
    print("MATL({}):".format(matl_id))

    # TODO: Error handling for missing properties?
    properties = read_DICT(vox)

    matl_type = properties['_type']
    weight = float(properties['_weight'])

    spec = material_spec(type_=matl_type, weight=weight)

    if '_plastic' in properties:
        value = float(properties['_plastic'])
        spec = spec._replace(Plastic = value)
    if '_rough' in properties:
        value = float(properties['_rough'])
        spec = spec._replace(Roughness = value)
    if '_spec' in properties:
        value = float(properties['_spec'])
        spec = spec._replace(Specular = value)
    if '_ior' in properties:
        value = float(properties['_ior'])
        spec = spec._replace(IOR = value)
    if '_att' in properties:
        value = float(properties['_att'])
        spec = spec._replace(Attenuation = value)
    Power = 1.0
    if '_glow' in properties:
        value = float(properties['_glow'])
        spec = spec._replace(Glow = value)

    # isTotalPower is never supplied by this
    material_specs.update({matl_id: spec})

    return spec

@RichysHub
Copy link
Owner

Hmm. Honestly, it might just be easier to use a bare dictionary, and forgo the namedtuple plan altogether.
For all the work that's going in to work around namedtuple, is it really giving any tangible benefit?

@wizardgsz
Copy link
Contributor

wizardgsz commented Oct 16, 2019

About MATT chunk and isTotalPower flag (property bit(7) set to 1): I just discovered that sometimes, but not always, MagicaVoxels adds 4 bytes for it!

It is better to save current offset and skip entirely the chunk at the end:

def read_MATT(vox, material_specs, s_self):
    # Get the current position of the file
    offset = vox.tell()

    ...
    ...

    vox.seek(offset)
    vox.read(s_self)

    material_specs.update({matt_id: spec})

    return spec

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants