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:
12
assets/blender/expressway/README.md
Normal file
12
assets/blender/expressway/README.md
Normal 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.
|
||||
124
assets/blender/expressway/author-expressway.py
Normal file
124
assets/blender/expressway/author-expressway.py
Normal 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)
|
||||
51
assets/blender/expressway/export-expressway.py
Normal file
51
assets/blender/expressway/export-expressway.py
Normal 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))
|
||||
12
assets/blender/expressway/nightdrive-expressway.asset.json
Normal file
12
assets/blender/expressway/nightdrive-expressway.asset.json
Normal 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"
|
||||
}
|
||||
BIN
assets/blender/expressway/nightdrive-expressway.blend
Normal file
BIN
assets/blender/expressway/nightdrive-expressway.blend
Normal file
Binary file not shown.
Reference in New Issue
Block a user