feat: sculpt portfolio into an immersive midnight expressway

Author physical road furniture, layered terrain and atmosphere, projected illuminated route signs, and detailed Blender coachwork. Refine camera mass, wheel contact, chapter composition, and compact reading space while preserving approved content and audio contracts.

Include isolated Blender source, before/after and hardware Chrome motion evidence, preservation hashes, validated preview checks, and documented inherited unit failures.
This commit is contained in:
2026-09-05 05:10:17 +03:00
parent c9ac36e249
commit b188cac7ed
74 changed files with 3168 additions and 1223 deletions

View File

@@ -0,0 +1,12 @@
# Expressway grand tourer
Original portfolio coachwork developed from the approved `../evolution/nightdrive-evolution.blend` at `c9ac36e`. No third-party model, texture, or generated project evidence is used.
- `nightdrive-expressway.blend`: isolated editable source. Open the **Midnight Expressway** scene. Body components, optical cells, intake vanes, wheel machining, panel seams, wing details, and look-development setup remain editable.
- `author-expressway.py`: appends the approved source into a separate scene and authors this edition. Run once in a fresh Blender session with `__file__` set to the script path. It saves only the isolated expressway library; it does not overwrite the approved evolution source.
- `export-expressway.py`: run with the expressway source open. It evaluates modifiers into a separate export scene, batches by material and wheel, exports the runtime GLB, and writes its provenance JSON. Four wheel parents remain independently rotatable. Cameras and look-development lights are excluded.
- `nightdrive-expressway.asset.json`: export path, SHA-256, triangle count, and source lineage.
Runtime: 31,288 triangles, 19 material batches, four independently rotating wheel groups. Geometry follows the existing Blender-to-glTF coordinate convention and runtime scale. Body suspension is applied separately from grounded wheels.
The runtime asset is `src/assets/nightdrive-grand-tourer-expressway.glb`. The review contains a Blender look-development render and actual in-browser views. The Static-mode JPEGs are captures of this portfolio's live Three.js world; they contain no fabricated project media.

View File

