@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,62 @@
|
|
|
1
|
+
"""Short strand groom from explicit image-space brow polygons and comb guides.
|
|
2
|
+
|
|
3
|
+
Sampling places designed fibers inside authored regions; it does not recognize hair.
|
|
4
|
+
"""
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def inside_polygon(points, polygon):
|
|
9
|
+
polygon=np.asarray(polygon,float);inside=np.zeros(len(points),bool)
|
|
10
|
+
for a,b in zip(polygon,np.roll(polygon,-1,axis=0)):
|
|
11
|
+
if abs(b[1]-a[1])<1e-12:continue
|
|
12
|
+
inside^=((a[1]>points[:,1])!=(b[1]>points[:,1]))&(points[:,0]<(b[0]-a[0])*(points[:,1]-a[1])/(b[1]-a[1])+a[0])
|
|
13
|
+
return inside
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def build_brow(profile,points,triangles,normals,target,right,up,direction,span,size,unit,inverse,origin):
|
|
17
|
+
polygon=np.asarray(profile['polygonPx'],float);guides=np.asarray(profile['combGuidesPx'],float)
|
|
18
|
+
if polygon.ndim!=2 or polygon.shape[1]!=2 or len(polygon)<3 or not np.isfinite(polygon).all():raise ValueError('Brow needs an explicit finite polygon')
|
|
19
|
+
if guides.ndim!=3 or guides.shape[1:]!=(2,2) or len(guides)<2 or not np.isfinite(guides).all():raise ValueError('Brow needs at least two explicit comb guides')
|
|
20
|
+
vectors=guides[:,1]-guides[:,0];lengths=np.linalg.norm(vectors,axis=1)
|
|
21
|
+
if np.any(lengths<1e-6):raise ValueError('Brow comb guides must have a direction')
|
|
22
|
+
vectors/=lengths[:,None]
|
|
23
|
+
pigment=np.asarray(profile['colorLinear'],float)
|
|
24
|
+
if pigment.shape!=(3,) or not np.isfinite(pigment).all() or np.any(pigment<0) or np.any(pigment>1):raise ValueError('Brow pigment must be finite linear RGB')
|
|
25
|
+
count=int(profile['strandCount']);radii=float(profile['radiusM'])*unit
|
|
26
|
+
low,high=np.asarray(profile['lengthRangeM'],float)*unit
|
|
27
|
+
if not 1<=count<=1000 or not 0<radii<.0001*unit or not 0<low<=high<.015*unit:raise ValueError('Invalid authored brow fiber dimensions')
|
|
28
|
+
rng=np.random.default_rng(profile['seed']);roots=[]
|
|
29
|
+
for batch in range(100):
|
|
30
|
+
candidates=rng.uniform(polygon.min(0),polygon.max(0),size=(count*4,2))
|
|
31
|
+
roots.extend(candidates[inside_polygon(candidates,polygon)])
|
|
32
|
+
if len(roots)>=count:break
|
|
33
|
+
if len(roots)<count:raise ValueError('Brow polygon has insufficient area')
|
|
34
|
+
roots=np.asarray(roots[:count])
|
|
35
|
+
projected=np.stack([((points-target)@right/span+.5)*size,(.5-(points-target)@up/span)*size],1)
|
|
36
|
+
bounds=projected[triangles];selected=np.all(bounds.max(1)>=polygon.min(0)-1,axis=1)&np.all(bounds.min(1)<=polygon.max(0)+1,axis=1)
|
|
37
|
+
triangles=triangles[selected];a=points[triangles[:,0]];e1=points[triangles[:,1]]-a;e2=points[triangles[:,2]]-a
|
|
38
|
+
h=np.cross(direction,e2);det=(e1*h).sum(1);safe=np.where(abs(det)>1e-12,det,1)
|
|
39
|
+
vertices=[];vertex_normals=[];faces=[];color=[]
|
|
40
|
+
for pixel in roots:
|
|
41
|
+
ray=target+right*((pixel[0]+.5)/size-.5)*span+up*(.5-(pixel[1]+.5)/size)*span-direction*span*4
|
|
42
|
+
s=ray-a;u=(s*h).sum(1)/safe;q=np.cross(s,e1);v=(q*direction).sum(1)/safe;t=(e2*q).sum(1)/safe
|
|
43
|
+
valid=(abs(det)>1e-12)&(u>=0)&(v>=0)&(u+v<=1)&(t>0)
|
|
44
|
+
if not valid.any():raise ValueError('Authored brow root misses visible source geometry')
|
|
45
|
+
face=int(np.argmin(np.where(valid,t,np.inf)));root=ray+direction*t[face]
|
|
46
|
+
normal=normals[triangles[face]].T@np.array([1-u[face]-v[face],u[face],v[face]]);normal/=np.linalg.norm(normal)
|
|
47
|
+
weights=1/np.maximum(np.sum((guides[:,0]-pixel)**2,axis=1),9);comb=weights@vectors/weights.sum()
|
|
48
|
+
tangent=right*comb[0]-up*comb[1];tangent-=normal*np.dot(normal,tangent);tangent/=np.linalg.norm(tangent)
|
|
49
|
+
length=rng.uniform(low,high);curve=[]
|
|
50
|
+
for fraction in np.linspace(0,1,8):
|
|
51
|
+
curve.append(root+tangent*length*fraction+normal*unit*(.00004+.00018*np.sin(np.pi*fraction)+.00008*fraction))
|
|
52
|
+
curve=np.asarray(curve);along=np.gradient(curve,axis=0);along/=np.linalg.norm(along,axis=1,keepdims=True)
|
|
53
|
+
side=np.cross(along,normal);side/=np.linalg.norm(side,axis=1,keepdims=True);other=np.cross(along,side)
|
|
54
|
+
angle=np.arange(5)*2*np.pi/5;tube_normals=side[:,None,:]*np.cos(angle)[None,:,None]+other[:,None,:]*np.sin(angle)[None,:,None]
|
|
55
|
+
tube=curve[:,None,:]+tube_normals*radii*np.linspace(1,.05,8)[:,None,None];start=len(vertices)
|
|
56
|
+
vertices.extend((tube.reshape(-1,3)-origin)@inverse.T)
|
|
57
|
+
transformed=tube_normals.reshape(-1,3)@np.linalg.inv(inverse);transformed/=np.linalg.norm(transformed,axis=1,keepdims=True);vertex_normals.extend(transformed)
|
|
58
|
+
shade=np.clip(pigment*rng.uniform(.8,1.2),0,1);color.extend(np.broadcast_to(shade,(40,3)))
|
|
59
|
+
for row in range(7):
|
|
60
|
+
for k in range(5):
|
|
61
|
+
i=start+row*5+k;j=start+row*5+(k+1)%5;faces.extend([[i,j,i+5],[j,j+5,i+5]])
|
|
62
|
+
return np.asarray(vertices),np.asarray(vertex_normals),np.asarray(color),np.asarray(faces)
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Register an attributed scan detail atlas to a generated face, preserving identity.
|
|
2
|
+
|
|
3
|
+
This transfers band-limited displacement, not the scan's face shape. Height scale
|
|
4
|
+
is an authoring calibration (not recovered physical capture metadata). No MERL or
|
|
5
|
+
MetaHuman textures are used. All derivatives carry source/license/hash provenance.
|
|
6
|
+
"""
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import hashlib,json
|
|
9
|
+
import numpy as np
|
|
10
|
+
from PIL import Image,ImageDraw
|
|
11
|
+
from scipy.ndimage import gaussian_filter, distance_transform_edt
|
|
12
|
+
from face_atlas import rasterize,sample,register
|
|
13
|
+
from glb_arrays import read,accessor
|
|
14
|
+
from workspace_paths import workspace_root
|
|
15
|
+
ROOT=workspace_root(__file__)
|
|
16
|
+
REFERENCE=ROOT/'highquality/references/lee-perry-smith'
|
|
17
|
+
|
|
18
|
+
def pixels_to_coords(points,bounds,n=900):
|
|
19
|
+
p=np.asarray(points,dtype=float)/n;p[:,1]=1-p[:,1]
|
|
20
|
+
return np.asarray(bounds[:2])+p*(np.asarray(bounds[2:])-bounds[:2])
|
|
21
|
+
|
|
22
|
+
def region_image(profile,size=2048):
|
|
23
|
+
im=Image.new('L',(size,size),0);draw=ImageDraw.Draw(im)
|
|
24
|
+
for value,kind in enumerate(['eyes','brows','lips'],1):
|
|
25
|
+
for polygon in profile['regions'][kind]:
|
|
26
|
+
draw.polygon([(x/profile['annotationImageSize']*(size-1),(1-y/profile['annotationImageSize'])*(size-1)) for x,y in polygon],fill=value)
|
|
27
|
+
return np.array(im)
|
|
28
|
+
|
|
29
|
+
def verify_landmark_evidence(profile,root,source):
|
|
30
|
+
if profile.get('version',1)<2:return 'legacy_unbound_landmarks'
|
|
31
|
+
if hashlib.sha256(source.read_bytes()).hexdigest()!=profile['sourceSha256']:
|
|
32
|
+
raise ValueError('Face landmarks belong to a different source mesh')
|
|
33
|
+
image=root/profile['evidenceImage']
|
|
34
|
+
if hashlib.sha256(image.read_bytes()).hexdigest()!=profile['evidenceImageSha256']:
|
|
35
|
+
raise ValueError('Face landmark evidence image changed')
|
|
36
|
+
with Image.open(image) as pixels:
|
|
37
|
+
if pixels.size!=(profile['annotationImageSize'],profile['annotationImageSize']):
|
|
38
|
+
raise ValueError('Face landmark evidence dimensions differ')
|
|
39
|
+
return 'source_and_image_hash_verified'
|
|
40
|
+
|
|
41
|
+
def build(sid='b3662'):
|
|
42
|
+
profile_path=ROOT/'highquality/materials/faces'/f'{sid}.json';profile=json.loads(profile_path.read_text())
|
|
43
|
+
evidence_status=verify_landmark_evidence(profile,ROOT,ROOT/'characters'/sid/'high/v1/source/hy31pro.glb')
|
|
44
|
+
folder=ROOT/'characters'/sid/'high/v1/materials/v2';folder.mkdir(parents=True,exist_ok=True)
|
|
45
|
+
source=REFERENCE/'LeePerrySmith.glb';j,b=read(source);pr=j['meshes'][0]['primitives'][0]
|
|
46
|
+
p=accessor(j,b,pr['attributes']['POSITION']);uv=accessor(j,b,pr['attributes']['TEXCOORD_0']);uv[:,1]=1-uv[:,1]
|
|
47
|
+
ids=accessor(j,b,pr['indices']).reshape(-1,3)
|
|
48
|
+
size=profile.get('atlasSize',2048)
|
|
49
|
+
if size not in (2048,4096):raise ValueError('Facial atlasSize must be 2048 or 4096')
|
|
50
|
+
pixel_scale=size/2048
|
|
51
|
+
scan_uv,scan_valid=rasterize(p,ids,uv,profile['sourceBounds'],size)
|
|
52
|
+
displacement=np.asarray(Image.open(REFERENCE/'Infinite-Level_02_Disp_NoSmoothUV-4096.jpg').convert('L'),dtype=float)/255
|
|
53
|
+
scan_height=sample(displacement,scan_uv)
|
|
54
|
+
# Inpaint uncovered pixels before filtering to avoid silhouette impulses.
|
|
55
|
+
nearest=distance_transform_edt(~scan_valid,return_distances=False,return_indices=True)
|
|
56
|
+
scan_height=scan_height[tuple(nearest)]
|
|
57
|
+
# Remove identity-scale form; retain scan-correlated intermediate and fine detail.
|
|
58
|
+
band=gaussian_filter(scan_height,.65*pixel_scale)-gaussian_filter(scan_height,24*pixel_scale)
|
|
59
|
+
central=band[size//3:3*size//4,size//3:2*size//3]
|
|
60
|
+
amplitude=np.percentile(np.abs(central),95)
|
|
61
|
+
band*=profile['detailAmplitudeM']/max(amplitude,1.e-6)
|
|
62
|
+
np.save(folder/'scan-band-height-m.npy',band.astype('float32'))
|
|
63
|
+
yy,xx=np.mgrid[0:size,0:size];coords=np.stack([xx,yy],-1)/(size-1)
|
|
64
|
+
bounds=np.array(profile['bounds']);target=coords*(bounds[2:]-bounds[:2])+bounds[:2]
|
|
65
|
+
mapped,valid=register(target.reshape(-1,2),pixels_to_coords(profile['targetPixels'],profile['bounds'],profile['annotationImageSize']),pixels_to_coords(profile['sourcePixels'],profile['sourceBounds'],profile.get('sourceAnnotationImageSize',900)))
|
|
66
|
+
sb=np.array(profile['sourceBounds']);lookup=((mapped-sb[:2])/(sb[2:]-sb[:2])).reshape(size,size,2)
|
|
67
|
+
height=sample(band,lookup)
|
|
68
|
+
regions=region_image(profile,size)
|
|
69
|
+
coverage=valid.reshape(size,size)&(regions==0)
|
|
70
|
+
if not coverage.any():raise ValueError('Registration has no uncovered skin; review landmark hull and exclusion polygons')
|
|
71
|
+
feather=np.clip(distance_transform_edt(coverage)/(48*pixel_scale),0,1)
|
|
72
|
+
# Roughness follows the transferred relief's local variation, with coarse oil regions.
|
|
73
|
+
dy,dx=np.gradient(height);variance=gaussian_filter(dx*dx+dy*dy,3*pixel_scale)
|
|
74
|
+
relief=np.clip(np.sqrt(variance)/max(np.percentile(np.sqrt(variance[coverage]),95),1e-9),0,1)
|
|
75
|
+
# Transfer only gentle chromatic variation from clean upper-face skin;
|
|
76
|
+
# reject beard/jaw pigment so the donor's stubble does not enter the queen.
|
|
77
|
+
scan_color=sample(np.asarray(Image.open(REFERENCE/'Map-COL.jpg'),dtype=float)/255,scan_uv)
|
|
78
|
+
chroma=np.log(np.maximum(scan_color[:,:,0],.02)/np.maximum(scan_color[:,:,1],.02))
|
|
79
|
+
chroma=gaussian_filter(chroma,4*pixel_scale)-gaussian_filter(chroma,50*pixel_scale)
|
|
80
|
+
pigment=sample(chroma,lookup)*np.clip((lookup[:,:,1]-.53)/.09,0,1)
|
|
81
|
+
pigment=np.clip(pigment*4,-.3,.3)
|
|
82
|
+
packed=np.stack([np.clip(.5+height/profile['detailHeightRangeM'],0,1),.45+.20*relief,.5+pigment,feather],axis=-1)
|
|
83
|
+
Image.fromarray(np.round(packed*255).astype('uint8'),'RGBA').save(folder/'face-detail.png')
|
|
84
|
+
# Authored appearance deltas, NOT captured expression scans. Modulate the
|
|
85
|
+
# registered relief within facial regions; the source HY geometry stays intact.
|
|
86
|
+
x=coords[:,:,0];z=coords[:,:,1]
|
|
87
|
+
forehead=np.exp(-((x-.5)/.22)**2-((z-.69)/.11)**2)
|
|
88
|
+
smile=np.exp(-((abs(x-.5)-.15)/.10)**2-((z-.23)/.13)**2)
|
|
89
|
+
squint=np.exp(-((abs(x-.5)-.19)/.10)**2-((z-.46)/.09)**2)
|
|
90
|
+
delta=np.stack([height*forehead*.8,height*smile*.6,height*squint*.7],-1)
|
|
91
|
+
dynamic=np.concatenate([np.clip(.5+delta/profile['detailHeightRangeM'],0,1),np.clip((smile+forehead+squint)*feather,0,1)[...,None]],axis=-1)
|
|
92
|
+
Image.fromarray(np.round(dynamic*255).astype('uint8'),'RGBA').save(folder/'face-expression.png')
|
|
93
|
+
np.save(folder/'face-height-m.npy' ,height.astype('float32'))
|
|
94
|
+
Image.fromarray(regions.astype('uint8')).save(folder/'face-regions.png')
|
|
95
|
+
# Useful independent normal preview in the registered face plane.
|
|
96
|
+
physical_height=json.loads((ROOT/'characters'/sid/'high/v1/output/viewer-profile.json').read_text())['height']
|
|
97
|
+
spacing=(bounds[2:]-bounds[:2])*physical_height/(size-1)
|
|
98
|
+
dy,dx=np.gradient(height,spacing[1],spacing[0]);n=np.stack([-dx,-dy,np.ones_like(height)],-1);n/=np.linalg.norm(n,axis=-1,keepdims=True)
|
|
99
|
+
Image.fromarray(np.round((n*.5+.5)*255).astype('uint8')).save(folder/'face-normal-preview.png')
|
|
100
|
+
report={'id':sid,'landmarkEvidence':evidence_status,'method':'landmark-registered, band-limited scan displacement','author':'Lee Perry-Smith / Infinite Realities','license':'CC BY 3.0','licenseUrl':'https://creativecommons.org/licenses/by/3.0/','sourceUrl':'https://github.com/mrdoob/three.js/tree/r186/examples/models/gltf/LeePerrySmith','changes':'Projected into a facial atlas, band-pass filtered, amplitude calibrated, landmark-warped and masked. Only upper-face chromatic residual is transferred; beard pigment and face shape are not copied.','expressionMaps':'Authored modulation of scan relief; not captured expressions or a biological color model','scale':'Authoring estimate, not measured displacement units','heightRangeM':profile['detailHeightRangeM'],'profile_sha256':hashlib.sha256(profile_path.read_bytes()).hexdigest(),'inputs':{f.name:hashlib.sha256(f.read_bytes()).hexdigest() for f in REFERENCE.iterdir() if f.is_file()},'source_sha256':hashlib.sha256((ROOT/'characters'/sid/'high/v1/source/hy31pro.glb').read_bytes()).hexdigest()}
|
|
101
|
+
report['atlasSize']=size;report['texelSpacingM']=spacing.tolist();report['filterScaleRelativeTo2048']=pixel_scale
|
|
102
|
+
(folder/'face-detail-provenance.json').write_text(json.dumps(report,indent=2)+'\n')
|
|
103
|
+
(folder/'ATTRIBUTION.txt').write_text('Facial detail adapted from Infinite, 3D Head Scan by Lee Perry-Smith / Infinite Realities.\nCC BY 3.0: https://creativecommons.org/licenses/by/3.0/\nSource: three.js r186 examples/models/gltf/LeePerrySmith.\nModified by landmark registration, filtering, masking and amplitude calibration.\n')
|
|
104
|
+
print(json.dumps({'id':sid,'atlasSize':size,'heightP95Mm':float(np.percentile(abs(height[coverage]),95)*1000)}))
|
|
105
|
+
if __name__=='__main__':
|
|
106
|
+
import argparse
|
|
107
|
+
parser=argparse.ArgumentParser();parser.add_argument('--id',default='b3662');build(parser.parse_args().id)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Build reviewed evidence into a separate surface/eye/runtime candidate, fail-fast.
|
|
2
|
+
|
|
3
|
+
Authoring and visual acceptance remain agent tasks. This command never selects anatomy,
|
|
4
|
+
promotes viewer readiness, modifies the original model, or runs paid generation.
|
|
5
|
+
"""
|
|
6
|
+
import argparse,json,subprocess,sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from workspace_paths import workspace_root
|
|
10
|
+
ROOT=workspace_root(__file__)
|
|
11
|
+
|
|
12
|
+
def build(character,evidence,masks,eyes=None,assemble=False):
|
|
13
|
+
base=ROOT/'characters'/character/'high/v1'
|
|
14
|
+
for file in [evidence/'manifest.json',masks/'report.json',masks/'material-0-surface.npz',base/'materials/v2/face-detail.png',base/'materials/v2/face-expression.png',base/'materials/v2/scan-band-height-m.npy']:
|
|
15
|
+
if not file.is_file():raise ValueError(f'Missing authored input: {file}')
|
|
16
|
+
if eyes and json.loads(eyes.read_text())['id']!=character:raise ValueError('Eye profile character mismatch')
|
|
17
|
+
def run(script,*args):
|
|
18
|
+
subprocess.run([sys.executable,str(Path(__file__).resolve().parent/script),*map(str,args)],cwd=ROOT,check=True)
|
|
19
|
+
run('vision_materials.py','--id',character,'--evidence',evidence,'--masks',masks)
|
|
20
|
+
run('vision_blend_materials.py','--id',character,'--all-surfaces')
|
|
21
|
+
if eyes:run('eye_assets.py','--profile',eyes)
|
|
22
|
+
if assemble:
|
|
23
|
+
run('assemble_character.py','--id',character,'--input','materials/v4/character.glb','--variant','surfaces')
|
|
24
|
+
if eyes:run('assemble_character.py','--id',character,'--input','eyes/v1/character.glb','--variant','eyes')
|
|
25
|
+
|
|
26
|
+
if __name__=='__main__':
|
|
27
|
+
p=argparse.ArgumentParser(description=__doc__);p.add_argument('--id',required=True);p.add_argument('--evidence',required=True,type=Path);p.add_argument('--masks',required=True,type=Path);p.add_argument('--eyes-profile',type=Path);p.add_argument('--assemble',action='store_true');a=p.parse_args()
|
|
28
|
+
build(a.id,a.evidence.resolve(),a.masks.resolve(),a.eyes_profile.resolve() if a.eyes_profile else None,a.assemble)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Designed lining between an explicitly authored canthus and the globe."""
|
|
2
|
+
import numpy as np
|
|
3
|
+
from eyelid_morph import conform_open_lid,surface_normals,surface_normal_groups
|
|
4
|
+
from eyelid_patch import lid_contact_depth,corner_contact_depth
|
|
5
|
+
|
|
6
|
+
def lining_depth_at_radius(radial,corner_radial,source_depth,radius,iris,unit,reconstructed=True):
|
|
7
|
+
"""Depth on the authored corner lining, for tissue supported by that lining."""
|
|
8
|
+
swept=iris*np.cos(.35)+np.sqrt(radius*radius-iris*iris)*np.sin(.35)
|
|
9
|
+
inner=max(radius*.88,swept+.00035*unit)
|
|
10
|
+
if inner>=radius or not inner<=radial<=corner_radial or corner_radial<=inner:
|
|
11
|
+
raise ValueError('Authored corner tissue lies outside its lining support')
|
|
12
|
+
globe=np.sqrt(max(0,radius*radius-corner_radial*corner_radial))
|
|
13
|
+
if reconstructed:boundary=float(lid_contact_depth(corner_radial,radius,globe,source_depth))+.00025*unit
|
|
14
|
+
else:boundary=max(source_depth,globe+.00025*unit) if corner_radial<radius and source_depth>0 else source_depth
|
|
15
|
+
inner_depth=np.sqrt(radius*radius-inner*inner)-.00035*unit
|
|
16
|
+
t=(radial-inner)/(corner_radial-inner)
|
|
17
|
+
return inner_depth*(1-t)+(boundary-.0003*unit)*t
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def build_canthus_wall(profile,opening,sample_surface,inverse,origin):
|
|
21
|
+
contour=np.asarray(opening['contour']);indices=profile['contourIndices']
|
|
22
|
+
if len(indices)<3:raise ValueError('A corner wall needs upper, corner and lower landmarks')
|
|
23
|
+
path=contour[indices];pixels=np.concatenate([a[None,:]*(1-np.arange(12)[:,None]/12)+b[None,:]*(np.arange(12)[:,None]/12) for a,b in zip(path[:-1],path[1:])]+[path[-1:]])
|
|
24
|
+
right=opening['right'];up=opening['up'];forward=-opening['direction'];center=opening['center'];radius=opening['radius'];unit=opening['unit']
|
|
25
|
+
plane=opening['target']+right[None,:]*((pixels[:,0]/opening['size']-.5)*opening['span'])[:,None]+up[None,:]*((.5-pixels[:,1]/opening['size'])*opening['span'])[:,None]
|
|
26
|
+
xy=np.stack([(plane-center)@right,(plane-center)@up],1);rad=np.linalg.norm(xy,axis=1)
|
|
27
|
+
swept_iris=opening['iris']*np.cos(.35)+np.sqrt(radius*radius-opening['iris']**2)*np.sin(.35)
|
|
28
|
+
inner_radius=max(radius*.88,swept_iris+.00035*unit)
|
|
29
|
+
if inner_radius>=radius:raise ValueError('Authored optical dimensions leave no lining clearance for the reviewed gaze range')
|
|
30
|
+
active=np.flatnonzero(rad>inner_radius)
|
|
31
|
+
if len(active)<3 or np.any(np.diff(active)!=1):raise ValueError('Reviewed corner must form one contiguous lining outside the globe center')
|
|
32
|
+
plane=plane[active];xy=xy[active];rad=rad[active]
|
|
33
|
+
boundary=np.asarray([sample_surface(p) for p in plane])
|
|
34
|
+
if profile.get('contactMethod') == 'reconstructed-lid-v1':
|
|
35
|
+
# Use precisely the reconstructed aperture's depth, rather than the
|
|
36
|
+
# old source eye surface, which is removed by the lid-band exporter.
|
|
37
|
+
source_depth=(boundary-center)@forward
|
|
38
|
+
globe_depth=np.sqrt(np.maximum(0,radius*radius-rad*rad))
|
|
39
|
+
depth=np.array([corner_contact_depth(r,g,s,p,opening) for r,g,s,p in zip(rad,globe_depth,source_depth,plane)])+.00025*unit
|
|
40
|
+
boundary=center+xy[:,0,None]*right+xy[:,1,None]*up+depth[:,None]*forward
|
|
41
|
+
elif profile.get('contactMethod') is not None:
|
|
42
|
+
raise ValueError('Unsupported canthus contact method')
|
|
43
|
+
boundary,_=conform_open_lid(boundary,opening)
|
|
44
|
+
inner_xy=xy*(inner_radius/rad)[:,None];inner=center+inner_xy[:,0,None]*right+inner_xy[:,1,None]*up+forward*(np.sqrt(radius*radius-inner_radius*inner_radius)-.00035*unit)
|
|
45
|
+
rows=7;vertices=[];roots=[];weights=[];faces=[];count=len(boundary)
|
|
46
|
+
for row,t in enumerate(np.linspace(0,1,rows)):
|
|
47
|
+
# The near-lid edge shares its support point with the source lid, but is
|
|
48
|
+
# slightly recessed. The far edge remains behind the globe during blink.
|
|
49
|
+
vertices.extend(inner*(1-t)+(boundary-forward*.0003*unit)*t)
|
|
50
|
+
roots.extend(boundary);weights.extend(np.full(count,t))
|
|
51
|
+
if row:
|
|
52
|
+
for j in range(count-1):
|
|
53
|
+
a=(row-1)*count+j;b=a+1;faces.extend([[a,b,a+count],[b,b+count,a+count]])
|
|
54
|
+
vertices=(np.asarray(vertices)-origin)@inverse.T;faces=np.asarray(faces)
|
|
55
|
+
normals=surface_normals(vertices,faces,surface_normal_groups(vertices))
|
|
56
|
+
colors=np.broadcast_to(profile['colorLinear'],vertices.shape).copy()
|
|
57
|
+
return (vertices,normals,colors,faces),(np.asarray(roots)-origin)@inverse.T,np.asarray(weights)
|
|
@@ -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()
|