@bitmagic/cli 0.1.66-dev.1 → 0.1.66-dev.10
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.
- package/README.md +37 -1
- package/assets/blender-level/find_blender.py +3 -1
- package/assets/character-rigging/SKILL.md +80 -0
- package/assets/character-rigging/references/articulation.md +55 -0
- package/assets/character-rigging/references/new-rig.md +51 -0
- package/assets/character-rigging/references/repairs.md +44 -0
- package/assets/character-rigging/references/verification.md +49 -0
- package/assets/character-rigging/scripts/rig_tool.py +197 -0
- package/assets/character-skin-authoring/SKILL.md +43 -0
- package/assets/character-skin-authoring/references/exporters.md +134 -0
- package/assets/character-skin-authoring/references/eyes.md +215 -0
- package/assets/character-skin-authoring/references/materials.md +115 -0
- package/assets/character-skin-authoring/references/playground.md +78 -0
- package/assets/character-skin-authoring/references/profiles.md +97 -0
- package/assets/character-skin-authoring/references/projection.md +190 -0
- package/assets/character-skin-authoring/references/regional-detail.md +72 -0
- package/assets/character-skin-authoring/scripts/assemble_character.py +205 -0
- package/assets/character-skin-authoring/scripts/authored_detail.py +47 -0
- package/assets/character-skin-authoring/scripts/authored_surface_patch.py +83 -0
- package/assets/character-skin-authoring/scripts/brow_groom.py +62 -0
- package/assets/character-skin-authoring/scripts/build_face_detail.py +107 -0
- package/assets/character-skin-authoring/scripts/build_surface_candidate.py +28 -0
- package/assets/character-skin-authoring/scripts/canthus_wall.py +57 -0
- package/assets/character-skin-authoring/scripts/character_build.py +54 -0
- package/assets/character-skin-authoring/scripts/eye_assets.py +333 -0
- package/assets/character-skin-authoring/scripts/eye_opening.py +114 -0
- package/assets/character-skin-authoring/scripts/eye_texture.py +35 -0
- package/assets/character-skin-authoring/scripts/eyelid_detail.py +34 -0
- package/assets/character-skin-authoring/scripts/eyelid_morph.py +171 -0
- package/assets/character-skin-authoring/scripts/eyelid_patch.py +173 -0
- package/assets/character-skin-authoring/scripts/face_atlas.py +51 -0
- package/assets/character-skin-authoring/scripts/geometry_repair.py +112 -0
- package/assets/character-skin-authoring/scripts/glb_arrays.py +13 -0
- package/assets/character-skin-authoring/scripts/glb_output.py +21 -0
- package/assets/character-skin-authoring/scripts/inspect_skin_point.py +66 -0
- package/assets/character-skin-authoring/scripts/normal_patch_height.py +38 -0
- package/assets/character-skin-authoring/scripts/prepare_face_reference.py +23 -0
- package/assets/character-skin-authoring/scripts/regional_atlas.py +51 -0
- package/assets/character-skin-authoring/scripts/select_preview.py +90 -0
- package/assets/character-skin-authoring/scripts/skin_views.py +267 -0
- package/assets/character-skin-authoring/scripts/surface_pbr_maps.py +46 -0
- package/assets/character-skin-authoring/scripts/texture_repair.py +98 -0
- package/assets/character-skin-authoring/scripts/vertex_detail_support.py +80 -0
- package/assets/character-skin-authoring/scripts/vision_blend_materials.py +74 -0
- package/assets/character-skin-authoring/scripts/vision_materials.py +276 -0
- package/assets/character-skin-authoring/scripts/workspace_paths.py +18 -0
- package/assets/metahuman/src/work/metahuman/MetaHumanActor.ts +580 -0
- package/assets/metahuman/src/work/metahuman/MetaHumanLabHooks.ts +122 -0
- package/assets/metahuman/src/work/metahuman/MetaHumanMaterials.ts +500 -0
- package/assets/metahuman/src/work/metahuman/MetaHumanSpeech.ts +242 -0
- package/assets/metahuman/src/work/metahuman/StudioEnvironment.ts +142 -0
- package/assets/metahuman/tools/lipsync/make_line.py +181 -0
- package/assets/metahuman/tools/metahuman/README.md +21 -0
- package/assets/metahuman/tools/metahuman/e2e/eval.mjs +34 -0
- package/assets/metahuman/tools/metahuman/e2e/probes/arm-pose-check.js +32 -0
- package/assets/metahuman/tools/metahuman/e2e/probes/bone-displacement.js +26 -0
- package/assets/metahuman/tools/metahuman/e2e/probes/clip-shoulder.js +41 -0
- package/assets/metahuman/tools/metahuman/e2e/probes/grafted-bones.js +11 -0
- package/assets/metahuman/tools/metahuman/e2e/probes/helper-follow-descent.js +38 -0
- package/assets/metahuman/tools/metahuman/e2e/probes/helper-follow-sweep.js +31 -0
- package/assets/metahuman/tools/metahuman/e2e/probes/helper-joints-inspect.js +10 -0
- package/assets/metahuman/tools/metahuman/e2e/probes/outfit-bad-vertices.js +26 -0
- package/assets/metahuman/tools/metahuman/e2e/probes/outfit-body-material.js +7 -0
- package/assets/metahuman/tools/metahuman/e2e/probes/outfit-own-skeleton.js +8 -0
- package/assets/metahuman/tools/metahuman/e2e/probes/shirt-bone-ownership.js +25 -0
- package/assets/metahuman/tools/metahuman/e2e/probes/shirt-edge-stretch.js +26 -0
- package/assets/metahuman/tools/metahuman/e2e/probes/shirt-long-edges.js +23 -0
- package/assets/metahuman/tools/metahuman/e2e/probes/walk-sampler.js +105 -0
- package/assets/metahuman/tools/metahuman/e2e/shot.mjs +125 -0
- package/assets/metahuman/tools/metahuman/e2e/walk-probe.mjs +43 -0
- package/assets/metahuman/tools/metahuman/merge-skeletons.mjs +69 -0
- package/assets/metahuman/tools/metahuman/package.json +13 -0
- package/assets/metahuman/tools/metahuman/prepare.sh +25 -0
- package/assets/metahuman/tools/metahuman/reorder-body-first.mjs +20 -0
- package/assets/metahuman/tools/metahuman/smooth-outfit-weights.mjs +48 -0
- package/assets/metahuman/tools/metahuman/strip-draco.mjs +32 -0
- package/assets/metahuman/tools/metahuman/transfer-skin-weights.mjs +90 -0
- package/dist/assets/voxelize-glb.d.ts +5 -1
- package/dist/assets/voxelize-glb.js +1 -0
- package/dist/assets/voxelize-glb.js.map +1 -1
- package/dist/assets/vxl-summary.js +2 -2
- package/dist/commands/forge.js +3 -1
- package/dist/commands/forge.js.map +1 -1
- package/dist/commands/generate.d.ts +62 -0
- package/dist/commands/generate.js +241 -2
- package/dist/commands/generate.js.map +1 -1
- package/dist/commands/init.js +2 -1
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/judge.js +5 -0
- package/dist/commands/judge.js.map +1 -1
- package/dist/commands/verify.js +7 -0
- package/dist/commands/verify.js.map +1 -1
- package/dist/editor/asset-viewer.js +10 -5
- package/dist/editor/asset-viewer.js.map +1 -1
- package/dist/generate/audio-file.d.ts +6 -0
- package/dist/generate/audio-file.js +42 -0
- package/dist/generate/audio-file.js.map +1 -0
- package/dist/generate/model.js +3 -3
- package/dist/generate/model.js.map +1 -1
- package/dist/generate/music.d.ts +4 -3
- package/dist/generate/music.js +6 -23
- package/dist/generate/music.js.map +1 -1
- package/dist/generate/prop.d.ts +19 -1
- package/dist/generate/prop.js +34 -6
- package/dist/generate/prop.js.map +1 -1
- package/dist/generate/stream.d.ts +20 -0
- package/dist/generate/stream.js +15 -1
- package/dist/generate/stream.js.map +1 -1
- package/dist/scaffold/asset-pipeline-skill-files.js +6 -1
- package/dist/scaffold/asset-pipeline-skill-files.js.map +1 -1
- package/dist/scaffold/character-rigging-skill-files.d.ts +2 -0
- package/dist/scaffold/character-rigging-skill-files.js +17 -0
- package/dist/scaffold/character-rigging-skill-files.js.map +1 -0
- package/dist/scaffold/character-skin-skill-files.d.ts +2 -0
- package/dist/scaffold/character-skin-skill-files.js +49 -0
- package/dist/scaffold/character-skin-skill-files.js.map +1 -0
- package/dist/scaffold/metahuman-skill-files.d.ts +21 -0
- package/dist/scaffold/metahuman-skill-files.js +306 -0
- package/dist/scaffold/metahuman-skill-files.js.map +1 -0
- package/dist/scaffold/project-files.js +28 -19
- package/dist/scaffold/project-files.js.map +1 -1
- package/dist/scaffold/project.js +6 -0
- package/dist/scaffold/project.js.map +1 -1
- package/dist/scaffold/skill-files.js +5 -2
- package/dist/scaffold/skill-files.js.map +1 -1
- package/dist/telemetry/command-context.d.ts +23 -0
- package/dist/telemetry/command-context.js +11 -0
- package/dist/telemetry/command-context.js.map +1 -1
- package/dist/telemetry/report.d.ts +1 -0
- package/dist/telemetry/report.js +4 -1
- package/dist/telemetry/report.js.map +1 -1
- package/dist/tools/bundles.js +8 -0
- package/dist/tools/bundles.js.map +1 -1
- package/dist/verify/browser.js +25 -0
- package/dist/verify/browser.js.map +1 -1
- package/dist/verify/categories.d.ts +16 -0
- package/dist/verify/categories.js +51 -0
- package/dist/verify/categories.js.map +1 -0
- package/package.json +10 -7
|
@@ -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
|