Files
josiefied-qwen-spatial-engine/blender-vision.py
ModelHub XC 342de956ef 初始化项目,由ModelHub XC社区提供模型
Model: megamindbrian/josiefied-qwen-spatial-engine
Source: Original Platform
2026-07-01 13:33:20 +08:00

146 lines
4.9 KiB
Python

# blender_render_script.py
import bpy
import os
import math
from array import array
from mathutils import Vector
# NOTE: this script runs inside Blender's bundled Python, which usually has
# neither PIL nor numpy, so the 2x2 grid is assembled with Blender's own image
# API instead.
# These will be replaced by the main script before running
MODEL_PATH = r"{{MODEL_PATH}}"
OUTPUT_GRID_PATH = r"{{OUTPUT_GRID_PATH}}"
# ====================== BLENDER RENDER LOGIC ======================
# Start from a clean scene. open_mainfile("") raises, so just delete the
# default cube/camera/light instead.
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete()
# Import GLB (Blender's glTF importer handles KHR_draco_mesh_compression)
bpy.ops.import_scene.gltf(filepath=MODEL_PATH)
# Select meshes and compute bounds
bpy.ops.object.select_all(action='DESELECT')
for obj in bpy.data.objects:
if obj.type == 'MESH':
obj.select_set(True)
bpy.context.view_layer.objects.active = obj
bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY', center='BOUNDS')
# Calculate scene bounds
min_pt = Vector((float('inf'),) * 3)
max_pt = Vector((-float('inf'),) * 3)
for obj in bpy.data.objects:
if obj.type == 'MESH':
for vert in obj.data.vertices:
world_coord = obj.matrix_world @ vert.co
min_pt = Vector((min(min_pt.x, world_coord.x), min(min_pt.y, world_coord.y), min(min_pt.z, world_coord.z)))
max_pt = Vector((max(max_pt.x, world_coord.x), max(max_pt.y, world_coord.y), max(max_pt.z, world_coord.z)))
center = (min_pt + max_pt) / 2
extents = max_pt - min_pt
max_dim = max(extents.x, extents.y, extents.z) or 1.0
print(f"Blender bounds → Center: {center} | Max Dim: {max_dim:.3f}")
# Scene settings
scene = bpy.context.scene
scene.render.resolution_x = 512
scene.render.resolution_y = 512
scene.render.image_settings.file_format = 'PNG'
# EEVEE was renamed BLENDER_EEVEE_NEXT in Blender 4.2+; pick whatever this build
# actually offers so the script works across versions.
engine_options = scene.render.bl_rna.properties['engine'].enum_items.keys()
if 'BLENDER_EEVEE_NEXT' in engine_options:
scene.render.engine = 'BLENDER_EEVEE_NEXT'
elif 'BLENDER_EEVEE' in engine_options:
scene.render.engine = 'BLENDER_EEVEE'
scene.eevee.taa_render_samples = 64
scene.render.film_transparent = True
# Add sunlight
light = bpy.data.lights.new(name="Sun", type='SUN')
light.energy = 5.0
light_node = bpy.data.objects.new("Sun", light)
bpy.context.collection.objects.link(light_node)
light_node.rotation_euler = (math.radians(45), 0, math.radians(30))
# Camera
cam_data = bpy.data.cameras.new(name='Camera')
cam = bpy.data.objects.new('Camera', cam_data)
bpy.context.collection.objects.link(cam)
scene.camera = cam
distance = max_dim * 2.5
views = [
("front", 8, 0),
("side", 5, 90),
("top", 75, 45),
("angle", -12, 35),
]
temp_dir = os.path.dirname(OUTPUT_GRID_PATH)
image_paths = []
for i, (name, pitch, yaw) in enumerate(views):
pitch_rad = math.radians(pitch)
yaw_rad = math.radians(yaw)
dx = distance * math.cos(pitch_rad) * math.sin(yaw_rad)
dy = -distance * math.cos(pitch_rad) * math.cos(yaw_rad)
dz = distance * math.sin(pitch_rad)
cam.location = center + Vector((dx, dy, dz))
direction = center - cam.location
cam.rotation_euler = direction.to_track_quat('-Z', 'Y').to_euler()
output = os.path.join(temp_dir, f"view_{i}.png")
scene.render.filepath = output
bpy.ops.render.render(write_still=True)
image_paths.append(output)
print(f"Rendered view: {name}")
# Create 2x2 grid using only Blender's image API (no PIL/numpy required).
GRID = 1024
bg = (30 / 255.0, 30 / 255.0, 35 / 255.0, 1.0)
dest = array('f', list(bg) * (GRID * GRID))
# (screen-space top-left x, top-left y) for each tile; Blender images are stored
# bottom-up, so convert the y origin accordingly.
placements = [(0, 0), (512, 0), (0, 512), (512, 512)]
for path, (px, py) in zip(image_paths, placements):
img = bpy.data.images.load(path)
sw, sh = img.size
src = array('f', [0.0] * (sw * sh * 4))
img.pixels.foreach_get(src)
base_row = GRID - py - sh # bottom-up row of the tile's bottom edge
for sr in range(sh):
grid_row = base_row + sr
if grid_row < 0 or grid_row >= GRID:
continue
d0 = (grid_row * GRID + px) * 4
s0 = sr * sw * 4
dest[d0:d0 + sw * 4] = src[s0:s0 + sw * 4]
bpy.data.images.remove(img)
grid_img = bpy.data.images.new("grid", width=GRID, height=GRID, alpha=True)
grid_img.pixels.foreach_set(dest)
grid_img.filepath_raw = OUTPUT_GRID_PATH
grid_img.file_format = 'PNG'
grid_img.save()
print(f"✅ Blender 4-view grid saved → {OUTPUT_GRID_PATH}")
# Cleanup temp view files
for p in image_paths:
try:
os.remove(p)
except:
pass