Compare commits

...

18 Commits

Author SHA1 Message Date
MayaTheShy
d42f24274b feat: enhance material creation to ensure Principled BSDF node exists and apply materials to exported objects
Some checks failed
CI / build-and-test (push) Has been cancelled
Rust Quality Checks / rust-checks (push) Has been cancelled
2025-11-08 23:13:04 -05:00
MayaTheShy
026b33fb41 feat: enhance export process by improving object deletion and output messages 2025-11-08 23:11:47 -05:00
MayaTheShy
4d63f37421 feat: enhance material creation to ensure Principled BSDF node exists and set properties 2025-11-08 23:09:41 -05:00
MayaTheShy
ab56fdeb21 fix: remove duplicate export messages for sphere and cube 2025-11-08 23:06:30 -05:00
MayaTheShy
6a89f890b5 feat: update export messages to include model colors and locations 2025-11-08 23:06:18 -05:00
MayaTheShy
b925a1ae90 feat: improve export messages and apply materials to cube and ico sphere 2025-11-08 23:06:06 -05:00
MayaTheShy
ce667bad67 feat: refactor material creation and apply green material to UV sphere 2025-11-08 23:06:01 -05:00
MayaTheShy
662ad1bbd8 feat: add cube and sphere primitive models in GLTF format 2025-11-08 23:01:19 -05:00
MayaTheShy
d4ca24fd0b feat: add blender_export_primitives.py for exporting primitive models in GLTF format 2025-11-08 22:53:19 -05:00
MayaTheShy
142dd5fc9c feat: enhance loadBridge function to handle directory paths for bridge library 2025-11-08 22:50:54 -05:00
MayaTheShy
0e0bee57dc feat: refactor model loading in reify function to improve entity type handling 2025-11-08 22:47:27 -05:00
MayaTheShy
973ff95de8 feat: enhance model loading in reify function with entity type support 2025-11-08 22:47:02 -05:00
MayaTheShy
62928c2fd6 fix: update dirs dependency version in Cargo.toml and Cargo.lock 2025-11-08 22:46:58 -05:00
MayaTheShy
c01c299d8f feat: add generate_primitives.py for GLTF model generation of cube and sphere 2025-11-08 22:45:27 -05:00
MayaTheShy
dd19ff6c77 feat: add initial Cargo.toml for gltf_primitives with dependencies 2025-11-08 22:45:23 -05:00
MayaTheShy
4214c47ea2 feat: add embedded GLTF primitives for basic shapes with caching support 2025-11-08 22:45:19 -05:00
MayaTheShy
a232d3dbc1 fix: update element import in Rust bridge for consistency 2025-11-08 22:45:11 -05:00
MayaTheShy
49ea26e269 fix: avoid duplicate points in circle generation by adjusting loop range 2025-11-08 22:45:07 -05:00
10 changed files with 573 additions and 136 deletions

109
bridge/Cargo.lock generated
View File

