Exemple #1
0
def convert_rsm(rsm_file: str,
                data_folder: str = "data",
                glb: bool = False) -> None:
    """
    Converts an RSM file to glTF 2.0
    Parameters
    ----------
    rsm_file : string
        Path to the RSM file to convert

    data_folder : string
        Path to the data folder containing texture files

    glb : boolean
        Export as GLB (single binary file)
    """
    logging.basicConfig(level=logging.INFO)

    _LOGGER.info(f"Converting RSM file '{rsm_file}'")
    rsm_file_path = Path(rsm_file)
    try:
        rsm_obj = _parse_rsm_file(rsm_file_path)
    except FileNotFoundError:
        _LOGGER.error(f"'{rsm_file_path}' isn't a file or doesn't exist")
        sys.exit(1)
    except ValidationNotEqualError as ex:
        _LOGGER.error(f"Invalid RSM file: {ex}")
        sys.exit(1)

    gltf_model = GLTFModel(
        asset=Asset(version='2.0', generator="rag2gltf"),
        samplers=[
            Sampler(
                magFilter=9729,  # LINEAR
                minFilter=9987,  # LINEAR_MIPMAP_LINEAR
                wrapS=33071,  # CLAMP_TO_EDGE
                wrapT=33071  # CLAMP_TO_EDGE
            )
        ],
        nodes=[],
        meshes=[],
        buffers=[],
        bufferViews=[],
        accessors=[],
        images=[],
        textures=[],
        materials=[])

    gltf_resources: List[FileResource] = []
    _LOGGER.info("Converting textures ...")
    try:
        resources, tex_id_by_node = _convert_textures(rsm_obj,
                                                      Path(data_folder),
                                                      gltf_model)
    except FileNotFoundError as ex:
        _LOGGER.error(f"Cannot find texture file: {ex}")
        sys.exit(1)
    gltf_resources += resources

    _LOGGER.info("Converting 3D model ...")
    nodes = extract_nodes(rsm_obj)
    resources, root_nodes = _convert_nodes(rsm_obj.version, nodes,
                                           tex_id_by_node, gltf_model)
    gltf_model.scenes = [Scene(nodes=root_nodes)]
    gltf_resources += resources

    # Convert animations
    if rsm_obj.version >= 0x202:
        fps = rsm_obj.frame_rate_per_second
    else:
        fps = None

    _LOGGER.info("Converting animations ...")
    resources = _convert_animations(rsm_obj.version, fps, nodes, gltf_model)
    gltf_resources += resources

    if glb:
        destination_path = rsm_file_path.with_suffix(".glb").name
    else:
        destination_path = rsm_file_path.with_suffix(".gltf").name

    gltf = GLTF(model=gltf_model, resources=gltf_resources)
    gltf.export(destination_path)
    _LOGGER.info(f"Converted model has been saved as '{destination_path}'")

    sys.exit()
