@hraness/slopcamera 3.3.5 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -4
- package/apps/desktop/application/operation.ts +1 -1
- package/apps/desktop/cli/capability-manifest.ts +13 -0
- package/apps/desktop/cli/commands.ts +22 -20
- package/apps/desktop/cli/help.ts +12 -1
- package/apps/desktop/cli/html-overlay-renderer.ts +16 -0
- package/apps/desktop/cli/portable-surface.ts +2 -0
- package/apps/desktop/code/public.ts +1 -0
- package/apps/desktop/core/visual-style-look.ts +34 -0
- package/apps/desktop/dist/cli/main.js +134 -110
- package/apps/desktop/html-overlay/runtime.ts +15 -0
- package/dist/cli.js +6 -2
- package/dist/{index-11cb717y.js → index-abnk4h9c.js} +3 -2
- package/dist/index.js +1 -1
- package/docs/README.md +1 -0
- package/examples/style-portfolio/ANIMATION-STUDIES.md +34 -0
- package/examples/style-portfolio/FILM-PLATE.md +19 -0
- package/examples/style-portfolio/README.md +137 -0
- package/examples/style-portfolio/animation-studies.html +597 -0
- package/examples/style-portfolio/assets/instrument-serif-OFL.txt +93 -0
- package/examples/style-portfolio/assets/instrument-serif-latin-400.woff2 +0 -0
- package/examples/style-portfolio/build-gallery.ts +403 -0
- package/examples/style-portfolio/film-plate.html +76 -0
- package/examples/style-portfolio/finish-film.ts +152 -0
- package/examples/style-portfolio/market-street/README.md +43 -0
- package/examples/style-portfolio/market-street/mesh.py +145 -0
- package/examples/style-portfolio/market-street/scene.py +760 -0
- package/examples/style-portfolio/market-street/source.json +1 -0
- package/examples/style-portfolio/market-street/street_data.py +72 -0
- package/examples/style-portfolio/render-film-plate.ts +128 -0
- package/examples/style-portfolio/render.ts +89 -0
- package/examples/style-portfolio/verify-animation-source.mjs +94 -0
- package/package.json +11 -2
- package/skills/slopcamera/SKILL.md +9 -0
- package/skills/slopcamera/references/gateway-media.md +5 -0
- package/skills/slopcamera/references/install.md +1 -1
- package/skills/slopcamera/references/music-video.md +5 -0
- package/skills/slopcamera/references/native-studio.md +4 -0
- package/skills/slopcamera/references/visual-style-direction.md +143 -0
- package/src/cli.ts +28 -0
- package/src/index.ts +1 -0
- package/src/version.ts +1 -1
- package/src/visual-style.ts +375 -0
|
@@ -0,0 +1,760 @@
|
|
|
1
|
+
"""A short monochrome cable-car film study, authored for Slopcamera Studio.
|
|
2
|
+
|
|
3
|
+
Call build(context). The source reads no clocks, environment variables, network
|
|
4
|
+
resources, external fonts, or external media. Timing comes only from the job.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import math
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
import random
|
|
11
|
+
|
|
12
|
+
import bpy
|
|
13
|
+
from mathutils import Vector
|
|
14
|
+
|
|
15
|
+
from mesh import Batch, empty, text_label
|
|
16
|
+
import street_data as data
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def gray(value):
|
|
20
|
+
return (value, value, value, 1)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def surface(name, value, roughness=.8, metallic=0, texture='stone'):
|
|
24
|
+
mat = bpy.data.materials.new(name)
|
|
25
|
+
mat.diffuse_color = gray(value)
|
|
26
|
+
mat.use_nodes = True
|
|
27
|
+
tree = mat.node_tree
|
|
28
|
+
nodes, links = tree.nodes, tree.links
|
|
29
|
+
bsdf = nodes.get('Principled BSDF')
|
|
30
|
+
bsdf.inputs['Base Color'].default_value = gray(value)
|
|
31
|
+
bsdf.inputs['Roughness'].default_value = roughness
|
|
32
|
+
bsdf.inputs['Metallic'].default_value = metallic
|
|
33
|
+
if texture is None:
|
|
34
|
+
return mat
|
|
35
|
+
geometry = nodes.new('ShaderNodeNewGeometry')
|
|
36
|
+
noise = nodes.new('ShaderNodeTexNoise')
|
|
37
|
+
noise.inputs['Scale'].default_value = 2.8 if texture != 'cloth' else 160
|
|
38
|
+
noise.inputs['Detail'].default_value = 4
|
|
39
|
+
noise.inputs['Roughness'].default_value = .74
|
|
40
|
+
links.new(geometry.outputs['Position'], noise.inputs['Vector'])
|
|
41
|
+
ramp = nodes.new('ShaderNodeValToRGB')
|
|
42
|
+
ramp.color_ramp.elements[0].position = .16
|
|
43
|
+
ramp.color_ramp.elements[0].color = gray(value * .82)
|
|
44
|
+
ramp.color_ramp.elements[1].position = .84
|
|
45
|
+
ramp.color_ramp.elements[1].color = gray(min(.95, value * 1.08))
|
|
46
|
+
links.new(noise.outputs['Fac'], ramp.inputs[0])
|
|
47
|
+
links.new(ramp.outputs['Color'], bsdf.inputs['Base Color'])
|
|
48
|
+
bump = nodes.new('ShaderNodeBump')
|
|
49
|
+
bump.inputs['Strength'].default_value = .24
|
|
50
|
+
bump.inputs['Distance'].default_value = .035 if texture == 'stone' else .008
|
|
51
|
+
links.new(noise.outputs['Fac'], bump.inputs['Height'])
|
|
52
|
+
links.new(bump.outputs['Normal'], bsdf.inputs['Normal'])
|
|
53
|
+
if texture in ('brick', 'paving'):
|
|
54
|
+
split, combine = nodes.new('ShaderNodeSeparateXYZ'), nodes.new('ShaderNodeCombineXYZ')
|
|
55
|
+
links.new(geometry.outputs['Position'], split.inputs[0])
|
|
56
|
+
# Select X/Z or Y/Z by surface normal so exposed party walls keep
|
|
57
|
+
# brick joints instead of collapsing into repeated horizontal stripes.
|
|
58
|
+
if texture == 'paving':
|
|
59
|
+
links.new(split.outputs['X'], combine.inputs['X'])
|
|
60
|
+
links.new(split.outputs['Y'], combine.inputs['Y'])
|
|
61
|
+
else:
|
|
62
|
+
normal_abs = nodes.new('ShaderNodeVectorMath')
|
|
63
|
+
normal_abs.operation = 'ABSOLUTE'
|
|
64
|
+
normal_split = nodes.new('ShaderNodeSeparateXYZ')
|
|
65
|
+
links.new(geometry.outputs['Normal'], normal_abs.inputs[0])
|
|
66
|
+
links.new(normal_abs.outputs[0], normal_split.inputs[0])
|
|
67
|
+
first, second, summed = (nodes.new('ShaderNodeMath') for _ in range(3))
|
|
68
|
+
first.operation = second.operation = 'MULTIPLY'
|
|
69
|
+
summed.operation = 'ADD'
|
|
70
|
+
links.new(split.outputs['Y'], first.inputs[0])
|
|
71
|
+
links.new(normal_split.outputs['X'], first.inputs[1])
|
|
72
|
+
links.new(split.outputs['X'], second.inputs[0])
|
|
73
|
+
links.new(normal_split.outputs['Y'], second.inputs[1])
|
|
74
|
+
links.new(first.outputs[0], summed.inputs[0])
|
|
75
|
+
links.new(second.outputs[0], summed.inputs[1])
|
|
76
|
+
links.new(summed.outputs[0], combine.inputs['X'])
|
|
77
|
+
links.new(split.outputs['Z'], combine.inputs['Y'])
|
|
78
|
+
brick = nodes.new('ShaderNodeTexBrick')
|
|
79
|
+
links.new(combine.outputs[0], brick.inputs['Vector'])
|
|
80
|
+
brick.inputs['Scale'].default_value = 1
|
|
81
|
+
brick.inputs['Brick Width'].default_value = .31
|
|
82
|
+
brick.inputs['Row Height'].default_value = .17 if texture == 'paving' else .13
|
|
83
|
+
brick.inputs['Mortar Size'].default_value = .004 if texture == 'paving' else .006
|
|
84
|
+
brick.inputs['Mortar Smooth'].default_value = .007
|
|
85
|
+
brick.inputs['Color1'].default_value = gray(value * .85 if texture == 'paving' else value * .68)
|
|
86
|
+
brick.inputs['Color2'].default_value = gray(value * 1.08 if texture == 'paving' else value * 1.23)
|
|
87
|
+
brick.inputs['Mortar'].default_value = gray(value * .64 if texture == 'paving' else value * .36)
|
|
88
|
+
mix = nodes.new('ShaderNodeMixRGB')
|
|
89
|
+
mix.blend_type = 'MULTIPLY'
|
|
90
|
+
mix.inputs[0].default_value = .48
|
|
91
|
+
links.new(brick.outputs['Color'], mix.inputs[1])
|
|
92
|
+
links.new(ramp.outputs['Color'], mix.inputs[2])
|
|
93
|
+
links.new(mix.outputs[0], bsdf.inputs['Base Color'])
|
|
94
|
+
links.new(brick.outputs['Fac'], bump.inputs['Height'])
|
|
95
|
+
bump.inputs['Distance'].default_value = .025 if texture == 'paving' else .012
|
|
96
|
+
if texture == 'paving':
|
|
97
|
+
# Uneven moisture changes reflectance on the paving itself. It
|
|
98
|
+
# never adds opaque polygon islands above the road surface.
|
|
99
|
+
damp = nodes.new('ShaderNodeTexNoise')
|
|
100
|
+
damp.inputs['Scale'].default_value = .35
|
|
101
|
+
damp.inputs['Detail'].default_value = 2.4
|
|
102
|
+
links.new(geometry.outputs['Position'], damp.inputs['Vector'])
|
|
103
|
+
damp_ramp = nodes.new('ShaderNodeValToRGB')
|
|
104
|
+
damp_ramp.color_ramp.elements[0].position = .31
|
|
105
|
+
damp_ramp.color_ramp.elements[0].color = gray(.28)
|
|
106
|
+
damp_ramp.color_ramp.elements[1].position = .57
|
|
107
|
+
damp_ramp.color_ramp.elements[1].color = gray(.92)
|
|
108
|
+
links.new(damp.outputs['Fac'], damp_ramp.inputs[0])
|
|
109
|
+
links.new(damp_ramp.outputs['Color'], bsdf.inputs['Roughness'])
|
|
110
|
+
bsdf.inputs['Specular IOR Level'].default_value = .22
|
|
111
|
+
return mat
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def palette():
|
|
115
|
+
definitions = {
|
|
116
|
+
'stone': (.39, .87, 0, 'stone'),
|
|
117
|
+
'stone_light': (.55, .86, 0, 'stone'),
|
|
118
|
+
'brick': (.19, .92, 0, 'brick'),
|
|
119
|
+
'brick_light': (.47, .87, 0, 'brick'),
|
|
120
|
+
'plaster': (.52, .9, 0, 'stone'),
|
|
121
|
+
'reveal': (.045, .86, 0, None),
|
|
122
|
+
'glass': (.065, .18, .22, None),
|
|
123
|
+
'glass_gray': (.15, .25, .15, None),
|
|
124
|
+
'sash': (.36, .77, 0, 'stone'),
|
|
125
|
+
'curtain': (.55, .95, 0, 'cloth'),
|
|
126
|
+
'road': (.17, .76, 0, 'paving'),
|
|
127
|
+
'sidewalk': (.34, .92, 0, 'stone'),
|
|
128
|
+
'iron': (.026, .59, .24, 'stone'),
|
|
129
|
+
'rail': (.35, .22, .75, None),
|
|
130
|
+
'roof': (.16, .65, .15, 'stone'),
|
|
131
|
+
'paint': (.11, .72, 0, 'stone'),
|
|
132
|
+
'cream': (.71, .8, 0, 'cloth'),
|
|
133
|
+
'canvas': (.39, .91, 0, 'cloth'),
|
|
134
|
+
'wood': (.085, .86, 0, 'stone'),
|
|
135
|
+
'leather': (.045, .56, 0, 'cloth'),
|
|
136
|
+
'coat': (.055, .94, 0, 'cloth'),
|
|
137
|
+
'coat_gray': (.2, .96, 0, 'cloth'),
|
|
138
|
+
'skin': (.39, .76, 0, None),
|
|
139
|
+
'horse': (.11, .77, 0, None),
|
|
140
|
+
'horse_light': (.24, .81, 0, None),
|
|
141
|
+
'white': (.8, .9, 0, None),
|
|
142
|
+
}
|
|
143
|
+
return {key: surface(key.replace('_', ' '), *values) for key, values in definitions.items()}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def arch(batch, material, x, y, spring, width, rise, depth, facing='side'):
|
|
147
|
+
"""Dark arched opening on a facade plane, with rectangular lower part."""
|
|
148
|
+
points = [(math.cos(t * math.pi / 16) * width / 2,
|
|
149
|
+
math.sin(t * math.pi / 16) * rise) for t in range(17)]
|
|
150
|
+
if facing == 'front':
|
|
151
|
+
vertices = [(x + a, y, spring + z) for a, z in points]
|
|
152
|
+
else:
|
|
153
|
+
vertices = [(x, y + a, spring + z) for a, z in points]
|
|
154
|
+
batch.mesh(material, vertices, [tuple(range(len(vertices)))])
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def window(batch, p, face, direction, y, z, width, height, rng, arched=False):
|
|
158
|
+
fx = face + direction * .045
|
|
159
|
+
# The glazing is in front of the solid backing mass; the outer jambs
|
|
160
|
+
# project farther. This keeps the inset visible without costly booleans.
|
|
161
|
+
batch.box(p['reveal'], (fx, y, z), (.055, width + .22, height + .2))
|
|
162
|
+
glass = p['glass'] if rng.random() < .68 else p['glass_gray']
|
|
163
|
+
batch.box(glass, (fx + direction * .038, y, z), (.026, width, height))
|
|
164
|
+
if rng.random() < .32:
|
|
165
|
+
curtain_height = height * rng.uniform(.19, .48)
|
|
166
|
+
batch.box(p['curtain'], (fx + direction * .058, y, z + (height - curtain_height) / 2),
|
|
167
|
+
(.014, width - .05, curtain_height))
|
|
168
|
+
for offset in (-width / 2 - .11, width / 2 + .11):
|
|
169
|
+
batch.box(p['stone_light'], (fx + direction * .1, y + offset, z), (.17, .13, height + .32))
|
|
170
|
+
batch.box(p['sash'], (fx + direction * .11, y, z), (.06, .055, height))
|
|
171
|
+
batch.box(p['sash'], (fx + direction * .11, y, z + .06), (.07, width, .055))
|
|
172
|
+
batch.box(p['stone_light'], (fx + direction * .15, y, z - height / 2 - .16),
|
|
173
|
+
(.39, width + .49, .14))
|
|
174
|
+
if arched:
|
|
175
|
+
arch(batch, p['reveal'], fx + direction * .01, y, z + height / 2,
|
|
176
|
+
width + .1, width / 2, .02)
|
|
177
|
+
# Individually laid voussoirs give a semicircular masonry silhouette.
|
|
178
|
+
for i in range(11):
|
|
179
|
+
angle = math.pi * (i + .5) / 11
|
|
180
|
+
by = y + math.cos(angle) * (width / 2 + .16)
|
|
181
|
+
bz = z + height / 2 + math.sin(angle) * (width / 2 + .16)
|
|
182
|
+
batch.box(p['stone_light'], (fx + direction * .1, by, bz),
|
|
183
|
+
(.18, .24, .19), rotation=(angle - math.pi / 2, 0, 0))
|
|
184
|
+
else:
|
|
185
|
+
batch.box(p['stone_light'], (fx + direction * .09, y, z + height / 2 + .2),
|
|
186
|
+
(.22, width + .48, .16))
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def awning(batch, p, face, direction, y, width):
|
|
190
|
+
tip = face + direction * 1.7
|
|
191
|
+
batch.mesh(p['canvas'], [(face, y - width / 2, 4.0), (face, y + width / 2, 4.0),
|
|
192
|
+
(tip, y + width / 2, 3.25), (tip, y - width / 2, 3.25)],
|
|
193
|
+
[(0, 1, 2, 3)])
|
|
194
|
+
for i in range(max(3, int(width / .28))):
|
|
195
|
+
by = y - width / 2 + .14 + i * .28
|
|
196
|
+
batch.box(p['canvas'], (tip, by, 3.12), (.055, .28, .28))
|
|
197
|
+
batch.ellipsoid(p['canvas'], (tip, by, 2.99), (.035, .139, .055), 8, 4)
|
|
198
|
+
for by in (y - width / 2 + .04, y + width / 2 - .04):
|
|
199
|
+
batch.beam(p['iron'], (face, by, 3.0), (tip, by, 3.24), .02)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def oriel(batch, p, face, direction, y, z, rng):
|
|
203
|
+
"""Faceted projecting sash bay with dark angled cheek windows."""
|
|
204
|
+
profile = [(0, -1.04), (.33, -1.04), (.66, -.71),
|
|
205
|
+
(.66, .71), (.33, 1.04), (0, 1.04)]
|
|
206
|
+
vertices = [(face + direction * out, y + along, z + dz)
|
|
207
|
+
for dz in (-1.56, 1.56) for out, along in profile]
|
|
208
|
+
n = len(profile)
|
|
209
|
+
faces = [tuple(reversed(range(n))), tuple(range(n, n * 2))]
|
|
210
|
+
faces += [(i, (i + 1) % n, (i + 1) % n + n, i + n) for i in range(n)]
|
|
211
|
+
batch.mesh(p['stone'], vertices, faces)
|
|
212
|
+
window(batch, p, face + direction * .67, direction, y, z, 1.03, 2.02, rng)
|
|
213
|
+
for side in (-1, 1):
|
|
214
|
+
cheek = [(face + direction * .36, y + side * 1.025, z - .98),
|
|
215
|
+
(face + direction * .645, y + side * .741, z - .98),
|
|
216
|
+
(face + direction * .645, y + side * .741, z + .98),
|
|
217
|
+
(face + direction * .36, y + side * 1.025, z + .98)]
|
|
218
|
+
batch.mesh(p['glass'], cheek, [(0, 1, 2, 3)])
|
|
219
|
+
for dz in (-1.51, 1.52):
|
|
220
|
+
batch.box(p['stone_light'], (face + direction * .34, y, z + dz), (.88, 2.28, .16))
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def facade(batch, p, rng, side, parcel, index):
|
|
224
|
+
y, width, height, bays, material, roof = parcel
|
|
225
|
+
face = side * (data.ROAD_WIDTH / 2 + data.SIDEWALK_WIDTH)
|
|
226
|
+
direction = -side
|
|
227
|
+
depth = rng.uniform(14, 21)
|
|
228
|
+
center_x = face + side * depth / 2
|
|
229
|
+
batch.box(p[material], (center_x, y, height / 2), (depth, width, height))
|
|
230
|
+
# Ground-floor pilasters, individual storefronts, and transom divisions.
|
|
231
|
+
spacing = width / bays
|
|
232
|
+
for bay in range(bays):
|
|
233
|
+
by = y - width / 2 + (bay + .5) * spacing
|
|
234
|
+
batch.box(p['reveal'], (face + direction * .045, by, 2.0), (.08, spacing - .31, 3.52))
|
|
235
|
+
batch.box(p['glass'], (face + direction * .093, by, 1.92), (.04, spacing - .55, 3.2))
|
|
236
|
+
for z in (.53, 2.78, 3.55):
|
|
237
|
+
batch.box(p['wood'], (face + direction * .13, by, z), (.10, spacing - .25, .12))
|
|
238
|
+
batch.box(p['stone_light'], (face + direction * .14, by - spacing / 2, 2.06), (.31, .24, 4.12))
|
|
239
|
+
if (bay + index) % 3 == 0 and y > 75:
|
|
240
|
+
awning(batch, p, face, direction, by, spacing - .18)
|
|
241
|
+
for z, protrusion, thick in ((4.25, .25, .24), (4.55, .31, .13),
|
|
242
|
+
(height - .5, .31, .2), (height, .58, .24),
|
|
243
|
+
(height + .24, .71, .18)):
|
|
244
|
+
batch.box(p['stone_light'], (face + direction * protrusion / 2, y, z),
|
|
245
|
+
(protrusion, width + .22, thick))
|
|
246
|
+
rows = max(3, int((height - 5.7) / 3.45))
|
|
247
|
+
for row in range(rows):
|
|
248
|
+
z = 6.5 + row * (height - 7.6) / max(rows - 1, 1)
|
|
249
|
+
for bay in range(bays):
|
|
250
|
+
by = y - width / 2 + (bay + .5) * spacing
|
|
251
|
+
if material in ('stone', 'plaster') and bay % 3 == 1 and y > 90:
|
|
252
|
+
oriel(batch, p, face, direction, by, z, rng)
|
|
253
|
+
else:
|
|
254
|
+
window(batch, p, face, direction, by, z, min(1.48, spacing * .51),
|
|
255
|
+
1.95 if height < 28 else 2.1, rng,
|
|
256
|
+
arched=roof == 'pediment' and row == rows - 1)
|
|
257
|
+
if row in (0, rows - 2):
|
|
258
|
+
batch.box(p['stone_light'], (face + direction * .1, y, z - 1.36),
|
|
259
|
+
(.25, width, .12))
|
|
260
|
+
# Deep cornice brackets, parapets, corner quoins and drain pipes break
|
|
261
|
+
# repetition at both architectural and street scales.
|
|
262
|
+
for i in range(max(1, int(width / .72))):
|
|
263
|
+
by = y - width / 2 + .35 + i * .72
|
|
264
|
+
batch.box(p['stone_light'], (face + direction * .2, by, height - .17), (.6, .17, .34))
|
|
265
|
+
batch.box(p[material], (face - direction * .14, y, height + .58), (.30, width, .8))
|
|
266
|
+
for by in (y - width / 2 + .22, y + width / 2 - .22):
|
|
267
|
+
for z in range(5, int(height), 2):
|
|
268
|
+
batch.box(p['stone_light'], (face + direction * .08, by, z), (.20, .58, .45))
|
|
269
|
+
batch.beam(p['iron'], (face + direction * .12, y + width / 2 - .8, .3),
|
|
270
|
+
(face + direction * .12, y + width / 2 - .8, height), .045)
|
|
271
|
+
if roof == 'mansard':
|
|
272
|
+
batch.box(p['roof'], (center_x, y, height + 1.0), (depth, width, 1.7))
|
|
273
|
+
for bay in range(bays):
|
|
274
|
+
by = y - width / 2 + (bay + .5) * spacing
|
|
275
|
+
batch.box(p['stone_light'], (face + side * .8, by, height + 1.1), (1.5, 1.25, 1.45))
|
|
276
|
+
batch.box(p['glass'], (face + side * .04, by, height + 1.1), (.03, .73, 1.04))
|
|
277
|
+
batch.mesh(p['roof'], [(face - direction * .03, by - .79, height + 1.76),
|
|
278
|
+
(face - direction * .03, by + .79, height + 1.76),
|
|
279
|
+
(face - direction * .03, by, height + 2.39)], [(0, 1, 2)])
|
|
280
|
+
if roof == 'pediment':
|
|
281
|
+
batch.mesh(p['stone_light'], [(face + direction * .35, y - width * .23, height + .58),
|
|
282
|
+
(face + direction * .35, y + width * .23, height + .58),
|
|
283
|
+
(face + direction * .35, y, height + 3.1)], [(0, 1, 2)])
|
|
284
|
+
for chimney in range(2):
|
|
285
|
+
batch.box(p['brick'], (center_x + side * chimney * 3, y - width * .28, height + 1.2), (1, 1.3, 2.4))
|
|
286
|
+
if y > 90:
|
|
287
|
+
# Invented descriptive businesses; these are never asserted to be
|
|
288
|
+
# occupants of a particular historical parcel.
|
|
289
|
+
words = ['TEA & COFFEE', 'WHOLESALE DRY GOODS', 'HARDWARE', 'WINES & SPIRITS', 'BOOT & SHOE CO.']
|
|
290
|
+
batch.box(p['paint'], (face + direction * .2, y, 4.92), (.22, width * .83, .75))
|
|
291
|
+
text_label('Authored shop lettering', words[index % len(words)],
|
|
292
|
+
(face + direction * .34, y, 4.70), (direction, 0, 0), .46, p['cream'])
|
|
293
|
+
if index in (2, 4):
|
|
294
|
+
# An authored wall advertisement interrupts a large exposed party
|
|
295
|
+
# wall. Its generic wording is not attributed to an actual occupant.
|
|
296
|
+
batch.box(p['paint'], (center_x, y + width / 2 + .035, height * .73),
|
|
297
|
+
(depth * .76, .06, height * .20))
|
|
298
|
+
text_label('Authored painted wall advertisement', 'WHOLESALE\nTEA & COFFEE',
|
|
299
|
+
(center_x, y + width / 2 + .079, height * .70), (0, 1, 0),
|
|
300
|
+
.80, p['cream'])
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def ferry(batch, p):
|
|
304
|
+
y = data.FERRY_Y
|
|
305
|
+
batch.box(p['stone'], (0, y - 9, 10.1), (86, 18, 20.2))
|
|
306
|
+
for i in range(19):
|
|
307
|
+
x = (i - 9) * 4.24
|
|
308
|
+
batch.box(p['reveal'], (x, y + .05, 3.05), (2.98, .10, 6.1))
|
|
309
|
+
arch(batch, p['reveal'], x, y + .11, 6.08, 2.98, 1.49, .1, 'front')
|
|
310
|
+
for j in range(12):
|
|
311
|
+
angle = math.pi * (j + .5) / 12
|
|
312
|
+
batch.box(p['stone_light'], (x + math.cos(angle) * 1.66, y + .19,
|
|
313
|
+
6.08 + math.sin(angle) * 1.66),
|
|
314
|
+
(.39, .32, .31), rotation=(0, math.pi / 2 - angle, 0))
|
|
315
|
+
for offset in (-1.68, 1.68):
|
|
316
|
+
batch.box(p['stone_light'], (x + offset, y + .20, 3.07), (.3, .43, 6.14))
|
|
317
|
+
for z in (11.3, 15.4):
|
|
318
|
+
batch.box(p['reveal'], (x, y + .06, z), (1.82, .12, 2.64))
|
|
319
|
+
batch.box(p['sash'], (x, y + .14, z), (.075, .09, 2.64))
|
|
320
|
+
batch.box(p['sash'], (x, y + .14, z + .15), (1.83, .09, .07))
|
|
321
|
+
batch.box(p['stone_light'], (x, y + .28, z - 1.43), (2.17, .59, .17))
|
|
322
|
+
for z, protrusion in ((8.3, .3), (18.0, .3), (19.6, .62), (20.3, .8)):
|
|
323
|
+
batch.box(p['stone_light'], (0, y + protrusion / 2, z), (87, protrusion, .31))
|
|
324
|
+
# The recognizable campanile establishes the destination throughout.
|
|
325
|
+
batch.box(p['stone'], (0, y - 4.7, 31.1), (11.1, 11.3, 49.8))
|
|
326
|
+
for x in (-5.42, 5.42):
|
|
327
|
+
batch.box(p['stone_light'], (x, y + 1.04, 32), (.58, .53, 45))
|
|
328
|
+
for z in (22.0, 31.0, 42.0):
|
|
329
|
+
for x in (-2.72, 0, 2.72):
|
|
330
|
+
batch.box(p['reveal'], (x, y + 1.03, z), (1.25, .1, 4.32))
|
|
331
|
+
arch(batch, p['reveal'], x, y + 1.11, z + 2.16, 1.25, .625, .1, 'front')
|
|
332
|
+
for bar in range(9):
|
|
333
|
+
batch.box(p['sash'], (x, y + 1.18, z - 1.88 + bar * .45), (1.29, .09, .12))
|
|
334
|
+
for z, w in ((46.6, 12.0), (47.2, 12.8), (56.0, 12.0), (56.6, 13.0)):
|
|
335
|
+
batch.box(p['stone_light'], (0, y - 4.6, z), (w, 12.4, .44))
|
|
336
|
+
# Clock face with radial hour marks and hands at an authored 3:17.
|
|
337
|
+
batch.cone(p['iron'], (0, y + 1.21, 51.4), 2.34, .18, segments=64, rotation=(-math.pi / 2, 0, 0))
|
|
338
|
+
batch.cone(p['cream'], (0, y + 1.33, 51.4), 2.12, .14, segments=64, rotation=(-math.pi / 2, 0, 0))
|
|
339
|
+
for tick in range(60):
|
|
340
|
+
a = math.tau * tick / 60
|
|
341
|
+
r1 = 1.68 if tick % 5 == 0 else 1.88
|
|
342
|
+
batch.beam(p['iron'], (math.sin(a) * r1, y + 1.44, 51.4 + math.cos(a) * r1),
|
|
343
|
+
(math.sin(a) * 2.01, y + 1.44, 51.4 + math.cos(a) * 2.01), .035 if tick % 5 == 0 else .012, 6)
|
|
344
|
+
for fraction, length, radius in ((17 / 60, 1.65, .055), ((3 + 17 / 60) / 12, 1.08, .08)):
|
|
345
|
+
a = fraction * math.tau
|
|
346
|
+
batch.beam(p['iron'], (0, y + 1.48, 51.4),
|
|
347
|
+
(math.sin(a) * length, y + 1.48, 51.4 + math.cos(a) * length), radius)
|
|
348
|
+
batch.box(p['reveal'], (0, y - 4.6, 60.2), (9.4, 9.4, 6.7))
|
|
349
|
+
for x in (-4.9, -1.6, 1.6, 4.9):
|
|
350
|
+
batch.cone(p['stone_light'], (x, y + .2, 60), .35, 6.8, segments=12)
|
|
351
|
+
batch.box(p['stone_light'], (0, y - 4.6, 63.6), (12.0, 12.0, .55))
|
|
352
|
+
batch.mesh(p['roof'], [(-5.9, y + 1.3, 63.9), (5.9, y + 1.3, 63.9),
|
|
353
|
+
(5.9, y - 10.5, 63.9), (-5.9, y - 10.5, 63.9),
|
|
354
|
+
(0, y - 4.6, 71.7)], [(0, 1, 4), (1, 2, 4), (2, 3, 4), (3, 0, 4)])
|
|
355
|
+
batch.cone(p['iron'], (0, y - 4.6, 72.6), .14, 2.0, segments=10)
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def street(batch, p, rng):
|
|
359
|
+
batch.box(p['road'], (0, 106, -.12), (26, 260, .2))
|
|
360
|
+
for side in (-1, 1):
|
|
361
|
+
sx = side * (13 + data.SIDEWALK_WIDTH / 2)
|
|
362
|
+
batch.box(p['sidewalk'], (sx, 110, .10), (data.SIDEWALK_WIDTH, 260, .22))
|
|
363
|
+
# Individual curb blocks leave subtle seams and uneven catches of light.
|
|
364
|
+
for index in range(166):
|
|
365
|
+
batch.box(p['stone_light'], (side * 13.05, -17.4 + index * 1.55, .17),
|
|
366
|
+
(.32, 1.50, .34 + rng.uniform(-.02, .02)))
|
|
367
|
+
for y in range(-10, 240, 4):
|
|
368
|
+
batch.box(p['reveal'], (sx, y, .215), (5.25, .013, .006))
|
|
369
|
+
# Cable-car slots and rails; no overhead traction catenary before the quake.
|
|
370
|
+
for x in (-2.1, 2.1):
|
|
371
|
+
batch.box(p['iron'], (x, 111, .018), (.074, 258, .019))
|
|
372
|
+
for dx in (-.71, .71):
|
|
373
|
+
batch.box(p['rail'], (x + dx, 111, .025), (.067, 258, .026))
|
|
374
|
+
batch.box(p['iron'], (x + dx + .045, 111, .02), (.034, 258, .02))
|
|
375
|
+
for side in (-1, 1):
|
|
376
|
+
for y in (31, 80, 127, 173, 219):
|
|
377
|
+
x = side * 13.7
|
|
378
|
+
# Modest period-typical utility posts, not falsely named landmarks.
|
|
379
|
+
batch.cone(p['iron'], (x, y, 2.95), .065, 5.9, segments=10)
|
|
380
|
+
batch.cone(p['iron'], (x, y, .42), .17, .82, top=.10, segments=10)
|
|
381
|
+
batch.cone(p['iron'], (x, y, 5.95), .22, .18, top=.1, segments=10)
|
|
382
|
+
batch.box(p['cream'], (x, y, 5.66), (.27, .27, .45))
|
|
383
|
+
for dx in (-.16, .16):
|
|
384
|
+
batch.box(p['iron'], (x + dx, y, 5.66), (.025, .33, .51))
|
|
385
|
+
# Small goods along shops add a human scale without blocking foot traffic.
|
|
386
|
+
for side, y in ((-1, 140), (1, 162), (1, 90), (-1, 201)):
|
|
387
|
+
for i in range(3):
|
|
388
|
+
x = side * (17.0 - i * .62)
|
|
389
|
+
batch.box(p['wood'], (x, y + i * .75, .6), (.61, .70, 1.0))
|
|
390
|
+
for z in (.31, .56, .81, 1.06):
|
|
391
|
+
batch.box(p['iron'], (x - side * .315, y + i * .75, z), (.012, .70, .024))
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def person(p, name, position, rng, heading=0, dress=False, scale=1.0):
|
|
395
|
+
rig = empty(name)
|
|
396
|
+
rig.location = position
|
|
397
|
+
rig.rotation_euler.z = heading
|
|
398
|
+
rig.scale = (scale,) * 3
|
|
399
|
+
body = Batch(name + ' torso')
|
|
400
|
+
coat = p['coat_gray'] if rng.random() < .32 else p['coat']
|
|
401
|
+
if dress:
|
|
402
|
+
body.ellipsoid(coat, (0, 0, 1.14), (.24, .145, .34), 20, 12)
|
|
403
|
+
body.cone(coat, (0, 0, .61), .37, .8, top=.19, segments=24, smooth=True)
|
|
404
|
+
body.ellipsoid(coat, (0, .015, .94), (.23, .16, .17))
|
|
405
|
+
else:
|
|
406
|
+
# A continuous tailored coat encloses shoulders, waist and hem. Small
|
|
407
|
+
# radial folds read as cloth without a stack of disconnected shapes.
|
|
408
|
+
vertices, faces = [], []
|
|
409
|
+
sections = [(.60, .245, .16), (.94, .20, .133),
|
|
410
|
+
(1.24, .25, .15), (1.38, .22, .123), (1.43, .11, .09)]
|
|
411
|
+
for z, width, depth in sections:
|
|
412
|
+
for j in range(24):
|
|
413
|
+
angle = math.tau * j / 24
|
|
414
|
+
fold = 1 + .022 * math.cos(angle * 6)
|
|
415
|
+
vertices.append((math.cos(angle) * width * fold,
|
|
416
|
+
math.sin(angle) * depth * fold, z))
|
|
417
|
+
for section in range(len(sections) - 1):
|
|
418
|
+
for j in range(24):
|
|
419
|
+
k = (j + 1) % 24
|
|
420
|
+
faces.append((section * 24 + j, (section + 1) * 24 + j,
|
|
421
|
+
(section + 1) * 24 + k, section * 24 + k))
|
|
422
|
+
body.mesh(coat, vertices, faces, smooth=True)
|
|
423
|
+
body.box(p['white'], (0, -.129, 1.37), (.075, .012, .095))
|
|
424
|
+
body.box(p['iron'], (0, -.141, 1.34), (.025, .012, .095))
|
|
425
|
+
body.cone(p['skin'], (0, 0, 1.52), .064, .14, segments=12)
|
|
426
|
+
body.ellipsoid(p['skin'], (0, -.008, 1.65), (.112, .098, .147), 16, 12)
|
|
427
|
+
body.ellipsoid(p['skin'], (0, -.098, 1.65), (.024, .043, .033), 10, 6)
|
|
428
|
+
body.ellipsoid(p['iron'], (0, .015, 1.74), (.116, .097, .084))
|
|
429
|
+
# Brim, crown and band produce an identifiable silhouette at street scale.
|
|
430
|
+
hat_r = .23 if dress else .165
|
|
431
|
+
body.cone(p['iron'], (0, 0, 1.79), hat_r, .028, top=hat_r, segments=24)
|
|
432
|
+
body.cone(p['iron'], (0, 0, 1.87), .108, .14, top=.10, segments=20)
|
|
433
|
+
if dress:
|
|
434
|
+
body.ellipsoid(p['cream'], (.11, 0, 1.89), (.055, .07, .095), 10, 6)
|
|
435
|
+
body.finish(rig)
|
|
436
|
+
limbs = []
|
|
437
|
+
for side in (-1, 1):
|
|
438
|
+
leg = empty(name + ' leg', rig)
|
|
439
|
+
leg.location = (side * .105, 0, .85)
|
|
440
|
+
geometry = Batch(name + ' trousers')
|
|
441
|
+
geometry.cone(coat, (0, 0, -.42), .072, .72, top=.091, segments=16, smooth=True)
|
|
442
|
+
geometry.ellipsoid(p['iron'], (0, -.048, -.77), (.083, .137, .057), 16, 8)
|
|
443
|
+
geometry.finish(leg)
|
|
444
|
+
arm = empty(name + ' arm', rig)
|
|
445
|
+
arm.location = (side * .20, 0, 1.37)
|
|
446
|
+
geometry = Batch(name + ' sleeve')
|
|
447
|
+
geometry.beam(coat, (0, 0, 0), (side * .034, -.025, -.48), .073, 14)
|
|
448
|
+
geometry.ellipsoid(p['skin'], (side * .017, -.026, -.52), (.048, .044, .069), 10, 6)
|
|
449
|
+
geometry.finish(arm)
|
|
450
|
+
limbs.append((leg, arm, side))
|
|
451
|
+
return rig, limbs
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def animate_person(rig, limbs, start, velocity, fps, frames, phase, walking=True):
|
|
455
|
+
for frame in range(1, frames + 1):
|
|
456
|
+
t = (frame - 1) / fps
|
|
457
|
+
stride = math.sin(t * math.tau * 1.35 + phase) if walking else 0
|
|
458
|
+
rig.location = (start[0] + velocity[0] * t, start[1] + velocity[1] * t,
|
|
459
|
+
start[2] + (.016 * abs(stride) if walking else 0))
|
|
460
|
+
rig.keyframe_insert('location', frame=frame)
|
|
461
|
+
for leg, arm, side in limbs:
|
|
462
|
+
leg.rotation_euler.x = stride * .38 * side
|
|
463
|
+
arm.rotation_euler.x = -stride * .31 * side
|
|
464
|
+
leg.keyframe_insert('rotation_euler', frame=frame)
|
|
465
|
+
arm.keyframe_insert('rotation_euler', frame=frame)
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def cable_car(p, name, position, heading=0):
|
|
469
|
+
rig = empty(name)
|
|
470
|
+
rig.location = position
|
|
471
|
+
rig.rotation_euler.z = heading
|
|
472
|
+
batch = Batch(name)
|
|
473
|
+
for x in (-.74, .74):
|
|
474
|
+
for y in (-2.16, 2.16):
|
|
475
|
+
batch.wheel(p['iron'], (x, y, .48), .43, .10, 10)
|
|
476
|
+
batch.box(p['wood'], (0, 0, .92), (2.2, 7.05, .25))
|
|
477
|
+
batch.box(p['paint'], (0, .90, 1.75), (2.12, 3.92, 1.24))
|
|
478
|
+
# A dark open saloon with visible narrow wood frames, not an opaque box
|
|
479
|
+
# with a window decal hidden inside it.
|
|
480
|
+
batch.box(p['reveal'], (0, .85, 2.30), (1.97, 3.79, 1.10))
|
|
481
|
+
for side in (-1, 1):
|
|
482
|
+
for y in (-.8, .10, 1.0, 1.9, 2.8):
|
|
483
|
+
batch.box(p['cream'], (side * 1.075, y, 2.27), (.12, .085, 1.2))
|
|
484
|
+
for z in (1.7, 2.82):
|
|
485
|
+
batch.box(p['cream'], (side * 1.075, .95, z), (.14, 3.91, .1))
|
|
486
|
+
batch.box(p['wood'], (side * .77, -2.03, 1.37), (.45, 2.25, .14))
|
|
487
|
+
batch.box(p['wood'], (side * .92, -2.03, 1.7), (.09, 2.25, .62))
|
|
488
|
+
for y in (-3.39, -2.2, -1.12):
|
|
489
|
+
batch.beam(p['cream'], (side * 1.01, y, 1.0), (side * 1.01, y, 3.08), .046)
|
|
490
|
+
batch.box(p['wood'], (side * 1.13, -.3, .71), (.36, 5.86, .14))
|
|
491
|
+
batch.box(p['cream'], (0, -.11, 3.04), (2.45, 7.45, .18))
|
|
492
|
+
batch.box(p['roof'], (0, .30, 3.21), (1.76, 5.95, .20))
|
|
493
|
+
for i in range(14):
|
|
494
|
+
batch.box(p['iron'], (0, -2.41 + i * .41, 3.30), (1.81, .04, .16))
|
|
495
|
+
batch.box(p['paint'], (0, -3.53, 1.40), (2.08, .12, .79))
|
|
496
|
+
batch.cone(p['cream'], (0, -3.62, 1.73), .18, .12, segments=24, rotation=(math.pi / 2, 0, 0))
|
|
497
|
+
batch.box(p['iron'], (0, -3.7, .64), (1.62, .24, .16))
|
|
498
|
+
batch.finish(rig)
|
|
499
|
+
text_label(name + ' number', '24', (0, -3.61, 1.01), (0, -1, 0), .24, p['cream'], rig)
|
|
500
|
+
return rig
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def wagon(p, rng, name, position, heading=0):
|
|
504
|
+
rig = empty(name)
|
|
505
|
+
rig.location = position
|
|
506
|
+
rig.rotation_euler.z = heading
|
|
507
|
+
b = Batch(name)
|
|
508
|
+
for x in (-.82, .82):
|
|
509
|
+
for y, radius in ((-1.05, .52), (1.13, .65)):
|
|
510
|
+
b.wheel(p['wood'], (x, y, radius), radius, .095, 14)
|
|
511
|
+
b.box(p['wood'], (0, 0, 1.02), (1.58, 3.15, .14))
|
|
512
|
+
for side in (-1, 1):
|
|
513
|
+
for z in (1.19, 1.41, 1.63):
|
|
514
|
+
b.box(p['wood'], (side * .76, 0, z), (.085, 3.04, .18))
|
|
515
|
+
for y in (-1.31, 0, 1.31):
|
|
516
|
+
b.box(p['iron'], (side * .812, y, 1.39), (.032, .075, .72))
|
|
517
|
+
for y in (-1.51, 1.51):
|
|
518
|
+
b.box(p['wood'], (0, y, 1.4), (1.52, .08, .69))
|
|
519
|
+
for i in range(4):
|
|
520
|
+
b.box(p['canvas'], ((i % 2 - .5) * .7, (i // 2 - .5) * 1.25, 1.68), (.62, .92, .52))
|
|
521
|
+
# Anatomical masses overlap smoothly. Legs are articulated in a separate
|
|
522
|
+
# group, and the muzzle is narrower than the head rather than a cube.
|
|
523
|
+
horse = p['horse_light'] if rng.random() < .3 else p['horse']
|
|
524
|
+
hy = -3.22
|
|
525
|
+
b.ellipsoid(horse, (0, hy, 1.51), (.43, .91, .53), 24, 14)
|
|
526
|
+
b.ellipsoid(horse, (0, hy - .58, 1.50), (.39, .4, .58), 20, 12)
|
|
527
|
+
b.ellipsoid(horse, (0, hy + .64, 1.56), (.39, .4, .45), 20, 12)
|
|
528
|
+
b.beam(horse, (0, hy - .67, 1.56), (0, hy - 1.06, 2.31), .25, 16)
|
|
529
|
+
b.ellipsoid(horse, (0, hy - 1.10, 2.28), (.21, .27, .27), 20, 12)
|
|
530
|
+
b.ellipsoid(horse, (0, hy - 1.34, 2.13), (.15, .26, .17), 18, 10)
|
|
531
|
+
for side in (-1, 1):
|
|
532
|
+
b.cone(horse, (side * .105, hy - 1.02, 2.58), .066, .29, top=.01, segments=10)
|
|
533
|
+
b.ellipsoid(p['iron'], (side * .191, hy - 1.19, 2.31), (.016, .036, .025), 10, 6)
|
|
534
|
+
b.beam(p['leather'], (side * .33, hy - .61, 1.97), (side * .34, hy - .56, 1.1), .051)
|
|
535
|
+
b.beam(p['wood'], (side * .56, -1.56, 1.05), (side * .54, hy - .51, 1.20), .04)
|
|
536
|
+
b.beam(p['leather'], (side * .08, hy - 1.48, 2.11), (side * .33, -.85, 1.65), .016, 6)
|
|
537
|
+
b.beam(p['leather'], (0, hy - .61, 2.02), (0, hy - 1.02, 2.54), .071, 10)
|
|
538
|
+
b.beam(p['leather'], (0, hy + .82, 1.67), (0, hy + 1.16, .77), .079, 12)
|
|
539
|
+
b.finish(rig)
|
|
540
|
+
legs = []
|
|
541
|
+
for side in (-1, 1):
|
|
542
|
+
for front in (False, True):
|
|
543
|
+
leg = empty(name + ' horse leg', rig)
|
|
544
|
+
leg.location = (side * .28, hy + (-.58 if front else .64), 1.35)
|
|
545
|
+
geometry = Batch(name + ' horse leg')
|
|
546
|
+
geometry.beam(horse, (0, 0, 0), (0, .06 if front else -.08, -.61), .069, 12)
|
|
547
|
+
geometry.ellipsoid(horse, (0, .05, -.61), (.079, .083, .087), 12, 8)
|
|
548
|
+
geometry.beam(horse, (0, .05, -.61), (0, -.018, -1.16), .043, 10)
|
|
549
|
+
geometry.box(p['iron'], (0, -.065, -1.23), (.14, .23, .15))
|
|
550
|
+
geometry.finish(leg)
|
|
551
|
+
legs.append((leg, side * (1 if front else -1)))
|
|
552
|
+
return rig, legs
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def automobile(p, name, position, heading=0):
|
|
556
|
+
rig = empty(name)
|
|
557
|
+
rig.location = position
|
|
558
|
+
rig.rotation_euler.z = heading
|
|
559
|
+
b = Batch(name)
|
|
560
|
+
for x in (-.82, .82):
|
|
561
|
+
for y in (-1.11, 1.1):
|
|
562
|
+
b.wheel(p['iron'], (x, y, .52), .50, .10, 12)
|
|
563
|
+
# Curved mudguards made as narrow segments.
|
|
564
|
+
for i in range(12):
|
|
565
|
+
angle = math.pi * (i + .5) / 12
|
|
566
|
+
b.box(p['paint'], (x, y + .60 * math.cos(angle), .52 + .60 * math.sin(angle)),
|
|
567
|
+
(.24, .18, .04), rotation=(angle - math.pi / 2, 0, 0))
|
|
568
|
+
b.box(p['paint'], (0, 0, .81), (1.53, 3.10, .22))
|
|
569
|
+
b.box(p['paint'], (0, -.94, 1.16), (1.04, 1.14, .51))
|
|
570
|
+
b.box(p['rail'], (0, -1.54, 1.12), (.91, .06, .49))
|
|
571
|
+
for x in (-.36, -.18, 0, .18, .36):
|
|
572
|
+
b.box(p['iron'], (x, -1.578, 1.12), (.032, .021, .4))
|
|
573
|
+
b.box(p['leather'], (0, .53, 1.09), (1.32, 1.01, .21))
|
|
574
|
+
b.box(p['leather'], (0, .98, 1.36), (1.37, .20, .61))
|
|
575
|
+
for x in (-.61, .61):
|
|
576
|
+
b.ellipsoid(p['rail'], (x, -1.39, 1.38), (.12, .16, .14), 16, 8)
|
|
577
|
+
b.beam(p['iron'], (x, .7, 1.08), (x, .74, 2.3), .021)
|
|
578
|
+
b.box(p['canvas'], (0, .59, 2.28), (1.46, 1.44, .069))
|
|
579
|
+
b.finish(rig)
|
|
580
|
+
return rig
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
def linear_animation(obj, start, velocity, fps, frames):
|
|
584
|
+
for frame in (1, max(2, frames)):
|
|
585
|
+
t = (frame - 1) / fps
|
|
586
|
+
obj.location = tuple(start[i] + velocity[i] * t for i in range(3))
|
|
587
|
+
obj.keyframe_insert('location', frame=frame)
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
def build(context):
|
|
591
|
+
parameters = context.get('parameters', {})
|
|
592
|
+
render = context.get('render') or {}
|
|
593
|
+
rate = render.get('frameRate') or {'numerator': data.CAPTURE_FPS, 'denominator': 1}
|
|
594
|
+
fps = float(rate['numerator']) / float(rate['denominator'])
|
|
595
|
+
frames = int(parameters.get('frames', round(data.DURATION_SECONDS * fps)))
|
|
596
|
+
if not 1 <= frames <= 480 or not 1 <= fps <= 60:
|
|
597
|
+
raise ValueError('Use 1..480 frames and a 1..60 fps job rate.')
|
|
598
|
+
seed = int(parameters.get('seed', 19060414))
|
|
599
|
+
rng = random.Random(seed)
|
|
600
|
+
bpy.ops.object.select_all(action='SELECT')
|
|
601
|
+
bpy.ops.object.delete(use_global=False)
|
|
602
|
+
scene = bpy.context.scene
|
|
603
|
+
scene.unit_settings.system = 'METRIC'
|
|
604
|
+
scene.unit_settings.scale_length = 1
|
|
605
|
+
p = palette()
|
|
606
|
+
architecture = Batch('Architecture and street')
|
|
607
|
+
street(architecture, p, rng)
|
|
608
|
+
ferry(architecture, p)
|
|
609
|
+
for side, parcels in data.PARCELS.items():
|
|
610
|
+
for index, parcel in enumerate(parcels):
|
|
611
|
+
facade(architecture, p, rng, side, parcel, index)
|
|
612
|
+
architecture.finish()
|
|
613
|
+
|
|
614
|
+
# Deep street activity, a near crossing, and independently moving traffic
|
|
615
|
+
# form the shot's changing occlusions. No figure is a cylinder-only token.
|
|
616
|
+
for i in range(35):
|
|
617
|
+
side = -1 if i % 2 else 1
|
|
618
|
+
start = (side * rng.uniform(14.1, 16.6), rng.uniform(15, 211), .21)
|
|
619
|
+
speed = rng.uniform(.45, .91) * (-1 if i % 3 else 1)
|
|
620
|
+
heading = 0 if speed < 0 else math.pi
|
|
621
|
+
human, limbs = person(p, 'Sidewalk walker %02d' % i, start, rng, heading,
|
|
622
|
+
dress=i % 5 == 0, scale=rng.uniform(.92, 1.06))
|
|
623
|
+
animate_person(human, limbs, start, (0, speed), fps, frames, rng.random() * math.tau)
|
|
624
|
+
for i, (x, y, speed) in enumerate(((-7.0, 120, 1.16), (8.5, 109, -.87),
|
|
625
|
+
(-6.5, 85, .98), (10.5, 58, -1.06))):
|
|
626
|
+
start = (x, y, .035)
|
|
627
|
+
heading = math.pi / 2 if speed > 0 else -math.pi / 2
|
|
628
|
+
human, limbs = person(p, 'Crossing pedestrian %02d' % i, start, rng, heading,
|
|
629
|
+
dress=i == 1)
|
|
630
|
+
animate_person(human, limbs, start, (speed, .06), fps, frames, i * 1.41)
|
|
631
|
+
car = cable_car(p, 'Departing cable car', (-2.1, 107, .035))
|
|
632
|
+
linear_animation(car, (-2.1, 107, .035), (0, -3.55, 0), fps, frames)
|
|
633
|
+
driver, _ = person(p, 'Departing gripsman', (0, -2.8, 1.05), rng)
|
|
634
|
+
driver.parent = car
|
|
635
|
+
car = cable_car(p, 'Oncoming cable car', (2.1, 109, .035), math.pi)
|
|
636
|
+
linear_animation(car, (2.1, 109, .035), (0, 3.7, 0), fps, frames)
|
|
637
|
+
driver, _ = person(p, 'Oncoming gripsman', (0, -2.8, 1.05), rng)
|
|
638
|
+
driver.parent = car
|
|
639
|
+
for i, (x, y, speed) in enumerate(((-8.0, 126, -1.7), (8.0, 81, 1.4), (-7.4, 51, -1.3))):
|
|
640
|
+
start = (x, y, .04)
|
|
641
|
+
wagon_rig, legs = wagon(p, rng, 'Delivery wagon %d' % i, start, 0 if speed < 0 else math.pi)
|
|
642
|
+
linear_animation(wagon_rig, start, (0, speed, 0), fps, frames)
|
|
643
|
+
driver, limbs = person(p, 'Wagon driver %d' % i, (0, -.88, 1.0), rng, scale=.93)
|
|
644
|
+
driver.parent = wagon_rig
|
|
645
|
+
for leg, arm, _ in limbs:
|
|
646
|
+
leg.rotation_euler.x = -1.1
|
|
647
|
+
arm.rotation_euler.x = -.9
|
|
648
|
+
for frame in range(1, frames + 1):
|
|
649
|
+
t = (frame - 1) / fps
|
|
650
|
+
for leg, offset in legs:
|
|
651
|
+
leg.rotation_euler.x = math.sin(t * math.tau * 1.2 + offset * math.pi / 2) * .32
|
|
652
|
+
leg.keyframe_insert('rotation_euler', frame=frame)
|
|
653
|
+
auto = automobile(p, 'Runabout crossing the middle distance', (-10, 71, .04), math.pi / 2)
|
|
654
|
+
linear_animation(auto, (-10, 71, .04), (2.2, .1, 0), fps, frames)
|
|
655
|
+
driver, limbs = person(p, 'Runabout driver', (.26, .40, .85), rng, scale=.86)
|
|
656
|
+
driver.parent = auto
|
|
657
|
+
for leg, arm, _ in limbs:
|
|
658
|
+
leg.rotation_euler.x = -.95
|
|
659
|
+
arm.rotation_euler.x = -.9
|
|
660
|
+
|
|
661
|
+
# Neutral broad daylight, contact shadows, and mild volumetric extinction
|
|
662
|
+
# establish depth before the separate film-transfer finishing operation.
|
|
663
|
+
scene.world = bpy.data.worlds.new('Pale April sky')
|
|
664
|
+
scene.world.use_nodes = True
|
|
665
|
+
world_nodes, world_links = scene.world.node_tree.nodes, scene.world.node_tree.links
|
|
666
|
+
background = world_nodes.get('Background')
|
|
667
|
+
background.inputs['Color'].default_value = gray(.70)
|
|
668
|
+
background.inputs['Strength'].default_value = .27
|
|
669
|
+
# The reference's bright sky and dark doorways require separate visible
|
|
670
|
+
# sky exposure and ambient fill. Raising fill to whiten the sky flattens
|
|
671
|
+
# every shadow in the street, which was the first proof's main failure.
|
|
672
|
+
visible_sky = world_nodes.new('ShaderNodeBackground')
|
|
673
|
+
visible_sky.inputs['Color'].default_value = gray(1)
|
|
674
|
+
visible_sky.inputs['Strength'].default_value = 3.2
|
|
675
|
+
light_path = world_nodes.new('ShaderNodeLightPath')
|
|
676
|
+
mix_sky = world_nodes.new('ShaderNodeMixShader')
|
|
677
|
+
world_links.new(light_path.outputs['Is Camera Ray'], mix_sky.inputs[0])
|
|
678
|
+
world_links.new(background.outputs[0], mix_sky.inputs[1])
|
|
679
|
+
world_links.new(visible_sky.outputs[0], mix_sky.inputs[2])
|
|
680
|
+
world_links.new(mix_sky.outputs[0], world_nodes.get('World Output').inputs['Surface'])
|
|
681
|
+
sun_data = bpy.data.lights.new('Soft oblique afternoon light', 'SUN')
|
|
682
|
+
sun_data.energy = 2.55
|
|
683
|
+
sun_data.angle = math.radians(1.6)
|
|
684
|
+
sun = bpy.data.objects.new(sun_data.name, sun_data)
|
|
685
|
+
scene.collection.objects.link(sun)
|
|
686
|
+
sun.rotation_euler = Vector((.74, .31, -.60)).to_track_quat('-Z', 'Y').to_euler()
|
|
687
|
+
fill_data = bpy.data.lights.new('Broad sky bounce', 'AREA')
|
|
688
|
+
fill_data.energy = 800
|
|
689
|
+
fill_data.shape = 'DISK'
|
|
690
|
+
fill_data.size = 55
|
|
691
|
+
fill = bpy.data.objects.new(fill_data.name, fill_data)
|
|
692
|
+
scene.collection.objects.link(fill)
|
|
693
|
+
fill.location = (-12, 130, 45)
|
|
694
|
+
fill.rotation_euler = (Vector((0, 100, 4)) - fill.location).to_track_quat('-Z', 'Y').to_euler()
|
|
695
|
+
mist = bpy.data.materials.new('Distant maritime haze')
|
|
696
|
+
mist.use_nodes = True
|
|
697
|
+
mist.node_tree.nodes.clear()
|
|
698
|
+
scatter = mist.node_tree.nodes.new('ShaderNodeVolumeScatter')
|
|
699
|
+
scatter.inputs['Color'].default_value = gray(.81)
|
|
700
|
+
scatter.inputs['Density'].default_value = .0014
|
|
701
|
+
scatter.inputs['Anisotropy'].default_value = .18
|
|
702
|
+
output = mist.node_tree.nodes.new('ShaderNodeOutputMaterial')
|
|
703
|
+
mist.node_tree.links.new(scatter.outputs[0], output.inputs['Volume'])
|
|
704
|
+
haze = Batch('Atmosphere')
|
|
705
|
+
# Fog starts at Y=80, beyond all foreground traffic. Surface blacks stay
|
|
706
|
+
# intact near the camera while the terminal separates in the distance.
|
|
707
|
+
haze.box(mist, (0, -15, 65), (450, 190, 170))
|
|
708
|
+
haze.finish()
|
|
709
|
+
|
|
710
|
+
camera_data = bpy.data.cameras.new('Cable-car camera')
|
|
711
|
+
camera_data.lens = 32
|
|
712
|
+
camera_data.sensor_width = 36
|
|
713
|
+
camera_data.sensor_fit = 'HORIZONTAL'
|
|
714
|
+
camera_data.clip_end = 550
|
|
715
|
+
camera_data.dof.use_dof = False
|
|
716
|
+
camera = bpy.data.objects.new(camera_data.name, camera_data)
|
|
717
|
+
scene.collection.objects.link(camera)
|
|
718
|
+
scene.camera = camera
|
|
719
|
+
for frame in range(1, frames + 1):
|
|
720
|
+
t = (frame - 1) / fps
|
|
721
|
+
# Mechanical vibration is deliberately subpixel to a few pixels. Film
|
|
722
|
+
# gate weave belongs to the later transfer, not this physical camera.
|
|
723
|
+
camera.location = (-2.1 + math.sin(t * 7.3) * .018,
|
|
724
|
+
185 - t * 4.4, 2.72 + math.sin(t * 9.1) * .012)
|
|
725
|
+
target = Vector((0, data.FERRY_Y, 12.5))
|
|
726
|
+
camera.rotation_euler = (target - camera.location).to_track_quat('-Z', 'Y').to_euler()
|
|
727
|
+
camera.keyframe_insert('location', frame=frame)
|
|
728
|
+
camera.keyframe_insert('rotation_euler', frame=frame)
|
|
729
|
+
scene.render.resolution_x = int(render.get('width', 1920))
|
|
730
|
+
scene.render.resolution_y = int(render.get('height', 1440))
|
|
731
|
+
scene.render.resolution_percentage = 100
|
|
732
|
+
scene.render.fps = int(round(fps))
|
|
733
|
+
scene.render.fps_base = scene.render.fps / fps
|
|
734
|
+
scene.render.image_settings.color_mode = 'RGB'
|
|
735
|
+
scene.render.film_transparent = False
|
|
736
|
+
scene.view_settings.view_transform = 'AgX'
|
|
737
|
+
scene.view_settings.look = 'AgX - Medium High Contrast'
|
|
738
|
+
scene.view_settings.exposure = -.10
|
|
739
|
+
scene.view_settings.gamma = 1
|
|
740
|
+
scene.frame_start, scene.frame_end = 1, frames
|
|
741
|
+
scene.frame_set(int(render.get('startFrame', 1)))
|
|
742
|
+
metadata = {
|
|
743
|
+
'kind': 'historical-film-study', 'schemaVersion': 1,
|
|
744
|
+
'interpretation': data.INTERPRETATION, 'sources': data.SOURCES,
|
|
745
|
+
'seed': seed, 'frames': frames, 'captureFps': fps,
|
|
746
|
+
'camera': {'lensMm': 32, 'heightM': 2.72, 'speedMetersPerSecond': 4.4,
|
|
747
|
+
'destination': 'Ferry Building interpretation', 'direction': '-Y'},
|
|
748
|
+
'renderIntent': {'width': 1920, 'height': 1440, 'renderer': 'eevee',
|
|
749
|
+
'samples': 64, 'nativeAspect': '4:3', 'viewTransform': 'AgX'},
|
|
750
|
+
'meshObjects': sum(obj.type == 'MESH' for obj in scene.objects),
|
|
751
|
+
'objects': len(scene.objects),
|
|
752
|
+
}
|
|
753
|
+
scene['historical_film_study'] = json.dumps(metadata, sort_keys=True)
|
|
754
|
+
working = context.get('workingRoot')
|
|
755
|
+
if working:
|
|
756
|
+
path = Path(working) / 'creative-metadata.json'
|
|
757
|
+
with path.open('x') as stream:
|
|
758
|
+
json.dump(metadata, stream, indent=2)
|
|
759
|
+
stream.write('\n')
|
|
760
|
+
return scene
|