@@ -776,7 +776,16 @@ version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d"
dependencies = [
"dirs-sys",
"dirs-sys 0.5.0",
]
[[package]]
name = "dirs"
version = "5.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
dependencies = [
"dirs-sys 0.4.1",
]
[[package]]
@@ -785,7 +794,19 @@ version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
dependencies = [
"dirs-sys",
"dirs-sys 0.5.0",
]
[[package]]
name = "dirs-sys"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
dependencies = [
"libc",
"option-ext",
"redox_users 0.4.6",
"windows-sys 0.48.0",
]
[[package]]
@@ -796,7 +817,7 @@ checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
dependencies = [
"libc",
"option-ext",
"redox_users",
"redox_users 0.5.2",
"windows-sys 0.61.2",
]
@@ -1738,7 +1759,7 @@ version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -2148,6 +2169,17 @@ dependencies = [
"bitflags 2.10.0",
]
[[package]]
name = "redox_users"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
dependencies = [
"getrandom 0.2.16",
"libredox",
"thiserror 1.0.69",
]
[[package]]
name = "redox_users"
version = "0.5.2"
@@ -2403,7 +2435,7 @@ version = "0.45.0"
source = "git+https://github.com/StardustXR/core.git?branch=dev#5176d1e9d26f1e8a4bd774519f3e7d1fddc121a9"
dependencies = [
"color-eyre",
"dirs",
"dirs 6.0.0",
"global_counter",
"mint",
"nix 0.27.1",
@@ -2513,6 +2545,7 @@ dependencies = [
name = "stardust_bridge"
version = "0.1.0"
dependencies = [
"dirs 5.0.1",
"glam 0.28.0",
"lazy_static",
"serde",
@@ -3193,6 +3226,15 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"windows-targets 0.48.5",
]
[[package]]
name = "windows-sys"
version = "0.52.0"
@@ -3220,6 +3262,21 @@ dependencies = [
"windows-link",
]
[[package]]
name = "windows-targets"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
"windows_aarch64_gnullvm 0.48.5",
"windows_aarch64_msvc 0.48.5",
"windows_i686_gnu 0.48.5",
"windows_i686_msvc 0.48.5",
"windows_x86_64_gnu 0.48.5",
"windows_x86_64_gnullvm 0.48.5",
"windows_x86_64_msvc 0.48.5",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
@@ -3253,6 +3310,12 @@ dependencies = [
"windows_x86_64_msvc 0.53.1",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
@@ -3265,6 +3328,12 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
@@ -3277,6 +3346,12 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
@@ -3301,6 +3376,12 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
@@ -3313,6 +3394,12 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
@@ -3325,6 +3412,12 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
@@ -3337,6 +3430,12 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"

View File

@@ -14,6 +14,7 @@ lazy_static = "1.4"
zbus = { version = "5.5.0", features = ["tokio"] }
serde = { version = "1.0", features = ["derive"] }
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
dirs = "5.0"
[dependencies.stardust-xr-asteroids]
git = "https://github.com/StardustXR/asteroids.git"

View File

@@ -10,14 +10,15 @@ use glam::Mat4;
use stardust_xr_asteroids as ast; // alias for brevity
use stardust_xr_asteroids::{
client::ClientState,
elements::{PlaySpace, Spatial, Lines},
Migrate, Reify, CustomElement, Projector, Context,
elements::{PlaySpace, Spatial, Model},
Migrate, Reify, CustomElement, Projector, Context, Transformable,
};
use stardust_xr_molecules::accent_color::AccentColor;
use stardust_xr_fusion::objects::connect_client as fusion_connect_client;
use stardust_xr_fusion::node::NodeType;
use stardust_xr_fusion::root::RootAspect;
use tokio::runtime::Runtime;
use std::path::PathBuf;
#[derive(Clone, serde::Serialize, serde::Deserialize)]
struct BridgeState {
@@ -69,89 +70,25 @@ impl ClientState for BridgeState {
impl Reify for BridgeState {
fn reify(&self) -> impl ast::Element<Self> {
use stardust_xr_fusion::values::{color, Vector3};
use stardust_xr_fusion::drawable::{Line, LinePoint};
use stardust_xr_fusion::values::color;
eprintln!("[bridge/reify] Reifying {} nodes", self.nodes.len());
fn create_wireframe_cube(color_val: stardust_xr_fusion::values::Color, thickness: f32) -> Vec<Line> {
let h = 0.5; // half size
let points = [
[-h, -h, -h], [h, -h, -h], [h, h, -h], [-h, h, -h], // back face
[-h, -h, h], [h, -h, h], [h, h, h], [-h, h, h], // front face
];
// 12 edges of the cube
let edges = [
(0, 1), (1, 2), (2, 3), (3, 0), // back face
(4, 5), (5, 6), (6, 7), (7, 4), // front face
(0, 4), (1, 5), (2, 6), (3, 7), // connecting edges
];
edges.iter().map(|(a, b)| {
let pa = points[*a];
let pb = points[*b];
Line {
points: vec![
LinePoint { point: Vector3 { x: pa[0], y: pa[1], z: pa[2] }, thickness, color: color_val },
LinePoint { point: Vector3 { x: pb[0], y: pb[1], z: pb[2] }, thickness, color: color_val },
],
cyclic: false,
}
}).collect()
}
fn create_wireframe_sphere(color_val: stardust_xr_fusion::values::Color, thickness: f32) -> Vec<Line> {
let segments = 32;
let r = 0.5; // radius
let mut lines = Vec::new();
// Create 3 orthogonal circles (XY, XZ, YZ planes)
for axis in 0..3 {
let mut points = Vec::new();
for i in 0..=segments {
let angle = (i as f32 / segments as f32) * std::f32::consts::TAU;
let (sin, cos) = angle.sin_cos();
let point = match axis {
0 => Vector3 { x: cos * r, y: sin * r, z: 0.0 }, // XY plane
1 => Vector3 { x: cos * r, y: 0.0, z: sin * r }, // XZ plane
_ => Vector3 { x: 0.0, y: cos * r, z: sin * r }, // YZ plane
};
points.push(LinePoint { point, thickness, color: color_val });
}
lines.push(Line { points, cyclic: true });
fn get_model_path(entity_type: u8) -> Option<PathBuf> {
let cache_dir = dirs::cache_dir()?.join("starworld/primitives");
let filename = match entity_type {
1 => "cube.glb", // Box
2 => "sphere.glb", // Sphere
3 => "model.glb", // Model (using Suzanne as placeholder)
_ => return None,
};
let path = cache_dir.join(filename);
if path.exists() {
Some(path)
} else {
eprintln!("[bridge/reify] Model file not found: {}", path.display());
None
}
lines
}
fn create_octahedron_wireframe(color_val: stardust_xr_fusion::values::Color, thickness: f32) -> Vec<Line> {
let r = 0.5;
// 6 vertices of octahedron
let verts = [
Vector3 { x: 0.0, y: r, z: 0.0 }, // top
Vector3 { x: r, y: 0.0, z: 0.0 }, // +X
Vector3 { x: 0.0, y: 0.0, z: r }, // +Z
Vector3 { x: -r, y: 0.0, z: 0.0 }, // -X
Vector3 { x: 0.0, y: 0.0, z: -r }, // -Z
Vector3 { x: 0.0, y: -r, z: 0.0 }, // bottom
];
// 12 edges
let edges = [
(0, 1), (0, 2), (0, 3), (0, 4), // top pyramid
(5, 1), (5, 2), (5, 3), (5, 4), // bottom pyramid
(1, 2), (2, 3), (3, 4), (4, 1), // equator
];
edges.iter().map(|(a, b)| {
Line {
points: vec![
LinePoint { point: verts[*a], thickness, color: color_val },
LinePoint { point: verts[*b], thickness, color: color_val },
],
cyclic: false,
}
}).collect()
}
let children = self.nodes.iter().filter_map(|(id, node)| {
@@ -163,63 +100,38 @@ impl Reify for BridgeState {
let (scale, rot, trans) = node.transform.to_scale_rotation_translation();
let vis_scale = if dims.length() > 0.001 { dims } else { scale };
let node_color = color::rgba_linear!(node.color[0], node.color[1], node.color[2], node.color[3]);
let trans_array = [trans.x, trans.y, trans.z];
let rot_array = [rot.x, rot.y, rot.z, rot.w];
let scale_array = [vis_scale.x, vis_scale.y, vis_scale.z];
let transform = stardust_xr_fusion::spatial::Transform::from_translation_rotation_scale(trans_array, rot_array, scale_array);
// Create appropriate visual based on entity type
match node.entity_type {
1 => {
// Box - use wireframe cube with color
eprintln!("[bridge/reify] Creating box (wireframe) for node {}", id);
let cube_lines = create_wireframe_cube(node_color, 0.005);
Some((*id, Spatial::default()
.transform(transform)
.build()
.child(Lines::new(cube_lines).build())))
}
2 => {
// Sphere - use wireframe sphere with color
eprintln!("[bridge/reify] Creating sphere (wireframe) for node {}", id);
let sphere_lines = create_wireframe_sphere(node_color, 0.005);
Some((*id, Spatial::default()
.transform(transform)
.build()
.child(Lines::new(sphere_lines).build())))
}
3 => {
// Model - attempt to load from URL if provided, fallback to wireframe
if !node.model_url.is_empty() {
eprintln!("[bridge/reify] Creating model for node {} from URL: {}", id, node.model_url);
// For now, we can't easily load arbitrary HTTP URLs in the Overte format
// Fall back to wireframe octahedron as a distinct placeholder
let oct_lines = create_octahedron_wireframe(node_color, 0.005);
Some((*id, Spatial::default()
.transform(transform)
.build()
.child(Lines::new(oct_lines).build())))
} else {
eprintln!("[bridge/reify] Creating model placeholder for node {} (no URL)", id);
let oct_lines = create_octahedron_wireframe(node_color, 0.005);
Some((*id, Spatial::default()
.transform(transform)
.build()
.child(Lines::new(oct_lines).build())))
// Try to load the appropriate model based on entity type
let model_child = if let Some(model_path) = get_model_path(node.entity_type) {
eprintln!("[bridge/reify] Loading {} model for node {} from {}",
match node.entity_type {
1 => "cube",
2 => "sphere",
3 => "3D model",
_ => "unknown"
}, id, model_path.display());
match Model::direct(&model_path) {
Ok(model) => Some(model.build()),
Err(e) => {
eprintln!("[bridge/reify] Failed to load model for node {}: {}", id, e);
None
}
}
_ => {
// Unknown or unsupported type - render as wireframe cube
eprintln!("[bridge/reify] Creating wireframe for unknown node {} type={}", id, node.entity_type);
let cube_lines = create_wireframe_cube(node_color, 0.003);
Some((*id, Spatial::default()
.transform(transform)
.build()
.child(Lines::new(cube_lines).build())))
}
}
} else {
eprintln!("[bridge/reify] No model available for entity type {} (node {})", node.entity_type, id);
None
};
Some((*id, Spatial::default()
.transform(transform)
.build()
.maybe_child(model_child)))
});
PlaySpace.build().stable_children(children)

41
bridge/src/primitives.rs Normal file
View File

@@ -0,0 +1,41 @@
use std::path::PathBuf;
use std::fs;
// Embedded GLTF primitives for basic shapes
pub mod embedded_models {
use super::*;
use std::sync::OnceLock;
static CACHE_DIR: OnceLock<PathBuf> = OnceLock::new();
pub fn get_cache_dir() -> &'static PathBuf {
CACHE_DIR.get_or_init(|| {
let dir = dirs::cache_dir()
.unwrap_or_else(|| PathBuf::from("/tmp"))
.join("starworld/primitives");
let _ = std::fs::create_dir_all(&dir);
dir
})
}
pub fn get_cube_model() -> PathBuf {
let path = get_cache_dir().join("cube.glb");
if !path.exists() {
std::fs::write(&path, CUBE_GLB).expect("Failed to write cube.glb");
}
path
}
pub fn get_sphere_model() -> PathBuf {
let path = get_cache_dir().join("sphere.glb");
if !path.exists() {
std::fs::write(&path, SPHERE_GLB).expect("Failed to write sphere.glb");
}
path
}
// Minimal cube GLB (binary GLTF) - this is a placeholder
// TODO: Generate proper GLB data or bundle actual primitive files
const CUBE_GLB: &[u8] = b""; // Will be filled with actual data
const SPHERE_GLB: &[u8] = b""; // Will be filled with actual data
}

BIN
primitives/cube.glb Normal file

Binary file not shown.

BIN
primitives/sphere.glb Normal file

Binary file not shown.

View File

@@ -284,7 +284,14 @@ bool StardustBridge::loadBridge() {
const char* overridePath = std::getenv("STARWORLD_BRIDGE_PATH");
std::vector<std::string> candidates;
if (overridePath) {
candidates.emplace_back(std::string(overridePath));
std::string pathStr(overridePath);
// If it's a directory, append the library filename
struct stat st;
if (stat(pathStr.c_str(), &st) == 0 && S_ISDIR(st.st_mode)) {
candidates.emplace_back(pathStr + "/libstardust_bridge.so");
} else {
candidates.emplace_back(pathStr);
}
}
// Likely local dev output
candidates.emplace_back("./bridge/target/debug/libstardust_bridge.so");

View File

@@ -0,0 +1,133 @@
import bpy
import os
import traceback
# Clear existing objects
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete(use_global=False)
# Output directory
output_dir = os.path.expanduser("~/.cache/starworld/primitives")
os.makedirs(output_dir, exist_ok=True)
def create_material(name, base_color):
"""Create a PBR material with the specified base color (RGBA)"""
try:
mat = bpy.data.materials.new(name=name)
mat.use_nodes = True
nodes = mat.node_tree.nodes
# Find the Principled BSDF node (should be created by default)
bsdf = None
for node in nodes:
if node.type == 'BSDF_PRINCIPLED':
bsdf = node
break
# If not found, create it
if not bsdf:
bsdf = nodes.new(type='ShaderNodeBsdfPrincipled')
# Set material properties
bsdf.inputs['Base Color'].default_value = base_color
bsdf.inputs['Roughness'].default_value = 0.5
bsdf.inputs['Metallic'].default_value = 0.1
print(f" Created material '{name}' with color {base_color}")
return mat
except Exception as e:
print(f"ERROR creating material: {e}")
traceback.print_exc()
return None
try:
print("Creating GREEN sphere...")
# Create and export UV Sphere with GREEN material
bpy.ops.mesh.primitive_uv_sphere_add(segments=32, ring_count=16, radius=0.5, location=(0, 0, 0))
sphere = bpy.context.active_object
sphere.name = "Sphere"
sphere.select_set(True)
bpy.context.view_layer.objects.active = sphere
bpy.ops.object.shade_smooth()
# Apply green material
green_mat = create_material("SphereMaterial", (0.2, 1.0, 0.2, 1.0)) # Green
if green_mat:
sphere.data.materials.append(green_mat)
sphere_path = os.path.join(output_dir, "sphere.glb")
print(f" Exporting to {sphere_path}...")
bpy.ops.export_scene.gltf(
filepath=sphere_path,
export_format='GLB',
use_selection=True
)
print(f"✓ Exported sphere.glb (GREEN)")
except Exception as e:
print(f"ERROR with sphere: {e}")
traceback.print_exc()
try:
print("\nCreating RED cube...")
# Delete sphere and create cube with RED material
bpy.ops.object.delete(use_global=False)
bpy.ops.mesh.primitive_cube_add(size=1.0, location=(0, 0, 0))
cube = bpy.context.active_object
cube.name = "Cube"
cube.select_set(True)
bpy.context.view_layer.objects.active = cube
bpy.ops.object.shade_smooth()
# Apply red material
red_mat = create_material("CubeMaterial", (1.0, 0.2, 0.2, 1.0)) # Red
if red_mat:
cube.data.materials.append(red_mat)
cube_path = os.path.join(output_dir, "cube.glb")
print(f" Exporting to {cube_path}...")
bpy.ops.export_scene.gltf(
filepath=cube_path,
export_format='GLB',
use_selection=True
)
print(f"✓ Exported cube.glb (RED)")
except Exception as e:
print(f"ERROR with cube: {e}")
traceback.print_exc()
try:
print("\nCreating BLUE icosphere...")
# Delete cube and create ico sphere for the "model" placeholder with BLUE material
bpy.ops.object.delete(use_global=False)
bpy.ops.mesh.primitive_ico_sphere_add(subdivisions=2, radius=0.5, location=(0, 0, 0))
ico = bpy.context.active_object
ico.name = "IcoSphere"
ico.select_set(True)
bpy.context.view_layer.objects.active = ico
bpy.ops.object.shade_smooth()
# Apply blue material
blue_mat = create_material("IcoSphereMaterial", (0.2, 0.2, 1.0, 1.0)) # Blue
if blue_mat:
ico.data.materials.append(blue_mat)
model_path = os.path.join(output_dir, "model.glb")
print(f" Exporting to {model_path}...")
bpy.ops.export_scene.gltf(
filepath=model_path,
export_format='GLB',
use_selection=True
)
print(f"✓ Exported model.glb (BLUE)")
except Exception as e:
print(f"ERROR with icosphere: {e}")
traceback.print_exc()
print("\n" + "="*60)
print("✓ EXPORT COMPLETE!")
print("="*60)
print(f"Output directory: {output_dir}")
print(" - cube.glb (RED)")
print(" - sphere.glb (GREEN)")
print(" - model.glb (BLUE)")
print("="*60)

232
tools/generate_primitives.py Executable file
View File

@@ -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")

View File

@@ -0,0 +1,12 @@
[package]
name = "gltf_primitives"
version = "0.1.0"
edition = "2021"
[[bin]]
name = "generate_primitives"
path = "generate.rs"
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"