193 lines
7.9 KiB
Python
193 lines
7.9 KiB
Python
"""Run with Blender --background INPUT.blend --python this.py -- --output OUTPUT.blend.
|
|
|
|
Add an illustrative turbine to either public v007 or private v008. Never publish
|
|
an output based on the licensed v008 scene. Dimensions are visual placeholders.
|
|
"""
|
|
import argparse
|
|
import bpy
|
|
import json
|
|
import math
|
|
import sys
|
|
from pathlib import Path
|
|
from mathutils import Vector
|
|
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument('--output', required=True)
|
|
parser.add_argument('--render')
|
|
args = parser.parse_args(sys.argv[sys.argv.index('--') + 1:])
|
|
output = Path(args.output).resolve()
|
|
output.parent.mkdir(parents=True, exist_ok=True)
|
|
s = bpy.context.scene
|
|
assert '39 WIND - property turbine concept' not in bpy.data.collections, 'Already added'
|
|
detail = bpy.data.collections['DETAIL - full geometry for renders']
|
|
fast = bpy.data.collections['00 FAST TOUR - lightweight viewport']
|
|
detail.hide_viewport = False
|
|
fast.hide_viewport = True
|
|
coll = bpy.data.collections.new('39 WIND - property turbine concept')
|
|
detail.children.link(coll)
|
|
|
|
def material(name, color, metallic=0):
|
|
m = bpy.data.materials.new(name)
|
|
m.diffuse_color = (*color, 1)
|
|
m.use_nodes = True
|
|
shader = m.node_tree.nodes.get('Principled BSDF')
|
|
shader.inputs['Base Color'].default_value = (*color, 1)
|
|
shader.inputs['Metallic'].default_value = metallic
|
|
shader.inputs['Roughness'].default_value = .38
|
|
return m
|
|
|
|
white = material('WIND - warm ivory composite', (.79, .80, .74))
|
|
steel = material('WIND - galvanised mast', (.42, .47, .45), .65)
|
|
dark = material('WIND - dark service enclosure', (.095, .14, .12), .35)
|
|
concrete = material('WIND - foundation concept', (.37, .35, .29))
|
|
|
|
def adopt(ob, name, mat):
|
|
ob.name = 'WIND - ' + name
|
|
for c in list(ob.users_collection):
|
|
c.objects.unlink(ob)
|
|
coll.objects.link(ob)
|
|
ob.data.materials.append(mat)
|
|
return ob
|
|
|
|
def box(name, p, dimensions, mat):
|
|
bpy.ops.mesh.primitive_cube_add(size=1, location=p)
|
|
ob = adopt(bpy.context.object, name, mat)
|
|
ob.scale = dimensions
|
|
return ob
|
|
|
|
def cone(name, a, b, r1, r2, mat):
|
|
a, b = Vector(a), Vector(b)
|
|
bpy.ops.mesh.primitive_cone_add(vertices=40, radius1=r1, radius2=r2,
|
|
depth=(b-a).length, location=(a+b)/2)
|
|
ob = adopt(bpy.context.object, name, mat)
|
|
ob.rotation_euler = (b-a).to_track_quat('Z', 'Y').to_euler()
|
|
for face in ob.data.polygons:
|
|
face.use_smooth = True
|
|
return ob
|
|
|
|
x, y = -36., 40.
|
|
ground = lambda x, y: -.024*y + .004*x + .12*math.sin(x*.19)*math.cos(y*.14) + .055*math.sin(x*.72+y*.32)
|
|
z = ground(x, y)
|
|
hub = z + 18.
|
|
box('foundation cap - indicative only', (x, y, z+.10), (2.1, 2.1, .30), concrete)
|
|
cone('tapered freestanding mast', (x,y,z+.25), (x,y,hub), .27, .13, steel)
|
|
cone('base flange', (x,y,z+.23), (x,y,z+.30), .42, .42, steel)
|
|
for i in range(8):
|
|
angle = i*math.tau/8
|
|
bx, by = x+.34*math.cos(angle), y+.34*math.sin(angle)
|
|
cone('anchor bolt %02d' % i, (bx,by,z+.3), (bx,by,z+.37), .025, .025, dark)
|
|
cone('nacelle', (x,y-.35,hub), (x,y+.7,hub), .24, .18, white)
|
|
cone('rotor hub', (x,y-.32,hub), (x,y-.73,hub), .23, .07, white)
|
|
cone('tail boom', (x,y+.45,hub), (x,y+2.0,hub), .055, .035, steel)
|
|
box('tail vane', (x,y+1.9,hub+.32), (.055,.8,.65), white)
|
|
|
|
# Three tapered swept blades, with closed airfoil-like cross sections.
|
|
for blade in range(3):
|
|
angle = blade*math.tau/3
|
|
vs, faces = [], []
|
|
stations = [(.19,.10,0),(.55,.25,0),(1.1,.23,.04),(2.,.16,.13),(2.8,.08,.22),(3.,.025,.25)]
|
|
for r, halfwidth, sweep in stations:
|
|
for tangent, depth in [(-halfwidth,0),(0,-.055),(halfwidth,0),(0,.045)]:
|
|
t = tangent+sweep
|
|
vs.append((x+t*math.cos(angle)+r*math.sin(angle), y-.48+depth,
|
|
hub-t*math.sin(angle)+r*math.cos(angle)))
|
|
for j in range(len(stations)-1):
|
|
for k in range(4):
|
|
faces.append((4*j+k,4*j+(k+1)%4,4*(j+1)+(k+1)%4,4*(j+1)+k))
|
|
faces.extend([(3,2,1,0),tuple(range(len(vs)-4,len(vs)))])
|
|
mesh = bpy.data.meshes.new('WIND blade mesh')
|
|
mesh.from_pydata(vs, [], faces)
|
|
mesh.update()
|
|
ob = bpy.data.objects.new('WIND - rotor blade %d' % (blade+1), mesh)
|
|
coll.objects.link(ob)
|
|
mesh.materials.append(white)
|
|
for face in mesh.polygons:
|
|
face.use_smooth = True
|
|
box('lockable isolation cabinet - indicative', (x+.72,y,z+.92), (.45,.3,.65), dark)
|
|
cone('service conduit', (x+.72,y,z+.1), (x+.72,y,z+.63), .045,.045,steel)
|
|
|
|
# Exact low-poly display copies keep the lightweight viewport aligned.
|
|
bpy.context.view_layer.update()
|
|
for ob in list(coll.objects):
|
|
proxy = ob.copy()
|
|
proxy.data = ob.data.copy()
|
|
proxy.name = 'FAST | ' + ob.name
|
|
fast.objects.link(proxy)
|
|
proxy.hide_render = True
|
|
|
|
tour = bpy.data.collections['22 PLAY - guided camera tour']
|
|
cam = bpy.data.objects['PLAY TOUR camera']
|
|
title = '32 / WIND POWER - PROPERTY CONCEPT'
|
|
for frame, position in [(3721,(-57,66,25)),(3840,(-54,62,24))]:
|
|
s.frame_set(frame)
|
|
cam.location = position
|
|
cam.rotation_euler = (Vector((x,y,hub*.57))-cam.location).to_track_quat('-Z','Y').to_euler()
|
|
cam.keyframe_insert('location', frame=frame)
|
|
cam.keyframe_insert('rotation_euler', frame=frame)
|
|
cam.data.lens = 38
|
|
cam.data.keyframe_insert('lens', frame=frame)
|
|
ref = next(o for o in tour.objects if o.type=='FONT' and o.data.body.startswith('31 /'))
|
|
ref.scale = (0,0,0)
|
|
ref.keyframe_insert('scale', frame=3721)
|
|
caption = ref.copy()
|
|
caption.data = ref.data.copy()
|
|
caption.animation_data_clear()
|
|
tour.objects.link(caption)
|
|
caption.name = 'Tour caption 32 - wind turbine concept'
|
|
caption.data.body = title
|
|
caption.location = (-36/38*.91,36/38*9/16*.84,-2)
|
|
for frame, value in [(1,0),(3720,0),(3721,1),(3840,1)]:
|
|
caption.scale = (value,value,value)
|
|
caption.keyframe_insert('scale', frame=frame)
|
|
for ob in (ref,caption):
|
|
for layer in ob.animation_data.action.layers:
|
|
for strip in layer.strips:
|
|
for bag in strip.channelbags:
|
|
for curve in bag.fcurves:
|
|
for key in curve.keyframe_points:
|
|
key.interpolation = 'CONSTANT'
|
|
s.frame_end = 3840
|
|
s.timeline_markers.new(title, frame=3721)
|
|
s['Wind turbine concept'] = 'v009: three-blade freestanding turbine at (-36,40); 18 m hub / 6 m rotor are visual placeholders. No measured wind resource, selected product, yield, engineering or approvals.'
|
|
s['PLAY THE TOUR'] = 'Hover over the 3D view and press Space. 32 chapters, 2 min 40 sec. Shift+Left Arrow restarts. Wind turbine: chapter 32.'
|
|
text = bpy.data.texts.get('START HERE - PRESS PLAY')
|
|
if text:
|
|
text.write('\n\n'+s['PLAY THE TOUR']+'\n'+s['Wind turbine concept'])
|
|
|
|
# A separate overview camera leaves all existing inspection chapters intact.
|
|
camera_data = bpy.data.cameras.new('Wind property overview')
|
|
overview = bpy.data.objects.new('Wind property overview', camera_data)
|
|
s.collection.objects.link(overview)
|
|
overview.location = (-85,90,60)
|
|
overview.rotation_euler = (Vector((-5,5,4))-overview.location).to_track_quat('-Z','Y').to_euler()
|
|
camera_data.type = 'ORTHO'
|
|
camera_data.ortho_scale = 108
|
|
camera_data.clip_end = 1000
|
|
s.frame_set(3721)
|
|
s.camera = cam
|
|
detail.hide_viewport = True
|
|
fast.hide_viewport = False
|
|
s.render.resolution_x, s.render.resolution_y = 2560, 1440
|
|
s.render.resolution_percentage = 100
|
|
bpy.ops.wm.save_as_mainfile(filepath=str(output))
|
|
if args.render:
|
|
s.camera = overview
|
|
for ob in tour.objects:
|
|
if ob.type == 'FONT':
|
|
ob.hide_render = True
|
|
detail.hide_viewport = False
|
|
fast.hide_viewport = True
|
|
s.render.engine = 'CYCLES'
|
|
s.render.resolution_percentage = 60
|
|
s.cycles.samples = 32
|
|
s.cycles.use_denoising = True
|
|
prefs = bpy.context.preferences.addons['cycles'].preferences
|
|
prefs.compute_device_type = 'METAL'
|
|
prefs.get_devices()
|
|
for device in prefs.devices:
|
|
device.use = device.type == 'METAL'
|
|
s.cycles.device = 'GPU'
|
|
s.render.filepath = str(Path(args.render).resolve())
|
|
bpy.ops.render.render(write_still=True)
|
|
print('WIND_REVISION_SAVED', output, flush=True)
|