diff --git a/design/channels/blender/build_3d_ident.py b/design/channels/blender/build_3d_ident.py index 080d337..1d47265 100644 --- a/design/channels/blender/build_3d_ident.py +++ b/design/channels/blender/build_3d_ident.py @@ -562,12 +562,13 @@ def flat(name, polygons, location, facing, mat, shapes=()): obj.location = location; obj.rotation_euler = (Vector(facing) - Vector(location)).to_track_quat('-Y', 'Z').to_euler() return obj -def caped_hero(where, facing, mat, size=1.0): +def caped_hero(where, facing, mat, size=1.0, cape_on=True): """A hero on a rooftop, hands on hips, cape in the wind. Only ever a silhouette against the moon.""" body = [[(-.95, 0), (-.45, 0), (-.08, 2.7), (-.6, 3.3)], [(.95, 0), (.45, 0), (.08, 2.7), (.6, 3.3)], [(-.6, 3.3), (.6, 3.3), (.68, 3.75), (1.08, 5.15), (.5, 5.42), (-.5, 5.42), (-1.08, 5.15), (-.68, 3.75)], [(-1.08, 5.15), (-1.75, 4.1), (-.64, 3.42), (-.62, 3.72), (-1.28, 4.15), (-.75, 4.95)], [(1.08, 5.15), (1.75, 4.1), (.64, 3.42), (.62, 3.72), (1.28, 4.15), (.75, 4.95)], [(-.17, 5.42), (.17, 5.42), (.17, 5.75), (-.17, 5.75)], [(.42 * math.cos(i / 16 * math.tau), 6.05 + .48 * math.sin(i / 16 * math.tau)) for i in range(16)]] hero = flat('hero', [[(x * size, z * size) for x, z in p] for p in body], where, facing, mat) + if not cape_on: return hero rows, columns = 12, 6 def cape_at(phase, wind): points = [] @@ -784,6 +785,239 @@ def case_board(): for i in range(30): box(f'slat{i}', -70, -50, 40, 1, .6, slat, 16 + i * 1.5).visible_camera = False +def clone(original): + """A copy with no animation of its own yet. A plain copy would move in step with the original.""" + one = original.copy(); one.animation_data_clear(); bpy.context.collection.objects.link(one) + return one + +def ground(name, colour, patch, size=1600, rough=.95, y=400): + m, n, l = material(name); bsdf = n['Principled BSDF']; bsdf.inputs['Roughness'].default_value = rough + l.new(noise_ramp(n, l, n.new('ShaderNodeTexCoord').outputs['Object'], .06, 7, [(.35, colour), (.65, patch)]).outputs['Color'], bsdf.inputs['Base Color']) + return box(name, 0, y, size, size, .1, m, -.1) + +def acacia(x, y, mat, seed, scale=1.0): + """A flat-topped tree, seen against the sun.""" + rng = random.Random(seed) + box(f'trunk{seed}', x, y, .9 * scale, .9 * scale, 8 * scale, mat) + for i in range(7): + top = sphere(f'canopy{seed}{i}', 1, 16); top.data.materials.append(mat) + top.location = (x + rng.uniform(-6, 6) * scale, y + rng.uniform(-2, 2) * scale, (8 + rng.uniform(0, 1.6)) * scale) + top.scale = (rng.uniform(3, 5.2) * scale, 2.6 * scale, rng.uniform(.5, 1.1) * scale) + +GIRAFFE = [[(-1.7, 0), (-1.35, 0), (-.9, 3.7), (-1.5, 3.8)], [(1.3, 0), (1.65, 0), (1.45, 3.9), (.85, 3.8)], [(-.9, 0), (-.55, 0), (-.6, 3.7), (-1.1, 3.8)], [(.6, 0), (.95, 0), (1.05, 3.8), (.5, 3.8)], + [(-1.7, 3.6), (1.5, 3.5), (1.75, 5.2), (.4, 5.2), (-1.5, 4.5)], [(.75, 5), (1.75, 4.9), (2.9, 9.4), (2.45, 9.5)], [(2.35, 9.2), (3.05, 9.7), (4.05, 9.25), (4, 8.95), (3, 8.85)], + [(2.55, 9.6), (2.68, 9.6), (2.65, 10.2), (2.5, 10.2)], [(2.85, 9.6), (2.98, 9.6), (2.96, 10.2), (2.82, 10.2)], [(-1.7, 4.4), (-1.5, 4.5), (-2.05, 2.4), (-2.2, 2.45)]] +ELEPHANT = [[(-3.1, 0), (-1.9, 0), (-1.7, 2.6), (-3.2, 2.8)], [(-1, 0), (.15, 0), (.3, 2.6), (-.95, 2.6)], [(1, 0), (2.05, 0), (2.2, 2.5), (.95, 2.6)], [(2.3, 0), (3.3, 0), (3.2, 2.5), (2.2, 2.6)], + [(-3.4, 2.4), (-3.3, 4.2), (-2.3, 5.7), (.5, 5.9), (2.1, 5.5), (3.4, 4.6), (3.6, 2.2), (-3.2, 2)], [(2.6, 5.9), (4.2, 6.2), (5.2, 5.2), (5.3, 3.6), (3.4, 3)], + [(4.7, 4.5), (5.4, 4.2), (6, 2), (6.5, .3), (6, .25), (5.3, 1.9), (4.8, 3.2)], [(2.1, 5.8), (3.3, 5.9), (3.3, 3.3), (2.5, 3.1), (1.7, 4.1)], [(-3.4, 4.2), (-3.2, 4.2), (-3.75, 1.9), (-3.95, 2)]] + +def fireball(name, where, start, size): + """An explosion, as a real volume of burning gas: it bursts fast, keeps billowing and burns down to smoke.""" + ball = sphere(name, 1, 48); ball.location = where + m, n, l = material(name); out = n['Material Output']; n.remove(n['Principled BSDF']) + coord = n.new('ShaderNodeTexCoord').outputs['Object'] + noise = n.new('ShaderNodeTexNoise'); noise.noise_dimensions = '4D'; noise.inputs['Scale'].default_value = 2.1; noise.inputs['Detail'].default_value = 9; noise.inputs['Roughness'].default_value = .6; noise.inputs['Distortion'].default_value = .5; l.new(coord, noise.inputs['Vector']) + churn = noise.inputs['W'] + churn.default_value = 0; churn.keyframe_insert('default_value', frame=1); churn.default_value = 3.2; churn.keyframe_insert('default_value', frame=350) + away = n.new('ShaderNodeVectorMath'); away.operation = 'LENGTH'; l.new(coord, away.inputs[0]) + # The fire thins out well inside the mesh, so its edge is billows, not the sphere. + spread = n.new('ShaderNodeMath'); spread.operation = 'MULTIPLY'; spread.inputs[1].default_value = 1.6; l.new(away.outputs['Value'], spread.inputs[0]) + inner = n.new('ShaderNodeMath'); inner.operation = 'SUBTRACT'; inner.inputs[0].default_value = 1; l.new(spread.outputs[0], inner.inputs[1]) + lumps = n.new('ShaderNodeMapRange'); lumps.inputs['From Min'].default_value = .3; lumps.inputs['From Max'].default_value = .7; lumps.inputs['To Min'].default_value = -.7; lumps.inputs['To Max'].default_value = .7; l.new(noise.outputs['Fac'], lumps.inputs['Value']) + body = n.new('ShaderNodeMath'); body.operation = 'ADD'; l.new(inner.outputs[0], body.inputs[0]); l.new(lumps.outputs['Result'], body.inputs[1]) + thick = ramp(n, [(.1, '#000000'), (.26, '#FFFFFF')]); l.new(body.outputs[0], thick.inputs['Fac']) + density = n.new('ShaderNodeMath'); density.operation = 'MULTIPLY'; density.inputs[1].default_value = 1.6; l.new(thick.outputs['Color'], density.inputs[0]) + tone = ramp(n, [(.05, '#2A0600'), (.26, '#C02800'), (.5, '#FF7A10'), (.78, '#FFDC80')]); hot = n.new('ShaderNodeMapRange'); hot.inputs['From Min'].default_value = .1; hot.inputs['From Max'].default_value = 1.1; l.new(body.outputs[0], hot.inputs['Value']); l.new(hot.outputs['Result'], tone.inputs['Fac']) + volume = n.new('ShaderNodeVolumePrincipled'); volume.inputs['Color'].default_value = hexrgb('#120C0A') + l.new(density.outputs[0], volume.inputs['Density']); l.new(tone.outputs['Color'], volume.inputs['Emission Color']) + # Only the gas itself burns; empty space inside the mesh gives off nothing. + burn = n.new('ShaderNodeMath'); burn.operation = 'MULTIPLY'; l.new(thick.outputs['Color'], burn.inputs[0]); l.new(burn.outputs[0], volume.inputs['Emission Strength']) + power = burn.inputs[1] + for frame, value in ((start, 30), (start + 30, 20), (start + 120, 11), (350, 6)): + power.default_value = value; power.keyframe_insert('default_value', frame=frame) + l.new(volume.outputs[0], out.inputs['Volume']) + scene.cycles.volume_bounces = 1; scene.cycles.transparent_max_bounces = 16 + ball.data.materials.append(m); ball.visible_shadow = False + for frame, scale in ((1, 0), (start - 1, 0), (start, .06), (start + 22, .72), (start + 70, .92), (350, 1.0 + (350 - start) * .0008)): + ball.scale = (size * scale,) * 3; ball.keyframe_insert('scale', frame=frame) + data = bpy.data.lights.new(name + 'light', 'POINT'); data.color = hexrgb('#FF7A30')[:3]; data.shadow_soft_size = size * .5 + light = bpy.data.objects.new(name + 'light', data); bpy.context.collection.objects.link(light); light.location = (where[0], where[1] - size * 1.1, where[2]) + for frame, value in ((1, 0), (start - 1, 0), (start + 4, 420000 * size / 16), (start + 60, 120000 * size / 16), (350, 36000 * size / 16)): + data.energy = value; data.keyframe_insert('energy', frame=frame) + return ball + +def sparks(where, start, count, seed, reach): + rng = random.Random(seed) + bpy.ops.mesh.primitive_ico_sphere_add(subdivisions=1, radius=.16); first = bpy.context.active_object; first.data.materials.append(glowing('spark', '#FF9A30', 5)) + for i in range(count): + one = first if i == 0 else clone(first) + away = Vector((rng.uniform(-1, 1), rng.uniform(-1, .3), rng.uniform(-.1, 1))).normalized() * rng.uniform(.4, 1) * reach + life = rng.randint(70, 170) + one.location = where; one.scale = (0, 0, 0); one.keyframe_insert('scale', frame=1); one.keyframe_insert('scale', frame=start - 1); one.keyframe_insert('location', frame=start) + one.scale = (1, 1, 1); one.keyframe_insert('scale', frame=start) + one.location = Vector(where) + away * .75; one.keyframe_insert('location', frame=start + life // 2) + one.location = Vector(where) + away + Vector((0, 0, -reach * .3)); one.keyframe_insert('location', frame=start + life); one.keyframe_insert('scale', frame=start + life - 8) + one.scale = (0, 0, 0); one.keyframe_insert('scale', frame=start + life) + +def rope(name, a, b, mat, sag=1.1): + curve = bpy.data.curves.new(name, 'CURVE'); curve.dimensions = '3D'; curve.bevel_depth = .09 + spline = curve.splines.new('POLY'); spline.points.add(10) + for i in range(11): + u = i / 10; p = Vector(a).lerp(Vector(b), u); spline.points[i].co = (p.x, p.y, p.z - math.sin(u * math.pi) * sag, 1) + obj = bpy.data.objects.new(name, curve); bpy.context.collection.objects.link(obj); curve.materials.append(mat) + +def red_carpet(): + """A film premiere: a red carpet between gold posts and ropes, up to a lit picture palace, under camera flashes.""" + carpet, n, l = material('carpet'); bsdf = n['Principled BSDF']; bsdf.inputs['Base Color'].default_value = hexrgb('#C00A1C'); bsdf.inputs['Roughness'].default_value = .9 + box('carpet', 0, 70, 12, 260, .08, carpet) + box('pavement', 0, 70, 400, 300, .1, stone('pavement', '#14141C', .35), -.1) + gold = stone('gold', '#E0AC48', .25, 1); velvet = stone('velvet', '#A00C18', .7) + for side in (-1, 1): + last = None + for y in range(-40, 190, 12): + post = box(f'post{side}{y}', side * 7.4, y, .26, .26, 3.6, gold); ball = sphere(f'postball{side}{y}', .3, 16); ball.location = (side * 7.4, y, 3.7); ball.data.materials.append(gold) + box(f'postfoot{side}{y}', side * 7.4, y, .8, .8, .12, gold) + if last is not None: rope(f'rope{side}{y}', (side * 7.4, last, 3.2), (side * 7.4, y, 3.2), velvet) + last = y + # The picture palace at the end, its columns lit and its canopy running with bulbs. + box('palace', 0, 214, 150, 30, 70, stone('palacewall', '#1A1420', .5)) + box('palacecornice', 0, 197, 150, 4, 2.4, gold, 46) + for x in range(-60, 61, 15): + if abs(x) > 12: box(f'column{x}', x, 197, 3.4, 3.4, 46, stone('columnstone', '#8A7858', .6)) + box('canopy', 0, 188, 66, 18, 2.4, stone('canopytop', '#18141C', .4), 14) + box('doors', 0, 198.8, 24, .2, 13, glowing('doors', '#FFC873', 2.4)) + bulbs = glowing('bulbs', '#FFE2A0', 9) + for x in range(-32, 33, 2): + bulb_ = sphere(f'canopybulb{x}', .36, 10); bulb_.location = (x, 178.8, 15.2); bulb_.data.materials.append(bulbs) + for z, width in ((24, 44), (40, 44)): + for x in range(-width // 2, width // 2 + 1, 2): + bulb_ = sphere(f'signbulb{x}{z}', .34, 10); bulb_.location = (x, 195, z); bulb_.data.materials.append(bulbs) + for z in range(25, 40, 2): + for x in (-22, 22): + bulb_ = sphere(f'signbulb{x}{z}', .34, 10); bulb_.location = (x, 195, z); bulb_.data.materials.append(bulbs) + box('palacename', 0, 196, 42, .3, 14, glowing('palacename', '#2A1408', .7), 25) + for x in (-30, 30): + data = bpy.data.lights.new(f'wash{x}', 'SPOT'); data.energy = 300000; data.spot_size = math.radians(48); data.spot_blend = .9; data.color = hexrgb('#FFD8A8')[:3] + wash = bpy.data.objects.new(f'wash{x}', data); bpy.context.collection.objects.link(wash); wash.location = (x, 120, 30); wash.rotation_euler = (Vector((x * .5, 195, 18)) - Vector(wash.location)).to_track_quat('-Z', 'Y').to_euler() + data = bpy.data.lights.new('carpetspot', 'SPOT'); data.energy = 500000; data.spot_size = math.radians(80); data.spot_blend = .9; data.color = hexrgb('#FFE8D0')[:3] + wash = bpy.data.objects.new('carpetspot', data); bpy.context.collection.objects.link(wash); wash.location = (0, 60, 70); wash.rotation_euler = (0, 0, 0) + # Press flashes from behind the ropes, going off at random. + rng = random.Random(77) + bulb = glowing('bulb', '#FFFFFF', 0) + for i in range(34): + side = rng.choice((-1, 1)); x, y, z = side * rng.uniform(9, 15), rng.uniform(84, 180), rng.uniform(3, 6.5) + data = bpy.data.lights.new(f'flash{i}', 'POINT'); data.color = (1, 1, 1); data.shadow_soft_size = .3 + light = bpy.data.objects.new(f'flash{i}', data); bpy.context.collection.objects.link(light); light.location = (x, y, z) + m = glowing(f'flashball{i}', '#FFFFFF', 0); ball = sphere(f'flashball{i}', .3, 12); ball.location = (x, y, z); ball.data.materials.append(m) + strength = m.node_tree.nodes['Emission'].inputs['Strength'] + data.energy = 0; data.keyframe_insert('energy', frame=1); strength.default_value = 0; strength.keyframe_insert('default_value', frame=1) + for pop in range(4): + frame = rng.randint(4, 346) + for at, level in ((frame - 1, 0), (frame, 1), (frame + 2, 0)): + data.energy = level * 2600; data.keyframe_insert('energy', frame=at); strength.default_value = level * 90; strength.keyframe_insert('default_value', frame=at) + # A silhouette of the photographer behind each flash. + box(f'press{i}', x + side * .9, y, 1.4, .7, z - .4, stone('press', '#07070C')) + data = bpy.data.lights.new('carpetlight', 'SUN'); data.energy = .25; data.color = hexrgb('#FFE2C0')[:3]; lamp = bpy.data.objects.new('carpetlight', data); bpy.context.collection.objects.link(lamp); lamp.rotation_euler = (math.radians(60), 0, math.radians(10)) + +def lanterns(count, seed, area): + """Paper lanterns rising from the water, each with a candle's glow.""" + rng = random.Random(seed) + glow_colours = ('#FF9A4A', '#FFB45C', '#FF8A5A') + mats = [glowing(f'lantern{i}', c, 2.2) for i, c in enumerate(glow_colours)] + bpy.ops.mesh.primitive_uv_sphere_add(segments=16, ring_count=10, radius=1); seed_ball = bpy.context.active_object; bpy.ops.object.shade_smooth() + for i in range(count): + one = seed_ball if i == 0 else clone(seed_ball) + if i: one.data = seed_ball.data.copy() + one.data.materials.clear(); one.data.materials.append(rng.choice(mats)) + x, y, z = rng.uniform(*area[0]), rng.uniform(*area[1]), rng.uniform(*area[2]) + size = rng.uniform(.7, 1.15); one.scale = (size * .8, size * .8, size) + one.location = (x, y, z); one.keyframe_insert('location', frame=1) + one.location = (x + rng.uniform(-2, 2), y + rng.uniform(-2, 2), z + rng.uniform(12, 26)); one.keyframe_insert('location', frame=350) + +def petals(count, seed, area): + """Rose petals falling past the camera.""" + rng = random.Random(seed) + bpy.ops.mesh.primitive_plane_add(size=1); first = bpy.context.active_object + bm = first.data; first.data.materials.append(stone('petal', '#FF5A8C', .45)) + for i in range(count): + one = first if i == 0 else clone(first) + x, y, z = rng.uniform(*area[0]), rng.uniform(*area[1]), rng.uniform(*area[2]) + one.scale = (rng.uniform(.28, .5), rng.uniform(.18, .32), 1) + one.location = (x, y, z); one.rotation_euler = (rng.random() * 6, rng.random() * 6, rng.random() * 6); one.keyframe_insert('location', frame=1); one.keyframe_insert('rotation_euler', frame=1) + one.location = (x + rng.uniform(1, 6), y + rng.uniform(-2, 2), z - rng.uniform(10, 22)); one.rotation_euler = (rng.random() * 12, rng.random() * 12, rng.random() * 12); one.keyframe_insert('location', frame=350); one.keyframe_insert('rotation_euler', frame=350) + +def archive(): + """A long, dark records room: filing cabinets each side, strip lights failing overhead.""" + steel = stone('cabinet', '#3A4048', .45, .7); front = stone('drawer', '#4A5058', .5, .6); handle = stone('handle', '#B8BCC0', .3, 1); label = stone('label', '#D8D0B8', .8); red = glowing('stamp', '#C81828', 1.4) + box('archivefloor', 0, 110, 60, 320, .1, stone('archivefloormat', '#101114', .28, .1), -.1) + box('archiveroof', 0, 110, 60, 320, .1, stone('roof', '#0A0A0D', .9), 18) + box('archiveend', 0, 270, 60, .5, 18, stone('endwall', '#15161A', .9)) + rng = random.Random(31) + for side in (-1, 1): + for row in range(26): + y = -30 + row * 11 + box(f'cabinet{side}{row}', side * 9, y, 3.4, 10.4, 15, steel) + for level in range(5): + z = .5 + level * 2.9 + box(f'drawer{side}{row}{level}', side * 7.25, y, .1, 9.6, 2.6, front, z) + box(f'handle{side}{row}{level}', side * 7.15, y, .12, 1.6, .22, handle, z + 1.15) + box(f'label{side}{row}{level}', side * 7.15, y - 2.6, .05, 2, .8, red if rng.random() < .08 else label, z + 1.3) + for i, y in enumerate(range(-20, 260, 34)): + lamp = box(f'strip{i}', 0, y, .5, 8, .2, glowing(f'strip{i}', '#BFD6E8', 7), 17.7) + data = bpy.data.lights.new(f'stripglow{i}', 'POINT'); data.color = hexrgb('#BFD6E8')[:3]; data.shadow_soft_size = 1.5 + light = bpy.data.objects.new(f'stripglow{i}', data); bpy.context.collection.objects.link(light); light.location = (0, y, 16.8) + power = lamp.data.materials[0].node_tree.nodes['Emission'].inputs['Strength'] + for frame in sorted({1, 350} | {rng.randint(2, 349) for _ in range(9 if i % 3 == 0 else 2)}): + on = rng.random() > (.42 if i % 3 == 0 else .15) + data.energy = 2200 if on else 140; data.keyframe_insert('energy', frame=frame); power.default_value = 7 if on else .2; power.keyframe_insert('default_value', frame=frame) + for fc in (data.animation_data.action.layers[0].strips[0].channelbag(data.animation_data.action_slot).fcurves if hasattr(data.animation_data.action, 'layers') else data.animation_data.action.fcurves): + for p in fc.keyframe_points: p.interpolation = 'CONSTANT' + # Dust hanging in the air. + rng = random.Random(8) + bpy.ops.mesh.primitive_ico_sphere_add(subdivisions=1, radius=.05); dust = bpy.context.active_object; dust.data.materials.append(glowing('dustmote', '#C8D0D8', 1.6)) + for i in range(240): + one = dust if i == 0 else clone(dust) + one.location = (rng.uniform(-6, 6), rng.uniform(-20, 240), rng.uniform(.5, 16)); one.keyframe_insert('location', frame=1); one.location.z += rng.uniform(-.6, .8); one.location.x += rng.uniform(-.8, .8); one.keyframe_insert('location', frame=350) + +def desert_road(): + """A straight highway across the desert at dusk, with mesas, telegraph poles and a neon motel sign.""" + ground('desert', '#7A3C34', '#B05C40') + road, n, l = material('blacktop'); bsdf = n['Principled BSDF']; bsdf.inputs['Base Color'].default_value = hexrgb('#16141A'); bsdf.inputs['Roughness'].default_value = .5 + box('highway', 0, 400, 13, 1500, .06, road) + paint = stone('paint', '#D8C89A', .6) + for y in range(-60, 1100, 16): + box(f'dash{y}', 0, y, .32, 6, .08, paint) + for side in (-1, 1): + box(f'edge{side}', side * 6, 400, .22, 1500, .08, paint) + rock_, n, l = material('mesa'); bsdf = n['Principled BSDF']; bsdf.inputs['Roughness'].default_value = 1 + l.new(noise_ramp(n, l, n.new('ShaderNodeTexCoord').outputs['Object'], .9, 5, [(.3, '#7A2C30'), (.65, '#B84C3C')]).outputs['Color'], bsdf.inputs['Base Color']) + rng = random.Random(61) + for i, (x, y, w, h) in enumerate(((-120, 520, 90, 48), (-210, 700, 140, 70), (150, 620, 110, 62), (260, 820, 170, 80), (70, 860, 90, 56), (-90, 900, 120, 66))): + bpy.ops.mesh.primitive_cylinder_add(vertices=9, radius=w / 2, depth=h, location=(x, y, h / 2)); m = bpy.context.active_object; m.name = f'mesa{i}' + for v in m.data.vertices: + if v.co.z > 0: v.co.x *= .68; v.co.y *= .68 + v.co.x *= rng.uniform(.9, 1.1) + m.data.materials.append(rock_) + pole = stone('pole', '#120C10', .9) + for y in range(-40, 760, 54): + box(f'pole{y}', -13, y, .4, .4, 11, pole); box(f'arm{y}', -13, y, 3.4, .25, .25, pole, 9.8) + # The motel: its long low block, and the sign with a neon arrow. + wall = window_wall('#2A2030', 2, ('#FFC873', '#FF8AB4', '#FFD27A'), .3, (2.2, 2.6)) + box('motel', 64, 216, 60, 11, 6, wall) + box('motelroof', 64, 216, 64, 13, .7, stone('motelroofmat', '#0C0A10'), 6) + box('signpost', 30, 186, .9, .9, 19, pole) + pink, cyan, amber = glowing('neonpink', '#FF2E7E', 9), glowing('neoncyan', '#28E0FF', 7), glowing('neonamber', '#FFB42E', 8) + board = [(-5, 0), (5, 0), (5, 5.5), (-5, 5.5)] + flat('signboard', [board], (30, 186, 18), (14, 60, 18), stone('signback', '#1C1022', .5)) + outline = [[(-5, -.15), (5, -.15), (5, .1), (-5, .1)], [(-5, 5.4), (5, 5.4), (5, 5.65), (-5, 5.65)], [(-5.15, 0), (-4.9, 0), (-4.9, 5.5), (-5.15, 5.5)], [(4.9, 0), (5.15, 0), (5.15, 5.5), (4.9, 5.5)]] + flat('signedge', outline, (30, 185.9, 18), (14, 60, 18), pink) + bars = [[(-4 + i * 1.9, 3.3), (-2.6 + i * 1.9, 3.3), (-2.6 + i * 1.9, 4.6), (-4 + i * 1.9, 4.6)] for i in range(4)] + flat('signbars', bars, (30, 185.85, 18), (14, 60, 18), cyan) + flat('signstrip', [[(-4, 1.4), (4, 1.4), (4, 2.5), (-4, 2.5)]], (30, 185.85, 18), (14, 60, 18), amber) + # The arrow, pointing the way off the highway. + flat('signarrow', [[(-6, -3.2), (2.6, -3.2), (2.6, -2), (-6, -2)], [(2.6, -4.2), (5.4, -2.6), (2.6, -1)]], (30, 185.85, 18), (14, 60, 18), pink) + data = bpy.data.lights.new('signglow', 'POINT'); data.energy = 2600; data.color = hexrgb('#FF4A90')[:3]; lamp = bpy.data.objects.new('signglow', data); bpy.context.collection.objects.link(lamp); lamp.location = (29, 178, 14) + if SCENE == 'orbit-1': world_tint = ('#061A3C', '#0E3A6E') stars_world() @@ -917,7 +1151,7 @@ elif SCENE == 'orbit-gold': rng = random.Random(21) bpy.ops.mesh.primitive_ico_sphere_add(subdivisions=1, radius=.09); seed = bpy.context.active_object; seed.data.materials.append(glowing('dust', '#FFCB5A', 5)) for i in range(420): - speck = seed.copy(); bpy.context.collection.objects.link(speck); speck.location = (rng.uniform(-70, 40), rng.uniform(-40, 60), rng.uniform(-30, 30)); speck.scale = (rng.uniform(.5, 2.2),) * 3 + speck = clone(seed); speck.location = (rng.uniform(-70, 40), rng.uniform(-40, 60), rng.uniform(-30, 30)); speck.scale = (rng.uniform(.5, 2.2),) * 3 speck.keyframe_insert('location', frame=1); speck.location.z += rng.uniform(2, 7); speck.location.x += rng.uniform(-3, 3); speck.keyframe_insert('location', frame=350) camera_path([(1, (34, -46, -14), (6, 0, 0)), (260, (-27, -78, 8), (-24, 0, 1)), (350, (-27, -78, 8), (-24, 0, 1))]) elif SCENE == 'evidence': @@ -948,6 +1182,73 @@ elif SCENE == 'orbit-britx': box('southbank', -160, 300, 170, 800, 3, slate); box('northbank', 180, 300, 230, 800, 3, slate) # Skims the water, climbs over the walkway of the bridge, and comes down the river to Big Ben. camera_path([(1, (-6, -60, 7), (0, 70, 24)), (130, (-2, 62, 44), (10, 200, 42)), (285, (-32, 112, 30), (-8, 262, 58)), (350, (-32, 116, 30), (-8, 262, 58))], lens=30) +elif SCENE == 'orbit-wild': + sky_world([(0, '#FFB050'), (.06, '#E86A30'), (.16, '#8A2E44'), (.42, '#22183C')]) + ground('savannah', '#7A4418', '#B87428') + black = stone('shade', '#050302', 1) + sun((-.5, .8, -.32), 1.5, '#FFAA60') + flare((320, 520, 58), 96, '#FFD27A', 3.2) + for i, (x, y, size) in enumerate(((-70, 170, 1.2), (84, 92, 1.5), (120, 300, 1.5), (-120, 330, 1.4), (-20, 300, 1), (-160, 200, 1.6))): + acacia(x, y, black, i, size) + # A giraffe family and an elephant herd crossing in front of the sun. + for i, (x, y, size, side) in enumerate(((22, 60, 1.6, 1), (31, 66, 1.3, 1), (15, 68, .95, 1))): + flat(f'giraffe{i}', [[(p[0] * size * side, p[1] * size) for p in part] for part in GIRAFFE], (x, y, 0), (x, -50, 0), black) + for i, (x, y, size) in enumerate(((58, 112, 1.6), (44, 120, 1.15), (72, 124, 1.25))): + flat(f'elephant{i}', [[(p[0] * size, p[1] * size) for p in part] for part in ELEPHANT], (x, y, 0), (x, -50, 0), black) + camera_path([(1, (-40, -40, 4), (-10, 100, 24)), (280, (-14, -6, 4.5), (2, 100, 24)), (350, (-14, -3, 4.5), (2, 100, 24))], lens=30) +elif SCENE == 'orbit-action': + sky_world([(0, '#2A0E08'), (.08, '#14080C'), (.3, '#05060C')]) + ground('dirt', '#0E0A08', '#1E1610') + black = stone('shade', '#040304', 1) + # A refinery on the skyline, about to go up. + for i, (x, w, h) in enumerate(((-40, 18, 16), (-14, 12, 24), (6, 22, 13), (30, 16, 26), (54, 22, 18), (80, 18, 12))): + box(f'plant{i}', x, 72, w, 14, h, stone('plant', '#16120F', .7)) + for i, x in enumerate((-30, 18, 62)): + bpy.ops.mesh.primitive_cylinder_add(vertices=48, radius=7, depth=14, location=(x, 58, 7)); bpy.context.active_object.data.materials.append(stone('tank', '#2A2420', .5, .5)) + for i, x in enumerate((-4, 42)): + box(f'stack{i}', x, 76, 2, 2, 44, stone('plant', '#16120F', .7)) + fireball('blast1', (30, 56, 12), 28, 34); fireball('blast2', (56, 64, 13), 96, 26); fireball('blast3', (14, 64, 15), 150, 24) + sparks((30, 54, 10), 28, 170, 1, 66); sparks((14, 60, 12), 150, 110, 2, 58) + # Someone walking away from it, and a wreck on the road. + caped_hero((10, -6, 0), (10, -90, 0), black, 1.15, cape_on=False) + box('wreck', -18, 8, 9, 4, 2, black); box('wrecktop', -18, 8, 4.5, 3.4, 1.4, black, 2) + camera_path([(1, (-22, -66, 3), (6, 30, 12)), (24, (-22, -66, 3), (6, 30, 12)), (34, (-21.4, -65, 3.5), (6.2, 30, 11.4)), (44, (-22.2, -64, 2.8), (5.8, 30, 12.3)), (280, (-20, -46, 4), (0, 30, 11)), (350, (-20, -44, 4), (0, 30, 11))], lens=30) +elif SCENE == 'orbit-premiere': + world_tint = ('#080A18', '#141A34') + stars_world(.6) + red_carpet() + for i, (x, keys) in enumerate(((-70, [(1, -24), (160, 18), (350, -8)]), (-34, [(1, 16), (170, -20), (350, 10)]), (34, [(1, -18), (160, 22), (350, -8)]), (70, [(1, 22), (180, -16), (350, 12)]))): + sweep(beam(f'search{i}', (x, 238, 66), 190, 10, '#FFE2A8', .55), keys) + camera_path([(1, (0, -46, 3), (0, 100, 8)), (280, (-5, 36, 8), (-44, 200, 30)), (350, (-5, 40, 8), (-44, 200, 30))], lens=30) +elif SCENE == 'orbit-romance': + sky_world([(0, '#FFB088'), (.07, '#F0708C'), (.2, '#8A3C90'), (.48, '#1C1648')]) + water, n, l = material('lake'); bsdf = n['Principled BSDF']; bsdf.inputs['Base Color'].default_value = hexrgb('#141030'); bsdf.inputs['Roughness'].default_value = .1; bsdf.inputs['Metallic'].default_value = .4 + ripple = n.new('ShaderNodeTexNoise'); ripple.inputs['Scale'].default_value = .8; ripple.inputs['Detail'].default_value = 5 + tiny = n.new('ShaderNodeBump'); tiny.inputs['Strength'].default_value = .08; l.new(ripple.outputs['Fac'], tiny.inputs['Height']); l.new(tiny.outputs[0], bsdf.inputs['Normal']) + box('lake', 0, 500, 1800, 1800, .1, water, -.1) + hills = stone('hills', '#120C2A', 1) + rng = random.Random(4) + for i in range(16): + bump = sphere(f'hill{i}', 1, 24); bump.data.materials.append(hills); bump.location = (-420 + i * 56, 620 + rng.uniform(-30, 30), 0); bump.scale = (rng.uniform(60, 110), 40, rng.uniform(16, 48)) + flare((300, 560, 74), 34, '#FFC090', 1.6) + lanterns(170, 3, ((-110, 130), (20, 300), (1, 42))) + petals(220, 12, ((-34, 26), (-34, 18), (-6, 34))) + camera_path([(1, (-12, -60, 3), (2, 200, 26)), (280, (-14, -24, 5), (2, 200, 38)), (350, (-14, -21, 5), (2, 200, 38))], lens=30) +elif SCENE == 'orbit-dark-files': + scene.world = bpy.data.worlds.new('dark'); scene.world.use_nodes = True; scene.world.node_tree.nodes['Background'].inputs['Color'].default_value = (.001, .001, .002, 1) + archive() + cam = camera_path([(1, (0, -30, 7), (0, 90, 7)), (285, (-.8, 150, 6.5), (2.6, 262, 7)), (350, (-.8, 158, 6.5), (2.6, 262, 7))], lens=28) + # A hand torch, searching the drawers. + data = bpy.data.lights.new('torch', 'SPOT'); data.energy = 9000; data.spot_size = math.radians(26); data.spot_blend = .35; data.color = hexrgb('#FFF0D8')[:3]; data.shadow_soft_size = .1 + torch = bpy.data.objects.new('torch', data); bpy.context.collection.objects.link(torch); torch.parent = cam; torch.location = (.6, -.5, 0) + for frame, turn_x, turn_y in ((1, 0, -.5), (90, -.14, .55), (190, .2, -.42), (280, -.1, .3), (350, .02, .22)): + torch.rotation_euler = (turn_x, turn_y, 0); torch.keyframe_insert('rotation_euler', frame=frame) +elif SCENE == 'orbit-detour': + sky_world([(0, '#FF8A4A'), (.05, '#F04A78'), (.16, '#7A2A98'), (.42, '#141440')]) + sun((.3, .85, -.3), 1.3, '#FFB8A0') + desert_road() + flare((170, 980, 26), 30, '#FFD27A', 3) + camera_path([(1, (0, -60, 2.2), (0, 60, 3)), (285, (-3, 116, 3.2), (6, 240, 15)), (350, (-3, 120, 3.2), (6, 240, 15))], lens=30) else: raise SystemExit('unknown scene') diff --git a/design/channels/blender/claw_marks.py b/design/channels/blender/claw_marks.py new file mode 100644 index 0000000..cea5348 --- /dev/null +++ b/design/channels/blender/claw_marks.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +"""Three claw marks slashed beside the Wild logo, as frames to lay over the finished ident, just before the logo settles. + claw_marks.py +""" +import sys +from pathlib import Path +from PIL import Image, ImageDraw, ImageFilter +MARKS_FROM = 940 +word = Image.open(sys.argv[1]).convert('RGBA') +word.paste((0, 0, 0, 0), (MARKS_FROM, 0, word.width, word.height)) +word.save(sys.argv[2]) +out = Path(sys.argv[3]); out.mkdir(parents=True, exist_ok=True) +SS = 2 +# Each mark runs from its top right to its bottom left, widest in the middle. Screen position as compose_ident.sh places the logo. +MARKS = [dict(top=(668, 244), bottom=(626, 416), start=232), dict(top=(708, 236), bottom=(664, 422), start=238), dict(top=(748, 244), bottom=(704, 416), start=244)] +STROKE = 6 +HALF = 11 + +def shape(mark, reach): + (tx, ty), (bx, by) = mark['top'], mark['bottom'] + def at(u): return tx + (bx - tx) * u, ty + (by - ty) * u + points = [at(0)] + for u, side in ((.3, 1), (.6, 1)): + x, y = at(u * reach / 1); points.append((x + HALF * side * (1 - abs(u - .45)), y + 1)) + points.append(at(reach)) + for u, side in ((.6, -1), (.3, -1)): + x, y = at(u * reach / 1); points.append((x + HALF * side * (1 - abs(u - .45)) * .9, y - 1)) + return [(x * SS, y * SS) for x, y in points] + +def frame_picture(frame): + solid = Image.new('L', (1280 * SS, 720 * SS)); hot = Image.new('L', (1280 * SS, 720 * SS)) + for mark in MARKS: + age = frame - mark['start'] + if age < 0: continue + reach = min(1, (age + 1) / STROKE) + ImageDraw.Draw(solid).polygon(shape(mark, reach), fill=255) + heat = max(0, 1 - age / 16) + if heat: ImageDraw.Draw(hot).polygon(shape(mark, reach), fill=int(255 * heat)) + picture = Image.new('RGBA', (1280 * SS, 720 * SS), (240, 128, 24, 0)); picture.putalpha(solid) + white = Image.new('RGBA', picture.size, (255, 240, 200, 255)); picture = Image.composite(white, picture, hot) + # A hot glow that dies back to a low ember. + glow_colour = Image.new('RGBA', picture.size, (255, 110, 20, 255)) + glow = solid.filter(ImageFilter.GaussianBlur(16)).point(lambda v: v * .55 + 0) + under = glow_colour.copy(); under.putalpha(glow) + under.alpha_composite(picture) + return under.resize((1280, 720), Image.LANCZOS) + +settled = frame_picture(MARKS[-1]['start'] + 40) +blank = Image.new('RGBA', (1280, 720)) +for frame in range(1, 351): + if frame < MARKS[0]['start']: picture = blank + elif frame >= MARKS[-1]['start'] + 40: picture = settled + else: picture = frame_picture(frame) + picture.save(out / f'over_{frame:04d}.png')