179 lines
6.2 KiB
Python
179 lines
6.2 KiB
Python
|
|
import argparse
|
||
|
|
import io
|
||
|
|
import struct
|
||
|
|
import typing
|
||
|
|
import itertools
|
||
|
|
from pathlib import Path
|
||
|
|
from dataclasses import dataclass
|
||
|
|
|
||
|
|
# region: Kernel
|
||
|
|
|
||
|
|
T = typing.TypeVar('T')
|
||
|
|
Vector = tuple[float, ...]
|
||
|
|
Indices = tuple[int, ...]
|
||
|
|
|
||
|
|
def get_file_length(fs: typing.BinaryIO) -> int:
|
||
|
|
"""
|
||
|
|
Get the full length of given file in bytes.
|
||
|
|
|
||
|
|
:param fs: File stream for measuring.
|
||
|
|
:return: File length in bytes.
|
||
|
|
"""
|
||
|
|
pos = fs.tell()
|
||
|
|
fs.seek(0, io.SEEK_END)
|
||
|
|
fsize = fs.tell()
|
||
|
|
fs.seek(pos, io.SEEK_SET)
|
||
|
|
return fsize
|
||
|
|
|
||
|
|
def evaluate_count(filename: Path, unit_size: int) -> int:
|
||
|
|
"""
|
||
|
|
Evaluate the count of items in given file.
|
||
|
|
|
||
|
|
:param filename: File name to evaluate.
|
||
|
|
:param unit_size: Size of each item in bytes.
|
||
|
|
:return: Count of items in given file.
|
||
|
|
"""
|
||
|
|
with open(filename, 'rb') as fs:
|
||
|
|
file_size = get_file_length(fs)
|
||
|
|
count, modrem = divmod(file_size, unit_size)
|
||
|
|
if modrem != 0:
|
||
|
|
raise Exception("invalid file length")
|
||
|
|
return count
|
||
|
|
|
||
|
|
def assert_file_size(fs: typing.BinaryIO, expected_size: int):
|
||
|
|
"""
|
||
|
|
Check whether given file has expected size.
|
||
|
|
|
||
|
|
:param fs: File stream to check.
|
||
|
|
:param expected_size: Expected file size.
|
||
|
|
"""
|
||
|
|
if expected_size != get_file_length(fs):
|
||
|
|
raise Exception("invalid file length")
|
||
|
|
|
||
|
|
def read_f32s(filename: Path, count: int) -> tuple[float, ...]:
|
||
|
|
with open(filename, 'rb') as fs:
|
||
|
|
# construct class
|
||
|
|
cstruct = struct.Struct(f'<{count}f')
|
||
|
|
# assert file size
|
||
|
|
assert_file_size(fs, cstruct.size)
|
||
|
|
# read
|
||
|
|
return cstruct.unpack(fs.read(cstruct.size))
|
||
|
|
|
||
|
|
def read_u16s(filename: Path, count: int) -> tuple[int, ...]:
|
||
|
|
with open(filename, 'rb') as fs:
|
||
|
|
# construct class
|
||
|
|
cstruct = struct.Struct(f'<{count}H')
|
||
|
|
# assert file size
|
||
|
|
assert_file_size(fs, cstruct.size)
|
||
|
|
# read
|
||
|
|
return cstruct.unpack(fs.read(cstruct.size))
|
||
|
|
|
||
|
|
def batched_tuple(full_list: tuple[T, ...], couple_count: int) -> tuple[tuple[T, ...], ...]:
|
||
|
|
"""
|
||
|
|
Batch a tuple into a tuple of tuples.
|
||
|
|
|
||
|
|
This function will check whether given tuple can be batched without any remnants.
|
||
|
|
If it is, throw exception, otherwise return the batched tuple.
|
||
|
|
|
||
|
|
For example, given `('roses', 'red', 'violets', 'blue', 'sugar', 'sweet')`,
|
||
|
|
it will produce `(('roses', 'red'), ('violets', 'blue'), ('sugar', 'sweet'))`.
|
||
|
|
|
||
|
|
:param full_list: The tuple to batch.
|
||
|
|
:param couple_count: The count of items in each batch.
|
||
|
|
:return: The batched tuple.
|
||
|
|
"""
|
||
|
|
# TODO: Replace the whole body with itertools.batched once we upgrade into Python 3.12
|
||
|
|
# return itertools.batched(full_list, couple_count, strict=True)
|
||
|
|
count, modrem = divmod(len(full_list), couple_count)
|
||
|
|
if modrem != 0:
|
||
|
|
raise Exception("invalid tuple length")
|
||
|
|
|
||
|
|
return tuple(map(lambda x: tuple(full_list[x * couple_count:x * couple_count + couple_count]), range(count)))
|
||
|
|
|
||
|
|
def build_obj_file(filename: Path, vpos: tuple[Vector, ...], vnml: tuple[Vector, ...], vuv: tuple[Vector, ...], findices: tuple[Indices, ...]):
|
||
|
|
with open(filename, 'w', encoding='utf-8') as fs:
|
||
|
|
for v in vpos:
|
||
|
|
fs.write(f'v {v[0]} {v[1]} {v[2]}\n')
|
||
|
|
for v in vnml:
|
||
|
|
fs.write(f'vn {v[0]} {v[1]} {v[2]}\n')
|
||
|
|
for v in vuv:
|
||
|
|
fs.write(f'vt {v[0]} {v[1]}\n')
|
||
|
|
for f in findices:
|
||
|
|
fs.write(f'f {f[0] + 1}/{f[0] + 1}/{f[0] + 1} {f[1] + 1}/{f[1] + 1}/{f[1] + 1} {f[2] + 1}/{f[2] + 1}/{f[2] + 1}\n')
|
||
|
|
fs.write('g obj\n')
|
||
|
|
|
||
|
|
# endregion
|
||
|
|
|
||
|
|
# region Command Line Processor
|
||
|
|
|
||
|
|
@dataclass
|
||
|
|
class Cli:
|
||
|
|
"""Command Line Arguments"""
|
||
|
|
|
||
|
|
in_vpos: Path
|
||
|
|
"""The path to file storing vertex positions"""
|
||
|
|
in_vnml: Path
|
||
|
|
"""The path to file storing vertex normals"""
|
||
|
|
in_vuv: Path
|
||
|
|
"""The path to file storing vertex UVs"""
|
||
|
|
in_findices: Path
|
||
|
|
"""The path to file storing face indices"""
|
||
|
|
out_obj: Path
|
||
|
|
"""The path to output OBJ file"""
|
||
|
|
out_mtl: Path
|
||
|
|
"""The path to output MTL file"""
|
||
|
|
|
||
|
|
def parse() -> Cli:
|
||
|
|
# construct parser
|
||
|
|
parser = argparse.ArgumentParser(description='The mesh data combinator for libcmo21 debugging.')
|
||
|
|
parser.add_argument('-p', '--in-vpos', required=True, type=str, action='store', dest='in_vpos', metavar='vpos.bin',
|
||
|
|
help='''The path to file storing vertex positions''')
|
||
|
|
parser.add_argument('-n', '--in-vnml', required=True, type=str, action='store', dest='in_vnml', metavar='vnml.bin',
|
||
|
|
help='''The path to file storing vertex normals''')
|
||
|
|
parser.add_argument('-u', '--in-vuv', required=True, type=str, action='store', dest='in_vuv', metavar='vuv.bin',
|
||
|
|
help='''The path to file storing vertex UVs''')
|
||
|
|
parser.add_argument('-i', '--in-findices', required=True, type=str, action='store', dest='in_findices', metavar='findices.bin',
|
||
|
|
help='''The path to file storing face indices''')
|
||
|
|
parser.add_argument('-o', '--out-obj', required=True, type=str, action='store', dest='out_obj', metavar='mesh.obj',
|
||
|
|
help='''The path to output OBJ file''')
|
||
|
|
parser.add_argument('-m', '--out-mtl', required=True, type=str, action='store', dest='out_mtl', metavar='mesh.mtl',
|
||
|
|
help='''The path to output MTL file''')
|
||
|
|
|
||
|
|
# parse arg
|
||
|
|
args = parser.parse_args()
|
||
|
|
# return value
|
||
|
|
return Cli(
|
||
|
|
Path(args.in_vpos),
|
||
|
|
Path(args.in_vnml),
|
||
|
|
Path(args.in_vuv),
|
||
|
|
Path(args.in_findices),
|
||
|
|
Path(args.out_obj),
|
||
|
|
Path(args.out_mtl)
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
# endregion
|
||
|
|
|
||
|
|
def main():
|
||
|
|
# parse arguments
|
||
|
|
opts = parse()
|
||
|
|
|
||
|
|
vertex_count = evaluate_count(opts.in_vpos, 3 * 4) # 3 float(4 bytes)
|
||
|
|
print(f'Vertex Count Evaluated: {vertex_count}')
|
||
|
|
vpos = batched_tuple(read_f32s(opts.in_vpos, 3 * vertex_count), 3)
|
||
|
|
|
||
|
|
vnml = batched_tuple(read_f32s(opts.in_vnml, 3 * vertex_count), 3)
|
||
|
|
|
||
|
|
vuv = batched_tuple(read_f32s(opts.in_vuv, 2 * vertex_count), 2)
|
||
|
|
|
||
|
|
face_count = evaluate_count(opts.in_findices, 3 * 2) # 3 WORD(2 bytes)
|
||
|
|
print(f'Face Count Evaluated: {face_count}')
|
||
|
|
findices = batched_tuple(read_u16s(opts.in_findices, 3 * face_count), 3)
|
||
|
|
|
||
|
|
build_obj_file(opts.out_obj, vpos, vnml, vuv, findices)
|
||
|
|
print('Done')
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == '__main__':
|
||
|
|
main()
|