初始化项目,由ModelHub XC社区提供模型
Model: megamindbrian/josiefied-qwen-spatial-engine Source: Original Platform
This commit is contained in:
146
blender-vision.py
Normal file
146
blender-vision.py
Normal file
@@ -0,0 +1,146 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user