nitro-engine/tools/obj2dl/obj2dl.py
2024-01-01 03:50:16 +01:00

227 lines
7.3 KiB
Python
Executable File

#!/usr/bin/env python3
# SPDX-License-Identifier: MIT
#
# Copyright (c) 2022-2024 Antonio Niño Díaz <antonio_nd@outlook.com>
from display_list import DisplayList
class OBJFormatError(Exception):
pass
VALID_TEXTURE_SIZES = [8, 16, 32, 64, 128, 256, 512, 1024]
def is_valid_texture_size(size):
return size in VALID_TEXTURE_SIZES
def convert_obj(input_file, output_file, texture_size,
model_scale, model_translation, use_vertex_color):
vertices = []
texcoords = []
normals = []
faces = []
with open(input_file, 'r') as obj_file:
for line in obj_file:
# Remove comments
line = line.split('#')[0]
# Parse line
tokens = line.split()
# Empty line, skip it
if len(tokens) < 2:
continue
cmd = tokens[0]
tokens = tokens[1:]
if cmd == 'v': # Vertex
if len(tokens) == 3: # (x, y, z)
if use_vertex_color:
raise OBJFormatError(f"Found vertex with no color info: {tokens}")
v = [float(tokens[i]) for i in range(3)]
elif len(tokens) == 6: # (x, y, z, r, g, b)
v = [float(tokens[i]) for i in range(6)]
else:
raise OBJFormatError(f"Unsupported vertex command: {tokens}")
vertices.append(v)
elif cmd == 'vt': # Texture coordinate
v = (float(tokens[0]), float(tokens[1]))
texcoords.append(v)
elif cmd == 'vn': # Normal
v = (float(tokens[0]), float(tokens[1]), float(tokens[2]))
normals.append(v)
elif cmd == 'f': # Face
v = tokens
faces.append(v)
elif cmd == 'l': # Polyline
raise OBJFormatError(f"Unsupported polyline command: {tokens}")
else:
print(f"Ignored unsupported command: {cmd} {tokens}")
print("Vertices: " + str(len(vertices)))
print("Texcoords: " + str(len(texcoords)))
print("Normals: " + str(len(normals)))
print("Faces: " + str(len(faces)))
print("")
dl = DisplayList()
for face in faces:
# Check the polygon type
faces_number = len(face)
if faces_number == 3:
dl.switch_vtxs("triangles")
elif faces_number == 4:
dl.switch_vtxs("quads")
else:
raise OBJFormatError(
f"Unsupported polygons with {faces_number} faces. "
"Please, split the polygons in your model to triangles."
)
# Read vertices
for vertex in face:
tokens = vertex.split('/')
vertex_index = None
texcoord_index = None
normal_index = None
if len(tokens) == 1:
vertex_index = int(tokens[0])
elif len(tokens) == 2:
vertex_index = int(tokens[0])
texcoord_index = int(tokens[1])
elif len(tokens) == 3:
vertex_index = int(tokens[0])
if len(tokens[1]) > 0:
texcoord_index = int(tokens[1])
# Only generate normal commands if there is no vertex color, as
# it would overwrite it.
if not use_vertex_color:
normal_index = int(tokens[2])
else:
raise OBJFormatError(f"Invalid face {face}")
# Vertex indices must always be provided
if vertex_index < 0:
raise OBJFormatError(f"Unsupported negative indices")
vertex_index -= 1
if texcoord_index is not None:
if texcoord_index < 0:
raise OBJFormatError(f"Unsupported negative indices")
texcoord_index -= 1
if normal_index is not None:
if normal_index < 0:
raise OBJFormatError(f"Unsupported negative indices")
normal_index -= 1
if texcoord_index is not None:
u, v = texcoords[texcoord_index]
# In the OBJ format (0, 0) is the bottom-left corner. In the DS,
# (0, 0) is the top left corner. We need tp flip top and bottom.
v = 1.0 - v
u *= texture_size[0]
v *= texture_size[1]
dl.texcoord(u, v)
if normal_index is not None:
n = normals[normal_index]
dl.normal(n[0], n[1], n[2])
vtx = []
for i in range(3):
v = vertices[vertex_index][i]
v += model_translation[i]
v *= model_scale
vtx.append(v)
if use_vertex_color:
rgb = [vertices[vertex_index][i] for i in range(3, 6)]
dl.color(*rgb)
# Let the DisplayList class pick the best vtx command
dl.vtx(vtx[0], vtx[1], vtx[2])
dl.end_vtxs()
dl.finalize()
dl.save_to_file(output_file)
if __name__ == "__main__":
import argparse
import sys
import traceback
print("obj2dl v0.1.2")
print("Copyright (c) 2022-2024 Antonio Niño Díaz <antonio_nd@outlook.com>")
print("All rights reserved")
print("")
parser = argparse.ArgumentParser(
description='Convert Wavefront OBJ files into NDS display lists.')
# Required arguments
parser.add_argument("--input", required=True,
help="input file")
parser.add_argument("--output", required=True,
help="output file")
parser.add_argument("--texture", required=True, type=int,
nargs="+", action="extend",
help="texture width and height (e.g. '--texture 32 64')")
# Optional arguments
parser.add_argument("--translation", default=[0, 0, 0], type=float,
nargs="+", action="extend",
help="translate model by this value")
parser.add_argument("--scale", default=1.0, type=float,
help="scale model by this value (after the translation)")
parser.add_argument("--use-vertex-color", required=False,
action='store_true',
help="use vertex colors instead of normals")
args = parser.parse_args()
if len(args.texture) != 2:
print("Please, provide exactly 2 values to the --texture argument")
sys.exit(1)
if not is_valid_texture_size(args.texture[0]):
print(f"Invalid texture width. Valid values: {VALID_TEXTURE_SIZES}")
sys.exit(1)
if not is_valid_texture_size(args.texture[1]):
print(f"Invalid texture height. Valid values: {VALID_TEXTURE_SIZES}")
sys.exit(1)
if len(args.translation) != 3:
print("Please, provide exactly 3 values to the --translation argument")
sys.exit(1)
try:
convert_obj(args.input, args.output, args.texture,
args.scale, args.translation, args.use_vertex_color)
except BaseException as e:
print("ERROR: " + str(e))
traceback.print_exc()
sys.exit(1)
except OBJFormatError as e:
print("ERROR: Invalid OBJ file: " + str(e))
traceback.print_exc()
sys.exit(1)
print("Done!")
sys.exit(0)