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 0000000..20a3468 Binary files /dev/null and b/src/objmc/__pycache__/__init__.cpython-311.pyc differ diff --git a/src/objmc/__pycache__/__main__.cpython-311.pyc b/src/objmc/__pycache__/__main__.cpython-311.pyc new file mode 100644 index 0000000..3df8910 Binary files /dev/null and b/src/objmc/__pycache__/__main__.cpython-311.pyc differ diff --git a/src/objmc/__pycache__/cli.cpython-311.pyc b/src/objmc/__pycache__/cli.cpython-311.pyc new file mode 100644 index 0000000..7504705 Binary files /dev/null and b/src/objmc/__pycache__/cli.cpython-311.pyc differ diff --git a/src/objmc/__pycache__/models.cpython-311.pyc b/src/objmc/__pycache__/models.cpython-311.pyc new file mode 100644 index 0000000..11f52e6 Binary files /dev/null and b/src/objmc/__pycache__/models.cpython-311.pyc differ 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