Initial commit: RHINE LAB · ANALYSIS OS:三维界面与动效实验(本仓库不含个人归档索引数据)
This commit is contained in:
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
@@ -0,0 +1,208 @@
|
||||
import bpy, math, os
|
||||
from mathutils import Vector
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = str(Path(__file__).resolve().parents[1])
|
||||
scene = bpy.data.scenes.new('Rhine_Archive_Work')
|
||||
bpy.context.window.scene = scene
|
||||
for old in list(bpy.data.scenes):
|
||||
if old != scene and old.name.startswith('Rhine_Archive_Asset'):
|
||||
for obj in list(old.objects):
|
||||
if len(obj.users_scene)==1:bpy.data.objects.remove(obj,do_unlink=True)
|
||||
bpy.data.scenes.remove(old)
|
||||
scene.name='Rhine_Archive_Asset'
|
||||
for m in list(bpy.data.materials):
|
||||
if m.users==0:bpy.data.materials.remove(m)
|
||||
|
||||
def material(name, color, rough=.3, metal=0, transmission=0):
|
||||
m=bpy.data.materials.new(name); m.diffuse_color=(*color,1); m.use_nodes=True
|
||||
p=m.node_tree.nodes.get('Principled BSDF')
|
||||
p.inputs['Base Color'].default_value=(*color,1)
|
||||
p.inputs['Roughness'].default_value=rough
|
||||
p.inputs['Metallic'].default_value=metal
|
||||
p.inputs['Transmission Weight'].default_value=transmission
|
||||
p.inputs['IOR'].default_value=1.46
|
||||
return m
|
||||
|
||||
shell=material('Frosted_Polymer',(.985,.975,.963),.36,0,.78)
|
||||
edge=material('Ivory_Edges',(.94,.916,.892),.28,.02,.72)
|
||||
core=material('Internal_Ceramic',(.74,.705,.68),.52,.06)
|
||||
metal=material('Titanium_Fasteners',(.58,.60,.61),.19,.82)
|
||||
gold=material('Champagne_Index',(.64,.46,.29),.33,.48)
|
||||
paper=material('Printed_Label',(.91,.89,.84),.65)
|
||||
diffuser=material('Optical_Diffuser',(.925,.902,.881),.67,0,0)
|
||||
optics=material('Subsurface_Optics',(.70,.675,.66),.39,.12)
|
||||
optical_edge=material('Optical_Edges',(.94,.92,.90),.3,.03,.45)
|
||||
ink=material('Carbon_Ink',(.025,.026,.023),.75)
|
||||
|
||||
def cube(name, loc, size, mat, bevel=.015):
|
||||
bpy.ops.mesh.primitive_cube_add(size=1, location=loc)
|
||||
o=bpy.context.object; o.name=name; o.dimensions=size
|
||||
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
|
||||
o.data.materials.append(mat)
|
||||
if bevel:
|
||||
m=o.modifiers.new('Precision radiused edge','BEVEL'); m.width=bevel; m.segments=3
|
||||
bpy.context.view_layer.objects.active=o; bpy.ops.object.modifier_apply(modifier=m.name)
|
||||
o.modifiers.new('Weighted corner normals','WEIGHTED_NORMAL')
|
||||
return o
|
||||
|
||||
def torus(name,x,z,radius,tube,mat,y=-.091):
|
||||
bpy.ops.mesh.primitive_torus_add(major_radius=radius,minor_radius=tube,major_segments=64,minor_segments=10,location=(x,y,z),rotation=(math.pi/2,0,0))
|
||||
o=bpy.context.object; o.name=name; o.data.materials.append(mat)
|
||||
for p in o.data.polygons:p.use_smooth=True
|
||||
return o
|
||||
|
||||
def text(name, body,x,z,size,mat=ink):
|
||||
c=bpy.data.curves.new(name,'FONT'); c.body=body;c.size=size;c.extrude=.0002;c.space_character=1.05
|
||||
o=bpy.data.objects.new(name,c);scene.collection.objects.link(o)
|
||||
o.location=(x,-.123,z);o.rotation_euler=(math.pi/2,0,0);c.materials.append(mat)
|
||||
return o
|
||||
|
||||
def annular_profile(name, x, z, profile, mat, segments=128, start=0, end=2*math.pi, sharp=False):
|
||||
# Closed revolved cross-section: a shallow moulded lens, not a round tube.
|
||||
vertices=[]; faces=[]; n=len(profile)
|
||||
closed=abs(end-start-2*math.pi)<1e-6
|
||||
segments=max(8,math.ceil(segments*(end-start)/(2*math.pi)))
|
||||
rows=segments if closed else segments+1
|
||||
for i in range(rows):
|
||||
a=start+(end-start)*i/segments
|
||||
for r,y in profile: vertices.append((x+r*math.cos(a),y,z+r*math.sin(a)))
|
||||
for i in range(segments):
|
||||
for j in range(n):
|
||||
faces.append((i*n+j,((i+1)%rows)*n+j,((i+1)%rows)*n+(j+1)%n,i*n+(j+1)%n))
|
||||
if not closed:
|
||||
faces.extend([tuple(reversed(range(n))),tuple(segments*n+j for j in range(n))])
|
||||
mesh=bpy.data.meshes.new(name);mesh.from_pydata(vertices,[],faces);mesh.update()
|
||||
obj=bpy.data.objects.new(name,mesh);scene.collection.objects.link(obj);mesh.materials.append(mat)
|
||||
# The clockwise section above yields outward normals, including the bore.
|
||||
for p in mesh.polygons:p.use_smooth=len(p.vertices)==4
|
||||
if sharp:
|
||||
# Keep each section edge hard, but interpolate around the circumference.
|
||||
# Flat shading alone would facet the circle; all-smooth shading balloons
|
||||
# the roof/wall junction into a rounded tube.
|
||||
normals=[]
|
||||
for face in mesh.polygons:
|
||||
if face.index>=segments*n:
|
||||
face.use_smooth=False
|
||||
normals.extend([tuple(face.normal)]*len(face.loop_indices))
|
||||
continue
|
||||
j=face.index%n
|
||||
dr=profile[(j+1)%n][0]-profile[j][0]
|
||||
dy=profile[(j+1)%n][1]-profile[j][1]
|
||||
for loop in face.loop_indices:
|
||||
row=mesh.loops[loop].vertex_index//n
|
||||
angle=start+(end-start)*row/segments
|
||||
normal=Vector((-dy*math.cos(angle),dr,-dy*math.sin(angle))).normalized()
|
||||
normals.append(tuple(normal))
|
||||
mesh.normals_split_custom_set(normals)
|
||||
return obj
|
||||
|
||||
def channel(name, points, depth, radius, mat):
|
||||
curve=bpy.data.curves.new(name,'CURVE');curve.dimensions='3D'
|
||||
curve.resolution_u=8;curve.bevel_depth=radius;curve.bevel_resolution=2
|
||||
spline=curve.splines.new('POLY');spline.points.add(len(points)-1)
|
||||
for point,(x,z) in zip(spline.points,points):point.co=(x,depth,z,1)
|
||||
obj=bpy.data.objects.new(name,curve);scene.collection.objects.link(obj);curve.materials.append(mat)
|
||||
return obj
|
||||
|
||||
front_cover=cube('Front frosted optical cover',(0,-.095,1.85),(5,.016,3.7),shell,.007)
|
||||
cube('Rear translucent carrier',(0,.055,1.85),(4.97,.02,3.68),edge,.009)
|
||||
cube('Information substrate',(0,.025,1.86),(4.80,.012,3.47),diffuser,.006)
|
||||
for z in [.028,3.672]:cube('Polished perimeter rail',(0,-.021,z),(4.95,.155,.034),edge,.009)
|
||||
for x in [-2.476,2.476]:cube('Polished perimeter rail',(x,-.021,1.85),(.034,.155,3.66),edge,.009)
|
||||
# Wide optical cavities sit BEHIND the frosted cover. Their lenticular profiles
|
||||
# are shallow; no torus protrudes from the exterior face.
|
||||
for x,z,r in [(-.44,1.92,.79),(1.13,2.48,.435)]:
|
||||
width=.145 if r>.5 else .102
|
||||
annular_profile('Embedded optical cavity',x,z,[
|
||||
(r-width,.016),(r+width,.016),(r+width+.012,.003),
|
||||
(r+width,-.014),(r+width-.022,-.027),(r+.032,-.040),
|
||||
(r-.012,-.039),(r-.045,-.052),(r-width+.025,-.054),
|
||||
(r-width,-.036)],optics)
|
||||
annular_profile('Subsurface refractive shoulder',x,z,[
|
||||
(r+.028,-.031),(r+width+.025,-.009),(r+width+.029,-.020),
|
||||
(r+width+.012,-.032),(r+.056,-.053),(r+.028,-.049)],optical_edge)
|
||||
annular_profile('Inner optical bevel',x,z,[
|
||||
(r-width-.012,-.017),(r-width+.036,-.041),
|
||||
(r-width+.041,-.057),(r-width+.023,-.064),
|
||||
(r-width+.004,-.061),(r-width-.012,-.036)],optical_edge)
|
||||
for offset,tube,y in [(width+.009,.006,-.031),(.030,.007,-.054),(-width+.022,.006,-.064)]:
|
||||
o=torus('Concentric optical machining',x,z,r+offset,tube,optical_edge,y)
|
||||
o.scale.z=.42
|
||||
if r<.5:
|
||||
annular_profile('Embedded amber annulus',x,z,[
|
||||
(r-.170,-.047),(r-.070,-.047),(r-.067,-.060),
|
||||
(r-.077,-.071),(r-.156,-.071),(r-.170,-.060)],gold)
|
||||
cube('Serial label',(-1.36,-.099,3.04),(.99,.02,.41),paper,.003)
|
||||
cube('Label top rule',(-1.36,-.116,3.23),(.98,.004,.008),ink,0)
|
||||
cube('Label bottom rule',(-1.36,-.116,2.847),(.98,.004,.005),ink,0)
|
||||
text('Company label','RHINE LAB, LLC.',-1.825,3.105,.105)
|
||||
text('Database label','INTERNAL DATABASE',-1.825,3.017,.042,core)
|
||||
text('Serial number','NO.001',-1.825,2.875,.148)
|
||||
text('Information label','INFO',-.99,3.075,.076)
|
||||
text('Symbol label','+ / -',-.99,2.9,.095)
|
||||
for i in range(16):
|
||||
o=cube('Laser etched vent',(1.04+i*.054,-.111,.57),(.023,.009,.1),core,.003);o.rotation_euler.y=.4
|
||||
for i in range(25):cube('Calibration mark',(-2.23,-.108,.61+i*.052),(.035 if i%5 else .075,.005,.006),core,0)
|
||||
text('Edge inscription','R H I N E L A B',-1.81,.28,.063,core)
|
||||
for z,x1,x2 in [(3.36,-1.8,.2),(3.36,.4,1.55),(.4,-1.45,1.75)]:
|
||||
cube('Engraved circuit trace',((x1+x2)/2,-.108,z),(x2-x1,.004,.005),core,.002)
|
||||
# Two sides of a shallow pressed channel, observed in the 37–39 second close-up.
|
||||
# Keep the entire channel behind the front cover to avoid coplanar stippling.
|
||||
top=[(-2.27,3.06),(-2.27,3.30),(-2.10,3.45),(-1.72,3.45),(-1.61,3.51),
|
||||
(-.65,3.51),(-.55,3.46),(.37,3.46),(.43,3.52),(.49,3.46),
|
||||
(1.08,3.46),(1.17,3.52),(1.20,3.50),(1.13,3.42),
|
||||
(1.79,3.42),(1.89,3.51),(2.19,3.51),(2.29,3.41),(2.29,3.03)]
|
||||
channel('Moulded circuit channel shadow',top,-.071,.005,core)
|
||||
channel('Moulded circuit channel lip',[(x,z-.018) for x,z in top],-.075,.008,optical_edge)
|
||||
perimeter=[(-2.18,2.85),(-2.23,2.72),(-2.23,.43),(-2.12,.30),
|
||||
(2.08,.30),(2.24,.44),(2.24,2.97)]
|
||||
channel('Moulded inner perimeter',perimeter,-.067,.008,optical_edge)
|
||||
for z in [.080,3.620]:
|
||||
cube('Carrier mating seam',(0,.002,z),(4.82,.012,.010),optical_edge,.003)
|
||||
for x in [-2.420,2.420]:
|
||||
cube('Carrier mating seam',(x,.002,1.85),(.010,.012,3.54),optical_edge,.003)
|
||||
# Small raised pads under the diagonal calibration vents.
|
||||
for i in range(16):
|
||||
cube('Moulded vent footing',(1.04+i*.054,-.071,.435),(.015,.014,.018),optical_edge,.004)
|
||||
|
||||
detail_script=Path(ROOT)/'art/clear_reference_details.py'
|
||||
exec(compile(detail_script.read_text(encoding='utf-8-sig'),str(detail_script),'exec'))
|
||||
|
||||
# Convert text, bake modifiers, and group by material for efficient instancing.
|
||||
bpy.ops.object.select_all(action='SELECT')
|
||||
for o in list(scene.objects):
|
||||
bpy.context.view_layer.objects.active=o
|
||||
if o.type in ['FONT','CURVE']:bpy.ops.object.convert(target='MESH')
|
||||
for m in list(o.modifiers):
|
||||
try:bpy.ops.object.modifier_apply(modifier=m.name)
|
||||
except:pass
|
||||
for mat in list(dict.fromkeys(o.data.materials[0] for o in scene.objects if o.type=='MESH' and o.data.materials)):
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
obs=[o for o in scene.objects if o.type=='MESH' and o.data.materials and o.data.materials[0]==mat]
|
||||
if not obs:continue
|
||||
for o in obs:o.select_set(True)
|
||||
bpy.context.view_layer.objects.active=obs[0];bpy.ops.object.join()
|
||||
o=bpy.context.object;o.name=mat.name
|
||||
scene.cursor.location=(0,0,0);bpy.ops.object.origin_set(type='ORIGIN_CURSOR')
|
||||
# Bake orientation to make every exported group share the same coordinate frame.
|
||||
bpy.ops.object.transform_apply(location=False,rotation=True,scale=True)
|
||||
# Annotated reference: the visible end face occupies roughly half a row pitch.
|
||||
for obj in scene.objects:
|
||||
if obj.type=='MESH':
|
||||
if obj.data.materials[0].name.startswith(('Optical_Glass_', 'Optical_Bridge_Glass', 'Amber_Optical_Inlay')):
|
||||
bpy.context.view_layer.objects.active=obj
|
||||
bpy.ops.object.select_all(action='DESELECT');obj.select_set(True)
|
||||
obj.scale.y *= 2.0
|
||||
bpy.ops.object.transform_apply(location=False,rotation=False,scale=True)
|
||||
else:
|
||||
for vertex in obj.data.vertices:vertex.co.y *= 2.0
|
||||
bpy.ops.object.select_all(action='SELECT')
|
||||
export_path=Path(ROOT)/'art/.cache/archive-cassette.glb'
|
||||
export_path.parent.mkdir(parents=True,exist_ok=True)
|
||||
bpy.ops.export_scene.gltf(filepath=str(export_path),export_format='GLB',use_selection=True,use_active_scene=True,export_apply=True)
|
||||
os.replace(str(export_path),ROOT+'/public/assets/archive-cassette.glb')
|
||||
bpy.data.libraries.write(ROOT+'/art/rhine-archive.blend', {scene}, fake_user=True)
|
||||
print('Exported archive cassette:',len(scene.objects),'material groups')
|
||||
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import bpy
|
||||
from pathlib import Path
|
||||
BUILD_ROOT = Path(__file__).resolve().parents[1]
|
||||
ROOT = BUILD_ROOT
|
||||
source = (ROOT/'art/build_archive.py').read_text(encoding='utf-8')
|
||||
prefix = source.split('# Convert text, bake modifiers')[0]
|
||||
prefix = prefix.replace("scene = bpy.data.scenes.new('Rhine_Archive_Work')", "scene = bpy.data.scenes.new('Rhine_Assembly_Work')")
|
||||
prefix = prefix.replace("if old != scene and old.name.startswith('Rhine_Archive_Asset'):", "if False:")
|
||||
prefix = prefix.replace("scene.name='Rhine_Archive_Asset'", "scene.name='Rhine_Assembly_Asset'")
|
||||
exec(compile(prefix, str(ROOT/'art/build_archive.py'), 'exec'))
|
||||
ROOT = BUILD_ROOT
|
||||
def part_for(name):
|
||||
if name.startswith(('Rear translucent carrier', 'Polished perimeter rail', 'Ivory spine cap', 'Carrier mating seam')):
|
||||
return 'carrier'
|
||||
if name.startswith('Information substrate'): return 'substrate'
|
||||
if name.startswith(('Embedded optical cavity', 'Embedded amber annulus', 'Folded optical tab')): return 'optical-core'
|
||||
if name.startswith(('Subsurface refractive shoulder', 'Inner optical bevel', 'Concentric optical machining', 'Optical ribbon')):
|
||||
return 'optical-lenses'
|
||||
if name.startswith(('Countersunk washer', 'Machined screw', 'Screw slot')): return 'fasteners'
|
||||
return 'cover'
|
||||
for obj in list(scene.objects):
|
||||
obj['assemblyPart'] = part_for(obj.name)
|
||||
bpy.context.view_layer.objects.active = obj
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
obj.select_set(True)
|
||||
if obj.type in ['FONT','CURVE']: bpy.ops.object.convert(target='MESH')
|
||||
for mod in list(obj.modifiers):
|
||||
bpy.ops.object.modifier_apply(modifier=mod.name)
|
||||
if obj.data.materials[0].name.split('.')[0] == 'Carbon_Ink':
|
||||
bpy.data.objects.remove(obj, do_unlink=True)
|
||||
pairs = {}
|
||||
for obj in scene.objects:
|
||||
key = (obj['assemblyPart'], obj.data.materials[0].name)
|
||||
pairs.setdefault(key, []).append(obj)
|
||||
for (part, surface), objects in pairs.items():
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
for obj in objects: obj.select_set(True)
|
||||
bpy.context.view_layer.objects.active = objects[0]
|
||||
bpy.ops.object.join()
|
||||
obj = bpy.context.object
|
||||
obj.name = part+'__'+surface.split('.')[0]
|
||||
obj['assemblyPart'] = part
|
||||
scene.cursor.location = (0,0,0)
|
||||
bpy.ops.object.origin_set(type='ORIGIN_CURSOR')
|
||||
bpy.ops.object.transform_apply(location=False, rotation=True, scale=True)
|
||||
if surface.startswith(('Optical_Glass_', 'Optical_Bridge_Glass', 'Amber_Optical_Inlay')):
|
||||
obj.scale.y *= 2.0
|
||||
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
|
||||
else:
|
||||
for vertex in obj.data.vertices:vertex.co.y *= 2.0
|
||||
bpy.ops.object.select_all(action='SELECT')
|
||||
export_path=ROOT/'art/.cache/archive-assembly.glb'
|
||||
export_path.parent.mkdir(parents=True,exist_ok=True)
|
||||
bpy.ops.export_scene.gltf(filepath=str(export_path), export_format='GLB', use_selection=True, use_active_scene=True, export_apply=True, export_extras=True)
|
||||
os.replace(str(export_path),str(ROOT/'public/assets/archive-assembly.glb'))
|
||||
bpy.data.libraries.write(str(ROOT/'art/archive-assembly.blend'), {scene}, fake_user=True)
|
||||
print('Assembly exported:', len(scene.objects), 'meshes, parts:', sorted({o['assemblyPart'] for o in scene.objects}))
|
||||
@@ -0,0 +1,47 @@
|
||||
# Detail reconstruction from the clear 40–42 second reference.
|
||||
# Executed by build_archive.py in the same scene; assembly uses this identical source.
|
||||
# All dimensions below are BEFORE the shared x2 thickness bake.
|
||||
from pathlib import Path
|
||||
|
||||
obsolete=('Embedded optical cavity','Subsurface refractive shoulder','Inner optical bevel',
|
||||
'Concentric optical machining','Embedded amber annulus','Moulded circuit channel',
|
||||
'Moulded inner perimeter','Moulded vent footing','Laser etched vent',
|
||||
'Calibration mark','Edge inscription','Engraved circuit trace')
|
||||
for obj in list(scene.objects):
|
||||
if obj.name.startswith(obsolete): bpy.data.objects.remove(obj,do_unlink=True)
|
||||
|
||||
emboss=material('Moulded_Lettering',(.81,.786,.75),.24,.13)
|
||||
architecture_script=Path(ROOT)/'art/internal_architecture.py'
|
||||
exec(compile(architecture_script.read_text(encoding='utf-8'),str(architecture_script),'exec'))
|
||||
|
||||
# Rebuild the moulding from normalized positions on the complete 41.0-second face.
|
||||
def face_point(px,py):return ((px-432)/1022*5-2.5,(1018-py)/676*3.7)
|
||||
def mould(name,pixels,depth=-.074,radius=.004,mat=core):
|
||||
return channel(name,[face_point(x,y) for x,y in pixels],depth,radius,mat)
|
||||
|
||||
shell_script=Path(ROOT)/'art/shell_reference_details.py'
|
||||
exec(compile(shell_script.read_text(encoding='utf-8'),str(shell_script),'exec'))
|
||||
|
||||
# Fine, lightly recessed rectangular backing routes, beneath the ring structures.
|
||||
for route in [[(681,470),(681,899),(1061,899),(1061,821),(1310,821),(1310,464),(1028,464),(1028,406)]]:
|
||||
mould('Information substrate fine route',route,.014,.0018,core)
|
||||
|
||||
# Embossed vertical company inscription belongs to the inner cover, not the ink label.
|
||||
inscription=text('Moulded vertical lettering','RHINE LAB, LLC.',0,0,.155,emboss)
|
||||
# Text baseline follows Blender local X; rotate it down the left edge in the X/Z plane.
|
||||
inscription.location=(-2.27,-.078,2.29)
|
||||
inscription.rotation_euler=(math.pi/2,math.pi/2,0)
|
||||
inscription.data.extrude=.003;inscription.data.bevel_depth=.0015;inscription.data.bevel_resolution=1
|
||||
inscription.data.space_character=1.10
|
||||
# Two small registration pads under the floating tabs.
|
||||
for x in [-1.61,-1.45]:cube('Information substrate registration pad',(x,-.029,.53),(.014,.009,.038),gold,.002)
|
||||
|
||||
# Lower-right circular service detail with a slanted slotted rail, seen under haze.
|
||||
px,pz=face_point(1312,923)
|
||||
for radius,depth,mat in [(.145,-.070,optical_edge),(.120,-.065,core),(.083,-.072,optical_edge)]:
|
||||
torus('Moulded service socket',px,pz,radius,.0055,mat,depth).scale.z=.45
|
||||
annular_profile('Moulded service socket center',px,pz,[(.021,-.040),(.043,-.040),(.048,-.060),(.035,-.074),(.021,-.068)],gold,32)
|
||||
for i in range(11):
|
||||
x=1.03+i*.063
|
||||
channel('Moulded diagonal vent',[(x-.035,.38),(x+.030,.53)],-.075,.008,optical_edge)
|
||||
channel('Moulded diagonal vent recess',[(x-.029,.38),(x+.036,.53)],-.071,.003,core)
|
||||
@@ -0,0 +1,107 @@
|
||||
"""User-selected first internal model, restored from revision 89c7742.
|
||||
|
||||
Keep the current outer case and reveal animation. This shared Blender source
|
||||
restores the annular lenses, inner hubs, muted orange and paired ribbons.
|
||||
"""
|
||||
amber_inlay=material('Amber_Optical_Inlay',(.48,.18,.052),.32,.28)
|
||||
film=material('Optical_Film',(.79,.755,.712),.30,.06)
|
||||
film_edge=material('Optical_Film_Edge',(.57,.545,.515),.27,.20)
|
||||
clip_metal=material('Optical_Clips',(.34,.335,.32),.34,.55)
|
||||
|
||||
# Large annular volume: broad, segmented shallow chamber surrounding a narrow raised bore.
|
||||
# The previous uniformly thick concentric tubes hid the asymmetric breaks.
|
||||
bx,bz=-.425,1.80
|
||||
outer_profile=[(.625,.013),(.918,.013),(.950,.004),(.949,-.010),
|
||||
(.928,-.023),(.897,-.037),(.848,-.046),(.717,-.048),
|
||||
(.667,-.044),(.635,-.030)]
|
||||
for start,end in [(1,89.7),(90.3,177.5),(181,227.5),(229,308.7),(309.3,359.7)]:
|
||||
annular_profile('Embedded optical cavity segment',bx,bz,outer_profile,optics,128,
|
||||
math.radians(start),math.radians(end))
|
||||
# Each lip has its own depth so the cross-section stays readable when unassembled.
|
||||
annular_profile('Inner optical bevel hub',bx,bz,[
|
||||
(.501,-.020),(.614,-.020),(.635,-.033),(.632,-.057),
|
||||
(.615,-.069),(.547,-.071),(.512,-.056),(.501,-.040)],optical_edge)
|
||||
for start,end in [(2,177),(181,227),(230,359)]:
|
||||
annular_profile('Subsurface refractive shoulder sector',bx,bz,[
|
||||
(.670,-.031),(.929,-.010),(.948,-.017),(.942,-.026),
|
||||
(.914,-.034),(.867,-.043),(.731,-.054),(.673,-.049)],optical_edge,128,
|
||||
math.radians(start),math.radians(end))
|
||||
# Thin glazing lips and a partial inner copper track seen at the upper-left.
|
||||
for radius,depth in [(.943,-.029),(.650,-.054),(.526,-.064)]:
|
||||
annular_profile('Concentric optical machining lip',bx,bz,[
|
||||
(radius-.004,depth+.003),(radius+.004,depth+.003),
|
||||
(radius+.005,depth-.001),(radius,depth-.004),
|
||||
(radius-.004,depth-.002)],film_edge)
|
||||
annular_profile('Embedded amber annulus inner arc',bx,bz,[
|
||||
(.546,-.061),(.568,-.061),(.574,-.070),(.568,-.075),(.548,-.075)],amber_inlay,
|
||||
128,math.radians(109),math.radians(225))
|
||||
annular_profile('Embedded amber annulus outer arc',bx,bz,[
|
||||
(.910,.009),(.982,.009),(.986,-.004),(.978,-.011),(.953,-.020),(.922,-.017)],amber_inlay,
|
||||
128,math.radians(229),math.radians(364))
|
||||
# Radial joints cross the wide outer lens, staying under the cover.
|
||||
for angle in [90,103,179,228,260,309]:
|
||||
a=math.radians(angle)
|
||||
channel('Concentric optical machining radial seam',
|
||||
[(bx+r*math.cos(a),bz+r*math.sin(a)) for r in [.674,.74,.88,.937]],
|
||||
-.057,.0024,film_edge)
|
||||
|
||||
# Small annular volume: concentric clear shoulder, darker seat, flat amber annulus and hub.
|
||||
sx,sz=1.125,2.455
|
||||
annular_profile('Embedded optical cavity small',sx,sz,[
|
||||
(.220,.014),(.511,.014),(.555,.000),(.560,-.015),
|
||||
(.529,-.035),(.471,-.050),(.360,-.055),(.231,-.037)],optics)
|
||||
annular_profile('Subsurface refractive shoulder small',sx,sz,[
|
||||
(.388,-.034),(.536,-.013),(.573,-.017),(.579,-.030),
|
||||
(.553,-.047),(.502,-.059),(.424,-.063),(.386,-.051)],optical_edge)
|
||||
annular_profile('Embedded amber annulus small',sx,sz,[
|
||||
(.230,-.045),(.347,-.045),(.356,-.056),(.351,-.071),
|
||||
(.241,-.071),(.230,-.060)],amber_inlay)
|
||||
annular_profile('Inner optical bevel small hub',sx,sz,[
|
||||
(.170,-.041),(.226,-.041),(.235,-.054),(.231,-.069),
|
||||
(.184,-.069),(.172,-.057)],optical_edge)
|
||||
for radius in [.381,.478,.565]:
|
||||
annular_profile('Concentric optical machining small lip',sx,sz,[
|
||||
(radius-.003,-.057),(radius+.003,-.057),(radius+.004,-.062),
|
||||
(radius,-.064),(radius-.003,-.062)],film_edge)
|
||||
|
||||
# The reference shows two nearly clear ribbon-like spans. Their physical role is
|
||||
# unknown; recreate visible sheet/rim geometry without inventing a mechanism.
|
||||
def ribbon(name, controls, width, depth):
|
||||
points=[];verts=[];faces=[];segments=40
|
||||
for i in range(segments+1):
|
||||
t=i/segments;u=1-t
|
||||
x=sum(w*p[0] for w,p in zip([u**3,3*u*u*t,3*u*t*t,t**3],controls))
|
||||
z=sum(w*p[1] for w,p in zip([u**3,3*u*u*t,3*u*t*t,t**3],controls))
|
||||
dx=3*u*u*(controls[1][0]-controls[0][0])+6*u*t*(controls[2][0]-controls[1][0])+3*t*t*(controls[3][0]-controls[2][0])
|
||||
dz=3*u*u*(controls[1][1]-controls[0][1])+6*u*t*(controls[2][1]-controls[1][1])+3*t*t*(controls[3][1]-controls[2][1])
|
||||
length=math.hypot(dx,dz);nx=-dz/length;nz=dx/length
|
||||
points.append(((x-width*nx/2,z-width*nz/2),(x+width*nx/2,z+width*nz/2)))
|
||||
for yy in [depth-.002,depth+.002]:
|
||||
for sign in [-1,1]:verts.append((x+sign*width*nx/2,yy,z+sign*width*nz/2))
|
||||
for i in range(segments):
|
||||
a=i*4;b=(i+1)*4
|
||||
faces.extend([(a,b,b+1,a+1),(a+2,a+3,b+3,b+2),
|
||||
(a,a+2,b+2,b),(a+1,b+1,b+3,a+3)])
|
||||
faces.extend([(0,1,3,2),(segments*4,segments*4+2,segments*4+3,segments*4+1)])
|
||||
mesh=bpy.data.meshes.new(name);mesh.from_pydata(verts,[],faces);mesh.update()
|
||||
obj=bpy.data.objects.new(name,mesh);scene.collection.objects.link(obj);mesh.materials.append(film)
|
||||
for face in mesh.polygons:face.use_smooth=True
|
||||
for side in [0,1]:channel(name+' edge',[p[side] for p in points],depth-.003,.0018,film_edge)
|
||||
|
||||
ribbon('Optical ribbon inner',[(.31,1.16),(.99,1.33),(1.00,1.78),(.73,2.20)],.10,-.008)
|
||||
ribbon('Optical ribbon outer',[(.03,.94),(.97,1.13),(1.14,1.75),(.84,2.13)],.11,.007)
|
||||
channel('Optical ribbon tangent upper',[(-.67,2.73),(.98,3.01)],.003,.0018,film_edge)
|
||||
channel('Optical ribbon tangent lower',[(.06,.91),(1.44,2.03)],.006,.0018,film_edge)
|
||||
|
||||
# Omit the three tall structures to the left, as requested: their depth would
|
||||
# exceed the cassette. Use the 46–51 second views only for the annular buildings.
|
||||
# Closely spaced facade mullions sit under the roof edge, visible from oblique views.
|
||||
for cx,cz,r,count in [(bx,bz,.945,84),(sx,sz,.544,52)]:
|
||||
for i in range(count):
|
||||
angle=2*math.pi*i/count
|
||||
if cx==bx and 177<math.degrees(angle)<182:continue
|
||||
obj=cube('Embedded optical cavity facade mullion',
|
||||
(cx+r*math.cos(angle),-.011,cz+r*math.sin(angle)),(.009,.056,.0034),film_edge,.001)
|
||||
obj.rotation_euler.y=-angle
|
||||
annular_profile('Embedded amber annulus small lower fascia',sx,sz,[
|
||||
(.526,.010),(.557,.010),(.561,.001),(.558,-.009),(.532,-.009)],amber_inlay)
|
||||
Binary file not shown.
@@ -0,0 +1,22 @@
|
||||
import bpy
|
||||
from mathutils import Vector
|
||||
from pathlib import Path
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
s=bpy.context.scene
|
||||
for obj in list(s.objects):
|
||||
if obj.type in ['CAMERA','LIGHT']:bpy.data.objects.remove(obj,do_unlink=True)
|
||||
bpy.ops.object.camera_add(location=(-7,-11,6))
|
||||
c=bpy.context.object;c.name='Asset_Review_Camera'
|
||||
c.rotation_euler=((Vector((0,0,1.9))-c.location).to_track_quat('-Z','Y').to_euler())
|
||||
c.data.type='ORTHO';c.data.ortho_scale=7.3;s.camera=c
|
||||
s.world=bpy.data.worlds.new('Warm_Studio_World');s.world.use_nodes=True
|
||||
s.world.node_tree.nodes['Background'].inputs[0].default_value=(.8,.77,.72,1)
|
||||
s.world.node_tree.nodes['Background'].inputs[1].default_value=.65
|
||||
for name,loc,power,size in [('Softbox_Key',(-4,-5,8),950,7),('Softbox_Rim',(5,1,6),1100,5),('Softbox_Fill',(1,-7,3),350,6)]:
|
||||
bpy.ops.object.light_add(type='AREA',location=loc)
|
||||
light=bpy.context.object;light.name=name;light.data.energy=power;light.data.shape='DISK';light.data.size=size
|
||||
light.rotation_euler=((Vector((0,0,2))-light.location).to_track_quat('-Z','Y').to_euler())
|
||||
s.render.engine='CYCLES';s.cycles.samples=48;s.cycles.use_denoising=True
|
||||
s.render.resolution_x=1200;s.render.resolution_y=1000;s.render.resolution_percentage=100
|
||||
s.render.film_transparent=False;s.render.filepath=str(ROOT/'art/archive-studio.png')
|
||||
bpy.ops.wm.save_as_mainfile(filepath=str(ROOT/'art/rhine-archive.blend'))
|
||||
@@ -0,0 +1,63 @@
|
||||
# Reference face: x=24..1021, y=6..667. Shared by both Blender exports.
|
||||
# The engraved frame is below the cover, so the moving frost masks every route.
|
||||
engraving=material('Case_Engraving',(.57,.54,.50),.39,.12)
|
||||
lip=material('Case_Engraving_Highlight',(.90,.87,.83),.25,.16)
|
||||
inlay=material('Index_Inlay',(.67,.53,.40),.52,.04)
|
||||
def case_point(px,py):return ((px-24)/997*5-2.5,(667-py)/661*3.7)
|
||||
def route(name,pixels):
|
||||
points=[case_point(x,y) for x,y in pixels]
|
||||
channel('Case engraved '+name,points,-.075,.0045,engraving)
|
||||
channel('Case pressed lip '+name,[(x+.007,z-.009) for x,z in points],-.079,.004,lip)
|
||||
|
||||
# The top patch finishes at the case's top and front planes: no raised block.
|
||||
x,z=case_point(92,26)
|
||||
cut=cube('Temporary inlay recess',(x,-.100,z),(.25,.030,2*(3.7-z)+.002),core,0)
|
||||
bpy.context.view_layer.objects.active=front_cover
|
||||
mod=front_cover.modifiers.new('Flush index pocket','BOOLEAN');mod.operation='DIFFERENCE';mod.object=cut
|
||||
bpy.ops.object.modifier_apply(modifier=mod.name);bpy.data.objects.remove(cut,do_unlink=True)
|
||||
cube('Index flush inlay',(x,-.102,z),(.25,.002,2*(3.7-z)),inlay,.0008)
|
||||
|
||||
route('outer upper left',[(974,14),(35,14),(35,612)])
|
||||
route('outer lower right',[(1011,64),(1011,655),(80,655)])
|
||||
route('upper circuit',[(35,171),(54,171),(78,143),(78,76),(98,57),(172,57),
|
||||
(191,46),(383,46),(405,57),(598,57),(617,68),(650,68),(655,64),(650,57),
|
||||
(620,57),(605,44),(600,43),(596,47),(597,54),(614,67),(760,67),
|
||||
(779,53),(782,47),(778,43),(773,44),(757,57),(722,57),(719,61),(720,68),
|
||||
(763,68),(780,57),(894,57),(917,40),(973,40)])
|
||||
route('left hook',[(68,47),(51,64),(51,105),(53,111),(60,113),(64,109),
|
||||
(64,70),(90,42),(128,42),(134,39),(135,34),(131,28),(119,28)])
|
||||
route('inner frame',[(115,565),(115,84),(132,67),(948,67),(968,85),(968,162),
|
||||
(981,192),(981,550),(909,622),(131,622),(118,609),(118,590)])
|
||||
route('lower return',[(128,629),(911,629),(985,556)])
|
||||
route('bottom catch',[(793,652),(798,648),(890,648),(894,653),(1010,653)])
|
||||
|
||||
for label,px,py,start,end in [('upper right',999,37,0.30*math.pi,1.84*math.pi),
|
||||
('lower left',57,642,-.75*math.pi,.78*math.pi)]:
|
||||
x,z=case_point(px,py)
|
||||
# C-shaped shallow boss, tied into the pressed frame rather than a raised ring.
|
||||
for radius in [.104,.127]:
|
||||
points=[(x+radius*math.cos(start+(end-start)*i/64),
|
||||
z+radius*math.sin(start+(end-start)*i/64)) for i in range(65)]
|
||||
channel('Case engraved screw boss '+label,points,-.076,.0045,engraving)
|
||||
channel('Case pressed screw boss '+label,[(a+.006,b-.008) for a,b in points],-.080,.004,lip)
|
||||
# A real opening leaves the flush metal head readable under a frosted cover.
|
||||
bpy.ops.mesh.primitive_cylinder_add(vertices=64,radius=.082,depth=.06,
|
||||
location=(x,-.095,z),rotation=(math.pi/2,0,0))
|
||||
cut=bpy.context.object
|
||||
bpy.context.view_layer.objects.active=front_cover
|
||||
mod=front_cover.modifiers.new('Flush fastener opening','BOOLEAN');mod.operation='DIFFERENCE';mod.object=cut
|
||||
bpy.ops.object.modifier_apply(modifier=mod.name);bpy.data.objects.remove(cut,do_unlink=True)
|
||||
annular_profile('Countersunk washer '+label,x,z,
|
||||
[(.061,-.094),(.080,-.094),(.080,-.100),(.071,-.103),(.063,-.103),(.061,-.099)],lip,64)
|
||||
bpy.ops.mesh.primitive_cylinder_add(vertices=48,radius=.060,depth=.010,
|
||||
location=(x,-.098,z),rotation=(math.pi/2,0,0))
|
||||
head=bpy.context.object;head.name='Machined screw '+label;head.data.materials.append(metal)
|
||||
mod=head.modifiers.new('Head chamfer','BEVEL');mod.width=.003;mod.segments=3
|
||||
bpy.ops.object.modifier_apply(modifier=mod.name)
|
||||
for angle in [-.22,math.pi/2-.22]:
|
||||
cut=cube('Temporary drive cutter',(x,-.103,z),(.069,.008,.014),core,.001)
|
||||
cut.rotation_euler.y=angle
|
||||
bpy.context.view_layer.objects.active=head
|
||||
mod=head.modifiers.new('Cross recessed drive','BOOLEAN');mod.operation='DIFFERENCE';mod.object=cut
|
||||
bpy.ops.object.modifier_apply(modifier=mod.name);bpy.data.objects.remove(cut,do_unlink=True)
|
||||
head.modifiers.new('Fastener normals','WEIGHTED_NORMAL')
|
||||
Reference in New Issue
Block a user