diff --git a/tools/generate_primitives.py b/tools/generate_primitives.py new file mode 100755 index 0000000..ecec014 --- /dev/null +++ b/tools/generate_primitives.py @@ -0,0 +1,232 @@ +#!/usr/bin/env python3 +""" +Generate simple GLTF primitive models for Starworld entity rendering. +Requires: pip install pygltflib numpy +""" + +import numpy as np +from pygltflib import * +import struct +import os +from pathlib import Path + +def create_cube_gltf(output_path): + """Create a 1x1x1 cube centered at origin""" + + # Cube vertices (8 vertices) + vertices = np.array([ + [-0.5, -0.5, -0.5], # 0 + [ 0.5, -0.5, -0.5], # 1 + [ 0.5, 0.5, -0.5], # 2 + [-0.5, 0.5, -0.5], # 3 + [-0.5, -0.5, 0.5], # 4 + [ 0.5, -0.5, 0.5], # 5 + [ 0.5, 0.5, 0.5], # 6 + [-0.5, 0.5, 0.5], # 7 + ], dtype=np.float32) + + # Cube indices (12 triangles, 36 indices) + indices = np.array([ + # Front face + 0, 1, 2, 0, 2, 3, + # Back face + 5, 4, 7, 5, 7, 6, + # Top face + 3, 2, 6, 3, 6, 7, + # Bottom face + 4, 5, 1, 4, 1, 0, + # Right face + 1, 5, 6, 1, 6, 2, + # Left face + 4, 0, 3, 4, 3, 7, + ], dtype=np.uint16) + + # Normals (per-face normals repeated for each vertex of triangle) + normals = np.array([ + # Front + [0, 0, -1], [0, 0, -1], [0, 0, -1], [0, 0, -1], [0, 0, -1], [0, 0, -1], + # Back + [0, 0, 1], [0, 0, 1], [0, 0, 1], [0, 0, 1], [0, 0, 1], [0, 0, 1], + # Top + [0, 1, 0], [0, 1, 0], [0, 1, 0], [0, 1, 0], [0, 1, 0], [0, 1, 0], + # Bottom + [0, -1, 0], [0, -1, 0], [0, -1, 0], [0, -1, 0], [0, -1, 0], [0, -1, 0], + # Right + [1, 0, 0], [1, 0, 0], [1, 0, 0], [1, 0, 0], [1, 0, 0], [1, 0, 0], + # Left + [-1, 0, 0], [-1, 0, 0], [-1, 0, 0], [-1, 0, 0], [-1, 0, 0], [-1, 0, 0], + ], dtype=np.float32) + + # Create binary data + vertices_binary = vertices.tobytes() + indices_binary = indices.tobytes() + + # Build GLTF + gltf = GLTF2( + scene=0, + scenes=[Scene(nodes=[0])], + nodes=[Node(mesh=0)], + meshes=[ + Mesh(primitives=[ + Primitive( + attributes=Attributes(POSITION=0), + indices=1, + ) + ]) + ], + accessors=[ + Accessor( + bufferView=0, + componentType=FLOAT, + count=len(vertices), + type=VEC3, + max=vertices.max(axis=0).tolist(), + min=vertices.min(axis=0).tolist(), + ), + Accessor( + bufferView=1, + componentType=UNSIGNED_SHORT, + count=len(indices), + type=SCALAR, + ), + ], + bufferViews=[ + BufferView( + buffer=0, + byteOffset=0, + byteLength=len(vertices_binary), + target=ARRAY_BUFFER, + ), + BufferView( + buffer=0, + byteOffset=len(vertices_binary), + byteLength=len(indices_binary), + target=ELEMENT_ARRAY_BUFFER, + ), + ], + buffers=[ + Buffer(byteLength=len(vertices_binary) + len(indices_binary)) + ], + ) + + gltf.set_binary_blob(vertices_binary + indices_binary) + gltf.save(output_path) + print(f"Created cube: {output_path}") + +def create_sphere_gltf(output_path, segments=32, rings=16): + """Create a UV sphere""" + + vertices = [] + normals = [] + indices = [] + + # Generate vertices + for ring in range(rings + 1): + theta = ring * np.pi / rings + sin_theta = np.sin(theta) + cos_theta = np.cos(theta) + + for seg in range(segments + 1): + phi = seg * 2 * np.pi / segments + sin_phi = np.sin(phi) + cos_phi = np.cos(phi) + + x = cos_phi * sin_theta + y = cos_theta + z = sin_phi * sin_theta + + vertices.append([x * 0.5, y * 0.5, z * 0.5]) # radius 0.5 + normals.append([x, y, z]) + + # Generate indices + for ring in range(rings): + for seg in range(segments): + first = ring * (segments + 1) + seg + second = first + segments + 1 + + indices.extend([first, second, first + 1]) + indices.extend([second, second + 1, first + 1]) + + vertices = np.array(vertices, dtype=np.float32) + normals = np.array(normals, dtype=np.float32) + indices = np.array(indices, dtype=np.uint16) + + vertices_binary = vertices.tobytes() + normals_binary = normals.tobytes() + indices_binary = indices.tobytes() + + gltf = GLTF2( + scene=0, + scenes=[Scene(nodes=[0])], + nodes=[Node(mesh=0)], + meshes=[ + Mesh(primitives=[ + Primitive( + attributes=Attributes(POSITION=0, NORMAL=1), + indices=2, + ) + ]) + ], + accessors=[ + Accessor( + bufferView=0, + componentType=FLOAT, + count=len(vertices), + type=VEC3, + max=vertices.max(axis=0).tolist(), + min=vertices.min(axis=0).tolist(), + ), + Accessor( + bufferView=1, + componentType=FLOAT, + count=len(normals), + type=VEC3, + ), + Accessor( + bufferView=2, + componentType=UNSIGNED_SHORT, + count=len(indices), + type=SCALAR, + ), + ], + bufferViews=[ + BufferView( + buffer=0, + byteOffset=0, + byteLength=len(vertices_binary), + target=ARRAY_BUFFER, + ), + BufferView( + buffer=0, + byteOffset=len(vertices_binary), + byteLength=len(normals_binary), + target=ARRAY_BUFFER, + ), + BufferView( + buffer=0, + byteOffset=len(vertices_binary) + len(normals_binary), + byteLength=len(indices_binary), + target=ELEMENT_ARRAY_BUFFER, + ), + ], + buffers=[ + Buffer(byteLength=len(vertices_binary) + len(normals_binary) + len(indices_binary)) + ], + ) + + gltf.set_binary_blob(vertices_binary + normals_binary + indices_binary) + gltf.save(output_path) + print(f"Created sphere: {output_path}") + +if __name__ == "__main__": + # Create output directory + cache_dir = Path.home() / ".cache" / "starworld" / "primitives" + cache_dir.mkdir(parents=True, exist_ok=True) + + # Generate primitives + create_cube_gltf(str(cache_dir / "cube.glb")) + create_sphere_gltf(str(cache_dir / "sphere.glb")) + + print(f"\n✓ Primitive models generated in: {cache_dir}") + print(f" - cube.glb") + print(f" - sphere.glb")