@bitmagic/cli 0.1.66-dev.5 → 0.1.66-dev.7

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.
Files changed (99) hide show
  1. package/README.md +36 -1
  2. package/assets/character-skin-authoring/SKILL.md +43 -0
  3. package/assets/character-skin-authoring/references/exporters.md +134 -0
  4. package/assets/character-skin-authoring/references/eyes.md +215 -0
  5. package/assets/character-skin-authoring/references/materials.md +115 -0
  6. package/assets/character-skin-authoring/references/playground.md +78 -0
  7. package/assets/character-skin-authoring/references/profiles.md +97 -0
  8. package/assets/character-skin-authoring/references/projection.md +190 -0
  9. package/assets/character-skin-authoring/references/regional-detail.md +72 -0
  10. package/assets/character-skin-authoring/scripts/assemble_character.py +205 -0
  11. package/assets/character-skin-authoring/scripts/authored_detail.py +47 -0
  12. package/assets/character-skin-authoring/scripts/authored_surface_patch.py +83 -0
  13. package/assets/character-skin-authoring/scripts/brow_groom.py +62 -0
  14. package/assets/character-skin-authoring/scripts/build_face_detail.py +107 -0
  15. package/assets/character-skin-authoring/scripts/build_surface_candidate.py +28 -0
  16. package/assets/character-skin-authoring/scripts/canthus_wall.py +57 -0
  17. package/assets/character-skin-authoring/scripts/character_build.py +54 -0
  18. package/assets/character-skin-authoring/scripts/eye_assets.py +333 -0
  19. package/assets/character-skin-authoring/scripts/eye_opening.py +114 -0
  20. package/assets/character-skin-authoring/scripts/eye_texture.py +35 -0
  21. package/assets/character-skin-authoring/scripts/eyelid_detail.py +34 -0
  22. package/assets/character-skin-authoring/scripts/eyelid_morph.py +171 -0
  23. package/assets/character-skin-authoring/scripts/eyelid_patch.py +173 -0
  24. package/assets/character-skin-authoring/scripts/face_atlas.py +51 -0
  25. package/assets/character-skin-authoring/scripts/geometry_repair.py +112 -0
  26. package/assets/character-skin-authoring/scripts/glb_arrays.py +13 -0
  27. package/assets/character-skin-authoring/scripts/glb_output.py +21 -0
  28. package/assets/character-skin-authoring/scripts/inspect_skin_point.py +66 -0
  29. package/assets/character-skin-authoring/scripts/normal_patch_height.py +38 -0
  30. package/assets/character-skin-authoring/scripts/prepare_face_reference.py +23 -0
  31. package/assets/character-skin-authoring/scripts/regional_atlas.py +51 -0
  32. package/assets/character-skin-authoring/scripts/select_preview.py +90 -0
  33. package/assets/character-skin-authoring/scripts/skin_views.py +267 -0
  34. package/assets/character-skin-authoring/scripts/surface_pbr_maps.py +46 -0
  35. package/assets/character-skin-authoring/scripts/texture_repair.py +98 -0
  36. package/assets/character-skin-authoring/scripts/vertex_detail_support.py +80 -0
  37. package/assets/character-skin-authoring/scripts/vision_blend_materials.py +74 -0
  38. package/assets/character-skin-authoring/scripts/vision_materials.py +276 -0
  39. package/assets/character-skin-authoring/scripts/workspace_paths.py +18 -0
  40. package/assets/metahuman/src/work/metahuman/MetaHumanActor.ts +580 -0
  41. package/assets/metahuman/src/work/metahuman/MetaHumanLabHooks.ts +122 -0
  42. package/assets/metahuman/src/work/metahuman/MetaHumanMaterials.ts +500 -0
  43. package/assets/metahuman/src/work/metahuman/MetaHumanSpeech.ts +242 -0
  44. package/assets/metahuman/src/work/metahuman/StudioEnvironment.ts +142 -0
  45. package/assets/metahuman/tools/lipsync/make_line.py +181 -0
  46. package/assets/metahuman/tools/metahuman/README.md +21 -0
  47. package/assets/metahuman/tools/metahuman/e2e/eval.mjs +34 -0
  48. package/assets/metahuman/tools/metahuman/e2e/probes/arm-pose-check.js +32 -0
  49. package/assets/metahuman/tools/metahuman/e2e/probes/bone-displacement.js +26 -0
  50. package/assets/metahuman/tools/metahuman/e2e/probes/clip-shoulder.js +41 -0
  51. package/assets/metahuman/tools/metahuman/e2e/probes/grafted-bones.js +11 -0
  52. package/assets/metahuman/tools/metahuman/e2e/probes/helper-follow-descent.js +38 -0
  53. package/assets/metahuman/tools/metahuman/e2e/probes/helper-follow-sweep.js +31 -0
  54. package/assets/metahuman/tools/metahuman/e2e/probes/helper-joints-inspect.js +10 -0
  55. package/assets/metahuman/tools/metahuman/e2e/probes/outfit-bad-vertices.js +26 -0
  56. package/assets/metahuman/tools/metahuman/e2e/probes/outfit-body-material.js +7 -0
  57. package/assets/metahuman/tools/metahuman/e2e/probes/outfit-own-skeleton.js +8 -0
  58. package/assets/metahuman/tools/metahuman/e2e/probes/shirt-bone-ownership.js +25 -0
  59. package/assets/metahuman/tools/metahuman/e2e/probes/shirt-edge-stretch.js +26 -0
  60. package/assets/metahuman/tools/metahuman/e2e/probes/shirt-long-edges.js +23 -0
  61. package/assets/metahuman/tools/metahuman/e2e/probes/walk-sampler.js +105 -0
  62. package/assets/metahuman/tools/metahuman/e2e/shot.mjs +125 -0
  63. package/assets/metahuman/tools/metahuman/e2e/walk-probe.mjs +43 -0
  64. package/assets/metahuman/tools/metahuman/merge-skeletons.mjs +69 -0
  65. package/assets/metahuman/tools/metahuman/package.json +13 -0
  66. package/assets/metahuman/tools/metahuman/prepare.sh +25 -0
  67. package/assets/metahuman/tools/metahuman/reorder-body-first.mjs +20 -0
  68. package/assets/metahuman/tools/metahuman/smooth-outfit-weights.mjs +48 -0
  69. package/assets/metahuman/tools/metahuman/strip-draco.mjs +32 -0
  70. package/assets/metahuman/tools/metahuman/transfer-skin-weights.mjs +90 -0
  71. package/dist/commands/generate.d.ts +49 -0
  72. package/dist/commands/generate.js +168 -1
  73. package/dist/commands/generate.js.map +1 -1
  74. package/dist/generate/audio-file.d.ts +6 -0
  75. package/dist/generate/audio-file.js +42 -0
  76. package/dist/generate/audio-file.js.map +1 -0
  77. package/dist/generate/music.d.ts +4 -3
  78. package/dist/generate/music.js +6 -23
  79. package/dist/generate/music.js.map +1 -1
  80. package/dist/generate/stream.d.ts +13 -0
  81. package/dist/generate/stream.js +14 -1
  82. package/dist/generate/stream.js.map +1 -1
  83. package/dist/scaffold/asset-pipeline-skill-files.js +2 -0
  84. package/dist/scaffold/asset-pipeline-skill-files.js.map +1 -1
  85. package/dist/scaffold/character-skin-skill-files.d.ts +2 -0
  86. package/dist/scaffold/character-skin-skill-files.js +49 -0
  87. package/dist/scaffold/character-skin-skill-files.js.map +1 -0
  88. package/dist/scaffold/metahuman-skill-files.d.ts +21 -0
  89. package/dist/scaffold/metahuman-skill-files.js +306 -0
  90. package/dist/scaffold/metahuman-skill-files.js.map +1 -0
  91. package/dist/scaffold/project-files.js +4 -4
  92. package/dist/scaffold/project-files.js.map +1 -1
  93. package/dist/scaffold/project.js +4 -0
  94. package/dist/scaffold/project.js.map +1 -1
  95. package/dist/scaffold/skill-files.js +3 -1
  96. package/dist/scaffold/skill-files.js.map +1 -1
  97. package/dist/tools/bundles.js +8 -0
  98. package/dist/tools/bundles.js.map +1 -1
  99. package/package.json +4 -4
