"""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)