feat: initialize sunrise with 100-player shared rooms
This commit is contained in:
@@ -0,0 +1,443 @@
|
||||
"""Run in Blender through MCP. Reuses the snapshot of src/constants/structures.ts.
|
||||
|
||||
Builds a separate scene, then exposes export_assets() and setup_studio().
|
||||
No source scene objects or external textures are changed.
|
||||
"""
|
||||
import bpy
|
||||
import bmesh
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
from mathutils import Vector
|
||||
|
||||
ROOT = Path('D:/project/sunrise')
|
||||
SOURCE = ROOT / 'assets/summit'
|
||||
OUTPUT = ROOT / 'public/models/summit'
|
||||
OUTPUT.mkdir(parents=True, exist_ok=True)
|
||||
SPECS = json.loads((SOURCE / 'structure-specs.json').read_text(encoding='utf-8'))
|
||||
SCENE = bpy.data.scenes.get('Summit Assets - refined') or bpy.data.scenes.new('Summit Assets - refined')
|
||||
bpy.context.window.scene = SCENE
|
||||
SCENE.unit_settings.system = 'METRIC'
|
||||
SCENE.unit_settings.scale_length = 1
|
||||
ASSETS = []
|
||||
|
||||
|
||||
def enum_value(owner, prop, identifier):
|
||||
return next(i.identifier for i in owner.bl_rna.properties[prop].enum_items if i.identifier == identifier)
|
||||
|
||||
|
||||
def material(name, hex_color, roughness=0.65, metal=0):
|
||||
existing = bpy.data.materials.get('Summit / ' + name)
|
||||
if existing:
|
||||
return existing
|
||||
rgb = [int(hex_color[i:i+2], 16)/255 for i in (0, 2, 4)]
|
||||
rgba = [v/12.92 if v <= .04045 else ((v+.055)/1.055)**2.4 for v in rgb] + [1]
|
||||
mat = bpy.data.materials.new('Summit / ' + name)
|
||||
mat.diffuse_color = rgba
|
||||
mat.use_nodes = True
|
||||
node = next(n for n in mat.node_tree.nodes if n.type == 'BSDF_PRINCIPLED')
|
||||
node.inputs['Base Color'].default_value = rgba
|
||||
node.inputs['Roughness'].default_value = roughness
|
||||
node.inputs['Metallic'].default_value = metal
|
||||
return mat
|
||||
|
||||
|
||||
MATS = {
|
||||
'galvanized': material('Galvanized steel', 'a5b0b5', .4, .7),
|
||||
'red': material('Faded aviation red', 'a94f3d', .64, .25),
|
||||
'dark': material('Graphite metal', '37413e', .55, .45),
|
||||
'concrete': material('Warm concrete', 'b7b4a8', .92),
|
||||
'green': material('Muted green cladding', '6e8b7f', .7, .15),
|
||||
'seam': material('Panel joints', '8f9c98', .85),
|
||||
'white': material('Antenna ivory', 'e2e4dc', .52, .08),
|
||||
'panel0': material('Radome warm white', 'e8e9df', .65),
|
||||
'panel1': material('Radome pearl', 'dce2da', .65),
|
||||
'panel2': material('Radome light gray', 'd2dcd5', .68),
|
||||
'wood0': material('Deck weathered oak', '9b896c', .88),
|
||||
'wood1': material('Deck warm oak', 'aa9475', .88),
|
||||
'wood2': material('Deck gray oak', '918774', .9),
|
||||
'wood3': material('Deck dark oak', '84785f', .9),
|
||||
'groove': material('Deck grooves', '6e6654', .95),
|
||||
'rail': material('Dark timber railing', '454b42', .86),
|
||||
}
|
||||
|
||||
|
||||
class Meshes:
|
||||
"""Accumulate disconnected primitives by material instead of thousands of objects."""
|
||||
def __init__(self):
|
||||
self.parts = {}
|
||||
|
||||
def add(self, mat, verts, faces):
|
||||
vs, fs = self.parts.setdefault(mat, ([], []))
|
||||
offset = len(vs)
|
||||
vs.extend(tuple(v) for v in verts)
|
||||
fs.extend(tuple(offset+i for i in face) for face in faces)
|
||||
|
||||
def box(self, mat, center, size):
|
||||
x, y, z = center
|
||||
a, b, c = (v/2 for v in size)
|
||||
self.add(mat, [(x+dx*a, y+dy*b, z+dz*c) for dx, dy, dz in
|
||||
[(-1,-1,-1),(1,-1,-1),(1,1,-1),(-1,1,-1),
|
||||
(-1,-1,1),(1,-1,1),(1,1,1),(-1,1,1)]],
|
||||
[(0,3,2,1),(4,5,6,7),(0,1,5,4),(1,2,6,5),(2,3,7,6),(3,0,4,7)])
|
||||
|
||||
def beam(self, mat, a, b, radius, sides=8, end_radius=None):
|
||||
a, b = Vector(a), Vector(b)
|
||||
direction = (b-a).normalized()
|
||||
helper = Vector((0,0,1)) if abs(direction.z) < .95 else Vector((1,0,0))
|
||||
u = direction.cross(helper).normalized()
|
||||
v = direction.cross(u)
|
||||
verts = []
|
||||
for point, r in [(a, radius), (b, radius if end_radius is None else end_radius)]:
|
||||
verts.extend(point + r*(math.cos(i*math.tau/sides)*u + math.sin(i*math.tau/sides)*v) for i in range(sides))
|
||||
faces = [tuple(reversed(range(sides))), tuple(range(sides, 2*sides))]
|
||||
faces.extend((i,(i+1)%sides,(i+1)%sides+sides,i+sides) for i in range(sides))
|
||||
self.add(mat, verts, faces)
|
||||
|
||||
def finish(self, root):
|
||||
for key, (verts, faces) in self.parts.items():
|
||||
mesh = bpy.data.meshes.new(root.name + '/' + key)
|
||||
mesh.from_pydata(verts, [], faces)
|
||||
mesh.update()
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(mesh)
|
||||
bmesh.ops.recalc_face_normals(bm, faces=list(bm.faces))
|
||||
bm.to_mesh(mesh)
|
||||
bm.free()
|
||||
obj = bpy.data.objects.new(mesh.name, mesh)
|
||||
root.users_collection[0].objects.link(obj)
|
||||
obj.parent = root
|
||||
obj.data.materials.append(MATS[key])
|
||||
if key.startswith('wood') or key == 'rail':
|
||||
modifier = obj.modifiers.new('Subtle edge wear', enum_value(bpy.types.Modifier, 'type', 'BEVEL'))
|
||||
modifier.width = .006
|
||||
modifier.segments = 1
|
||||
obj['materialBatch'] = key
|
||||
|
||||
|
||||
def root_for(spec, kind, gallery):
|
||||
coll = bpy.data.collections.new(spec['id'])
|
||||
SCENE.collection.children.link(coll)
|
||||
root = bpy.data.objects.new(spec['id'], None)
|
||||
coll.objects.link(root)
|
||||
root.location = gallery
|
||||
root['assetId'] = spec['id']
|
||||
root['dimensionsEstimated'] = True
|
||||
root['source'] = spec['source']
|
||||
root['kind'] = kind
|
||||
root['units'] = 'metres'
|
||||
root['generationSpecJson'] = json.dumps(spec)
|
||||
root.asset_mark()
|
||||
root.asset_data.description = 'Gwanaksan ' + kind + '; estimated dimensions; photo-informed approximation.'
|
||||
ASSETS.append({'root': root, 'spec': spec, 'kind': kind, 'supports': []})
|
||||
return root
|
||||
|
||||
|
||||
def tower(spec, index):
|
||||
root = root_for(spec, 'tower', ((index-2)*12, 17, 0))
|
||||
m = Meshes()
|
||||
h, w = spec['height'], spec['width']
|
||||
levels = 12
|
||||
def half(t):
|
||||
return w/2 * (1-2.5*t if t < .28 else .3 - .1*(t-.28)/.72)
|
||||
def corner(t, j):
|
||||
r = half(t)
|
||||
return Vector(([-r,r,r,-r][j],[-r,-r,r,r][j],t*h))
|
||||
for i in range(levels):
|
||||
t, t1 = i/levels, (i+1)/levels
|
||||
color = 'galvanized' if index == 0 or index == 2 else ('red' if (i//2)%2 == 0 else 'galvanized')
|
||||
for j in range(4):
|
||||
k = (j+1)%4
|
||||
a,b,c,d = corner(t,j),corner(t,k),corner(t1,j),corner(t1,k)
|
||||
m.beam(color,a,c,.075 if index == 0 else .095)
|
||||
m.beam(color,a,b,.055)
|
||||
m.beam(color,a,d,.034)
|
||||
m.beam(color,b,c,.034)
|
||||
if i == levels-1:
|
||||
m.beam(color,c,d,.055)
|
||||
# Base plates and anchors; terrain foundation itself stays in the application.
|
||||
for j in range(4):
|
||||
x,y,_ = corner(0,j)
|
||||
m.box('dark',(x,y,.035),(.38,.38,.07))
|
||||
for dx in [-.135,.135]:
|
||||
for dy in [-.135,.135]:
|
||||
m.beam('galvanized',(x+dx,y+dy,.07),(x+dx,y+dy,.13),.026,6)
|
||||
# Maintenance platforms, open in the centre, with railings.
|
||||
for fraction in [.32,.64,.93]:
|
||||
z = h*fraction
|
||||
r = half(fraction)+.43
|
||||
for side in [-1,1]:
|
||||
m.box('galvanized',(0,side*(r-.13),z),(2*r,.26,.085))
|
||||
m.box('galvanized',(side*(r-.13),0,z),(.26,2*r-.52,.085))
|
||||
for j in range(4):
|
||||
a = Vector(([-r,r,r,-r][j],[-r,-r,r,r][j],z))
|
||||
b = Vector(([-r,r,r,-r][(j+1)%4],[-r,-r,r,r][(j+1)%4],z))
|
||||
for v in [a,b,(a+b)/2]:
|
||||
m.beam('galvanized',v,v+Vector((0,0,.95)),.022)
|
||||
for dz in [.48,.95]:
|
||||
m.beam('galvanized',a+Vector((0,0,dz)),b+Vector((0,0,dz)),.019)
|
||||
# Continuous service ladder and cable trunk.
|
||||
for x in [-.22,.22]:
|
||||
m.beam('dark',(x,0,.15),(x,0,h-.1),.025)
|
||||
for n in range(int(h/.32)):
|
||||
z = .25+n*.32
|
||||
m.beam('galvanized',(-.22,0,z),(.22,0,z),.017,6)
|
||||
m.beam('dark',(.32,.08,.1),(.32,.08,h),.045)
|
||||
# Dish covers and radial brackets, distributed by height and azimuth.
|
||||
dish_count = 3 if index == 0 else 8
|
||||
for n in range(dish_count):
|
||||
fraction = (.40+n*.16) if index == 0 else (.17+n*.065)
|
||||
z = h*fraction
|
||||
theta = (n*137.5 + index*31)*math.pi/180
|
||||
direction = Vector((math.cos(theta),math.sin(theta),0))
|
||||
base = direction*half(fraction)+Vector((0,0,z))
|
||||
center = base+direction*.65
|
||||
radius = .27 if index == 0 else .42+(n%3)*.13
|
||||
m.beam('galvanized',base,center,.05)
|
||||
m.beam('dark',center-direction*.08,center+direction*.03,radius,20)
|
||||
m.beam('white',center+direction*.03,center+direction*.16,radius*.98,24,radius*.9)
|
||||
for t in [.56,.75,.87]:
|
||||
x = half(t)+.18
|
||||
m.box('white',(x,0,t*h),(.22,.28,1.45 if index == 0 else 1.9))
|
||||
m.beam('galvanized',(half(t),0,t*h),(x,0,t*h),.04)
|
||||
# Keep the original four-metre antenna extension.
|
||||
for n in range(4):
|
||||
m.beam('red' if index and n%2 == 0 else 'white',(0,0,h+n),(0,0,h+n+1),.11 if index == 0 else .17,12)
|
||||
for z in [h+.4,h+1.4,h+2.4]:
|
||||
m.beam('dark',(-.25,0,z),(.25,0,z),.024)
|
||||
m.finish(root)
|
||||
|
||||
|
||||
def clipped(poly, floor):
|
||||
out = []
|
||||
for a,b in zip(poly, poly[1:]+poly[:1]):
|
||||
if a.z >= floor:
|
||||
out.append(a)
|
||||
if (a.z >= floor) != (b.z >= floor):
|
||||
out.append(a+(b-a)*((floor-a.z)/(b.z-a.z)))
|
||||
return out
|
||||
|
||||
|
||||
def radar():
|
||||
spec = {'id':'weather-radar', **SPECS['radar']}
|
||||
root = root_for(spec,'radar',(-17,-8,0))
|
||||
m = Meshes()
|
||||
h,r = spec['buildingHeight'],spec['radius']
|
||||
m.beam('concrete',(0,0,0),(0,0,h-.2),4,32,3.7)
|
||||
# Ribbed green cladding accents and concrete belt courses.
|
||||
for z in [.18,2.8,5.5]:
|
||||
m.beam('concrete',(0,0,z),(0,0,z+.18),4-.3*z/h+.055,32)
|
||||
for n in range(32):
|
||||
a = n*math.tau/32
|
||||
p = Vector((math.cos(a),math.sin(a),0))
|
||||
m.beam('green',p*3.97+Vector((0,0,.35)),p*3.73+Vector((0,0,h-.25)),.07,6)
|
||||
m.beam('green',(0,0,h-.22),(0,0,h+.02),4.4,48)
|
||||
m.beam('white',(0,0,h+.02),(0,0,h+.15),4.43,48)
|
||||
# Balcony ring rails.
|
||||
for n in range(48):
|
||||
a,b = n*math.tau/48,(n+1)*math.tau/48
|
||||
p,q = Vector((4.36*math.cos(a),4.36*math.sin(a),h+.15)),Vector((4.36*math.cos(b),4.36*math.sin(b),h+.15))
|
||||
m.beam('green',p,p+Vector((0,0,.85)),.022)
|
||||
for dz in [.43,.85]:
|
||||
m.beam('green',p+Vector((0,0,dz)),q+Vector((0,0,dz)),.019)
|
||||
m.box('dark',(0,-3.96,1.2),(1.25,.12,2.4))
|
||||
m.box('green',(0,-4.04,1.2),(1.08,.07,2.25))
|
||||
m.beam('galvanized',(.38,-4.09,1.03),(.38,-4.09,1.35),.024)
|
||||
m.box('concrete',(0,-4.18,.09),(1.5,.5,.18))
|
||||
for n in range(9):
|
||||
m.box('dark',(.95,-3.85,2.9+n*.075),(.65,.10,.035))
|
||||
# Dual of an icosphere creates hexagonal/pentagonal panels as in photo-09.
|
||||
bm = bmesh.new()
|
||||
bmesh.ops.create_icosphere(bm,subdivisions=3,radius=1)
|
||||
bm.verts.ensure_lookup_table()
|
||||
center = Vector((0,0,h+r*.8))
|
||||
for index,v in enumerate(bm.verts):
|
||||
axis = v.co.normalized()
|
||||
u = axis.cross(Vector((0,0,1)) if abs(axis.z)<.95 else Vector((1,0,0))).normalized()
|
||||
w = axis.cross(u)
|
||||
points = [f.calc_center_median().normalized() for f in v.link_faces]
|
||||
points.sort(key=lambda p: math.atan2(p.dot(w),p.dot(u)))
|
||||
polygon = clipped([center+p*r for p in points], h+.17)
|
||||
if len(polygon)<3:
|
||||
continue
|
||||
centroid = sum(polygon,Vector())/len(polygon)
|
||||
# Slight inset leaves readable dark joints with no photographic texture.
|
||||
outer = [centroid+(p-centroid)*.985+axis*.018 for p in polygon]
|
||||
inner = [p-axis*.04 for p in polygon]
|
||||
n = len(polygon)
|
||||
m.add('seam',polygon+inner,[tuple(range(n)),tuple(reversed(range(n,2*n)))] + [(i,(i+1)%n,(i+1)%n+n,i+n) for i in range(n)])
|
||||
m.add('panel'+str(index%3),outer,[tuple(range(n))])
|
||||
bm.free()
|
||||
m.finish(root)
|
||||
|
||||
|
||||
def deck(spec,index):
|
||||
root = root_for(spec,'deck', (index*15,-9,4))
|
||||
m = Meshes()
|
||||
w,L,run,drop = (spec[k] for k in ['width','length','stairLength','stairDrop'])
|
||||
thickness = SPECS['deck']['thickness']
|
||||
rail_h = SPECS['railing']['height']
|
||||
supports = ASSETS[-1]['supports']
|
||||
# Blender +Y corresponds to Three.js -Z (the stair direction).
|
||||
def plank(y,z,depth,index):
|
||||
m.box('wood'+str(index%4),(0,y,z-thickness/2),(w,depth,thickness))
|
||||
# Shallow grooves and visible fasteners, all batched by material.
|
||||
for offset in [-.28,0,.28]:
|
||||
m.box('groove',(0,y+offset*depth,z+.0005),(w-.035,.004,.001))
|
||||
for x in [-w/2+.12,w/2-.12]:
|
||||
m.beam('galvanized',(x,y,z),(x,y,z+.006),.012,6)
|
||||
count = math.ceil(L/SPECS['deck']['plankWidth'])
|
||||
for i in range(count):
|
||||
plank(-L/2+(i+.5)*L/count,0,L/count-.009,i)
|
||||
steps = math.ceil(drop/.18)
|
||||
for i in range(1,steps+1):
|
||||
y,z = L/2+(i-.5)*run/steps,-i*drop/steps
|
||||
plank(y,z,run/steps+.015,i)
|
||||
m.box('dark',(0,y-run/(2*steps),z-.08),(w-.06,.025,.12))
|
||||
# Joists and stair stringers remain fixed geometry.
|
||||
for x in [-w*.37,0,w*.37]:
|
||||
m.box('dark',(x,0,-thickness-.085),(.11,L,.17))
|
||||
m.beam('dark',(x,L/2,-.27),(x,L/2+run,-drop-.27),.09,4)
|
||||
for i in range(math.ceil(L/.9)+1):
|
||||
y = -L/2+i*L/math.ceil(L/.9)
|
||||
m.box('dark',(0,y,-.27),(w,.08,.13))
|
||||
seen = set()
|
||||
def railing(a,b):
|
||||
a,b = Vector(a),Vector(b)
|
||||
count = math.ceil((b-a).length/SPECS['railing']['maxSpan'])
|
||||
for i in range(count+1):
|
||||
p = a.lerp(b,i/count)
|
||||
key = tuple(round(v,5) for v in p)
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
m.beam('rail',p+Vector((0,0,-.32)),p+Vector((0,0,rail_h)),.072,8)
|
||||
m.beam('rail',p+Vector((0,0,rail_h)),p+Vector((0,0,rail_h+.025)),.078,8)
|
||||
m.box('dark',p+Vector((0,0,-.2)),(.19,.19,.05))
|
||||
supports.append({'position':[p.x,p.z-.32,-p.y],'postWidth':SPECS['railing']['postWidth']})
|
||||
for dz in [.5,rail_h-.035]:
|
||||
m.beam('rail',a+Vector((0,0,dz)),b+Vector((0,0,dz)),.045,4)
|
||||
for x in [-w/2,w/2]:
|
||||
railing((x,-L/2,0),(x,L/2,0))
|
||||
railing((x,L/2,0),(x,L/2+run,-drop))
|
||||
m.finish(root)
|
||||
root['terrainSupportAnchorsJson'] = json.dumps(supports)
|
||||
|
||||
|
||||
if not any(obj.get('assetId') for obj in SCENE.objects):
|
||||
for index,spec in enumerate(SPECS['towers']):
|
||||
tower(spec,index)
|
||||
radar()
|
||||
for index,spec in enumerate(SPECS['decks']):
|
||||
deck(spec,index)
|
||||
else:
|
||||
specs = {s['id']:s for s in SPECS['towers'] + SPECS['decks']}
|
||||
specs['weather-radar'] = {'id':'weather-radar', **SPECS['radar']}
|
||||
for obj in SCENE.objects:
|
||||
if obj.get('assetId'):
|
||||
spec = specs[obj['assetId']]
|
||||
if json.loads(obj['generationSpecJson']) != spec:
|
||||
raise ValueError('Dimension snapshot changed: regenerate in a new Blender file; existing geometry was preserved.')
|
||||
anchors = json.loads(obj.get('terrainSupportAnchorsJson', '[]'))
|
||||
ASSETS.append({'root':obj,'spec':spec,'kind':obj['kind'],'supports':anchors})
|
||||
bpy.context.view_layer.update()
|
||||
print('Created',len(ASSETS),'assets;',sum(len(a['root'].children) for a in ASSETS),'material batches')
|
||||
|
||||
|
||||
def export_assets():
|
||||
manifest = {'version':1,'units':'metres','upAxis':'Y','northAxis':'-Z',
|
||||
'dimensionsEstimated':True,'photographsEmbedded':False,
|
||||
'sourceConstants':'src/constants/structures.ts','assets':[]}
|
||||
# Blender 5.2 exposes these dynamic items through the exporter callback, not RNA.
|
||||
import io_scene_gltf2
|
||||
formats = io_scene_gltf2.get_format_items(None, bpy.context)
|
||||
fmt = next(item[0] for item in formats if item[0] == 'GLB')
|
||||
for asset in ASSETS:
|
||||
root,spec = asset['root'],asset['spec']
|
||||
original = root.location.copy()
|
||||
root.location = (0,0,0)
|
||||
bpy.context.view_layer.update()
|
||||
for obj in SCENE.objects:
|
||||
obj.select_set(False)
|
||||
root.select_set(True)
|
||||
for obj in root.children:
|
||||
obj.select_set(True)
|
||||
bpy.context.view_layer.objects.active = root
|
||||
try:
|
||||
bpy.ops.export_scene.gltf(filepath=str(OUTPUT/(root.name+'.glb')),export_format=fmt,
|
||||
use_selection=True,use_active_scene=True,export_apply=True,export_yup=True,export_extras=True,
|
||||
export_animations=False,export_cameras=False,export_lights=False)
|
||||
finally:
|
||||
root.location = original
|
||||
manifest['assets'].append({'id':root.name,'file':root.name+'.glb','kind':asset['kind'],
|
||||
'latitude':spec['latitude'],'longitude':spec['longitude'],'heading':spec.get('heading',0),
|
||||
'dimensionsEstimated':True,'positionEstimated':spec.get('estimatedPosition',True),
|
||||
'source':spec['source'],'dimensions':{k:spec[k] for k in ['height','width','length','radius','buildingHeight','stairLength','stairDrop'] if k in spec},
|
||||
'origin':'upper deck surface' if asset['kind']=='deck' else 'base contact plane',
|
||||
'terrainSupportAnchors':asset['supports']})
|
||||
bpy.context.view_layer.update()
|
||||
(OUTPUT/'manifest.json').write_text(json.dumps(manifest,ensure_ascii=False,indent=2),encoding='utf-8')
|
||||
print('Exported',len(manifest['assets']),'GLBs and manifest')
|
||||
|
||||
|
||||
def setup_studio():
|
||||
if SCENE.camera:
|
||||
return SCENE.camera
|
||||
studio = bpy.data.collections.new('Presentation only - excluded from exports')
|
||||
SCENE.collection.children.link(studio)
|
||||
groundmat = material('Studio slate','23333d',.9)
|
||||
mesh = bpy.data.meshes.new('Studio ground')
|
||||
mesh.from_pydata([(-2000,-2000,-.45),(2000,-2000,-.45),(2000,2000,-.45),(-2000,2000,-.45)],[],[(0,1,2,3)])
|
||||
ground = bpy.data.objects.new('Studio ground',mesh)
|
||||
studio.objects.link(ground)
|
||||
ground.data.materials.append(groundmat)
|
||||
world = bpy.data.worlds.new('Summit studio world')
|
||||
SCENE.world = world
|
||||
world.use_nodes = True
|
||||
bg = next(n for n in world.node_tree.nodes if n.type=='BACKGROUND')
|
||||
bg.inputs['Color'].default_value = (.38,.46,.55,1)
|
||||
bg.inputs['Strength'].default_value = .55
|
||||
for name,loc,power,size in [('Key',(-25,-35,65),85000,35),('Fill',(40,-10,35),45000,25),('Rim',(0,45,55),110000,30)]:
|
||||
data = bpy.data.lights.new('Studio '+name,enum_value(bpy.types.Light,'type','AREA'))
|
||||
obj = bpy.data.objects.new(data.name,data)
|
||||
studio.objects.link(obj)
|
||||
obj.location = loc
|
||||
obj.rotation_euler = (Vector((0,5,15))-obj.location).to_track_quat('-Z','Y').to_euler()
|
||||
data.energy = power
|
||||
data.shape = enum_value(bpy.types.AreaLight,'shape','DISK')
|
||||
data.size = size
|
||||
data = bpy.data.cameras.new('Asset preview camera')
|
||||
camera = bpy.data.objects.new(data.name,data)
|
||||
studio.objects.link(camera)
|
||||
data.type = enum_value(bpy.types.Camera,'type','ORTHO')
|
||||
data.lens = 50
|
||||
SCENE.camera = camera
|
||||
SCENE.render.resolution_percentage = 100
|
||||
SCENE.render.image_settings.file_format = enum_value(bpy.types.ImageFormatSettings,'file_format','PNG')
|
||||
try:
|
||||
SCENE.render.engine = 'CYCLES'
|
||||
SCENE.cycles.samples = 24
|
||||
SCENE.cycles.use_denoising = True
|
||||
except TypeError:
|
||||
pass
|
||||
SCENE.render.film_transparent = False
|
||||
preview_camera((64,-92,64),(0,8,23),96,1600,1200)
|
||||
return camera
|
||||
|
||||
|
||||
def preview_camera(location,target,scale,width=1400,height=1100):
|
||||
camera = SCENE.camera
|
||||
camera.location = location
|
||||
camera.rotation_euler = (Vector(target)-camera.location).to_track_quat('-Z','Y').to_euler()
|
||||
camera.data.ortho_scale = scale
|
||||
SCENE.render.resolution_x = width
|
||||
SCENE.render.resolution_y = height
|
||||
|
||||
|
||||
def save_source():
|
||||
bpy.ops.wm.save_as_mainfile(filepath=str(SOURCE/'summit-structures.blend'))
|
||||
print('Saved editable Blender source')
|
||||
|
||||
|
||||
# MCP executes each request in a fresh namespace. Retain this task's helpers explicitly.
|
||||
bpy.app.driver_namespace['summit_assets'] = globals()
|
||||
Reference in New Issue
Block a user