Initial commit: RHINE LAB · ANALYSIS OS:三维界面与动效实验(本仓库不含个人归档索引数据)

This commit is contained in:
WpyQwq
2026-09-19 13:04:56 +08:00
commit 3f422218a5
1098 changed files with 39410 additions and 0 deletions
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

+208
View File
@@ -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')
+57
View File
@@ -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}))
+47
View File
@@ -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)
+107
View File
@@ -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.
+22
View File
@@ -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'))
+63
View File
@@ -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')