Files

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)