@@ -0,0 +1,54 @@
1
+ """Run the reviewed character exporters from an installed skill and explicit workspace.
2
+
3
+ This assembles authored inputs. It performs no anatomical recognition, asset download,
4
+ paid generation, rig fitting or automatic visual acceptance.
5
+ """
6
+ import argparse,json,os,re,subprocess,sys
7
+ from pathlib import Path
8
+
9
+ def command(args):
10
+ root=args.workspace.expanduser().resolve()
11
+ if not root.is_dir():raise ValueError('Workspace must be an existing directory')
12
+ if not re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9_-]*',args.id):raise ValueError('Character ID must be one path component')
13
+ base=root/'characters'/args.id/'high/v1'
14
+ if root not in base.resolve().parents:raise ValueError('Character output escapes workspace')
15
+ def path(value):return (root/value).resolve() if value else None
16
+ evidence,masks,eyes=map(path,[args.evidence,args.masks,args.eyes_profile])
17
+ script=Path(__file__).resolve().parent;required=[]
18
+ if args.stage=='face':
19
+ required=[root/'highquality/materials/faces'/f'{args.id}.json',base/'source/hy31pro.glb',base/'output/viewer-profile.json']
20
+ required += [root/'highquality/references/lee-perry-smith'/name for name in ['LeePerrySmith.glb','Infinite-Level_02_Disp_NoSmoothUV-4096.jpg','Map-COL.jpg']]
21
+ argv=[sys.executable,str(script/'build_face_detail.py'),'--id',args.id]
22
+ elif args.stage=='surfaces':
23
+ if not evidence or not masks:raise ValueError('Surfaces require --evidence and --masks')
24
+ required=[evidence/'manifest.json',masks/'report.json',masks/'material-0-surface.npz',base/'output/viewer-profile.json',root/'highquality/materials/faces'/f'{args.id}.json']
25
+ required += [base/'materials/v2'/name for name in ['face-detail.png','face-expression.png','scan-band-height-m.npy','ATTRIBUTION.txt']]
26
+ argv=[sys.executable,str(script/'build_surface_candidate.py'),'--id',args.id,'--evidence',str(evidence),'--masks',str(masks)]
27
+ if eyes:argv+=['--eyes-profile',str(eyes)]
28
+ if args.assemble:argv+=['--assemble']
29
+ elif args.stage=='eyes':
30
+ if not eyes:raise ValueError('Eyes require --eyes-profile')
31
+ argv=[sys.executable,str(script/'eye_assets.py'),'--profile',str(eyes)]
32
+ else:
33
+ required=[base/args.input]
34
+ argv=[sys.executable,str(script/'assemble_character.py'),'--id',args.id,'--input',args.input,'--variant',args.variant]
35
+ if eyes:
36
+ required += [eyes,root/'highquality/references/makehuman-eyes/blue_eye.png',root/'highquality/references/makehuman-eyes/LICENSE.md']
37
+ if eyes.is_file() and json.loads(eyes.read_text()).get('id')!=args.id:raise ValueError('Eye profile character mismatch')
38
+ if args.assemble or args.stage=='assemble':required += [base/'output/character.glb',base/'rig-report.json']
39
+ missing=[str(p) for p in required if not p.is_file()]
40
+ if missing:raise ValueError('Missing authored inputs:\n'+'\n'.join(missing))
41
+ return root,argv,required
42
+
43
+ def main():
44
+ p=argparse.ArgumentParser(description=__doc__)
45
+ p.add_argument('--workspace',required=True,type=Path);p.add_argument('--id',required=True)
46
+ p.add_argument('--stage',choices=['face','surfaces','eyes','assemble'],default='surfaces')
47
+ p.add_argument('--evidence',type=Path);p.add_argument('--masks',type=Path);p.add_argument('--eyes-profile',type=Path)
48
+ p.add_argument('--assemble',action='store_true');p.add_argument('--input',default='eyes/v1/character.glb');p.add_argument('--variant',choices=['eyes','surfaces'],default='eyes')
49
+ p.add_argument('--check',action='store_true',help='Check required inputs without modifying assets; exporters validate their full contracts when run')
50
+ args=p.parse_args();root,argv,required=command(args)
51
+ if args.check:print(json.dumps({'status':'inputs_present_not_visual_acceptance','workspace':str(root),'stage':args.stage,'requiredFiles':len(required)}));return
52
+ subprocess.run(argv,cwd=root,env={**os.environ,'BITMAGIC_CHARACTER_WORKSPACE':str(root)},check=True)
53
+
54
+ if __name__=='__main__':main()
@@ -0,0 +1,333 @@
1
+ """Independent optical eye geometry fitted from agent-authored source-view landmarks.
2
+
3
+ The adapter preserves the skin revision and writes a separate eye revision. Geometry,
4
+ UV conventions and optical surfaces are a single contract; no anatomy classifier runs.
5
+ """
6
+ import argparse,copy,hashlib,json,struct,shutil
7
+ from pathlib import Path
8
+ import numpy as np
9
+ from scipy.spatial.transform import Rotation
10
+ from glb_arrays import read,accessor
11
+ from glb_output import append_accessor
12
+ from vision_materials import source_world_points
13
+ from eye_opening import cut_openings,refine_authored_contours
14
+ from eyelid_morph import add_blinks,conform_open_lid,surface_normal_groups,surface_normals,transport_normals
15
+ from eye_texture import build_texture,optical_uv
16
+ from workspace_paths import workspace_root
17
+ ROOT=workspace_root(__file__)
18
+
19
+ def ray_hit(origin,direction,points,triangles):
20
+ a=points[triangles[:,0]];e1=points[triangles[:,1]]-a;e2=points[triangles[:,2]]-a
21
+ h=np.cross(direction,e2);det=(e1*h).sum(1);safe=np.where(abs(det)>1e-12,det,1);s=origin-a;u=(s*h).sum(1)/safe;q=np.cross(s,e1);v=(q*direction).sum(1)/safe;t=(e2*q).sum(1)/safe
22
+ valid=(abs(det)>1e-12)&(u>=0)&(v>=0)&(u+v<=1)&(t>0)
23
+ if not valid.any():raise ValueError('An eye landmark ray misses the source; review the exact evidence')
24
+ return origin+direction*t[valid].min()
25
+
26
+ def projected_ray_candidates(projected,triangles,contour):
27
+ """Conservative screen-space broad phase; retain all depths/occluders."""
28
+ contour=np.asarray(contour,float);lo=contour.min(0)-1;hi=contour.max(0)+1
29
+ bounds=projected[triangles]
30
+ return triangles[((bounds.min(1)<=hi)&(bounds.max(1)>=lo)).all(1)]
31
+
32
+ def constrain_orbital_backing(depth,radial_distance,limbus,unit):
33
+ # Continue the interior cap outside the globe too. Keeping source-surface
34
+ # depth at those canthi leaves backing ahead of the closing lid.
35
+ inner_radius=limbus-.0013*unit
36
+ envelope=np.sqrt(np.maximum(0,inner_radius*inner_radius-radial_distance*radial_distance))-.0001*unit
37
+ return np.minimum(depth,envelope)
38
+
39
+ def radial_mesh(radii,depths,colors,segments=192):
40
+ theta=np.arange(segments)*2*np.pi/segments;c=np.cos(theta);s=np.sin(theta)
41
+ radius=np.broadcast_to(np.asarray(radii)[:,None],(len(radii),segments));depth=np.broadcast_to(np.asarray(depths)[:,None],radius.shape).copy()
42
+ if callable(colors):color,relief=colors(radius,theta[None,:]);depth+=relief
43
+ else:color=np.broadcast_to(colors,(*radius.shape,3)).copy()
44
+ p=np.stack([radius*c,radius*s,depth],-1).reshape(-1,3);indices=[]
45
+ for row in range(len(radii)-1):
46
+ for k in range(segments):
47
+ a=row*segments+k;b=row*segments+(k+1)%segments;c1=b+segments;d=a+segments;indices.extend([[a,b,d],[b,c1,d]])
48
+ ids=np.asarray(indices)[:,::-1];n=np.zeros_like(p);face=np.cross(p[ids[:,1]]-p[ids[:,0]],p[ids[:,2]]-p[ids[:,0]])
49
+ for k in range(3):np.add.at(n,ids[:,k],face)
50
+ n/=np.maximum(np.linalg.norm(n,axis=1,keepdims=True),1e-12)
51
+ return p,n,color.reshape(-1,3),ids
52
+
53
+ def write_glb(doc,data,path):
54
+ doc['buffers'][0]['byteLength']=len(data);payload=json.dumps(doc,separators=(',',':')).encode();payload+=b' '*((-len(payload))%4);data.extend(b'\0'*((-len(data))%4))
55
+ raw=struct.pack('<III',0x46546c67,2,28+len(payload)+len(data))+struct.pack('<II',len(payload),0x4e4f534a)+payload+struct.pack('<II',len(data),0x004e4942)+data
56
+ temporary=path.with_suffix('.part.glb');temporary.write_bytes(raw);temporary.replace(path);return hashlib.sha256(raw).hexdigest()
57
+
58
+ def build(profile_path):
59
+ fit=json.loads(profile_path.read_text());sid=fit['id'];base=ROOT/'characters'/sid/'high/v1';out=base/'eyes/v1';out.mkdir(parents=True,exist_ok=True)
60
+ fit['eyes']=[refine_authored_contours(eye) for eye in fit['eyes']]
61
+ if sorted(e['anatomicalSide'] for e in fit['eyes'])!=['left','right']:raise ValueError('This adapter requires one explicitly authored left and right eye')
62
+ evidence=Path(fit['evidence']);manifest=json.loads((evidence/'manifest.json').read_text());raw_source=Path(manifest['source'])
63
+ if manifest.get('sourceSha256')!=fit['sourceSha256']:raise ValueError('Eye evidence belongs to another source')
64
+ if hashlib.sha256(raw_source.read_bytes()).hexdigest()!=fit['sourceSha256']:raise ValueError('Eye fit source hash mismatch')
65
+ source,source_data=read(raw_source);prim=source['meshes'][0]['primitives'][0];local=accessor(source,source_data,prim['attributes']['POSITION']);tri=accessor(source,source_data,prim['indices']).reshape(-1,3);points=source_world_points(source,local)
66
+ revision=fit.get('skinRevision','v2')
67
+ if revision not in ['v2','v3','v4']:raise ValueError('Unsupported skin revision')
68
+ skin_path=base/'materials'/revision/'character.glb'
69
+ doc,data=read(skin_path);skin_hash=hashlib.sha256(skin_path.read_bytes()).hexdigest()
70
+ repair_report=None
71
+ if fit.get('sourceAlbedoRepairMask'):
72
+ from texture_repair import apply_reviewed_repair
73
+ repair_report=apply_reviewed_repair(doc,data,ROOT/fit['sourceAlbedoRepairMask'],out,fit['sourceSha256'])
74
+ old=doc['meshes'][0]['primitives'];removed=sum(accessor(doc,data,p['indices']).size//3 for p in old if doc['materials'][p['material']].get('extras',{}).get('characterSurface')=='eyes')
75
+ if not removed and fit.get('openingAuthority')!='reviewed-contours':
76
+ raise ValueError('No reviewed ocular primitives; explicitly authorize reviewed-contours after inspecting the source eye evidence')
77
+ openings=[];lid_detail_bindings=[]
78
+ height=np.ptp(points[:,2]);center=(points.max(0)+points.min(0))/2;physical_height=json.loads((base/'output/viewer-profile.json').read_text())['height']
79
+ if not np.isfinite(physical_height) or physical_height<=0:raise ValueError('Character physical height must be positive finite metres')
80
+ unit=height/physical_height
81
+ origin_basis=source_world_points(source,np.zeros((1,3)))[0];basis=(source_world_points(source,np.eye(3))-origin_basis).T;inverse=np.linalg.inv(basis)
82
+ parent=next(i for i,node in enumerate(doc['nodes']) if node.get('mesh')==0);eye_reports=[];texture_provenance=[]
83
+ for eye in fit['eyes']:
84
+ view=next(v for v in manifest['views'] if v['name']==eye['view']);image_path=evidence/(eye['view']+'.png')
85
+ if hashlib.sha256(image_path.read_bytes()).hexdigest()!=manifest['evidenceHashes'][image_path.name]:raise ValueError('Eye evidence image changed')
86
+ direction=np.array(view['direction'],float);direction/=np.linalg.norm(direction);up=np.array(view.get('up',[0,0,1]),float);right=np.cross(direction,up);right/=np.linalg.norm(right);up=np.cross(right,direction)
87
+ target=center+height*np.array(view.get('targetOffset',[0,0,0]));span=height*view['span'];x,y=eye['irisCenterPx'];point=target+right*((x+.5)/manifest['size']-.5)*span+up*(.5-(y+.5)/manifest['size'])*span
88
+ front=ray_hit(point-direction*height*2,direction,points,tri)
89
+ projected=np.stack([((points-target)@right/span+.5)*manifest['size'],(.5-(points-target)@up/span)*manifest['size']],1)
90
+ openings.append({'pixels':projected,'contour':eye['openingPx'],'surfaceHit':front,'radius':eye['globeRadiusM']*unit,'side':eye['anatomicalSide']})
91
+ radius=eye['globeRadiusM']*unit;iris=eye['irisRadiusPx']/manifest['size']*span;pupil=iris*eye['pupilRatio'];bulge=eye['corneaBulgeM']*unit
92
+ if not 0<pupil<iris<radius or not 0<bulge<radius*.3:raise ValueError('Eye dimensions must satisfy pupil < iris < globe and a positive, shallow cornea')
93
+ limbus=np.sqrt(radius*radius-iris*iris)
94
+ eye_center=front+direction*(limbus+bulge-eye['frontOffsetM']*unit);axes=inverse@np.stack([right,up,-direction],1);rotation=Rotation.from_matrix(axes).as_quat();translation=inverse@(eye_center-origin_basis)
95
+ group=len(doc['nodes']);doc['nodes'].append({'name':'OpticalEye_'+eye['name'],'translation':translation.tolist(),'rotation':rotation.tolist(),'children':[],'extras':{'characterEye':True,'forwardAxis':'+Z','fitSource':fit['sourceSha256']}});doc['nodes'][parent].setdefault('children',[]).append(group)
96
+ def add(name,geometry,material,parent_node=group,uv=None):
97
+ p,n,c,ids=geometry;mi=len(doc['materials']);doc['materials'].append({'name':eye['name']+'_'+name,'extras':{'characterSurface':'eyes','eyeLayer':name,'eyeSide':eye['anatomicalSide']},**material})
98
+ attributes={key:append_accessor(doc,data,arr.astype('<f4'),'VEC3',5126) for key,arr in [('POSITION',p),('NORMAL',n)]}
99
+ attributes['COLOR_0']=append_accessor(doc,data,c.astype('<f4'),'VEC4' if c.shape[1]==4 else 'VEC3',5126)
100
+ if uv is not None:attributes['TEXCOORD_0']=append_accessor(doc,data,uv.astype('<f4'),'VEC2',5126)
101
+ mesh=len(doc['meshes']);doc['meshes'].append({'name':eye['name']+'_'+name,'primitives':[{'attributes':attributes,'indices':append_accessor(doc,data,ids.astype('<u4').reshape(-1),'SCALAR',5125),'material':mi}]})
102
+ node=len(doc['nodes']);doc['nodes'].append({'name':eye['name']+'_'+name,'mesh':mesh});doc['nodes'][parent_node]['children'].append(node)
103
+ def material(rough,extensions=None):return {'pbrMetallicRoughness':{'baseColorFactor':[1,1,1,1],'metallicFactor':0,'roughnessFactor':rough},'extensions':extensions or {}}
104
+ theta=np.linspace(np.arcsin(iris/radius),np.pi-.001,64)
105
+ eye_texture,provenance=build_texture(doc,data,out,eye['irisColorLinear']);texture_provenance.append(provenance)
106
+ sclera=radial_mesh(radius*np.sin(theta),radius*np.cos(theta),[.70,.67,.65])
107
+ sclera_material=material(.28,{'KHR_materials_clearcoat':{'clearcoatFactor':1,'clearcoatRoughnessFactor':.08},'KHR_materials_ior':{'ior':1.376}})
108
+ sclera_material['pbrMetallicRoughness']['baseColorTexture']={'index':eye_texture}
109
+ add('sclera',sclera,sclera_material,uv=optical_uv(sclera[0],iris))
110
+ # The transparent cornea supplies the air-interface reflection. Do not
111
+ # add another broad wet-surface reflection on the recessed iris or pupil.
112
+ rr=np.linspace(pupil,iris,64);depth=limbus-.0008*unit*(1-(rr-pupil)/(iris-pupil))
113
+ iris_geometry=radial_mesh(rr,depth,[1,1,1]);iris_material=material(.5,{'KHR_materials_specular':{'specularFactor':0}})
114
+ iris_material['pbrMetallicRoughness']['baseColorTexture']={'index':eye_texture}
115
+ add('iris',iris_geometry,iris_material,uv=optical_uv(iris_geometry[0],iris))
116
+ add('pupil',radial_mesh(np.linspace(0,pupil*1.02,8),np.full(8,limbus-.00095*unit),[.001,.001,.001]),material(.7,{'KHR_materials_specular':{'specularFactor':0}}))
117
+ rr=np.linspace(0,iris*1.002,64);zz=limbus+bulge*np.maximum(0,1-(rr/iris)**2)
118
+ add('cornea',radial_mesh(rr,zz,[1,1,1]),material(.025,{'KHR_materials_transmission':{'transmissionFactor':1},'KHR_materials_volume':{'thicknessFactor':.0005*unit,'attenuationDistance':10,'attenuationColor':[1,1,1]},'KHR_materials_ior':{'ior':1.376}}))
119
+ # A head-fixed contact strip follows the authored lid contour, never the gaze node.
120
+ contour=np.asarray(eye['openingPx'],float);middle=contour.mean(0);rings=[];alpha=[]
121
+ aperture_triangles=projected_ray_candidates(projected,tri,contour)
122
+ openings[-1].update(center=eye_center,right=right,up=up,direction=direction,target=target,span=span,size=manifest['size'],iris=iris,bulge=bulge,unit=unit,cornerDepths=eye.get('cornerDepths',[]))
123
+ from eyelid_patch import authored_corner_support
124
+ def corner_surface(plane):
125
+ return authored_corner_support(ray_hit(plane-direction*height*2,direction,points,aperture_triangles),plane,openings[-1])
126
+ # Mucosal backing closes the orbital opening outside the spherical globe.
127
+ # It is behind the cornea/iris and attached to the head, not the gaze node.
128
+ boundary_depth=[]
129
+ for pixel in contour:
130
+ on_plane=target+right*(pixel[0]/manifest['size']-.5)*span+up*(.5-pixel[1]/manifest['size'])*span
131
+ boundary_depth.append((corner_surface(on_plane)-eye_center)@(-direction)-.0001*unit)
132
+ edge_steps=np.linspace(0,1,8,endpoint=False)
133
+ boundary=np.concatenate([a[None,:]*(1-edge_steps[:,None])+b[None,:]*edge_steps[:,None] for a,b in zip(contour,np.roll(contour,-1,axis=0))])
134
+ boundary_depth=np.concatenate([a*(1-edge_steps)+b*edge_steps for a,b in zip(boundary_depth,np.roll(boundary_depth,-1))])
135
+ backing=[];radial_steps=20
136
+ for fraction in np.linspace(0,1,radial_steps):
137
+ pixels=middle[None,:]*(1-fraction)+boundary*fraction
138
+ plane=target[None,:]+right[None,:]*((pixels[:,0]/manifest['size']-.5)*span)[:,None]+up[None,:]*((.5-pixels[:,1]/manifest['size'])*span)[:,None]
139
+ xy=np.stack([(plane-eye_center)@right,(plane-eye_center)@up],1);r=np.linalg.norm(xy,axis=1)
140
+ depth=(limbus-.004*unit)*(1-fraction)+boundary_depth*fraction
141
+ # A coarse fan can cut across the globe. Sample the backing finely and
142
+ # constrain it behind every optical layer wherever their domains overlap.
143
+ # A spherical inset is gaze-invariant: a neutral-view iris-depth cap
144
+ # can cross the recessed iris when the globe rotates.
145
+ depth=constrain_orbital_backing(depth,r,limbus,unit)
146
+ world=eye_center+xy[:,0,None]*right+xy[:,1,None]*up-depth[:,None]*direction
147
+ backing.extend((world-origin_basis)@inverse.T)
148
+ backing=np.asarray(backing);backing_ids=[];count=len(boundary)
149
+ for row in range(radial_steps-1):
150
+ for k in range(count):
151
+ a=row*count+k;b=row*count+(k+1)%count;backing_ids.extend([[a,b,a+count],[b,b+count,a+count]])
152
+ backing_material=material(.55,{'KHR_materials_specular':{'specularFactor':.2}});backing_material['doubleSided']=True
153
+ add('orbital-mucosa',(backing,np.broadcast_to(inverse@(-direction),backing.shape),np.broadcast_to([.11,.035,.03],backing.shape),np.asarray(backing_ids)),backing_material,parent)
154
+ if eye.get('caruncle'):
155
+ authored=eye['caruncle'];corner=np.asarray(eye['openingPx'][authored['innerContourIndex']],float)
156
+ toward=np.asarray(eye['irisCenterPx'],float)-corner;toward/=np.linalg.norm(toward)
157
+ pixel=corner+toward*(authored['insetM']*unit/span*manifest['size'])
158
+ plane=target+right*(pixel[0]/manifest['size']-.5)*span+up*(.5-pixel[1]/manifest['size'])*span
159
+ xy=np.array([(plane-eye_center)@right,(plane-eye_center)@up])
160
+ dimensions=np.asarray(authored['radiiM'],float)*unit
161
+ if dimensions.shape!=(3,) or not np.isfinite(dimensions).all() or np.any(dimensions<=0) or np.any(dimensions>radius*.2):raise ValueError('Invalid authored caruncle dimensions')
162
+ theta=np.linspace(.001,np.pi-.001,20)
163
+ tissue,_,colors,faces=radial_mesh(dimensions[0]*np.sin(theta),dimensions[2]*np.cos(theta),authored['colorLinear'],48)
164
+ tissue[:,1]*=dimensions[1]/dimensions[0]
165
+ lateral=tissue[:,:2]+xy
166
+ cap=np.sqrt(np.maximum(0,radius*radius-np.sum(lateral*lateral,axis=1)))
167
+ center_depth=np.sqrt(max(0,radius*radius-np.dot(xy,xy)))-dimensions[2]-.00045*unit
168
+ depths=np.minimum(tissue[:,2]+center_depth,cap-.0004*unit)
169
+ if authored.get('support') == 'corner-lining-v1':
170
+ from canthus_wall import lining_depth_at_radius
171
+ walls=[wall for wall in eye.get('canthusWalls',[]) if authored['innerContourIndex'] in wall['contourIndices']]
172
+ if len(walls)!=1:raise ValueError('Corner tissue requires one explicitly authored supporting lining')
173
+ corner_plane=target+right*(corner[0]/manifest['size']-.5)*span+up*(.5-corner[1]/manifest['size'])*span
174
+ corner_xy=np.array([(corner_plane-eye_center)@right,(corner_plane-eye_center)@up])
175
+ corner_hit=corner_surface(corner_plane)
176
+ support_depth=lining_depth_at_radius(np.linalg.norm(xy),np.linalg.norm(corner_xy),(corner_hit-eye_center)@(-direction),radius,iris,unit,walls[0].get('contactMethod')=='reconstructed-lid-v1')
177
+ depths=tissue[:,2]+support_depth+.00005*unit
178
+ # Medial tissue may cover the peripheral sclera. Clamping it
179
+ # behind the whole globe creates a deep discontinuity and hides
180
+ # it behind the lining. Protect the swept iris instead.
181
+ swept=iris*np.cos(.35)+np.sqrt(radius*radius-iris*iris)*np.sin(.35)
182
+ if np.linalg.norm(lateral,axis=1).min()<swept+.00035*unit:
183
+ raise ValueError('Authored corner tissue encroaches on the reviewed iris gaze envelope')
184
+ elif authored.get('support') is not None:raise ValueError('Unsupported corner tissue support')
185
+ world=eye_center+lateral[:,0,None]*right+lateral[:,1,None]*up-depths[:,None]*direction
186
+ local_tissue=(world-origin_basis)@inverse.T
187
+ normals=surface_normals(local_tissue,faces,surface_normal_groups(local_tissue))
188
+ wet=material(.4,{'KHR_materials_clearcoat':{'clearcoatFactor':.3,'clearcoatRoughnessFactor':.12},'KHR_materials_ior':{'ior':1.376}})
189
+ add('caruncle',(local_tissue,normals,colors,faces),wet,parent)
190
+ # Resolve the curved lid/globe contact between authored landmarks too.
191
+ # The sparse authoring contour is not a sufficient render tessellation.
192
+ contact_contour=boundary[::2]
193
+ for inset in [0,.07,.19]:
194
+ for pixel in contact_contour:
195
+ xy=pixel*(1-inset)+middle*inset
196
+ on_plane=target+right*(xy[0]/manifest['size']-.5)*span+up*(.5-xy[1]/manifest['size'])*span
197
+ surface_hit=ray_hit(on_plane-direction*height*2,direction,points,aperture_triangles)
198
+ local_xy=np.array([(on_plane-eye_center)@right,(on_plane-eye_center)@up]);rad=np.linalg.norm(local_xy)
199
+ optical=limbus+bulge*max(0,1-(rad/iris)**2) if rad<iris else np.sqrt(max(0,radius*radius-rad*rad))
200
+ forward_depth=optical+.00033*unit if eye.get('outerLidPx') else max((surface_hit-eye_center)@(-direction),optical)+.00008*unit
201
+ point=eye_center+right*local_xy[0]+up*local_xy[1]-direction*forward_depth
202
+ rings.append(inverse@(point-origin_basis));top=1 if pixel[1]<middle[1] else .55
203
+ alpha.append(top*{0:.60,.07:.18,.19:0}[inset])
204
+ contact_ids=[];count=len(contact_contour)
205
+ for row in range(2):
206
+ for k in range(count):
207
+ a=row*count+k;b=row*count+(k+1)%count;contact_ids.extend([[a,b,a+count],[b,b+count,a+count]])
208
+ contact_p=np.asarray(rings);contact_n=np.broadcast_to(inverse@(-direction),contact_p.shape);contact_c=np.zeros((len(rings),4));contact_c[:,3]=alpha
209
+ contact_mat=material(1,{'KHR_materials_specular':{'specularFactor':0}});contact_mat.update(alphaMode='BLEND',doubleSided=True)
210
+ add('lid-contact',(contact_p,contact_n,contact_c,np.asarray(contact_ids)),contact_mat,parent)
211
+ # A small fluid meniscus follows the reviewed lower lid, attached to the head.
212
+ lower=contact_p[np.asarray(eye['lowerContourIndices'])*4];path=np.concatenate([a[None,:]+np.linspace(0,1,8,endpoint=False)[:,None]*(b-a)[None,:] for a,b in zip(lower,lower[1:])]+[lower[-1:]])
213
+ tangent=np.gradient(path,axis=0);tangent/=np.maximum(np.linalg.norm(tangent,axis=1,keepdims=True),1e-12);fwd=inverse@(-direction);binormal=np.cross(tangent,fwd);binormal/=np.maximum(np.linalg.norm(binormal,axis=1,keepdims=True),1e-12)
214
+ theta=np.arange(8)*2*np.pi/8;meniscus_n=binormal[:,None,:]*np.cos(theta)[None,:,None]+fwd[None,None,:]*np.sin(theta)[None,:,None]
215
+ meniscus_p=path[:,None,:]+meniscus_n*(.00012*unit);meniscus_ids=[]
216
+ for j in range(len(path)-1):
217
+ for k in range(8):
218
+ a=j*8+k;b=j*8+(k+1)%8;meniscus_ids.extend([[a,b,a+8],[b,b+8,a+8]])
219
+ tear=material(.035,{'KHR_materials_transmission':{'transmissionFactor':.9},'KHR_materials_ior':{'ior':1.336}});tear['doubleSided']=True
220
+ add('tear-meniscus',(meniscus_p.reshape(-1,3),meniscus_n.reshape(-1,3),np.ones((len(path)*8,3)),np.asarray(meniscus_ids)),tear,parent)
221
+ if fit.get('lashes'):
222
+ lash_profile=fit['lashes'];lash_points=[];lash_normals=[];lash_roots=[];lash_ids=[]
223
+ for contour_indices,amount,length in [(eye['upperContourIndices'],lash_profile['upperCount'],lash_profile['upperLengthM']),(eye['lowerContourIndices'],lash_profile['lowerCount'],lash_profile['lowerLengthM'])]:
224
+ root_path=contact_p[np.asarray(contour_indices)*4];segments=np.linalg.norm(np.diff(root_path,axis=0),axis=1);arc=np.r_[0,np.cumsum(segments)]
225
+ upper=contour_indices==eye['upperContourIndices'];sign=1 if upper else -1
226
+ for strand,fraction in enumerate(np.linspace(.035,.965,amount)):
227
+ distance=fraction*arc[-1];edge=min(len(segments)-1,np.searchsorted(arc,distance,side='right')-1);t=(distance-arc[edge])/segments[edge]
228
+ root=root_path[edge]*(1-t)+root_path[edge+1]*t
229
+ variation=.82+.18*np.sin(strand*2.399963+len(lash_points))
230
+ size=length*unit*variation*(.55+.45*np.sin(np.pi*fraction))
231
+ outward=inverse@(-direction);vertical=inverse@up*sign;lateral=inverse@right
232
+ curve=[]
233
+ for q in np.linspace(0,1,8):
234
+ curve.append(root+size*(outward*(.8*q-.18*q*q)+vertical*(.15*q+.65*q*q)+lateral*((fraction-.5)*.32*q)))
235
+ curve=np.asarray(curve);tangent=np.gradient(curve,axis=0);tangent/=np.maximum(np.linalg.norm(tangent,axis=1,keepdims=True),1e-12)
236
+ side=np.cross(tangent,vertical);side/=np.maximum(np.linalg.norm(side,axis=1,keepdims=True),1e-12);other=np.cross(tangent,side)
237
+ angle=np.arange(5)*2*np.pi/5;normals=side[:,None,:]*np.cos(angle)[None,:,None]+other[:,None,:]*np.sin(angle)[None,:,None]
238
+ strand_radius=lash_profile['radiusM']*unit*np.linspace(1,.06,8)
239
+ vertices=curve[:,None,:]+normals*strand_radius[:,None,None];start=len(lash_points)
240
+ lash_points.extend(vertices.reshape(-1,3));lash_normals.extend(normals.reshape(-1,3));lash_roots.extend(np.repeat(root[None,:],40,axis=0))
241
+ for row in range(7):
242
+ for k in range(5):
243
+ a=start+row*5+k;b=start+row*5+(k+1)%5;lash_ids.extend([[a,b,a+5],[b,b+5,a+5]])
244
+ lash_points=np.asarray(lash_points);lash_material=material(.65,{'KHR_materials_specular':{'specularFactor':.35}})
245
+ add('eyelashes',(lash_points,np.asarray(lash_normals),np.broadcast_to([.012,.007,.004],lash_points.shape),np.asarray(lash_ids)),lash_material,parent)
246
+ lash_primitive=doc['meshes'][-1]['primitives'][0]
247
+ lash_primitive['attributes']['_LASH_ROOT']=append_accessor(doc,data,np.asarray(lash_roots,dtype='<f4'),'VEC3',5126)
248
+ doc['materials'][lash_primitive['material']]['extras']['characterSurface']='hair'
249
+ if eye.get('browGroom'):
250
+ from brow_groom import build_brow
251
+ source_normals=accessor(source,source_data,prim['attributes']['NORMAL'])@inverse
252
+ source_normals/=np.maximum(np.linalg.norm(source_normals,axis=1,keepdims=True),1e-12)
253
+ brow=build_brow(eye['browGroom'],points,tri,source_normals,target,right,up,direction,span,manifest['size'],unit,inverse,origin_basis)
254
+ add('eyebrows',brow,material(.72,{'KHR_materials_specular':{'specularFactor':.22}}),parent)
255
+ doc['materials'][-1]['extras']['characterSurface']='hair'
256
+ openings[-1].update(center=eye_center,right=right,up=up,direction=direction,target=target,span=span,size=manifest['size'],iris=iris,bulge=bulge,unit=unit)
257
+ for wall in eye.get('canthusWalls',[]):
258
+ from canthus_wall import build_canthus_wall
259
+ geometry,wall_roots,wall_weights=build_canthus_wall(wall,openings[-1],corner_surface,inverse,origin_basis)
260
+ wall_material=material(.55,{'KHR_materials_specular':{'specularFactor':.35}});wall_material['doubleSided']=True
261
+ add('canthus-wall',geometry,wall_material,parent)
262
+ wall_primitive=doc['meshes'][-1]['primitives'][0]
263
+ wall_primitive['attributes']['_LID_WALL_ROOT']=append_accessor(doc,data,wall_roots.astype('<f4'),'VEC3',5126)
264
+ wall_primitive['attributes']['_LID_WALL_WEIGHT']=append_accessor(doc,data,wall_weights.astype('<f4'),'SCALAR',5126)
265
+ if eye.get('outerLidPx'):
266
+ from eyelid_patch import build_lid_band
267
+ from glb_output import image_array
268
+ # The boundary joins the current material revision, including reviewed
269
+ # source-color repairs, rather than its unrepaired raw texture.
270
+ source_material=doc['materials'][doc['meshes'][0]['primitives'][0]['material']]
271
+ color_image=image_array(doc,data,source_material['pbrMetallicRoughness']['baseColorTexture']['index']).astype(float)
272
+ source_normals=accessor(source,source_data,prim['attributes']['NORMAL'])@inverse
273
+ source_normals/=np.maximum(np.linalg.norm(source_normals,axis=1,keepdims=True),1e-12)
274
+ geometry=build_lid_band(openings[-1],eye['outerLidPx'],points,tri,accessor(source,source_data,prim['attributes']['TEXCOORD_0']),color_image,inverse,origin_basis,source_normals,eye.get('lidSkinSamplesPx'),eye.get('outerEdgeSampling'),eye.get('fairLidDepth',False))
275
+ geometry=(geometry[0],geometry[1],geometry[2]*np.asarray(source_material['pbrMetallicRoughness'].get('baseColorFactor',[1,1,1,1])[:3]),geometry[3])
276
+ lid_material=material(.59,{'KHR_materials_specular':{'specularFactor':.65}})
277
+ add('eyelid-skin',geometry,lid_material,parent)
278
+ doc['materials'][-1]['extras'].update(characterSurface='skin',skinMicroDetail=False,skinDiffusionProfile=source_material.get('extras',{}).get('skinDiffusionProfile',[1,.55,.3]))
279
+ lid_primitive=doc['meshes'][-1]['primitives'][0]
280
+ lid_primitive['attributes']['_SCATTER']=append_accessor(doc,data,np.full(len(geometry[0]),.2,dtype='<f4'),'SCALAR',5126)
281
+ if 'skinFacialAtlasTexture' in source_material.get('extras',{}):
282
+ from eyelid_detail import bind_lid_detail
283
+ face_path=ROOT/'highquality/materials/faces'/f'{sid}.json'
284
+ face_profile=json.loads(face_path.read_text())
285
+ face_receipt=json.loads((base/'materials/v2/face-detail-provenance.json').read_text())
286
+ if hashlib.sha256(face_path.read_bytes()).hexdigest()!=face_receipt['profile_sha256']:
287
+ raise ValueError('Eyelid facial coordinates differ from the baked atlas')
288
+ bind_lid_detail(doc,data,lid_primitive,doc['materials'][-1],source_world_points(source,geometry[0]),center,height,face_profile,source_material['extras'],out)
289
+ lid_detail_bindings.append(eye['anatomicalSide'])
290
+ openings[-1]['cutContour']=eye['outerLidPx']
291
+ openings[-1]['cutRadius']=openings[-1]['radius']*3
292
+ eye_reports.append({'name':eye['name'],'centerSource':translation.tolist(),'globeRadiusM':eye['globeRadiusM'],'irisRadiusM':iris/unit,'sourceSurfaceHit':front.tolist(),'node':group})
293
+ cut_report=cut_openings(doc,data,points,openings)
294
+ if not cut_report['removedInteriorTriangles']:
295
+ raise ValueError('Authored eye contours removed no interior surface; review the source fit')
296
+ cut_report['authority']=fit.get('openingAuthority','reviewed-ocular-material-and-contours')
297
+ rest_fit=[]
298
+ if fit.get('conformOpenLids',False):
299
+ body=doc['meshes'][0];p=body['primitives'][0];local0=accessor(doc,data,p['attributes']['POSITION']);world0=source_world_points(source,local0);world1=world0.copy()
300
+ for opening in openings:
301
+ world1,moved=conform_open_lid(world1,opening)
302
+ rest_fit.append({'side':opening['side'],'movedVertices':int(moved.sum())})
303
+ local1=local0+(world1-world0)@inverse.T
304
+ faces=np.concatenate([accessor(doc,data,p['indices']).reshape(-1,3) for p in body['primitives']])
305
+ groups=surface_normal_groups(local0)
306
+ def normals(vertices):return surface_normals(vertices,faces,groups)
307
+ n0=accessor(doc,data,p['attributes']['NORMAL']);n1=transport_normals(n0,normals(local0),normals(local1))
308
+ position_index=append_accessor(doc,data,local1.astype('<f4'),'VEC3',5126);normal_index=append_accessor(doc,data,n1.astype('<f4'),'VEC3',5126)
309
+ for p in body['primitives']:
310
+ # Preserve the interpolated source surface for later body-weight
311
+ # transfer. Optical fitting must not change correspondence.
312
+ p['attributes'].setdefault('_BIND_SOURCE_POSITION',p['attributes']['POSITION'])
313
+ p['attributes']['POSITION']=position_index;p['attributes']['NORMAL']=normal_index
314
+ blink_report=add_blinks(doc,data,source,openings,[e['node'] for e in eye_reports])
315
+ extensions={e for m in doc['materials'] for e in m.get('extensions',{})};doc['extensionsUsed']=sorted(set(doc.get('extensionsUsed',[]))|extensions)
316
+ report={'id':sid,'status':'optical_fit_review_required','skinRevisionSha256':skin_hash,'sourceSha256':fit['sourceSha256'],'openingCut':cut_report,'eyelids':blink_report,'eyes':eye_reports,'implementation':'independent sclera, recessed fibrous iris, pupil cavity and refractive corneal dome; authored optical dimensions','limitations':['Eyelid deformation and tear meniscus under visual review','Procedural iris detail, not a captured iris','Fit must be inspected from multiple angles before promotion']}
317
+ report['eyelashes']=fit.get('lashes')
318
+ report['canthusWalls']=[eye.get('canthusWalls',[]) for eye in fit['eyes']]
319
+ report['browGrooms']=[eye.get('browGroom') for eye in fit['eyes']]
320
+ report['lidBandAuthoring']=fit.get('lidBandAuthoring')
321
+ report['lidDetailBindings']=lid_detail_bindings
322
+ if any(eye.get('outerLidPx') for eye in fit['eyes']):report['limitations'].extend(['Reconstructed lid bands have authored color and smooth geometry, not captured lid detail','Outer transition and corner fit require per-character visual acceptance'])
323
+ report['fitProfileSha256']=hashlib.sha256(profile_path.read_bytes()).hexdigest()
324
+ report['openLidFit']=rest_fit
325
+ report['textureProvenance']=texture_provenance
326
+ report['sourceAlbedoRepair']=repair_report
327
+ report['limitations'][1]='CC0 MakeHuman iris/sclera detail with authored pigment, not captured character eyes'
328
+ shutil.copyfile(ROOT/'highquality/references/makehuman-eyes/LICENSE.md',out/'MakeHuman-LICENSE.md')
329
+ skin_attribution=base/'materials/v2/ATTRIBUTION.txt'
330
+ if skin_attribution.exists():shutil.copyfile(skin_attribution,out/'Skin-ATTRIBUTION.txt')
331
+ report['outputSha256']=write_glb(doc,data,out/'character.glb');(out/'report.json').write_text(json.dumps(report,indent=2));(out/'fit.json').write_text(json.dumps(fit,indent=2));print(json.dumps(report))
332
+ if __name__=='__main__':
333
+ p=argparse.ArgumentParser();p.add_argument('--profile',required=True,type=Path);build(p.parse_args().profile)
@@ -0,0 +1,114 @@
1
+ """Clip a source surface against a vision-authored convex ocular contour.
2
+
3
+ Interpolates original attributes at intersections. It never infers an eye boundary.
4
+ """
5
+ import numpy as np
6
+ from glb_arrays import accessor
7
+ from glb_output import append_accessor
8
+
9
+ def half_polygon(weights,distances,inside):
10
+ output=[]
11
+ for i,a in enumerate(weights):
12
+ b=weights[(i+1)%len(weights)];da=distances[i];db=distances[(i+1)%len(weights)];ia=da>=0 if inside else da<0;ib=db>=0 if inside else db<0
13
+ if ia:output.append(a)
14
+ if ia!=ib:output.append(a+(b-a)*(da/(da-db)))
15
+ return np.asarray(output)
16
+
17
+ def subtract_convex(triangle,contour):
18
+ remaining=np.eye(3);outside=[]
19
+ for a,b in zip(contour,np.roll(contour,-1,axis=0)):
20
+ if len(remaining)<3:break
21
+ points=remaining@triangle;edge=b-a;distances=edge[0]*(points[:,1]-a[1])-edge[1]*(points[:,0]-a[0])
22
+ piece=half_polygon(remaining,distances,False)
23
+ if len(piece)>=3:outside.append(piece)
24
+ remaining=half_polygon(remaining,distances,True)
25
+ return outside
26
+
27
+ def cut_openings(doc,data,world,openings):
28
+ primitives=doc['meshes'][0]['primitives'];pool={}
29
+ for prim in primitives:
30
+ for name,index in prim['attributes'].items():
31
+ arr=accessor(doc,data,index)
32
+ if name in pool and not np.array_equal(pool[name],arr):raise ValueError('Shared opening adapter requires matching attribute arrays across material partitions')
33
+ pool[name]=arr
34
+ additions={name:[] for name in pool};next_index=len(world);changed=0;removed=0
35
+ for prim in primitives:
36
+ indices=accessor(doc,data,prim['indices']).reshape(-1,3);keep=np.ones(len(indices),bool);fresh=[]
37
+ for opening in openings:
38
+ xy=opening['pixels'];contour=np.asarray(opening.get('cutContour',opening['contour']),float)
39
+ if np.sum(contour[:,0]*np.roll(contour[:,1],-1)-contour[:,1]*np.roll(contour[:,0],-1))<0:contour=contour[::-1]
40
+ edges=np.roll(contour,-1,axis=0)-contour
41
+ if np.any(edges[:,0]*np.roll(edges[:,1],-1)-edges[:,1]*np.roll(edges[:,0],-1)<-1e-6):raise ValueError('Ocular contour must be convex; split nonconvex contours explicitly')
42
+ lo=contour.min(0);hi=contour.max(0);tri_xy=xy[indices]
43
+ candidate=keep&(tri_xy.min(1)<=hi).all(1)&(tri_xy.max(1)>=lo).all(1)&(np.linalg.norm(world[indices].mean(1)-opening['surfaceHit'],axis=1)<opening.get('cutRadius',opening['radius']*1.8))
44
+ for ti in np.flatnonzero(candidate):
45
+ ids=indices[ti];pieces=subtract_convex(tri_xy[ti],contour);keep[ti]=False;changed+=1
46
+ if not pieces:removed+=1
47
+ for weights in pieces:
48
+ for k in range(1,len(weights)-1):
49
+ w=weights[[0,k,k+1]]
50
+ for name,arr in pool.items():
51
+ value=w@arr[ids]
52
+ if name=='NORMAL':value/=np.maximum(np.linalg.norm(value,axis=1,keepdims=True),1e-12)
53
+ additions[name].extend(value)
54
+ fresh.append([next_index,next_index+1,next_index+2]);next_index+=3
55
+ combined=np.concatenate([indices[keep],np.asarray(fresh,dtype=np.uint32).reshape(-1,3)])
56
+ prim['indices']=append_accessor(doc,data,combined.astype('<u4').reshape(-1),'SCALAR',5125)
57
+ attributes={}
58
+ for name,arr in pool.items():
59
+ combined=np.concatenate([arr,np.asarray(additions[name]).reshape(-1,arr.shape[1])]);kind={1:'SCALAR',2:'VEC2',3:'VEC3',4:'VEC4'}[arr.shape[1]]
60
+ if np.issubdtype(arr.dtype,np.integer):raise ValueError('Rigged eye clipping needs discrete joint-index interpolation; source adapter is unskinned only')
61
+ attributes[name]=append_accessor(doc,data,combined.astype('<f4'),kind,5126)
62
+ for prim in primitives:
63
+ prim['attributes']={name:attributes[name] for name in prim['attributes']}
64
+ return {'intersectedTriangles':changed,'removedInteriorTriangles':removed,'newIntersectionVertices':next_index-len(world),'method':'exact convex-contour clipping with barycentric attribute interpolation'}
65
+
66
+
67
+ def refine_authored_contours(eye):
68
+ """Round convex landmark chains while retaining the two authored canthi.
69
+
70
+ Quadratic corner arcs stay inside the original convex selection. This is
71
+ geometric interpolation of reviewed anatomy, never a new eye detector.
72
+ """
73
+ import copy
74
+ if not eye.get('contourRefinement'):return eye
75
+ result=copy.deepcopy(eye);options=result.pop('contourRefinement')
76
+ samples=options.get('samplesPerLandmark',8)
77
+ if options.get('method')!='convex-quadratic-v1' or not isinstance(samples,int) or samples<4 or samples>16 or samples%2:
78
+ raise ValueError('Invalid authored contour refinement')
79
+ original=np.asarray(eye['openingPx'],float);count=len(original)
80
+ upper=eye['upperContourIndices'];lower=eye['lowerContourIndices']
81
+ corners=set([upper[0],upper[-1]])
82
+ if len(corners)!=2 or corners!=set([lower[0],lower[-1]]):raise ValueError('Upper and lower chains must share two authored eye corners')
83
+ outer_method=options.get('outerBoundary','rounded')
84
+ if outer_method not in ['rounded','preserve-authored']:raise ValueError('Unsupported outer contour refinement')
85
+ def rounded(points,preserve_all=False):
86
+ points=np.asarray(points,float)
87
+ if points.shape!=(count,2) or not np.isfinite(points).all():raise ValueError('Paired valid contour landmarks required')
88
+ output=[]
89
+ for i,p in enumerate(points):
90
+ entry=(points[(i-1)%count]+p)*.5;exit=(p+points[(i+1)%count])*.5
91
+ for t in np.arange(samples)/samples:
92
+ if preserve_all or i in corners:value=entry+(p-entry)*t*2 if t<=.5 else p+(exit-p)*(2*t-1)
93
+ else:value=(1-t)**2*entry+2*t*(1-t)*p+t*t*exit
94
+ output.append(value)
95
+ output=np.asarray(output);edge=np.roll(output,-1,axis=0)-output
96
+ turn=edge[:,0]*np.roll(edge[:,1],-1)-edge[:,1]*np.roll(edge[:,0],-1)
97
+ if np.any(turn>1e-6) and np.any(turn<-1e-6):raise ValueError('Refined eye contour is nonconvex; review authored landmarks')
98
+ return output.tolist()
99
+ midpoint=lambda i:int(i*samples+samples//2)
100
+ def chain(indices):
101
+ differences=np.diff(indices)%count
102
+ if np.all(differences==1):step=1
103
+ elif np.all(differences==count-1):step=-1
104
+ else:raise ValueError('Contour chains must follow consecutive authored landmarks')
105
+ start=midpoint(indices[0]);length=len(indices)-1
106
+ return [(start+step*i)%(count*samples) for i in range(length*samples+1)]
107
+ result['openingPx']=rounded(original)
108
+ if 'outerLidPx' in result:result['outerLidPx']=rounded(eye['outerLidPx'],outer_method=='preserve-authored')
109
+ for name in ['upperContourIndices','lowerContourIndices']:result[name]=chain(eye[name])
110
+ for wall in result.get('canthusWalls',[]):wall['contourIndices']=chain(wall['contourIndices'])
111
+ if result.get('caruncle'):result['caruncle']['innerContourIndex']=midpoint(result['caruncle']['innerContourIndex'])
112
+ result['contourRefinementApplied']={'method':'convex-quadratic-v1','samplesPerLandmark':samples,'authoredOpeningPx':eye['openingPx'],'authoredOuterLidPx':eye.get('outerLidPx'),'authoredCanthusIndices':sorted(corners),'maximumInsetPx':float(np.max(np.linalg.norm(np.asarray(result['openingPx'])[np.arange(count)*samples+samples//2]-original,axis=1)))}
113
+ result['contourRefinementApplied']['outerBoundary']=outer_method
114
+ return result
@@ -0,0 +1,35 @@
1
+ """Adapt the CC0 MakeHuman eye atlas to the optical eye's planar UV contract.
2
+
3
+ The source remains immutable. Iris pigment is an authored character parameter;
4
+ the asset supplies spatial detail. This is not a captured iris or recovered identity.
5
+ """
6
+ import hashlib
7
+ from pathlib import Path
8
+ import numpy as np
9
+ from PIL import Image
10
+ from glb_output import append_texture
11
+
12
+ from workspace_paths import workspace_root
13
+ ROOT=workspace_root(__file__)
14
+ ASSET=ROOT/'highquality/references/makehuman-eyes/blue_eye.png'
15
+ # Reviewed upper-right eye in MakeHuman's two-eye atlas, in source pixels.
16
+ CENTER=np.array([723.,304.])
17
+ IRIS_RADIUS=120.
18
+
19
+ def optical_uv(points,iris_radius):
20
+ return (CENTER+points[:,:2]*np.array([1,-1])*IRIS_RADIUS/iris_radius)/1024.
21
+
22
+ def build_texture(doc,data,out,pigment):
23
+ original=np.asarray(Image.open(ASSET).convert('RGB'),dtype=np.float32)/255
24
+ linear=np.where(original<=.04045,original/12.92,((original+.055)/1.055)**2.4)
25
+ yy,xx=np.indices(original.shape[:2]);radius=np.hypot(xx-CENTER[0],yy-CENTER[1])/IRIS_RADIUS
26
+ # Preserve the source fiber contrast while changing the artist-selected pigment.
27
+ luminance=linear@np.array([.2126,.7152,.0722])
28
+ band=(radius>.35)&(radius<.88)
29
+ detail=np.clip(luminance/max(float(np.mean(luminance[band])),1e-5),.025,3)
30
+ iris=np.asarray(pigment)[None,None,:]*detail[:,:,None]
31
+ coverage=np.clip((1.025-radius)/.055,0,1)
32
+ linear=linear*(1-coverage[:,:,None])+iris*coverage[:,:,None]
33
+ encoded=np.where(linear<=.0031308,linear*12.92,1.055*np.maximum(linear,0)**(1/2.4)-.055)
34
+ texture=append_texture(doc,data,np.clip(encoded*255,0,255),'makehuman-eye-pigment',out)
35
+ return texture,{'source':str(ASSET.relative_to(ROOT)),'sha256':hashlib.sha256(ASSET.read_bytes()).hexdigest(),'license':'CC0-1.0','upstream':'https://github.com/makehumancommunity/mpfb-eye-model-v2','adaptation':'Reviewed planar UV registration; authored iris pigment with source fiber contrast; source sclera retained'}
@@ -0,0 +1,34 @@
1
+ """Continue an independently authored facial atlas onto a reconstructed skin band."""
2
+ import numpy as np
3
+ from glb_output import append_accessor,append_texture
4
+
5
+ def bind_lid_detail(doc,data,primitive,material,world_positions,center,source_height,profile,source_extras,folder):
6
+ if 'skinFacialAtlasTexture' not in source_extras:return False
7
+ if source_extras.get('skinFacialAtlasLayout')!='detail-expression-halves-v1':
8
+ raise ValueError('Unsupported eyelid facial atlas layout')
9
+ if source_extras['skinAtlasHeightRangeM']!=profile['detailHeightRangeM']:
10
+ raise ValueError('Eyelid detail physical range differs from facial atlas')
11
+ bounds=np.asarray(profile['bounds'],float)
12
+ if bounds.shape!=(4,) or not np.isfinite(bounds).all() or np.any(bounds[2:]<=bounds[:2]) or not np.isfinite(source_height) or source_height<=0:
13
+ raise ValueError('Invalid eyelid detail projection')
14
+ normalized=(world_positions-center)/source_height
15
+ normalized[:,2]+=.5
16
+ uv=(normalized[:,[0,2]]-bounds[:2])/(bounds[2:]-bounds[:2])
17
+ region=np.asarray(source_extras.get('skinFacialAtlasRegion',[0,0,1,1]),float)
18
+ if region.shape!=(4,) or not np.isfinite(region).all() or np.any(region[:2]<0) or np.any(region[2:]>1) or np.any(region[2:]<=region[:2]):
19
+ raise ValueError('Invalid facial subregion inside the packed detail atlas')
20
+ uv=uv*(region[2:]-region[:2])+region[:2]
21
+ # Visibility is explicit: this geometry is the independently authored lid band,
22
+ # not an automatically inferred extension of the source skin mask.
23
+ count=len(uv);attributes=primitive['attributes']
24
+ attributes['_FACIAL_DETAIL']=append_accessor(doc,data,np.c_[uv,np.ones(count)].astype('<f4'),'VEC3',5126)
25
+ attributes['_FACE_UV']=append_accessor(doc,data,np.full((count,2),.5,dtype='<f4'),'VEC2',5126)
26
+ attributes['_FACE_WEIGHT']=append_accessor(doc,data,np.ones(count,dtype='<f4'),'SCALAR',5126)
27
+ neutral=append_texture(doc,data,np.array([[[128,150,128,255]]],np.uint8),'lid-neutral-detail',folder)
28
+ expression=append_texture(doc,data,np.array([[[128,128,128,0]]],np.uint8),'lid-neutral-expression',folder)
29
+ material['extras'].update(skinMicroDetail=True,skinAtlasTexture=neutral,
30
+ skinExpressionTexture=expression,skinAtlasHeightRangeM=profile['detailHeightRangeM'],
31
+ skinFacialAtlasTexture=source_extras['skinFacialAtlasTexture'],
32
+ skinFacialAtlasLayout='detail-expression-halves-v1',
33
+ skinAtlasAttribution=source_extras.get('skinAtlasAttribution','See ATTRIBUTION.txt'))
34
+ return True