diff --git a/Assets/Debuggers/.gitignore b/Assets/Debuggers/.gitignore deleted file mode 100644 index 030e331..0000000 --- a/Assets/Debuggers/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -# Ignore test used 3d Object -*.bin -*.obj -*.mtl diff --git a/Assets/Debuggers/MeshConv.py b/Assets/Debuggers/MeshConv.py deleted file mode 100644 index f1485c1..0000000 --- a/Assets/Debuggers/MeshConv.py +++ /dev/null @@ -1,92 +0,0 @@ -import argparse, io, math, struct - -# setup parser -parser = argparse.ArgumentParser(description='The Mesh Converter.') -parser.add_argument('-p', '--in-vpos', required=True, type=str, action='store', dest='in_vpos', metavar='vpos.bin') -parser.add_argument('-n', '--in-vbml', required=True, type=str, action='store', dest='in_vnml', metavar='vnml.bin') -parser.add_argument('-u', '--in-vuv', required=True, type=str, action='store', dest='in_vuv', metavar='vuv.bin') -parser.add_argument('-i', '--in-findices', required=True, type=str, action='store', dest='in_findices', metavar='findices.bin') -parser.add_argument('-o', '--out-obj', required=True, type=str, action='store', dest='out_obj', metavar='mesh.obj') -parser.add_argument('-m', '--out-mtl', required=True, type=str, action='store', dest='out_mtl', metavar='mesh.mtl') - -Vector = tuple[float] -Indices = tuple[int] - -def GetFileLength(fs: io.BufferedReader) -> int: - pos = fs.tell() - fs.seek(0, io.SEEK_END) - fsize = fs.tell() - fs.seek(pos, io.SEEK_SET) - return fsize - -def EvaluateCount(filename: str, unit_size: int) -> int: - with open(filename, 'rb') as fs: - filesize = GetFileLength(fs) - count, modrem = divmod(filesize, unit_size) - if modrem != 0: - raise Exception("invalid file length") - return count - -def AssertFileSize(fs: io.BufferedReader, expected_size: int): - if expected_size != GetFileLength(fs): - raise Exception("invalid file length") - -def ReadFloats(filename: str, count: int) -> tuple[float]: - with open(filename, 'rb') as fs: - # construct class - cstruct = struct.Struct(f'<{count}f') - # assert file size - AssertFileSize(fs, cstruct.size) - # read - return cstruct.unpack(fs.read(cstruct.size)) - -def ReadShorts(filename: str, count: int) -> tuple[float]: - with open(filename, 'rb') as fs: - # construct class - cstruct = struct.Struct(f'<{count}H') - # assert file size - AssertFileSize(fs, cstruct.size) - # read - return cstruct.unpack(fs.read(cstruct.size)) - -def RecoupleTuple(fulllist: tuple, couple_count: int) -> tuple[tuple]: - count, modrem = divmod(len(fulllist), couple_count) - if modrem != 0: - raise Exception("invalid tuple length") - - return tuple(map(lambda x: tuple(fulllist[x * couple_count:x * couple_count + couple_count]), range(count))) - -def GenerateObj(filename: str, 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('v {0} {1} {2}\n'.format(v[0], v[1], v[2])) - for v in vnml: - fs.write('vn {0} {1} {2}\n'.format(v[0], v[1], v[2])) - for v in vuv: - fs.write('vt {0} {1}\n'.format(v[0], v[1])) - for f in findices: - fs.write('f {0}/{0}/{0} {1}/{1}/{1} {2}/{2}/{2}\n'.format(f[0] + 1, f[1] + 1, f[2] + 1)) - fs.write('g obj\n') - -if __name__ == '__main__': - # parse arg - args = parser.parse_args() - - #input("Prepare VertexPositions please.") - vertexcount = EvaluateCount(args.in_vpos, 3 * 4) # 3 float(4 bytes) - print(f'Vertex Count Evaluated: {vertexcount}') - vpos = RecoupleTuple(ReadFloats(args.in_vpos, 3 * vertexcount), 3) - - #input("Prepare VertexNormals please.") - vnml = RecoupleTuple(ReadFloats(args.in_vnml, 3 * vertexcount), 3) - - #input("Prepare VertexUVs please.") - vuv = RecoupleTuple(ReadFloats(args.in_vuv, 2 * vertexcount), 2) - - #input("Prepare FaceIndices please.") - facecount = EvaluateCount(args.in_findices, 3 * 2) # 3 WORD(2 bytes) - print(f'Face Count Evaluated: {facecount}') - findices = RecoupleTuple(ReadShorts(args.in_findices, 3 * facecount), 3) - - GenerateObj(args.out_obj, vpos, vnml, vuv, findices) - print('Done') diff --git a/Assets/Debuggers/README.md b/Assets/Debuggers/README.md deleted file mode 100644 index f19cb3a..0000000 --- a/Assets/Debuggers/README.md +++ /dev/null @@ -1,20 +0,0 @@ -# Tools - -The developer need to know the loaded data whether is correct when testing LibCmo. So we create this folder and you can use Unvirt and the tools located in this folder to test the correction of loaded data. - -Unvirt can show the data of each CKObject, such as Texture, Mesh and etc. For example, Unvirt can provide vertex's position, normal, UV, even the face's indices data for CKMesh. You can use tools to broswer memory to get them, but you couldn't evaluate them how they shape a mesh. This is the reason why this folder existed and in this README I will tell you how to debug the loaded data. - -I suggest you to use HxD to broswer memory, but if you have other softwares, use it freely. - -## CKTexture - -* Install [PixelViewer](https://github.com/carina-studio/PixelViewer) first. -* Change profile to `BGRA_8888` (actually is little-endian RGBA8888, but I think the developer of PixelViewer get confused). -* The image resolution can be gotten from Uvirt. Set it in PixelViewer. -* The image address also can be gotten from Unvirt. Save the memory image data to local file and open it by PixelViewer. - -## CKMesh - -* Have a executable Python. -* Save VertexPosition, VertexNormal, VertexUV, FaceIndices data into file according to the given memory address by Unvirt. -* Call `MeshConv.py`, set the argument properly, then you will get a converted Wavefront OBJ file. diff --git a/Assets/Tools/MeshConv/.gitignore b/Assets/Tools/MeshConv/.gitignore new file mode 100644 index 0000000..9ce0c61 --- /dev/null +++ b/Assets/Tools/MeshConv/.gitignore @@ -0,0 +1,18 @@ +## ======== Personal ======== +# Ignore test used 3d Object +*.bin +*.obj +*.mtl + +## ======== Python ======== +# Python-generated files +__pycache__/ +*.py[oc] +build/ +dist/ +wheels/ +*.egg-info + +# Virtual environments +.venv + diff --git a/Assets/Tools/MeshConv/.python-version b/Assets/Tools/MeshConv/.python-version new file mode 100644 index 0000000..2c07333 --- /dev/null +++ b/Assets/Tools/MeshConv/.python-version @@ -0,0 +1 @@ +3.11 diff --git a/Assets/Tools/MeshConv/README.md b/Assets/Tools/MeshConv/README.md new file mode 100644 index 0000000..560c51e --- /dev/null +++ b/Assets/Tools/MeshConv/README.md @@ -0,0 +1,13 @@ +# MeshConv + +Build complete Wavefront OBJ file from separated data for debugging libcmo21. + +## Usage + +- Restore this project by Astral UV. +- Save all mesh components into separate files in this directory. + * Vertex position as `VertexPosition.bin` for example. + * Vertex normal as `VertexNormal.bin` for example. + * Vertex UV as `VertexUV.bin` for example. + * Face indices as `FaceIndices.bin` for example. +- Execute `uv run main.py -p VertexPosition.bin -n VertexNormal.bin -u VertexUV.bin -i FaceIndices.bin -o mesh.obj -m mesh.mtl` for example. It will utilize previous saved file to generate a Wavefront OBJ file `mesh.obj` and corresponding material file `mesh.mtl`. For the usage of these switches, please refer to the source code. diff --git a/Assets/Tools/MeshConv/main.py b/Assets/Tools/MeshConv/main.py new file mode 100644 index 0000000..fe0c6ed --- /dev/null +++ b/Assets/Tools/MeshConv/main.py @@ -0,0 +1,178 @@ +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() diff --git a/Assets/Tools/MeshConv/pyproject.toml b/Assets/Tools/MeshConv/pyproject.toml new file mode 100644 index 0000000..70b1b0f --- /dev/null +++ b/Assets/Tools/MeshConv/pyproject.toml @@ -0,0 +1,7 @@ +[project] +name = "mesh-conv" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.11" +dependencies = [] diff --git a/Assets/Tools/MeshConv/uv.lock b/Assets/Tools/MeshConv/uv.lock new file mode 100644 index 0000000..b668176 --- /dev/null +++ b/Assets/Tools/MeshConv/uv.lock @@ -0,0 +1,8 @@ +version = 1 +revision = 2 +requires-python = ">=3.11" + +[[package]] +name = "mesh-conv" +version = "0.1.0" +source = { virtual = "." } diff --git a/Assets/Tools/README.md b/Assets/Tools/README.md new file mode 100644 index 0000000..cc8e1fa --- /dev/null +++ b/Assets/Tools/README.md @@ -0,0 +1,24 @@ +# Tools + +The developer need to know the loaded data whether is correct when testing LibCmo. +So we create this folder and you can use Unvirt and the tools located in this folder to test the correction of loaded data. + +Unvirt can show the data of each CKObject, such as Texture, Mesh and etc. +For example, Unvirt can provide vertex's position, normal, UV, even the face's indices data for CKMesh. +You can use tools to broswer memory to get them, but you couldn't evaluate them how they shape a mesh. +This is the reason why this folder existed and in this README I will tell you how to debug the loaded data. + +## Memory Inspector + +I suggest you to use HxD to broswer memory, but if you have other softwares which you have been familiar with, use it freely. + +## CKTexture Debugging + +* Install [PixelViewer](https://github.com/carina-studio/PixelViewer) first. +* Change profile to `BGRA_8888` (actually is little-endian RGBA8888, but I think the developer of PixelViewer get confused). +* The image resolution can be gotten from Uvirt. Set it in PixelViewer. +* The image address also can be gotten from Unvirt. Save the image data from memory to local file and open it by PixelViewer. + +## CKMesh Debugging + +See [MeshConv README](./MeshConv/README.md)