From 9244e4428fc6c27963cdfdebb577ecdce16dfb29 Mon Sep 17 00:00:00 2001 From: Ritik Shah Date: Wed, 11 Oct 2023 00:06:36 -0500 Subject: [PATCH] models, CLI, obj parsing --- plan.md | 19 ++ requirements.txt | 1 + src/objmc/__init__.py | 0 src/objmc/__main__.py | 4 + .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 169 bytes .../__pycache__/__main__.cpython-311.pyc | Bin 0 -> 332 bytes src/objmc/__pycache__/cli.cpython-311.pyc | Bin 0 -> 4544 bytes src/objmc/__pycache__/models.cpython-311.pyc | Bin 0 -> 1705 bytes src/objmc/cli.py | 133 +++++++++++ src/objmc/gui.py | 0 src/objmc/main.py | 0 src/objmc/models.py | 213 ++++++++++++++++++ 12 files changed, 370 insertions(+) create mode 100644 plan.md create mode 100644 requirements.txt create mode 100644 src/objmc/__init__.py create mode 100644 src/objmc/__main__.py create mode 100644 src/objmc/__pycache__/__init__.cpython-311.pyc create mode 100644 src/objmc/__pycache__/__main__.cpython-311.pyc create mode 100644 src/objmc/__pycache__/cli.cpython-311.pyc create mode 100644 src/objmc/__pycache__/models.cpython-311.pyc create mode 100644 src/objmc/cli.py create mode 100644 src/objmc/gui.py create mode 100644 src/objmc/main.py create mode 100644 src/objmc/models.py diff --git a/plan.md b/plan.md new file mode 100644 index 0000000..805607d --- /dev/null +++ b/plan.md @@ -0,0 +1,19 @@ +## Input +[x] CLI +- GUI (tkinter) + - calls CLI + +## Output +- Model + Texture file +- Shader (provided, static) + +## Behavior +- CLI Arguments + OBJ -> Model + Texture File +- multiple obj files, validity check + +## Data Needs +[x] CLI Arguments (settings) +[x] OBJ +- list of vertices coordinates (xyz) +- list of uvs (xy) +- list of faces (ij) (w/ indicies uvs and vertices) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..3868fb1 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +pillow diff --git a/src/objmc/__init__.py b/src/objmc/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/objmc/__main__.py b/src/objmc/__main__.py new file mode 100644 index 0000000..77dccae --- /dev/null +++ b/src/objmc/__main__.py @@ -0,0 +1,4 @@ +from .cli import args + +if __name__ == "__main__": + print(args()) diff --git a/src/objmc/__pycache__/__init__.cpython-311.pyc b/src/objmc/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..20a3468837768f64d5426cd52d0eb9ac2651a8a6 GIT binary patch literal 169 zcmZ3^%ge<81l%%esUZ3>h=2h`DC095kTIPhg&~+hlhJP_LlF~@{~09t%SJ!6IJKx) zzbLaLGh5#$Gc8p=BqK8~y;$GZ*+@SnwM;)hDJwTwzqlwF%8HNA%*!l^kJl@x{Ka9D ho1apelWJGQ3N#F4T`@n9_`uA_$oPQ)Miemv#Q<^jCQ<+Z literal 0 HcmV?d00001 diff --git a/src/objmc/__pycache__/__main__.cpython-311.pyc b/src/objmc/__pycache__/__main__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3df891095bd80a3d62fd6b0a0346dbf3bdc4bf4d GIT binary patch literal 332 zcmZ3^%ge<81Z4_psV+eJF^B^LEKtU041WQ#KPwzDSgb zA(a`38J7Wht3g6wu#6GN17ZkZWJqBS24hW@mmpD1##=0jMd`)2IO5}T6EpMTKeRZts93)!vm`TH-zPIIRX-#n zGcUbZ-`CklKP9zHKR+oeH(9^9C>hFvnXgw+`HRCQH$SB`C)KWq8)z8F9mS47;sY}y zBjXJQ(F>^P0W)_;)g@-h3(S%?SU4M8J5)MUI@NmA8hu-Q8^G`bGXskhP`Zc{XbJ%2 CI#Lh- literal 0 HcmV?d00001 diff --git a/src/objmc/__pycache__/cli.cpython-311.pyc b/src/objmc/__pycache__/cli.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7504705d6af189661e8601fc42c2bc9cef48bac7 GIT binary patch literal 4544 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 literal 0 HcmV?d00001 diff --git a/src/objmc/cli.py b/src/objmc/cli.py new file mode 100644 index 0000000..e6db704 --- /dev/null +++ b/src/objmc/cli.py @@ -0,0 +1,133 @@ +from argparse import ArgumentParser + +from .models import Args + + +class ArgumentParserError(Exception): + ... + + +class ThrowingArgumentParser(ArgumentParser): + def error(self, message): + raise ArgumentParserError(message) + + +def create_parser(): + parser = ThrowingArgumentParser( + description=( + "python script to convert .OBJ files into Minecraft," + " rendering them in game with a core shader.\n" + "Github: https://github.com/Godlander/objmc" + ) + ) + args = Args() + + parser.add_argument( + "--objs", + help="List of object files", + nargs="*", + default=args.objs, + ) + parser.add_argument( + "--texs", + help="Specify a texture file", + nargs="*", + default=args.texs, + ) + parser.add_argument( + "--out", + type=str, + help="Output json and png", + nargs=2, + default=args.out, + ) + parser.add_argument( + "--offset", + type=float, + help="Offset of model in xyz", + nargs=3, + default=args.offset, + ) + parser.add_argument( + "--scale", + type=float, + help="Scale of model", + default=args.scale, + ) + parser.add_argument( + "--duration", + type=int, + help="Duration of the animation in ticks", + default=args.duration, + ) + parser.add_argument( + "--easing", + type=int, + help="Animation easing, 0: none, 1: linear, 2: in-out cubic, 3: 4-point bezier", + default=args.easing, + ) + parser.add_argument( + "--interpolation", + type=int, + help="Texture interpolation, 0: none, 1: fade", + default=args.interpolation, + ) + parser.add_argument( + "--colorbehavior", + type=str, + help=( + "Item color overlay behavior, \"xyz\": rotate" + ", 't': animation time offset, 'o': overlay hue" + ), + default=args.colorbehavior, + ) + parser.add_argument( + "--autorotate", + type=int, + help=( + "Attempt to estimate rotation with Normals" + "0: off, 1: yaw, 2: pitch, 3: both" + ), + default=args.autorotate, + ) + parser.add_argument( + "--autoplay", + action="store_true", + dest="autoplay", + help='Always interpolate animation, colorbehavior="ttt" overrides this', + default=args.autoplay, + ) + parser.add_argument( + "--visibility", + type=int, + help="Determines where the model is visible", + default=args.visibility, + ) + parser.add_argument( + "--flipuv", + action="store_true", + dest="flipuv", + help="Invert the texture to compensate for flipped UV", + ) + parser.add_argument( + "--noshadow", + action="store_true", + dest="noshadow", + help="Disable shadows from face normals", + ) + parser.add_argument( + "--nopow", + action="store_true", + dest="nopow", + help="Disable power of two textures", + ) + parser.add_argument( + "--join", nargs="*", dest="join", help="Joins multiple json models into one" + ) + + return parser + + +def args(): + parser = create_parser() + return parser.parse_args(namespace=Args()) diff --git a/src/objmc/gui.py b/src/objmc/gui.py new file mode 100644 index 0000000..e69de29 diff --git a/src/objmc/main.py b/src/objmc/main.py new file mode 100644 index 0000000..e69de29 diff --git a/src/objmc/models.py b/src/objmc/models.py new file mode 100644 index 0000000..5453126 --- /dev/null +++ b/src/objmc/models.py @@ -0,0 +1,213 @@ +from dataclasses import dataclass +from pathlib import Path +from typing import Literal, NamedTuple + + +@dataclass +class Args: + """ + Represents the arguments required to generate a Minecraft model. + + Args: + objs (List[Path]): A list of paths to the model files. + texs (List[Path]): A list of paths to the texture files. + out (List[str]): A list of output file names for the generated model. + offset (Tuple[float, float, float]): A tuple representing the position offset of the model. + scale (float): A float representing the scaling factor of the model. + duration (int): An integer representing the duration of the animation in ticks. + easing (Literal[0, 1, 2, 3]): An integer literal representing the type of animation easing. + interpolation (Literal[0, 1]): An integer literal representing the type of texture interpolation. + colorbehavior (str): A string representing the behavior of the RGB color values. + autorotate (Literal[0, 1, 2, 3]): An integer literal representing the type of auto-rotation. + autoplay (bool): A boolean representing whether the animation should auto-play. + flipuv (bool): A boolean representing whether the UV coordinates should be flipped. + noshadow (bool): A boolean representing whether the model should have shadows. + visibility (int): An integer representing the visibility of the model. + nopow (bool): A boolean representing whether the texture files should have power-of-two dimensions. + join (List[str]): A list of model names to join together. + """ + + objs: list[Path] = [""] + + # texture animations not supported yet + texs: list[Path] = [""] + + # Output json & png + out: list[str] = ["potion.json", "block/out.png"] + + # Position & Scaling + # just adds & multiplies vertex positions before encoding, so you dont have to re export the model + offset: tuple[float, float, float] = (0.0, 0.0, 0.0) + scale: float = 1.0 + + # Duration of the animation in ticks + duration: int = 0 + + # Animation Easing + # 0: none, 1: linear, 2: in-out cubic, 3: 4-point bezier + easing: Literal[0, 1, 2, 3] = 3 + + # Texture Interpolation + # 0: none, 1: fade + interpolation: Literal[0, 1] = 1 + + # Color Behavior + # defines the behavior of 3 bytes of rgb to rotation and animation frames, + # any 3 chars of 'x', 'y', 'z', 't', 'o' is valid + # 'xyz' = rotate, 't' = animation, 'o' = overlay hue + # multiple rotation bytes increase accuracy on that axis + # for 'ttt', autoplay is automatically on. numbers past 8388608 define paused frame to display (suso's idea) + # auto-play color can be calculated by: ((([time query gametime] % 24000) - starting frame) % total duration) + colorbehavior: str = "xyz" + + # Auto Rotate + # attempt to estimate rotation with Normals, added to colorbehavior rotation. + # one axis is ok but both is jittery. For display purposes color defined rotation is better. + # 0: none, 1: yaw, 2: pitch, 3: both + autorotate: Literal[0, 1, 2, 3] = 1 + + # Auto Play + # always interpolate frames, colorbehavior='aaa' overrides this. + autoplay: bool = False + + # Flip uv + # if your model renders but textures are not right try toggling this + # i find that blockbench ends up flipping uv, but blender does not. dont trust me too much on this tho i have no idea what causes it. + flipuv: bool = False + + # No Shadow + # disable face normal shading (lightmap color still applies) + # can be used for models with lighting baked into the texture + noshadow: bool = False + + # Visibility + # determins where the model is visible + # 3 bits for: world, hand, gui + visibility: int = 7 + + # No power of two textures + # i guess saves a bit of space maybe + # makes it not optifine compatible + nopow: bool = True + + # Joining multiple models + join: list[str] = [] + + +class Position(NamedTuple): + x: float + y: float + z: float + + +class UV(NamedTuple): + u: float + v: float + + +class Vertex(NamedTuple): + pos: Position + uv: UV + + +class VertexIndices(NamedTuple): + pos: int + uv: int + + +@dataclass +class ObjFormat: + vertices: list[Position] + uvs: list[UV] + faces: list[list[VertexIndices]] + + @classmethod + def parse(cls, content: str): + vertices = [] + uvs = [] + faces = [] + + # TODO: throw errors for invalid OBJ files, like missing vertices + # ex: ValueError -> float("tacos") + for line in content.splitlines(): + if line.startswith("v "): + cleaned = line[2:].split() # remove leading + double + vertices.append(Vertex(*(float(num) for num in cleaned))) + + elif line.startswith("vt "): + cleaned = line[3:].split() + uvs.append(UV(*(float(num) for num in cleaned))) + + elif line.startswith("f "): + face_vertices = line[1:].split(" ") + vertex_indicies = [] + for vertex in face_vertices: + splitted = vertex.split("/") + assert len(splitted) in (2, 3) # TODO + pos = int(splitted[0]) - 1 + uv = int(splitted[1]) - 1 + + vertex_indicies.append(VertexIndices(pos, uv)) + + faces.append(vertex_indicies) + + # TODO: handle error + # if nfaces > 0 and len(d["faces"]) != nfaces: + # print(col.err+"mismatched obj face count"+col.end) + + return cls(vertices, uvs, faces) + + +count = [0,0] +mem = {"positions":{},"uvs":{}} +data = {"positions":[],"uvs":[],"vertices":[]} +def readobj(name, nfaces): + obj = open(name, "r", encoding="utf-8") + d = {"positions":[],"uvs":[],"faces":[]} + for line in obj: + if line.startswith("v "): + d["positions"].append(tuple([float(i) for i in " ".join(line.split()).split(" ")[1:]])) + if line.startswith("vt "): + d["uvs"].append(tuple([float(i) for i in " ".join(line.split()).split(" ")[1:]])) + if line.startswith("f "): + d["faces"].append(tuple([[int(i)-1 for i in vert.split("/")] for vert in " ".join(line.split()).split(" ")[1:]])) + obj.close() + if nfaces > 0 and len(d["faces"]) != nfaces: + print(col.err+"mismatched obj face count"+col.end) + exit() + return d +#index vertices +def indexvert(o, vert): + global count + global mem + global data + v = [] + pos = o["positions"][vert[0]] + uv = o["uvs"][vert[1]] + posh = ','.join([str(i) for i in pos]) + uvh =','.join([str(i) for i in uv]) + try: + v.append(mem["positions"][posh]) + except: + mem["positions"][posh] = count[0] + data["positions"].append(pos) + v.append(count[0]) + count[0] += 1 + try: + v.append(mem["uvs"][uvh]) + except: + mem["uvs"][uvh] = count[1] + data["uvs"].append(uv) + v.append(count[1]) + count[1] += 1 + data["vertices"].append(v) +#index obj +def indexobj(o, frame, nframes, nfaces): + for face in range(0, len(o["faces"])): + if face % 1000 == 0: + print("\Reading obj ", frame+1, " of ", nframes, "...", "{:>15.2f}".format((frame*nfaces+face)*100/(nframes*nfaces)), "%\033[K", sep="", end="\r") + face = o["faces"][face] + for vert in face: + indexvert(o, vert) + if len(face) == 3: + indexvert(o, face[1]) indexvert(o, face[1]) \ No newline at end of file