"""
Object Loader
Loader of object data from an .loader file.
"""
import os
from picogl.logger import Logger as log
from picogl.utils.loader.helpers import log_properties
from picogl.utils.loader.object_data import ObjectData
[docs]
class ObjectLoader:
"""
Object Loader Class
"""
[docs]
def __init__(self, path: str):
# Resolve the path relative to the current working directory
if not os.path.isabs(path):
# Try to find the file relative to the current working directory
if os.path.exists(path):
path = os.path.abspath(path)
else:
# Fallback to relative to this script's directory
script_dir = os.path.dirname(os.path.abspath(__file__))
path = os.path.abspath(os.path.join(script_dir, "..", path))
if not os.path.exists(path):
raise FileNotFoundError(f"OBJ file not found: {path}")
with open(path, "r", encoding="utf-8") as object_file:
for line in object_file:
if not line.strip() or line.startswith("#"):
continue
parts = line.split()
code = parts[0]
if code == "v":
self.vertices.extend(map(float, parts[1:4]))
elif code == "vn":
self.normals.extend(map(float, parts[1:4]))
elif code == "vt":
self.texcoords.extend(map(float, parts[1:3]))
elif code == "f":
# Faces can be v, v/t, v//n, or v/t/n
face_indices = []
for ref in parts[1:]:
# split() result length can be 1, 2, or 3
subs = ref.split("/")
while len(subs) < 3:
subs.append("") # pad to 3 elements
# Parse indices (OBJ uses 1-based indexing)
v_idx = int(subs[0]) if subs[0] else 0
t_idx = int(subs[1]) if subs[1] else 0
n_idx = int(subs[2]) if subs[2] else 0
face_indices.append((v_idx, t_idx, n_idx))
# Store the face indices
self.indices.extend([idx for face in face_indices for idx in face])
elif code in ("s", "mtllib", "usemtl"):
# Optional properties
setattr(self, code, parts[1] if len(parts) > 1 else None)
elif code == "g":
# Group name — skip for now
continue
else:
print(f"Skipping unknown line: {line.strip()}")
self.log_properties()
[docs]
def log_properties(self):
"""log object properties"""
log_properties(self)
[docs]
def to_array_style(self) -> ObjectData:
"""Convert to array-style where each vertex attribute is stored separately"""
vertices, texcoords, normals = [], [], []
for i in range(0, len(self.indices), 3):
v_idx, t_idx, n_idx = self.indices[i : i + 3]
# Get vertex position (1-based to 0-based)
if v_idx > 0:
v_start = 3 * (v_idx - 1)
vertices.extend(self.vertices[v_start : v_start + 3])
else:
vertices.extend([0.0, 0.0, 0.0]) # Default vertex
# Get texture coordinates (1-based to 0-based)
if t_idx > 0 and self.texcoords:
t_start = 2 * (t_idx - 1)
texcoords.extend(self.texcoords[t_start : t_start + 2])
else:
texcoords.extend([0.0, 0.0]) # Default texcoord
# Get normal (1-based to 0-based)
if n_idx > 0 and self.normals:
n_start = 3 * (n_idx - 1)
normals.extend(self.normals[n_start : n_start + 3])
else:
normals.extend([0.0, 0.0, 1.0]) # Default normal
data = ObjectData(vertices, texcoords, normals)
return data
[docs]
def to_single_index_style(self) -> ObjectData:
"""Convert to single-index style where each unique vertex attribute combination is stored once"""
vertices, texcoords, normals, indices = [], [], [], []
combinations = {}
for i in range(0, len(self.indices), 3):
v_idx, t_idx, n_idx = self.indices[i: i + 3]
key = (v_idx, t_idx, n_idx)
if key not in combinations:
combinations[key] = len(combinations)
# Get vertex position (1-based to 0-based)
if v_idx > 0:
v_start = 3 * (v_idx - 1)
vertices.extend(self.vertices[v_start : v_start + 3])
else:
vertices.extend([0.0, 0.0, 0.0]) # Default vertex
# Get texture coordinates (1-based to 0-based)
if t_idx > 0 and self.texcoords:
t_start = 2 * (t_idx - 1)
texcoords.extend(self.texcoords[t_start : t_start + 2])
else:
texcoords.extend([0.0, 0.0]) # Default texcoord
# Get normal (1-based to 0-based)
if n_idx > 0 and self.normals:
n_start = 3 * (n_idx - 1)
normals.extend(self.normals[n_start: n_start + 3])
else:
normals.extend([0.0, 0.0, 1.0]) # Default normal
indices.append(combinations[key])
return ObjectData(vertices, texcoords, normals, indices)
if __name__ == "__main__":
# Test with the teapot model_matrix
try:
[docs]
obj = ObjectLoader("data/teapot.loader")
log_properties(obj)
except FileNotFoundError as e:
log.message(f"Error: {e}")
log.message("Make sure the teapot.loader file exists in the data/ directory")
except Exception as e:
log.message(f"Error loading OBJ file: {e}")