# Generic GLB inspection viewer for AI-generated meshes (Tripo / Meshy rigged). # Reads env vars: # VIEW_GLB - absolute path to the .glb to load # VIEW_SHOTS - absolute dir for screenshots # VIEW_PREFIX - filename prefix for shots # Takes turntable shots (auto-framed from the mesh AABB); if the GLB carries # animations, also captures frames across the first clip. Quits when done. # Set VIEW_INTERACTIVE=1 to skip shots and orbit live instead # (left-drag orbits, wheel zooms; animation loops if present). extends Node3D var model: Node3D var cam: Camera3D var shot_dir: String var prefix: String var interactive := false var dragging := false var orbit_yaw := 0.0 var orbit_pitch := 0.08 var orbit_dist := 3.4 var orbit_target := Vector3(0, 1.0, 0) func _ready() -> void: var glb_path := OS.get_environment("VIEW_GLB") shot_dir = OS.get_environment("VIEW_SHOTS") prefix = OS.get_environment("VIEW_PREFIX") interactive = OS.get_environment("VIEW_INTERACTIVE") == "1" if interactive and shot_dir == "": shot_dir = "C:/Users/CAN/AppData/Local/Temp/" if glb_path == "" or shot_dir == "": push_error("VIEW_GLB / VIEW_SHOTS not set") get_tree().quit(1) return if not shot_dir.ends_with("/"): shot_dir += "/" DirAccess.make_dir_recursive_absolute(shot_dir) var env := Environment.new() env.background_mode = Environment.BG_COLOR env.background_color = Color(0.12, 0.13, 0.16) env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR env.ambient_light_color = Color(0.75, 0.78, 0.85) env.ambient_light_energy = 0.7 env.tonemap_mode = Environment.TONE_MAPPER_FILMIC var we := WorldEnvironment.new() we.environment = env add_child(we) var sun := DirectionalLight3D.new() sun.rotation_degrees = Vector3(-40, -35, 0) sun.light_energy = 1.5 add_child(sun) var fill := DirectionalLight3D.new() fill.rotation_degrees = Vector3(-15, 140, 0) fill.light_energy = 0.6 add_child(fill) var doc := GLTFDocument.new() var state := GLTFState.new() if doc.append_from_file(glb_path, state) != OK: push_error("GLB load failed: " + glb_path) get_tree().quit(1) return model = doc.generate_scene(state) add_child(model) # inventory: meshes, verts, skeleton bones, animations var anim_player: AnimationPlayer = null var skel: Skeleton3D = null var tri_count := 0 var stack: Array = [model] while not stack.is_empty(): var n: Node = stack.pop_back() if n is AnimationPlayer and anim_player == null: anim_player = n if n is Skeleton3D and skel == null: skel = n if n is MeshInstance3D and n.mesh != null: for s in n.mesh.get_surface_count(): tri_count += n.mesh.surface_get_arrays(s)[Mesh.ARRAY_INDEX].size() / 3 for c in n.get_children(): stack.push_back(c) print("INFO tris=", tri_count) if skel != null: var names := PackedStringArray() for i in skel.get_bone_count(): names.append(skel.get_bone_name(i)) print("INFO bones=", skel.get_bone_count(), " names=", ",".join(names)) else: print("INFO bones=0") if anim_player != null: print("INFO anims=", ",".join(anim_player.get_animation_list())) # auto-frame from merged AABB (skinned meshes report bogus tiny AABBs — # fall back to skeleton bone rest positions) await get_tree().process_frame var aabb := _merged_aabb() if skel != null and max(aabb.size.x, max(aabb.size.y, aabb.size.z)) < 0.5: aabb = _skeleton_aabb(skel) print("INFO aabb pos=", aabb.position, " size=", aabb.size) var center := aabb.get_center() var radius: float = max(aabb.size.x, max(aabb.size.y, aabb.size.z)) * 0.5 var dist := radius * 2.6 cam = Camera3D.new() cam.fov = 50 add_child(cam) cam.current = true if interactive: orbit_target = center orbit_dist = dist _update_cam() if anim_player != null and anim_player.get_animation_list().size() > 0: var loop_anim: String = anim_player.get_animation_list()[0] anim_player.get_animation(loop_anim).loop_mode = Animation.LOOP_LINEAR anim_player.play(loop_anim) print("INTERACTIVE — left-drag orbit, wheel zoom") return # turntable var angles := {"01_front": 0.0, "02_three_quarter": PI * 0.25, "03_side": PI * 0.5, "04_back": PI} for shot_name in angles: var a: float = angles[shot_name] cam.look_at_from_position(center + Vector3(sin(a) * dist, radius * 0.35, cos(a) * dist), center) await _snap(shot_name) # head closeup (top of AABB) var head := center + Vector3(0, aabb.size.y * 0.38, 0) cam.look_at_from_position(head + Vector3(0, 0.05 * radius, dist * 0.35), head) await _snap("05_head") # animation frames if anim_player != null and anim_player.get_animation_list().size() > 0: var anim_name: String = anim_player.get_animation_list()[0] var anim := anim_player.get_animation(anim_name) var frames := 6 for i in frames: var t := anim.length * float(i) / float(frames) anim_player.play(anim_name) anim_player.seek(t, true) anim_player.pause() cam.look_at_from_position(center + Vector3(sin(PI * 0.2) * dist, radius * 0.35, cos(PI * 0.2) * dist), center) await _snap("anim_f%d" % i) print("SHOTS_DONE") get_tree().quit(0) func _unhandled_input(event: InputEvent) -> void: if not interactive: return if event is InputEventMouseButton: if event.button_index == MOUSE_BUTTON_LEFT: dragging = event.pressed elif event.pressed and event.button_index == MOUSE_BUTTON_WHEEL_UP: orbit_dist = clampf(orbit_dist * 0.90, 0.35, 12.0) _update_cam() elif event.pressed and event.button_index == MOUSE_BUTTON_WHEEL_DOWN: orbit_dist = clampf(orbit_dist / 0.90, 0.35, 12.0) _update_cam() elif event is InputEventMouseMotion and dragging: orbit_yaw -= event.relative.x * 0.008 orbit_pitch = clampf(orbit_pitch + event.relative.y * 0.008, -1.3, 1.3) _update_cam() func _update_cam() -> void: var dir := Vector3( sin(orbit_yaw) * cos(orbit_pitch), sin(orbit_pitch), cos(orbit_yaw) * cos(orbit_pitch)) cam.look_at_from_position(orbit_target + dir * orbit_dist, orbit_target) func _skeleton_aabb(skel: Skeleton3D) -> AABB: var box := AABB(skel.global_transform * skel.get_bone_global_rest(0).origin, Vector3.ZERO) for i in skel.get_bone_count(): box = box.expand(skel.global_transform * skel.get_bone_global_rest(i).origin) box = box.grow(box.size.y * 0.12) return box func _merged_aabb() -> AABB: var merged := AABB() var first := true var stack: Array = [model] while not stack.is_empty(): var n: Node = stack.pop_back() if n is MeshInstance3D: var box: AABB = n.global_transform * n.get_aabb() if first: merged = box first = false else: merged = merged.merge(box) for c in n.get_children(): stack.push_back(c) return merged func _snap(shot_name: String) -> void: await get_tree().process_frame await get_tree().process_frame await RenderingServer.frame_post_draw var img := get_viewport().get_texture().get_image() img.save_png(shot_dir + prefix + shot_name + ".png") print("shot saved: ", shot_dir + prefix + shot_name + ".png")