Exemple #2
0
def export_gltf(icon, filename, metadata=None):
    basename = PurePath(filename).stem

    vertex_info_format = ("3f" * icon.animation_shapes) + "3f 2f 3f"
    float_size = struct.calcsize("f")
    animation_speed = 0.1

    animation_present = icon.animation_shapes > 1

    model_data = bytearray()

    mins = {}
    maxs = {}

    for i, vertex in enumerate(icon.vertices):
        for j, position in enumerate(vertex.positions):
            if j == 0:
                values_basis = [
                    position.x / 4096, -position.y / 4096, -position.z / 4096
                ]
                values = values_basis
            else:
                # Subtract basis position to compensate for shape keys being relative to basis
                values = [
                    position.x / 4096 - values_basis[0],
                    -position.y / 4096 - values_basis[1],
                    -position.z / 4096 - values_basis[2]
                ]

            if j not in mins:
                mins[j] = values.copy()
            else:
                if values[0] < mins[j][0]: mins[j][0] = values[0]
                if values[1] < mins[j][1]: mins[j][1] = values[1]
                if values[2] < mins[j][2]: mins[j][2] = values[2]

            if j not in maxs:
                maxs[j] = values.copy()
            else:
                if values[0] > maxs[j][0]: maxs[j][0] = values[0]
                if values[1] > maxs[j][1]: maxs[j][1] = values[1]
                if values[2] > maxs[j][2]: maxs[j][2] = values[2]

            model_data.extend(struct.pack("3f", *values))

        model_data.extend(
            struct.pack("3f 2f 3f", vertex.normal.x / 4096,
                        -vertex.normal.y / 4096, -vertex.normal.z / 4096,
                        1.0 - (vertex.tex_coord.u / 4096),
                        1.0 - (vertex.tex_coord.v / 4096),
                        vertex.color.r / 255, vertex.color.g / 255,
                        vertex.color.b / 255))

    # Generate animation data if multiple animation shapes are present

    if animation_present:
        animation_offset = len(model_data)

        for i in range(icon.frame_count + 1):
            model_data.extend(struct.pack("f", i * animation_speed))

        for i, frame in enumerate(icon.frames + [icon.frames[0]]):
            segment = [struct.pack("f", 0.0)] * (icon.animation_shapes - 1)

            if frame.shape_id != 0:
                segment[frame.shape_id - 1] = struct.pack("f", 1.0)

            for item in segment:
                model_data.extend(item)

        animation_length = len(model_data) - animation_offset

    # Generate texture

    if isinstance(icon.texture, Ps2ico.CompressedTexture):
        image_data = convert_compressed_texture_data(icon.texture.size,
                                                     icon.texture.data)
    elif isinstance(icon.texture, Ps2ico.UncompressedTexture):
        image_data = convert_uncompressed_texture_data(icon.texture.data)

    with BytesIO() as png:
        PILImage.frombytes("RGB", (128, 128), image_data).save(png, "png")
        texture_data = png.getvalue()

    # Basic glTF info

    model = GLTFModel()

    model.asset = Asset(version="2.0", generator=f"ico2gltf v{VERSION}")

    model.scenes = [Scene(nodes=[0])]

    model.scene = 0

    model.nodes = [Node(mesh=0)]

    # If present, embed metadata

    if metadata:
        # Normalize title: turn japanese full-width characters into normal ones and insert the line break
        title = unicodedata.normalize("NFKC", metadata.title).rstrip("\x00")
        title = title[:metadata.offset_2nd_line //
                      2] + "\n" + title[metadata.offset_2nd_line // 2:]

        model.extras = {
            "title":
            title,
            "background_opacity":
            metadata.bg_opacity / 0x80,
            "background_bottom_left_color": [
                metadata.bg_color_lowerleft.r / 0x80,
                metadata.bg_color_lowerleft.g / 0x80,
                metadata.bg_color_lowerleft.b / 0x80,
                metadata.bg_color_lowerleft.a / 0x80
            ],
            "background_bottom_right_color": [
                metadata.bg_color_lowerright.r / 0x80,
                metadata.bg_color_lowerright.g / 0x80,
                metadata.bg_color_lowerright.b / 0x80,
                metadata.bg_color_lowerright.a / 0x80
            ],
            "background_top_left_color": [
                metadata.bg_color_upperleft.r / 0x80,
                metadata.bg_color_upperleft.g / 0x80,
                metadata.bg_color_upperleft.b / 0x80,
                metadata.bg_color_upperleft.a / 0x80
            ],
            "background_top_right_color": [
                metadata.bg_color_upperright.r / 0x80,
                metadata.bg_color_upperright.g / 0x80,
                metadata.bg_color_upperright.b / 0x80,
                metadata.bg_color_lowerright.a / 0x80
            ],
            "ambient_color": [
                metadata.light_ambient_color.r, metadata.light_ambient_color.g,
                metadata.light_ambient_color.b
            ],
            "light1_direction": [
                metadata.light1_direction.x, metadata.light1_direction.y,
                metadata.light1_direction.z
            ],
            "light1_color": [
                metadata.light1_color.r, metadata.light1_color.g,
                metadata.light1_color.b, metadata.light1_color.a
            ],
            "light2_direction": [
                metadata.light2_direction.x, metadata.light2_direction.y,
                metadata.light2_direction.z
            ],
            "light2_color": [
                metadata.light2_color.r, metadata.light2_color.g,
                metadata.light2_color.b, metadata.light2_color.a
            ],
            "light3_direction": [
                metadata.light3_direction.x, metadata.light3_direction.y,
                metadata.light3_direction.z
            ],
            "light3_color": [
                metadata.light3_color.r, metadata.light3_color.g,
                metadata.light3_color.b, metadata.light3_color.a
            ],
        }

    # Meshes

    primitive = Primitive(attributes=Attributes(
        POSITION=0,
        NORMAL=icon.animation_shapes,
        TEXCOORD_0=icon.animation_shapes + 1,
        COLOR_0=icon.animation_shapes + 2),
                          material=0)

    if animation_present:
        primitive.targets = [{
            "POSITION": i + 1
        } for i in range(icon.animation_shapes - 1)]

    model.meshes = [Mesh(name="Icon", primitives=[primitive])]

    # Buffers

    model.buffers = [
        Buffer(uri=f"{basename}.bin", byteLength=len(model_data)),
        Buffer(uri=f"{basename}.png", byteLength=len(texture_data))
    ]

    # Materials

    model.images = [Image(bufferView=1, mimeType="image/png")]

    model.textures = [Texture(source=0)]

    model.materials = [
        Material(name="Material",
                 pbrMetallicRoughness=PBRMetallicRoughness(
                     baseColorTexture=TextureInfo(index=0),
                     roughnessFactor=1,
                     metallicFactor=0))
    ]

    # Animations

    if animation_present:
        model.animations = [
            Animation(name="Default",
                      samplers=[
                          AnimationSampler(
                              input=icon.animation_shapes + 3,
                              output=icon.animation_shapes + 4,
                              interpolation=Interpolation.LINEAR.value)
                      ],
                      channels=[
                          Channel(sampler=0,
                                  target=Target(node=0, path="weights"))
                      ]),
        ]

    # Buffer Views

    model.bufferViews = [
        BufferView(name="Data",
                   buffer=0,
                   byteStride=struct.calcsize(vertex_info_format),
                   byteLength=len(model_data)),
        BufferView(name="Texture", buffer=1, byteLength=len(texture_data)),
    ]

    if animation_present:
        model.bufferViews.append(
            BufferView(name="Animation",
                       buffer=0,
                       byteOffset=animation_offset,
                       byteLength=animation_length), )

    # Accessors

    model.accessors = [
        Accessor(name=f"Position {i}",
                 bufferView=0,
                 byteOffset=i * 3 * float_size,
                 min=mins[i],
                 max=maxs[i],
                 count=len(icon.vertices),
                 componentType=ComponentType.FLOAT.value,
                 type=AccessorType.VEC3.value)
        for i in range(icon.animation_shapes)
    ]

    model.accessors.extend([
        Accessor(name="Normal",
                 bufferView=0,
                 byteOffset=((icon.animation_shapes - 1) * 3 * float_size) +
                 3 * float_size,
                 count=len(icon.vertices),
                 componentType=ComponentType.FLOAT.value,
                 type=AccessorType.VEC3.value),
        Accessor(name="UV",
                 bufferView=0,
                 byteOffset=((icon.animation_shapes - 1) * 3 * float_size) +
                 6 * float_size,
                 count=len(icon.vertices),
                 componentType=ComponentType.FLOAT.value,
                 type=AccessorType.VEC2.value),
        Accessor(name="Color",
                 bufferView=0,
                 byteOffset=((icon.animation_shapes - 1) * 3 * float_size) +
                 8 * float_size,
                 count=len(icon.vertices),
                 componentType=ComponentType.FLOAT.value,
                 type=AccessorType.VEC3.value),
    ])

    if animation_present:
        model.accessors.extend([
            Accessor(name="Animation Time",
                     bufferView=2,
                     byteOffset=0,
                     min=[0.0],
                     max=[(icon.frame_count) * animation_speed],
                     count=(icon.frame_count + 1),
                     componentType=ComponentType.FLOAT.value,
                     type=AccessorType.SCALAR.value),
            Accessor(name="Animation Data",
                     bufferView=2,
                     byteOffset=(icon.frame_count + 1) * float_size,
                     min=[0.0],
                     max=[1.0],
                     count=(icon.frame_count + 1) *
                     (icon.animation_shapes - 1),
                     componentType=ComponentType.FLOAT.value,
                     type=AccessorType.SCALAR.value)
        ])

    resources = [
        FileResource(f"{basename}.bin", data=model_data),
        FileResource(f"{basename}.png", data=texture_data)
    ]

    gltf = GLTF(model=model, resources=resources)
    gltf.export(filename)