@@ -0,0 +1,124 @@
"""Append the approved coachwork into its own scene; author only the new edition.
Run inside Blender with __file__ pointing here. No open project is overwritten.
"""
import bpy, math
from pathlib import Path
from mathutils import Vector
root_path = Path(__file__).resolve().parents[3]
source = root_path / 'assets/blender/evolution/nightdrive-evolution.blend'
with bpy.data.libraries.load(str(source), link=False) as (available, loaded):
loaded.scenes = ['Portfolio Evolution']
scene = loaded.scenes[0]
scene.name = 'Midnight Expressway'
bpy.context.window.scene = scene
car = next(o for o in scene.objects if o.type=='EMPTY' and o.name.startswith('chase-car'))
materials = {m.name.split('.')[0]: m for o in scene.objects if o.type=='MESH' for m in o.data.materials}
def finish(obj, name, material, bevel=0.007):
obj.name = 'expressway-' + name
obj.parent = car
obj.data.materials.append(materials[material])
if bevel:
mod = obj.modifiers.new('Machined radii', 'BEVEL'); mod.width=bevel; mod.segments=2
obj.modifiers.new('Surface normals', 'WEIGHTED_NORMAL')
return obj
def box(name, loc, dims, mat, bevel=0.007):
bpy.ops.mesh.primitive_cube_add(size=1, location=loc)
o=bpy.context.object; o.dimensions=dims
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
return finish(o,name,mat,bevel)
def shell(name, vertices, faces, mat, bevel=0.005):
mesh=bpy.data.meshes.new(name); mesh.from_pydata(vertices,[],faces); mesh.update()
o=bpy.data.objects.new(name,mesh); scene.collection.objects.link(o)
return finish(o,name,mat,bevel)
def line(name, coords, mat, radius=0.004):
curve=bpy.data.curves.new(name,'CURVE'); curve.dimensions='3D'
curve.resolution_u=1; curve.bevel_depth=radius; curve.bevel_resolution=2
spline=curve.splines.new('POLY'); spline.points.add(len(coords)-1)
for p,co in zip(spline.points,coords): p.co=(*co,1)
obj=bpy.data.objects.new(name,curve); scene.collection.objects.link(obj)
obj.parent=car; obj.data.materials.append(materials[mat]); obj.name='expressway-'+name
return obj
# Material story: cold painted metal, brushed alloy, smoke and deep graphite.
for name,color,metal,rough in [
('M_Body_Gunmetal',(0.18,0.28,0.36,1),0.72,0.27),
('M_Body_Haunch',(0.30,0.41,0.48,1),0.78,0.22),
('M_Lower_Graphite',(0.018,0.028,0.039,1),0.6,0.38),
('M_Wheel',(0.45,0.53,0.57,1),0.92,0.21),
('M_Tire',(0.009,0.012,0.016,1),0.0,0.9),
('M_Glass_Smoke',(0.014,0.045,0.065,1),0.36,0.14)]:
bsdf=materials[name].node_tree.nodes.get('Principled BSDF')
bsdf.inputs['Base Color'].default_value=color
bsdf.inputs['Metallic'].default_value=metal; bsdf.inputs['Roughness'].default_value=rough
if 'Body' in name:
bsdf.inputs['Coat Weight'].default_value=0.42; bsdf.inputs['Coat Roughness'].default_value=0.19
# Replace the plain lamp with fourteen paired optical cells and lit outer returns.
for obj in list(scene.objects):
if obj.name.startswith(('rear-light-bar','evolution-lamp-divider','signature-turbine-vane')):
bpy.data.objects.remove(obj,do_unlink=True)
box('rear-optical-cassette',(0,2.684,0.700),(1.79,0.06,0.21),'M_Lower_Graphite',0.025)
for side in [-1,1]:
for i in range(7):
x=side*(0.11+i*0.109)
for z in [0.664,0.746]:
box('rear-optic-cell',(x,2.720,z),(0.091,0.022,0.034),'M_Rear_Light',0.006)
box('lamp-return',(side*0.852,2.704,0.705),(0.025,0.04,0.139),'M_Rear_Light',0.009)
# Side intake recess, bevelled upper lip and three angled internal vanes.
x=side*1.073
box('side-intake',(x,0.16,0.53),(0.027,1.03,0.255),'M_Lower_Graphite',0.033)
box('intake-upper-lip',(side*1.093,0.13,0.670),(0.035,1.09,0.025),'M_Body_Haunch')
for i in range(4):
o=box('intake-vane',(side*1.095,-0.18+i*0.22,0.54),(0.025,0.022,0.21),'M_Wheel',0.003)
o.rotation_euler.x=-0.32
# Deep lower blade has a distinct kicked-up rear end.
poly=[(side*1.05,-1.03,0.24),(side*1.13,-1.03,0.20),(side*1.13,1.1,0.20),(side*1.11,1.30,0.36),(side*1.05,1.30,0.36)]
shell('sculpted-skirt',poly,[tuple(range(5))],'M_Body_Haunch')
line('skirt-channel',[(side*1.137,-0.8,0.224),(side*1.137,0.92,0.224),(side*1.117,1.17,0.31)],'M_Cyan_Accent',0.006)
# Rear deck channel and restrained panel separation follow the wedge surfaces.
line('rear-deck-seam',[(side*.72,1.12,.928),(side*.76,1.78,.876),(side*.85,2.25,.825)],'M_Lower_Graphite',0.006)
line('haunch-pinstripe',[(side*.91,1.2,.905),(side*1.005,1.67,.857),(side*.997,2.20,.759)],'M_Wheel',0.0035)
for i in range(4):
box('deck-extraction-slot',(side*.49,1.36+i*.116,.906-i*.012),(.31,.039,.018),'M_Lower_Graphite',0.009)
# Larger aerofoil endplates, kinked with a recessed coral slit.
box('wing-endplate-shell',(side*1.195,2.35,1.268),(.032,.43,.24),'M_Body_Haunch',.016)
box('wing-endplate-inlay',(side*1.213,2.39,1.28),(.009,.22,.025),'M_Rear_Light',.004)
# Exhausts inside the graphite recess, with dark cores instead of solid cylinders.
for x0 in [0.29,0.43]:
x=side*x0
box('rear-exhaust-surround',(x,2.7,.37),(.105,.115,.085),'M_Wheel',.018)
box('rear-exhaust-core',(x,2.762,.37),(.079,.01,.059),'M_Lower_Graphite',.014)
for i in range(5):
box('rear-cooling-slot',(side*(.53+i*.06),2.704,.497),(.028,.018,.064),'M_Lower_Graphite',.005)
# Diffuser undertray: channels and an alloy trailing edge remain readable at speed.
line('diffuser-frame',[(-.82,2.68,.29),(-.63,2.78,.24),(.63,2.78,.24),(.82,2.68,.29)],'M_Wheel',.009)
for x in [-.66,-.33,0,.33,.66]:
shell('diffuser-strake',[(x,2.25,.22),(x,2.74,.26),(x,2.78,.105),(x,2.36,.105)],[(0,1,2,3)],'M_Lower_Graphite',0)
# Turbine machining has a dished barrel, swept spokes and a shoulder ring.
for side in [-1,1]:
for axle,y,z,radius in [('front',-1.62,.42,.42),('rear',1.62,.46,.46)]:
label=f'wheel-{axle}-{"left" if side<0 else "right"}'
for rad,xoff,thick,mat in [(radius-.016,1.153,.009,'M_Tire'),(.282 if axle=='rear' else .26,1.181,.008,'M_Wheel')]:
coords=[(side*xoff,y+math.sin(i*math.tau/64)*rad,z+math.cos(i*math.tau/64)*rad) for i in range(65)]
line(label+'-shoulder',coords,mat,thick)
for i in range(10):
angle=i*math.tau/10
verts=[]
for rad,a,xoff in [(.075,angle,1.17),(.255,angle+.30,1.193),(.263,angle+.48,1.193),(.088,angle+.35,1.17)]:
verts.append((side*xoff,y+math.sin(a)*rad,z+math.cos(a)*rad))
shell(label+'-turbine',verts,[(0,1,2,3)],'M_Wheel',.004)
bpy.ops.mesh.primitive_uv_sphere_add(segments=16,ring_count=8,radius=1,location=(side*1.20,y,z))
hub=bpy.context.object; hub.scale=(.025,.064,.064)
bpy.ops.object.transform_apply(location=False,rotation=False,scale=True)
finish(hub,label+'-hub','M_Lower_Graphite',0)
# Write only this scene and its own dependencies, retaining individually editable objects.
output=root_path/'assets/blender/expressway/nightdrive-expressway.blend'
bpy.data.libraries.write(str(output),{scene},fake_user=True,compress=True)
print('AUTHORED',scene.name,len(scene.objects),'objects',output)

