Third batch of Blender ident scenes and the Wild claw marks

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-11-03 00:32:01 +00:00
co-authored by Claude Opus 5.5
parent ff415434aa
commit ef41d04dec
2 changed files with 357 additions and 2 deletions
+303 -2
View File
@@ -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')
+54
View File
@@ -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 <logo_word.png> <logo copy without marks, to write> <out folder>
"""
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')