import bpy import math import random from mathutils import Vector from pathlib import Path OUT = Path('/home/ec2-user/blender_test') OUT.mkdir(parents=True, exist_ok=True) random.seed(24) bpy.ops.object.select_all(action='SELECT') bpy.ops.object.delete(use_global=False) for block in list(bpy.data.collections): if block.name != 'Collection': bpy.data.collections.remove(block) base_collection = bpy.data.collections.get('Collection') if base_collection: bpy.data.collections.remove(base_collection) CURRENT = None def collection(name): global CURRENT CURRENT = bpy.data.collections.new(name) bpy.context.scene.collection.children.link(CURRENT) return CURRENT def move_to_collection(obj): for coll in list(obj.users_collection): coll.objects.unlink(obj) CURRENT.objects.link(obj) return obj def material(name, color, roughness=0.5, metallic=0.0, emission=None, strength=0.0): mat = bpy.data.materials.new(name) mat.diffuse_color = (*color, 1) mat.use_nodes = True bsdf = mat.node_tree.nodes.get('Principled BSDF') bsdf.inputs['Base Color'].default_value = (*color, 1) bsdf.inputs['Roughness'].default_value = roughness bsdf.inputs['Metallic'].default_value = metallic if emission: bsdf.inputs['Emission Color'].default_value = (*emission, 1) bsdf.inputs['Emission Strength'].default_value = strength return mat def assign(obj, mat): obj.data.materials.append(mat) return obj def bevel(obj, amount=0.05, segments=2): mod = obj.modifiers.new('Soft edges', 'BEVEL') mod.width = amount mod.segments = segments return obj def cube(name, location, dimensions, mat, edge=0.0, rotation=0.0): bpy.ops.mesh.primitive_cube_add(size=1, location=location) obj = move_to_collection(bpy.context.object) obj.name = name obj.dimensions = dimensions obj.rotation_euler.z = rotation bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) assign(obj, mat) if edge: bevel(obj, edge) return obj def cylinder(name, location, radius, depth, mat, vertices=48, edge=0.0): bpy.ops.mesh.primitive_cylinder_add(vertices=vertices, radius=radius, depth=depth, location=location) obj = move_to_collection(bpy.context.object) obj.name = name assign(obj, mat) if edge: bevel(obj, edge) for poly in obj.data.polygons: poly.use_smooth = len(poly.vertices) == 4 return obj def cone(name, location, radius1, radius2, depth, mat, vertices=48): bpy.ops.mesh.primitive_cone_add(vertices=vertices, radius1=radius1, radius2=radius2, depth=depth, location=location) obj = move_to_collection(bpy.context.object) obj.name = name assign(obj, mat) for poly in obj.data.polygons: poly.use_smooth = len(poly.vertices) == 4 return obj def sphere(name, location, scale, mat, ico=False, subdivisions=2): if ico: bpy.ops.mesh.primitive_ico_sphere_add(subdivisions=subdivisions, radius=1, location=location) else: bpy.ops.mesh.primitive_uv_sphere_add(segments=24, ring_count=12, radius=1, location=location) obj = move_to_collection(bpy.context.object) obj.name = name obj.scale = scale if hasattr(scale, '__len__') else (scale, scale, scale) assign(obj, mat) if not ico: for poly in obj.data.polygons: poly.use_smooth = True return obj def mesh_object(name, vertices, faces, mats, indices=None, smooth=False): mesh = bpy.data.meshes.new(name + ' Mesh') mesh.from_pydata(vertices, [], faces) mesh.update() obj = bpy.data.objects.new(name, mesh) CURRENT.objects.link(obj) for mat in mats: mesh.materials.append(mat) for i, poly in enumerate(mesh.polygons): poly.use_smooth = smooth if indices: poly.material_index = indices[i % len(indices)] return obj def rod(name, start, end, radius, mat, vertices=12): start, end = Vector(start), Vector(end) obj = cylinder(name, (start + end) / 2, radius, (end - start).length, mat, vertices) obj.rotation_euler = (end - start).to_track_quat('Z', 'Y').to_euler() return obj def line(name, points, radius, mat, cyclic=False): data = bpy.data.curves.new(name, 'CURVE') data.dimensions = '3D' data.resolution_u = 2 data.bevel_depth = radius data.bevel_resolution = 2 spline = data.splines.new('POLY') spline.points.add(len(points) - 1) for point, co in zip(spline.points, points): point.co = (*co, 1) spline.use_cyclic_u = cyclic obj = bpy.data.objects.new(name, data) CURRENT.objects.link(obj) assign(obj, mat) return obj def light(name, kind, location, color, energy, size=1.0, target=None): data = bpy.data.lights.new(name, kind) data.energy = energy data.color = color if kind == 'AREA': data.shape = 'DISK' data.size = size elif kind == 'POINT': data.shadow_soft_size = size obj = bpy.data.objects.new(name, data) CURRENT.objects.link(obj) obj.location = location if target: obj.rotation_euler = (Vector(target) - obj.location).to_track_quat('-Z', 'Y').to_euler() return obj # Palette ivory = material('Porcelain | warm ivory', (0.83, 0.79, 0.64), 0.34) red = material('Lighthouse | vermilion enamel', (0.52, 0.035, 0.045), 0.3, 0.12) red_dark = material('Roof | oxblood', (0.24, 0.018, 0.03), 0.38, 0.18) navy = material('Metal | midnight blue', (0.018, 0.035, 0.065), 0.3, 0.65) bronze = material('Metal | aged brass', (0.34, 0.19, 0.065), 0.3, 0.72) wood = material('Dock | warm cedar', (0.24, 0.105, 0.045), 0.72) wood_light = material('Dock | plank variation', (0.34, 0.17, 0.072), 0.68) grass = material('Island | velvet moss', (0.075, 0.20, 0.12), 0.95) grass_light = material('Island | moss highlights', (0.13, 0.28, 0.15), 0.9) rock_mats = [ material('Rock | slate', (0.12, 0.17, 0.20), 0.92), material('Rock | blue slate', (0.16, 0.22, 0.25), 0.9), material('Rock | pale edges', (0.22, 0.27, 0.28), 0.88), ] foam = material('Water | sea foam', (0.40, 0.78, 0.75), 0.32, 0.05, (0.10, 0.32, 0.30), 0.35) warm_glow = material('Light | warm glowing glass', (1.0, 0.48, 0.12), 0.24, emission=(1.0, 0.32, 0.055), strength=5.0) window_glow = material('Light | cottage windows', (1.0, 0.60, 0.23), 0.3, emission=(1.0, 0.39, 0.09), strength=2.5) leaf_dark = material('Pines | deep teal', (0.018, 0.12, 0.105), 0.9) leaf_light = material('Pines | jade tips', (0.035, 0.21, 0.16), 0.88) collection('01 | Ocean diorama') ocean = material('Ocean | deep turquoise', (0.012, 0.15, 0.205), 0.26, 0.32) nodes = ocean.node_tree.nodes links = ocean.node_tree.links noise = nodes.new('ShaderNodeTexNoise') noise.inputs['Scale'].default_value = 5.5 noise.inputs['Detail'].default_value = 3.0 noise.inputs['Roughness'].default_value = 0.65 bump = nodes.new('ShaderNodeBump') bump.inputs['Strength'].default_value = 0.2 bump.inputs['Distance'].default_value = 0.11 links.new(noise.outputs['Fac'], bump.inputs['Height']) links.new(bump.outputs['Normal'], nodes.get('Principled BSDF').inputs['Normal']) R, N = 7.25, 144 rings = 48 vertices = [(0, 0, -0.52)] for ring in range(1, rings + 1): radius = R * ring / rings for j in range(N): angle = 2 * math.pi * j / N x, y = radius * math.cos(angle), radius * math.sin(angle) z = -0.53 + 0.045 * math.sin(x * 2.3 + y * 1.1) + 0.025 * math.sin(y * 3.4 - x * 0.8) vertices.append((x, y, z)) faces = [] for j in range(N): faces.append((0, 1 + j, 1 + (j + 1) % N)) for ring in range(1, rings): a, b = 1 + (ring - 1) * N, 1 + ring * N for j in range(N): k = (j + 1) % N faces.append((a + j, b + j, b + k, a + k)) mesh_object('Sculpted water surface', vertices, faces, [ocean], smooth=True) base_mat = material('Ocean base | deep enamel', (0.012, 0.055, 0.10), 0.3, 0.35) cylinder('Rounded ocean base', (0, 0, -0.89), 7.24, 0.54, base_mat, 144, 0.11) bpy.ops.mesh.primitive_torus_add(major_segments=144, minor_segments=12, location=(0, 0, -0.55), major_radius=7.20, minor_radius=0.075) rim = move_to_collection(bpy.context.object) rim.name = 'Rounded ocean rim' assign(rim, base_mat) for poly in rim.data.polygons: poly.use_smooth = True # Broken foam contours around the island. for arc in range(9): start = arc * 2 * math.pi / 9 + 0.05 length = random.uniform(0.32, 0.55) points = [] for j in range(22): a = start + length * j / 21 wobble = 1 + 0.035 * math.sin(a * 9) points.append((3.23 * wobble * math.cos(a), 2.63 * wobble * math.sin(a), -0.38 + 0.025 * math.sin(a * 5))) line('Shore foam %02d' % arc, points, 0.026, foam) for i in range(20): a = random.uniform(0, 2 * math.pi) r = random.uniform(4.0, 6.8) x, y = r * math.cos(a), r * math.sin(a) points = [(x + j * 0.055, y + 0.055 * math.sin(j * 0.35), -0.44) for j in range(random.randint(5, 11))] line('Small whitecap %02d' % i, points, 0.012, foam) collection('02 | Rocky island') segments = 48 jitter = [random.uniform(0.88, 1.10) for _ in range(segments)] levels = [(-0.43, 1.03), (-0.08, 1.0), (0.30, 0.88), (0.61, 0.78)] verts = [] for level, (z, factor) in enumerate(levels): for j in range(segments): a = 2 * math.pi * j / segments verts.append((3.12 * factor * jitter[j] * math.cos(a), 2.52 * factor * jitter[j] * math.sin(a), z + random.uniform(-0.10, 0.08))) faces, indices = [], [] for level in range(len(levels) - 1): for j in range(segments): k = (j + 1) % segments a, b = level * segments + j, level * segments + k c, d = (level + 1) * segments + k, (level + 1) * segments + j if j % 2: faces.extend([(a, b, c), (a, c, d)]) else: faces.extend([(a, b, d), (b, c, d)]) indices.extend([random.randrange(3), random.randrange(3)]) mesh_object('Faceted island cliffs', verts, faces, rock_mats, indices) # Gently domed grass cap using the irregular upper cliff rim. top_verts = [(0, 0, 0.68)] for j in range(segments): a = 2 * math.pi * j / segments top_verts.append((1.25 * jitter[j] * math.cos(a), 1.02 * jitter[j] * math.sin(a), 0.68)) top_verts.extend(verts[-segments:]) top_faces = [] for j in range(segments): k = (j + 1) % segments top_faces.extend([(0, 1 + j, 1 + k), (1 + j, 1 + segments + j, 1 + segments + k, 1 + k)]) mesh_object('Soft moss island cap', top_verts, top_faces, [grass, grass_light], [0 if i % 7 else 1 for i in range(len(top_faces))], smooth=True) for i in range(18): a = 2 * math.pi * i / 18 + random.uniform(-0.12, 0.12) r = random.uniform(0.75, 0.98) loc = (2.8 * r * math.cos(a), 2.2 * r * math.sin(a), 0.45) obj = sphere('Shore boulder %02d' % i, loc, (random.uniform(0.18, 0.42), random.uniform(0.18, 0.36), random.uniform(0.16, 0.34)), rock_mats[i % 3], True, 1) obj.rotation_euler = (random.random(), random.random(), random.random()) collection('03 | Lighthouse') lx, ly, ground = -0.72, 0.34, 0.67 cylinder('Lighthouse foundation lower', (lx, ly, ground + 0.06), 0.91, 0.16, rock_mats[2], 64, 0.035) cylinder('Lighthouse foundation upper', (lx, ly, ground + 0.18), 0.79, 0.14, ivory, 64, 0.025) base_z = ground + 0.24 stripe_h = 0.64 stripes = 7 tower_h = stripe_h * stripes for i in range(stripes): z0, z1 = base_z + i * stripe_h, base_z + (i + 1) * stripe_h r0 = 0.70 - 0.20 * (i / stripes) r1 = 0.70 - 0.20 * ((i + 1) / stripes) cone('Tower stripe %02d' % (i + 1), (lx, ly, (z0 + z1) / 2), r0, r1, stripe_h + 0.006, red if i % 2 == 0 else ivory, 64) # Arched front door, extruded along Y. def arched_door(): width, height, depth = 0.46, 0.94, 0.045 radius = width / 2 shoulder = height - radius outline = [(-radius, 0), (radius, 0)] for j in range(13): a = math.pi * j / 12 outline.append((radius * math.cos(a), shoulder + radius * math.sin(a))) y = ly - 0.705 vs = [(lx + x, y + yoff, base_z + z) for yoff in (-depth / 2, depth / 2) for x, z in outline] count = len(outline) fs = [tuple(range(count - 1, -1, -1)), tuple(range(count, 2 * count))] for j in range(count): k = (j + 1) % count fs.append((j, k, count + k, count + j)) return mesh_object('Arched lighthouse door', vs, fs, [navy]) arched_door() sphere('Brass door knob', (lx + 0.13, ly - 0.742, base_z + 0.40), 0.035, bronze) for number, z in enumerate((base_z + 1.75, base_z + 3.18)): radius = 0.70 - 0.20 * ((z - base_z) / tower_h) y = ly - radius - 0.028 cube('Window stone surround %d' % number, (lx, y, z), (0.40, 0.07, 0.52), ivory, 0.06) cube('Warm lighthouse window %d' % number, (lx, y - 0.043, z), (0.25, 0.035, 0.36), window_glow, 0.045) cube('Window vertical mullion %d' % number, (lx, y - 0.067, z), (0.025, 0.025, 0.35), navy) cube('Window horizontal mullion %d' % number, (lx, y - 0.068, z), (0.24, 0.025, 0.025), navy) balcony_z = base_z + tower_h cylinder('Balcony corbel', (lx, ly, balcony_z + 0.02), 0.65, 0.24, ivory, 64, 0.025) cylinder('Balcony deck', (lx, ly, balcony_z + 0.17), 0.91, 0.13, navy, 64, 0.025) for j in range(16): a = 2 * math.pi * j / 16 x, y = lx + 0.80 * math.cos(a), ly + 0.80 * math.sin(a) rod('Balcony baluster %02d' % j, (x, y, balcony_z + 0.22), (x, y, balcony_z + 0.77), 0.022, bronze) for z in (balcony_z + 0.43, balcony_z + 0.77): points = [(lx + 0.80 * math.cos(2 * math.pi * j / 96), ly + 0.80 * math.sin(2 * math.pi * j / 96), z) for j in range(96)] line('Circular balcony rail', points, 0.026, bronze, True) lantern_z = balcony_z + 0.82 cylinder('Lantern lower frame', (lx, ly, lantern_z), 0.64, 0.12, navy, 64, 0.02) cylinder('Lantern upper frame', (lx, ly, lantern_z + 0.88), 0.65, 0.10, navy, 64, 0.02) glass = bpy.data.materials.new('Lantern | clear smoky glazing') glass.use_nodes = True gn = glass.node_tree.nodes gl = glass.node_tree.links gn.clear() gout = gn.new('ShaderNodeOutputMaterial') gtrans = gn.new('ShaderNodeBsdfTransparent') ggloss = gn.new('ShaderNodeBsdfPrincipled') ggloss.inputs['Base Color'].default_value = (0.28, 0.52, 0.58, 1) ggloss.inputs['Metallic'].default_value = 0.5 ggloss.inputs['Roughness'].default_value = 0.16 gmix = gn.new('ShaderNodeMixShader') gmix.inputs[0].default_value = 0.12 gl.new(gtrans.outputs[0], gmix.inputs[1]) gl.new(ggloss.outputs[0], gmix.inputs[2]) gl.new(gmix.outputs[0], gout.inputs[0]) for j in range(8): a = 2 * math.pi * j / 8 b = 2 * math.pi * (j + 1) / 8 p0 = (lx + 0.57 * math.cos(a), ly + 0.57 * math.sin(a), lantern_z + 0.06) p1 = (lx + 0.57 * math.cos(b), ly + 0.57 * math.sin(b), lantern_z + 0.06) p2 = (p1[0], p1[1], lantern_z + 0.84) p3 = (p0[0], p0[1], lantern_z + 0.84) mesh_object('Lantern glass pane %d' % j, [p0, p1, p2, p3], [(0, 1, 2, 3)], [glass]) rod('Lantern brass mullion %d' % j, p0, p3, 0.026, bronze) sphere('Fresnel lamp core', (lx, ly, lantern_z + 0.43), (0.25, 0.25, 0.31), warm_glow) for z in (lantern_z + 0.29, lantern_z + 0.43, lantern_z + 0.57): points = [(lx + 0.30 * math.cos(2 * math.pi * j / 48), ly + 0.30 * math.sin(2 * math.pi * j / 48), z) for j in range(48)] line('Fresnel lens ring', points, 0.018, warm_glow, True) cone('Lighthouse copper roof', (lx, ly, lantern_z + 1.17), 0.84, 0.08, 0.55, red_dark, 64) rod('Roof finial', (lx, ly, lantern_z + 1.42), (lx, ly, lantern_z + 1.72), 0.027, bronze) sphere('Roof finial ball', (lx, ly, lantern_z + 1.64), 0.075, bronze) light('Lantern warm pool', 'POINT', (lx, ly, lantern_z + 0.43), (1.0, 0.35, 0.08), 135, 0.45) # A restrained, translucent beacon cone. beam_mat = bpy.data.materials.new('Beacon | translucent golden mist') beam_mat.use_nodes = True bn = beam_mat.node_tree.nodes bl = beam_mat.node_tree.links bn.clear() bout = bn.new('ShaderNodeOutputMaterial') btrans = bn.new('ShaderNodeBsdfTransparent') bem = bn.new('ShaderNodeEmission') bem.inputs['Color'].default_value = (1.0, 0.67, 0.26, 1) bem.inputs['Strength'].default_value = 0.8 bmix = bn.new('ShaderNodeMixShader') bl.new(btrans.outputs[0], bmix.inputs[1]) bl.new(bem.outputs[0], bmix.inputs[2]) bl.new(bmix.outputs[0], bout.inputs[0]) # Fade the beam smoothly to transparent at its wide, distant end. btex = bn.new('ShaderNodeTexCoord') bsep = bn.new('ShaderNodeSeparateXYZ') binv = bn.new('ShaderNodeMath') binv.operation = 'SUBTRACT' binv.inputs[0].default_value = 1.0 bfade = bn.new('ShaderNodeMath') bfade.operation = 'MULTIPLY' bfade.inputs[1].default_value = 0.075 bl.new(btex.outputs['Generated'], bsep.inputs[0]) bl.new(bsep.outputs['Z'], binv.inputs[1]) bl.new(binv.outputs[0], bfade.inputs[0]) bl.new(bfade.outputs[0], bmix.inputs[0]) beam_start = Vector((lx - 0.22, ly + 0.1, lantern_z + 0.46)) beam_end = Vector((-6.4, 2.3, lantern_z + 0.35)) beam = cone('Visible lighthouse beacon', (beam_start + beam_end) / 2, 0.06, 1.25, (beam_end - beam_start).length, beam_mat, 48) beam.rotation_euler = (beam_end - beam_start).to_track_quat('Z', 'Y').to_euler() beam.visible_shadow = False collection('04 | Keeper cottage') cx, cy = 2.0, 0.38 cube('Cottage stone footing', (cx, cy, 0.74), (2.10, 1.65, 0.20), rock_mats[2], 0.06) cube('Cottage warm plaster', (cx, cy, 1.43), (1.92, 1.46, 1.30), ivory, 0.045) # Gabled roof, ridge running front to back. rv = [ (cx - 1.13, cy - 0.88, 1.98), (cx + 1.13, cy - 0.88, 1.98), (cx, cy - 0.88, 2.70), (cx - 1.13, cy + 0.88, 1.98), (cx + 1.13, cy + 0.88, 1.98), (cx, cy + 0.88, 2.70), ] mesh_object('Cottage gabled roof', rv, [(0, 1, 2), (5, 4, 3), (0, 3, 4, 1), (1, 4, 5, 2), (2, 5, 3, 0)], [red_dark]) rod('Roof ridge cap', (cx, cy - 0.94, 2.72), (cx, cy + 0.94, 2.72), 0.055, red, 16) for side in (-1, 1): for j in range(1, 6): t = j / 6 x = cx + side * 1.10 * t z = 2.70 - 0.70 * t rod('Roof tile seam', (x, cy - 0.88, z + 0.012), (x, cy + 0.88, z + 0.012), 0.014, red, 8) front_y = cy - 0.755 cube('Cottage door trim', (cx - 0.40, front_y - 0.02, 1.26), (0.56, 0.07, 1.04), wood_light, 0.045) cube('Cottage door', (cx - 0.40, front_y - 0.065, 1.25), (0.44, 0.045, 0.93), navy, 0.035) sphere('Cottage door knob', (cx - 0.25, front_y - 0.10, 1.24), 0.027, bronze) cube('Cottage window trim', (cx + 0.42, front_y - 0.025, 1.48), (0.66, 0.075, 0.66), wood_light, 0.045) cube('Cottage glowing window', (cx + 0.42, front_y - 0.07, 1.48), (0.52, 0.035, 0.52), window_glow, 0.025) cube('Cottage window vertical bar', (cx + 0.42, front_y - 0.095, 1.48), (0.035, 0.025, 0.52), navy) cube('Cottage window horizontal bar', (cx + 0.42, front_y - 0.096, 1.48), (0.52, 0.025, 0.035), navy) cube('Brick chimney', (cx + 0.58, cy + 0.34, 2.69), (0.38, 0.42, 1.05), rock_mats[0], 0.035) cube('Chimney cap', (cx + 0.58, cy + 0.34, 3.23), (0.47, 0.50, 0.12), rock_mats[2], 0.025) light('Cottage window spill', 'POINT', (cx + 0.42, front_y - 0.25, 1.48), (1.0, 0.38, 0.10), 28, 0.35) collection('05 | Trees and paths') def pine(name, x, y, height): z = 0.66 cylinder(name + ' trunk', (x, y, z + height * 0.31), height * 0.065, height * 0.62, wood, 12) for level in range(3): cone(name + ' foliage %d' % level, (x, y, z + height * (0.42 + level * 0.19)), height * (0.32 - level * 0.065), 0.015, height * 0.54, leaf_dark if level < 2 else leaf_light, 12) for data in [('Old pine', -2.12, 1.12, 1.82), ('High pine', -1.40, 1.70, 1.55), ('Cottage pine', 1.38, 1.69, 1.62), ('Small pine', 2.60, 1.25, 1.12)]: pine(*data) stone = material('Path | warm stepping stone', (0.38, 0.39, 0.32), 0.92) path_points = [(-0.67, -0.56), (-0.42, -0.88), (0.0, -1.10), (0.46, -1.25), (0.92, -1.37), (1.39, -1.43), (1.83, -1.50)] for i, (x, y) in enumerate(path_points): obj = cylinder('Stepping stone %02d' % i, (x, y, 0.68), 0.22, 0.055, stone, 7, 0.018) obj.scale.y = 0.72 obj.rotation_euler.z = i * 0.71 for i in range(4): cube('Cottage entry step %d' % i, (1.60, -0.52 - i * 0.20, 0.69), (0.46, 0.17, 0.06), stone, 0.025, -0.12) collection('06 | Timber dock') start = Vector((1.94, -1.45, 0.37)) end = Vector((3.73, -4.48, -0.25)) direction = end - start angle = math.atan2(direction.y, direction.x) - math.pi / 2 side = Vector((-direction.y, direction.x, 0)).normalized() for i in range(16): t = i / 15 pos = start.lerp(end, t) plank = cube('Dock plank %02d' % i, pos, (0.96, direction.length / 16 * 0.91, 0.12), wood_light if i % 3 == 0 else wood, 0.025, angle) plank.rotation_euler.x = math.atan2(-direction.z, math.hypot(direction.x, direction.y)) for t in (0.05, 0.48, 0.94): center = start.lerp(end, t) for sign in (-1, 1): p = center + side * 0.51 * sign rod('Dock support post', (p.x, p.y, -0.70), (p.x, p.y, p.z + 0.43), 0.065, wood, 12) sphere('Dock post cap', (p.x, p.y, p.z + 0.45), 0.085, wood_light, True, 1) for sign in (-1, 1): points = [] for i in range(33): t = 0.05 + 0.89 * i / 32 p = start.lerp(end, t) + side * 0.51 * sign p.z += 0.34 - 0.08 * abs(math.sin((t - 0.05) / 0.89 * math.pi * 3)) points.append(tuple(p)) line('Dock rope railing', points, 0.018, bronze) collection('07 | Moon and stars') scene = bpy.context.scene camera_data = bpy.data.cameras.new('Diorama Camera') camera = bpy.data.objects.new('Diorama Camera', camera_data) CURRENT.objects.link(camera) camera.location = (11.8, -17.5, 13.0) target = Vector((0, 0, 2.70)) camera.rotation_euler = (target - camera.location).to_track_quat('-Z', 'Y').to_euler() camera_data.type = 'ORTHO' camera_data.ortho_scale = 16.7 camera_data.lens = 48 scene.camera = camera quat = camera.rotation_euler.to_quaternion() right = quat @ Vector((1, 0, 0)) up = quat @ Vector((0, 1, 0)) back = quat @ Vector((0, 0, 1)) moon_mat = bpy.data.materials.new('Moon | pearl emission') moon_mat.use_nodes = True mn = moon_mat.node_tree.nodes ml = moon_mat.node_tree.links mn.clear() mout = mn.new('ShaderNodeOutputMaterial') mem = mn.new('ShaderNodeEmission') mem.inputs['Strength'].default_value = 1.2 mnoise = mn.new('ShaderNodeTexNoise') mnoise.inputs['Scale'].default_value = 4.5 mnoise.inputs['Detail'].default_value = 2.0 mramp = mn.new('ShaderNodeValToRGB') mramp.color_ramp.elements[0].position = 0.24 mramp.color_ramp.elements[0].color = (0.36, 0.48, 0.52, 1) mramp.color_ramp.elements[1].position = 0.78 mramp.color_ramp.elements[1].color = (1.0, 0.89, 0.63, 1) ml.new(mnoise.outputs['Fac'], mramp.inputs[0]) ml.new(mramp.outputs[0], mem.inputs['Color']) ml.new(mem.outputs[0], mout.inputs[0]) moon_pos = target + right * -4.85 + up * 4.95 - back * 7.0 sphere('Pearl moon', moon_pos, 0.72, moon_mat) star_mat = material('Stars | cool starlight', (0.65, 0.82, 1.0), 0.3, emission=(0.53, 0.73, 1.0), strength=4.0) for i in range(68): x = random.uniform(-7.3, 7.3) y = random.uniform(2.7, 7.5) if (x + 4.85) ** 2 + (y - 4.95) ** 2 < 1.1: continue pos = target + right * x + up * y - back * random.uniform(7.1, 8.0) size = random.uniform(0.015, 0.043) * (1.6 if i % 13 == 0 else 1.0) sphere('Star %02d' % i, pos, size, star_mat, True, 1) collection('08 | Studio lighting') light('Moon softbox', 'AREA', (-3, -4, 11), (0.60, 0.76, 1.0), 1550, 7.0, (0, 0, 1.0)) light('Warm front fill', 'AREA', (5, -7, 7), (1.0, 0.72, 0.48), 850, 6.0, (0, 0, 2.0)) light('Turquoise rim', 'AREA', (-4, 5, 8), (0.20, 0.72, 0.88), 1750, 5.0, (0, 0, 2.0)) light('Camera side fill', 'AREA', (8, 4, 5), (0.42, 0.54, 1.0), 700, 5.0, (0, 0, 2.0)) world = bpy.data.worlds.new('Midnight studio') world.use_nodes = True world.node_tree.nodes.get('Background').inputs['Color'].default_value = (0.008, 0.016, 0.045, 1) world.node_tree.nodes.get('Background').inputs['Strength'].default_value = 0.32 scene.world = world scene.render.engine = 'CYCLES' scene.cycles.device = 'CPU' scene.cycles.samples = 96 # This community ARM64 build's OIDN denoiser crashes; clean the image with samples instead. scene.cycles.use_denoising = False scene.cycles.use_adaptive_sampling = True scene.cycles.adaptive_threshold = 0.025 scene.cycles.max_bounces = 7 scene.cycles.diffuse_bounces = 3 scene.cycles.glossy_bounces = 3 scene.cycles.transmission_bounces = 4 scene.cycles.transparent_max_bounces = 10 scene.render.threads_mode = 'FIXED' scene.render.threads = 2 scene.render.resolution_x = 1200 scene.render.resolution_y = 1200 scene.render.resolution_percentage = 100 scene.render.image_settings.file_format = 'PNG' scene.render.image_settings.color_mode = 'RGBA' scene.render.image_settings.color_depth = '8' scene.render.image_settings.compression = 25 scene.render.film_transparent = False scene.render.filepath = str(OUT / 'moonlit_lighthouse.png') scene.view_settings.view_transform = 'AgX' try: scene.view_settings.look = 'AgX - Medium High Contrast' except TypeError: pass scene.view_settings.exposure = 0.35 # Blender 5 compositor: node group with a named image output. comp = bpy.data.node_groups.new('Moonlit finishing', 'CompositorNodeTree') comp.interface.new_socket(name='Image', in_out='OUTPUT', socket_type='NodeSocketColor') scene.compositing_node_group = comp rl = comp.nodes.new('CompositorNodeRLayers') rl.location = (-300, 0) glare = comp.nodes.new('CompositorNodeGlare') glare.location = (-60, 0) glare.inputs['Type'].default_value = 'Fog Glow' glare.inputs['Quality'].default_value = 'High' glare.inputs['Threshold'].default_value = 1.4 glare.inputs['Strength'].default_value = 0.32 glare.inputs['Size'].default_value = 0.36 output = comp.nodes.new('NodeGroupOutput') output.location = (200, 0) comp.links.new(rl.outputs['Image'], glare.inputs['Image']) comp.links.new(glare.outputs['Image'], output.inputs['Image']) scene['Title'] = 'Moonlit Lighthouse' scene['Description'] = 'A handcrafted procedural miniature: striped lighthouse, keeper cottage, sculpted sea, timber dock, and pearl moon.' scene['Generator'] = 'create_scene.py' scene['Blender Version'] = bpy.app.version_string for screen in bpy.data.screens: for area in screen.areas: if area.type == 'VIEW_3D': area.spaces.active.region_3d.view_perspective = 'CAMERA' area.spaces.active.overlay.show_overlays = False bpy.ops.object.select_all(action='DESELECT') camera.select_set(True) bpy.context.view_layer.objects.active = camera print('SCENE_READY', len(bpy.data.objects), 'objects', len(bpy.data.materials), 'materials', flush=True) bpy.ops.wm.save_as_mainfile(filepath=str(OUT / 'moonlit_lighthouse.blend')) print('PROJECT_SAVED', flush=True) bpy.ops.render.render(write_still=True) print('RENDER_COMPLETE', scene.render.filepath, flush=True)