View File

@@ -0,0 +1,51 @@
"""Export isolated authored source into material batches; leave lookdev editable."""
import bpy, json, hashlib
from mathutils import Matrix, Vector
from pathlib import Path
root_path=Path(__file__).resolve().parents[3]
scene=bpy.data.scenes['Midnight Expressway']
bpy.context.window.scene=scene
export_scene=bpy.data.scenes.new('Expressway Export')
root=bpy.data.objects.new('evolution-chase-car',None); export_scene.collection.objects.link(root)
names={'M_Body_Gunmetal':'car-body','M_Body_Haunch':'body-haunches','M_Glass_Smoke':'rear-glass','M_Rear_Light':'rear-light-bar','M_Cyan_Accent':'car-underglow-source','M_Lower_Graphite':'body-trim','M_Wheel':'wheel-machining','M_Tire':'tires'}
depsgraph=bpy.context.evaluated_depsgraph_get(); groups={}
for obj in scene.objects:
if obj.type not in ['MESH','CURVE'] or obj.name.startswith('LOOKDEV'): continue
evaluated=obj.evaluated_get(depsgraph)
mesh=bpy.data.meshes.new_from_object(evaluated,depsgraph=depsgraph); mesh.transform(obj.matrix_world)
clone=bpy.data.objects.new('export-'+obj.name,mesh); export_scene.collection.objects.link(clone)
key=obj.data.materials[0].name.split('.')[0]
wheel=None
for axle in ['front','rear']:
for side in ['left','right']:
if any(token in obj.name for token in [f'{axle}-wheel-{side}', f'{axle}-rim-{side}', f'{axle}-rim-ring-{side}', f'wheel-{axle}-{side}']):
wheel=f'wheel-{axle}-{side}'
groups.setdefault((key,wheel),[]).append(clone)
bpy.context.window.scene=export_scene
wheel_roots={}
for (key,wheel),objects in groups.items():
bpy.ops.object.select_all(action='DESELECT')
for obj in objects: obj.select_set(True)
bpy.context.view_layer.objects.active=objects[0]
if len(objects)>1: bpy.ops.object.join()
joined=bpy.context.object; joined.name='evolution-'+names[key]
if wheel:
if wheel not in wheel_roots:
axle='front' if 'front' in wheel else 'rear'; side=-1 if 'left' in wheel else 1
wheel_root=bpy.data.objects.new(wheel,None); export_scene.collection.objects.link(wheel_root)
wheel_root.parent=root; wheel_root.location=(side*1.01,-1.62 if axle=='front' else 1.62,.42 if axle=='front' else .46)
wheel_roots[wheel]=wheel_root
wheel_root=wheel_roots[wheel]
joined.data.transform(Matrix.Translation(-wheel_root.location))
joined.parent=wheel_root
else: joined.parent=root
bpy.ops.object.select_all(action='SELECT')
output=root_path/'src/assets/nightdrive-grand-tourer-expressway.glb'
bpy.ops.export_scene.gltf(filepath=str(output),export_format='GLB',use_selection=True,use_active_scene=True,export_apply=True,export_animations=False,export_cameras=False,export_lights=False)
triangles=0
for obj in export_scene.objects:
if obj.type=='MESH': obj.data.calc_loop_triangles(); triangles+=len(obj.data.loop_triangles)
provenance={'asset':'Nightdrive expressway grand tourer','source':'nightdrive-expressway.blend','base':'../evolution/nightdrive-evolution.blend at c9ac36e','authorship':'Original procedural modelling in Blender for this portfolio; no external assets or generated project evidence.','editable_scene':'Midnight Expressway','material_groups':len(groups),'independent_wheels':4,'triangles':triangles,'export':'../../../src/assets/nightdrive-grand-tourer-expressway.glb','sha256':hashlib.sha256(output.read_bytes()).hexdigest()}
(root_path/'assets/blender/expressway/nightdrive-expressway.asset.json').write_text(json.dumps(provenance,indent=2)+'\n')
bpy.context.window.scene=scene
print(json.dumps(provenance))

View File

@@ -0,0 +1,12 @@
{
"asset": "Nightdrive expressway grand tourer",
"source": "nightdrive-expressway.blend",
"base": "../evolution/nightdrive-evolution.blend at c9ac36e",
"authorship": "Original procedural modelling in Blender for this portfolio; no external assets or generated project evidence.",
"editable_scene": "Midnight Expressway",
"material_groups": 19,
"independent_wheels": 4,
"triangles": 31288,
"export": "../../../src/assets/nightdrive-grand-tourer-expressway.glb",
"sha256": "8418ff060b75c6fef200eaa526adae7fe51286b7fc30628671b9eb242641d1c3"
}

Binary file not shown.