@bitmagic/cli 0.1.66-dev.6 → 0.1.66-dev.8
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-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/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/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-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 +4 -0
- package/dist/scaffold/project.js.map +1 -1
- package/dist/scaffold/skill-files.js +4 -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,98 @@
|
|
|
1
|
+
"""Conservative albedo repair of explicitly vision-selected source defects.
|
|
2
|
+
|
|
3
|
+
The mask is evidence, not a detector. Solve a harmonic fill in linear light;
|
|
4
|
+
pixels outside the mask remain byte-identical. Never repair semantics this way.
|
|
5
|
+
"""
|
|
6
|
+
import hashlib,json
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import numpy as np
|
|
9
|
+
from PIL import Image
|
|
10
|
+
from scipy.ndimage import binary_fill_holes,label,distance_transform_edt
|
|
11
|
+
from scipy.sparse import lil_matrix
|
|
12
|
+
from scipy.sparse.linalg import spsolve
|
|
13
|
+
from glb_output import image_array,append_texture
|
|
14
|
+
|
|
15
|
+
def harmonic_fill(rgb,mask):
|
|
16
|
+
if rgb.shape[:2]!=mask.shape:raise ValueError('Repair mask must match source texture resolution')
|
|
17
|
+
yy,xx=np.nonzero(mask)
|
|
18
|
+
if not len(yy):raise ValueError('Empty repair selection')
|
|
19
|
+
if len(yy)>50000:raise ValueError('Large texture reconstruction needs a separately reviewed workflow')
|
|
20
|
+
if (yy==0).any() or (xx==0).any() or (yy==mask.shape[0]-1).any() or (xx==mask.shape[1]-1).any():raise ValueError('Repair touching atlas edge needs an explicit UV seam adapter')
|
|
21
|
+
values=rgb.astype(float)/255;linear=np.where(values<=.04045,values/12.92,((values+.055)/1.055)**2.4)
|
|
22
|
+
lookup=np.full(mask.shape,-1,np.int32);lookup[yy,xx]=np.arange(len(yy));matrix=lil_matrix((len(yy),len(yy)));rhs=np.zeros((len(yy),3))
|
|
23
|
+
for row,(y,x) in enumerate(zip(yy,xx)):
|
|
24
|
+
matrix[row,row]=4
|
|
25
|
+
for ny,nx in [(y-1,x),(y+1,x),(y,x-1),(y,x+1)]:
|
|
26
|
+
if mask[ny,nx]:matrix[row,lookup[ny,nx]]=-1
|
|
27
|
+
else:rhs[row]+=linear[ny,nx]
|
|
28
|
+
filled=np.clip(spsolve(matrix.tocsr(),rhs),0,1)
|
|
29
|
+
srgb=np.where(filled<=.0031308,filled*12.92,1.055*filled**(1/2.4)-.055)
|
|
30
|
+
result=rgb.copy();result[yy,xx]=np.round(srgb*255).astype(np.uint8)
|
|
31
|
+
assert np.array_equal(result[~mask],rgb[~mask])
|
|
32
|
+
return result
|
|
33
|
+
|
|
34
|
+
def surface_swatch_fill(rgb,mask,positions,donor_yx,allowed_mask=None,blend_width=0):
|
|
35
|
+
"""Interpolate agent-selected clean colors across UV islands in surface space."""
|
|
36
|
+
if positions.shape!=(*mask.shape,3):raise ValueError('Surface positions do not match repair selection')
|
|
37
|
+
donor_yx=np.asarray(donor_yx,dtype=int)
|
|
38
|
+
if len(donor_yx)<3:raise ValueError('Surface repair needs at least three reviewed clean swatches')
|
|
39
|
+
yy,xx=np.nonzero(mask);donors=positions[donor_yx[:,0],donor_yx[:,1]]
|
|
40
|
+
if not np.isfinite(donors).all():raise ValueError('Invalid donor surface position')
|
|
41
|
+
samples=rgb[donor_yx[:,0],donor_yx[:,1]].astype(float)/255
|
|
42
|
+
samples=np.where(samples<=.04045,samples/12.92,((samples+.055)/1.055)**2.4)
|
|
43
|
+
# The nearest clean swatches determine color, never the anatomical label.
|
|
44
|
+
# Limit to three donors so the opposite hand cannot flatten local variation.
|
|
45
|
+
distance=((positions[yy,xx,None,:]-donors[None,:,:])**2).sum(2)
|
|
46
|
+
nearest=np.argsort(distance,axis=1)[:,:3];d=np.take_along_axis(distance,nearest,axis=1)
|
|
47
|
+
weights=1/np.maximum(d,1e-16);weights/=weights.sum(1,keepdims=True)
|
|
48
|
+
linear=(samples[nearest]*weights[:,:,None]).sum(1)
|
|
49
|
+
if blend_width:
|
|
50
|
+
from scipy.spatial import cKDTree
|
|
51
|
+
if allowed_mask is None:raise ValueError('Surface feathering needs independent skin coverage')
|
|
52
|
+
boundary=allowed_mask&~mask
|
|
53
|
+
if not boundary.any():raise ValueError('Surface feathering needs an unchanged skin boundary')
|
|
54
|
+
distance=cKDTree(positions[boundary]).query(positions[yy,xx])[0]
|
|
55
|
+
blend=np.clip(distance/blend_width,0,1);blend=blend*blend*(3-2*blend)
|
|
56
|
+
original=rgb[yy,xx].astype(float)/255
|
|
57
|
+
original=np.where(original<=.04045,original/12.92,((original+.055)/1.055)**2.4)
|
|
58
|
+
linear=original*(1-blend[:,None])+linear*blend[:,None]
|
|
59
|
+
srgb=np.where(linear<=.0031308,linear*12.92,1.055*linear**(1/2.4)-.055)
|
|
60
|
+
result=rgb.copy();result[yy,xx]=np.round(np.clip(srgb,0,1)*255).astype(np.uint8)
|
|
61
|
+
assert np.array_equal(result[~mask],rgb[~mask])
|
|
62
|
+
return result
|
|
63
|
+
|
|
64
|
+
def apply_reviewed_repair(doc,data,folder,out,source_hash,allowed_mask=None,fill_enclosed_holes_up_to=0,surface_swatches=None):
|
|
65
|
+
folder=Path(folder);report=json.loads((folder/'report.json').read_text())
|
|
66
|
+
if report['sourceSha256']!=source_hash:raise ValueError('Texture repair evidence belongs to another source')
|
|
67
|
+
label_file=folder/'material-0-labels.png';mask=np.asarray(Image.open(label_file))==8
|
|
68
|
+
filled_holes=0
|
|
69
|
+
if fill_enclosed_holes_up_to:
|
|
70
|
+
if allowed_mask is None:raise ValueError('Repair hole closure requires independent anatomical evidence')
|
|
71
|
+
if not 0 < fill_enclosed_holes_up_to <= 64:raise ValueError('Repair hole closure is limited to 64 texels per enclosed hole')
|
|
72
|
+
holes=binary_fill_holes(mask)&~mask
|
|
73
|
+
components,count=label(holes)
|
|
74
|
+
sizes=np.bincount(components.ravel());sizes[0]=fill_enclosed_holes_up_to+1
|
|
75
|
+
additions=holes&(sizes[components]<=fill_enclosed_holes_up_to)&allowed_mask
|
|
76
|
+
# An enclosed repair-selection hole is not an anatomical classification.
|
|
77
|
+
# Only independently reviewed skin may receive the authored color repair.
|
|
78
|
+
filled_holes=int(additions.sum());mask|=additions
|
|
79
|
+
excluded=0
|
|
80
|
+
if allowed_mask is not None:
|
|
81
|
+
if allowed_mask.shape!=mask.shape:raise ValueError('Repair and anatomical mask resolution mismatch')
|
|
82
|
+
excluded=int((mask&~allowed_mask).sum());mask&=allowed_mask
|
|
83
|
+
# Conflicted/unobserved texels are deliberately excluded by the projection helper.
|
|
84
|
+
references={}
|
|
85
|
+
for primitive in doc['meshes'][0]['primitives']:
|
|
86
|
+
material=doc['materials'][primitive['material']];slot=material.get('pbrMetallicRoughness',{}).get('baseColorTexture')
|
|
87
|
+
if slot is not None:references.setdefault(slot['index'],[]).append(slot)
|
|
88
|
+
for index,slots in references.items():
|
|
89
|
+
pixels=image_array(doc,data,index)
|
|
90
|
+
repaired=harmonic_fill(pixels,mask) if surface_swatches is None else surface_swatch_fill(pixels,mask,surface_swatches['positions'],surface_swatches['donorYX'],allowed_mask=allowed_mask,blend_width=surface_swatches.get('blendWidth',0))
|
|
91
|
+
if surface_swatches is not None and 'occupied' in surface_swatches:
|
|
92
|
+
occupied=surface_swatches['occupied']
|
|
93
|
+
distance,nearest=distance_transform_edt(~occupied,return_indices=True)
|
|
94
|
+
gutters=(~occupied)&(distance<=4)&mask[tuple(nearest)]
|
|
95
|
+
repaired[gutters]=repaired[tuple(nearest[:,gutters])]
|
|
96
|
+
replacement=append_texture(doc,data,repaired,f'vision-repaired-albedo-{index}',out)
|
|
97
|
+
for slot in slots:slot['index']=replacement
|
|
98
|
+
return {'selectionSha256':hashlib.sha256(label_file.read_bytes()).hexdigest(),'annotationSha256':report['annotationSha256'],'selectedTexels':int(mask.sum()),'enclosedSelectionHoleTexels':filled_holes,'excludedOutsideAllowedRegion':excluded,'textures':len(references),'method':('linear-light harmonic fill' if surface_swatches is None else 'linear-light interpolation from reviewed clean swatches in surface space')+' strictly inside vision-selected defect mask','status':'shaded_review_required'}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Exact geometric support for detail vertices whose UV sample is unoccupied.
|
|
2
|
+
|
|
3
|
+
No anatomical inference: labels come from the same reviewed image annotations.
|
|
4
|
+
This changes vertex detail bindings only; the per-fragment skin atlas is untouched.
|
|
5
|
+
"""
|
|
6
|
+
import hashlib,json,subprocess,sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
ALGORITHM_VERSION=2
|
|
11
|
+
|
|
12
|
+
def resolve_labels(bits):
|
|
13
|
+
bits=bits.copy();subtypes=(bits&((1<<2)|(1<<3)|(1<<9)))!=0
|
|
14
|
+
bits[subtypes]&=np.uint16(65535^(1<<1))
|
|
15
|
+
labels=np.zeros(len(bits),np.uint8)
|
|
16
|
+
for kind in range(1,10):labels[bits==(1<<kind)]=kind
|
|
17
|
+
return labels
|
|
18
|
+
|
|
19
|
+
def worker(request):
|
|
20
|
+
import bpy
|
|
21
|
+
from mathutils import Vector
|
|
22
|
+
from mathutils.bvhtree import BVHTree
|
|
23
|
+
c=json.loads(Path(request).read_text());m=c['manifest'];bpy.ops.wm.read_factory_settings(use_empty=True);bpy.ops.import_scene.gltf(filepath=m['source'])
|
|
24
|
+
vertices=[];triangles=[]
|
|
25
|
+
for obj in bpy.context.scene.objects:
|
|
26
|
+
if obj.type!='MESH':continue
|
|
27
|
+
mesh=obj.data;mesh.calc_loop_triangles();offset=len(vertices)
|
|
28
|
+
vertices.extend(tuple(obj.matrix_world@v.co) for v in mesh.vertices)
|
|
29
|
+
triangles.extend(tuple(offset+i for i in t.vertices) for t in mesh.loop_triangles)
|
|
30
|
+
bvh=BVHTree.FromPolygons(vertices,triangles,all_triangles=True)
|
|
31
|
+
with np.load(c['queries'],allow_pickle=False) as q:points=q['points'];normals=q['normals']
|
|
32
|
+
bounds=np.asarray(m['worldBounds']);height=np.ptp(bounds[:,2]);center=bounds.mean(0);distance=np.linalg.norm(bounds[1]-bounds[0])*2
|
|
33
|
+
visible=np.zeros(len(points),np.uint32)
|
|
34
|
+
for index in c['viewIndices']:
|
|
35
|
+
v=m['views'][index];direction=np.asarray(v['direction'],float);direction/=np.linalg.norm(direction)
|
|
36
|
+
right=np.cross(direction,v.get('up',[0,0,1]));right/=np.linalg.norm(right);up=np.cross(right,direction)
|
|
37
|
+
local=points-(center+height*np.asarray(v.get('targetOffset',[0,0,0])));span=height*v.get('span',1.1)
|
|
38
|
+
x=np.floor((local@right/span+.5)*m['size']).astype(int);y=np.floor((.5-local@up/span)*m['size']).astype(int)
|
|
39
|
+
valid=(x>=0)&(x<m['size'])&(y>=0)&(y<m['size'])&(abs(normals@direction)>.2)
|
|
40
|
+
for i in np.flatnonzero(valid):
|
|
41
|
+
hit,_,_,_=bvh.ray_cast(Vector((points[i]-direction*distance).tolist()),Vector(direction.tolist()),distance*2)
|
|
42
|
+
if hit is not None and np.linalg.norm(np.asarray(hit)-points[i])<height*1e-5:visible[i]|=np.uint32(1<<index)
|
|
43
|
+
np.save(c['result'],visible,allow_pickle=False)
|
|
44
|
+
|
|
45
|
+
def recover(source,manifest,masks,points,normals,labels,visibility,missing,view_indices,out):
|
|
46
|
+
from workspace_paths import blender_executable
|
|
47
|
+
view_indices=sorted(set(view_indices))
|
|
48
|
+
if any(not isinstance(i,int) or i<0 or i>=min(32,len(manifest['views'])) for i in view_indices):raise ValueError('Invalid visibility view indices')
|
|
49
|
+
if points.shape!=normals.shape or points.ndim!=2 or points.shape[1]!=3 or any(np.asarray(a).shape!=(len(points),) for a in [labels,visibility,missing]):raise ValueError('Invalid vertex support arrays')
|
|
50
|
+
if not np.isfinite(points).all() or not np.isfinite(normals).all():raise ValueError('Non-finite vertex geometry')
|
|
51
|
+
if hashlib.sha256(Path(source).read_bytes()).hexdigest()!=manifest['sourceSha256']:raise ValueError('Vertex evidence source mismatch')
|
|
52
|
+
indices=np.flatnonzero(missing);out=Path(out);out.mkdir(parents=True,exist_ok=True)
|
|
53
|
+
if not len(indices):return labels,visibility,{'queriedVertices':0}
|
|
54
|
+
query=out/'detail-vertex-queries.npz';result=out/'detail-vertex-visibility.npy';receipt=out/'detail-vertex-visibility.json'
|
|
55
|
+
selected_points=np.asarray(points[indices],np.float64);selected_normals=np.asarray(normals[indices],np.float64)
|
|
56
|
+
key=hashlib.sha256(selected_points.tobytes()+selected_normals.tobytes()+json.dumps({'algorithm':ALGORITHM_VERSION,'source':manifest['sourceSha256'],'views':manifest['views'],'bounds':manifest['worldBounds'],'size':manifest['size'],'indices':view_indices},sort_keys=True).encode()).hexdigest()
|
|
57
|
+
cached=json.loads(receipt.read_text()) if receipt.exists() else {}
|
|
58
|
+
if cached.get('key')!=key or not result.exists() or hashlib.sha256(result.read_bytes()).hexdigest()!=cached.get('resultSha256'):
|
|
59
|
+
np.savez_compressed(query,points=selected_points,normals=selected_normals)
|
|
60
|
+
config=out/'detail-vertex-request.json';config.write_text(json.dumps({'manifest':manifest,'queries':str(query.resolve()),'result':str(result.resolve()),'viewIndices':view_indices}))
|
|
61
|
+
result.unlink(missing_ok=True)
|
|
62
|
+
subprocess.run([blender_executable(),'--background','--python-exit-code','1','--python',str(Path(__file__).resolve()),'--',str(config.resolve())],check=True)
|
|
63
|
+
receipt.write_text(json.dumps({'key':key,'resultSha256':hashlib.sha256(result.read_bytes()).hexdigest()}))
|
|
64
|
+
exact=np.load(result,allow_pickle=False)
|
|
65
|
+
if exact.shape!=(len(indices),) or exact.dtype!=np.uint32:raise ValueError('Invalid exact vertex visibility cache')
|
|
66
|
+
votes=np.zeros(len(indices),np.uint16);bounds=np.asarray(manifest['worldBounds']);height=np.ptp(bounds[:,2]);center=bounds.mean(0)
|
|
67
|
+
for index in view_indices:
|
|
68
|
+
view=manifest['views'][index];file=Path(masks)/(view['name']+'-annotation.npy')
|
|
69
|
+
if not file.exists():continue
|
|
70
|
+
annotation=np.load(file,allow_pickle=False)
|
|
71
|
+
if annotation.shape!=(manifest['size'],manifest['size']) or np.any(annotation>9):raise ValueError('Invalid reviewed annotation')
|
|
72
|
+
d=np.asarray(view['direction'],float);d/=np.linalg.norm(d);right=np.cross(d,view.get('up',[0,0,1]));right/=np.linalg.norm(right);up=np.cross(right,d)
|
|
73
|
+
p=selected_points-(center+height*np.asarray(view.get('targetOffset',[0,0,0])));span=height*view.get('span',1.1)
|
|
74
|
+
x=np.floor((p@right/span+.5)*manifest['size']).astype(int);y=np.floor((.5-p@up/span)*manifest['size']).astype(int);valid=(exact&(1<<index))!=0
|
|
75
|
+
if np.any(valid&((x<0)|(x>=manifest['size'])|(y<0)|(y>=manifest['size']))):raise ValueError('Visibility cache projects outside image')
|
|
76
|
+
kinds=annotation[y[valid],x[valid]].astype(np.uint16);votes[valid]|=np.where(kinds>0,np.uint16(1)<<kinds,0).astype(np.uint16)
|
|
77
|
+
recovered=resolve_labels(votes);new_labels=labels.copy();new_visibility=visibility.copy();new_labels[indices]=recovered;new_visibility[indices]=exact
|
|
78
|
+
return new_labels,new_visibility,{'queriedVertices':len(indices),'visibleVertices':int((exact!=0).sum()),'labelledVertices':int((recovered!=0).sum()),'geometryKey':key,'method':'exact source-geometry rays only for unoccupied UV vertex samples; original reviewed image labels; fragment coverage unchanged'}
|
|
79
|
+
|
|
80
|
+
if __name__=='__main__':worker(sys.argv[sys.argv.index('--')+1])
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Opt-in mask-blended skin revision. Native material response survives outside skin.
|
|
2
|
+
|
|
3
|
+
This changes no semantic labels and does not add anatomical coverage. Kept separate
|
|
4
|
+
from accepted material revisions while mixed surface rendering is evaluated.
|
|
5
|
+
"""
|
|
6
|
+
import argparse,copy,hashlib,json,shutil
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import numpy as np
|
|
9
|
+
from PIL import Image
|
|
10
|
+
from scipy.ndimage import distance_transform_edt
|
|
11
|
+
from glb_arrays import read,accessor
|
|
12
|
+
from glb_output import append_accessor,append_texture,image_array
|
|
13
|
+
from eye_assets import write_glb
|
|
14
|
+
|
|
15
|
+
from workspace_paths import workspace_root
|
|
16
|
+
ROOT=workspace_root(__file__)
|
|
17
|
+
|
|
18
|
+
def build(sid,all_surfaces=False):
|
|
19
|
+
base=ROOT/'characters'/sid/'high/v1';source=base/'materials/v2/character.glb';out=base/'materials'/('v4' if all_surfaces else 'v3');out.mkdir(parents=True,exist_ok=True)
|
|
20
|
+
doc,data=read(source);primitives=doc['meshes'][0]['primitives']
|
|
21
|
+
skin=next(p for p in primitives if doc['materials'][p['material']].get('extras',{}).get('characterSurface')=='skin')
|
|
22
|
+
native=[p for p in primitives if doc['materials'][p['material']].get('extras',{}).get('characterSurface') not in ['skin','hair','lips','eyes','nails']]
|
|
23
|
+
if len(native)!=1:raise ValueError('Mixed adapter currently requires one native source material')
|
|
24
|
+
combined=native+[skin]
|
|
25
|
+
if all_surfaces:combined += [p for p in primitives if doc['materials'][p['material']].get('extras',{}).get('characterSurface') in ['hair','lips','nails']]
|
|
26
|
+
ids=np.concatenate([accessor(doc,data,p['indices']) for p in combined])
|
|
27
|
+
for p in combined:
|
|
28
|
+
if any(p['attributes'][key]!=skin['attributes'][key] for key in ['POSITION','NORMAL','TEXCOORD_0']):raise ValueError('Mixed primitives must share source geometry and UVs')
|
|
29
|
+
material=copy.deepcopy(doc['materials'][native[0]['material']]);skin_material=doc['materials'][skin['material']]
|
|
30
|
+
material['name']='Vision_mask_blended_skin';material['extras']=copy.deepcopy(skin_material['extras']);material['extras']['skinSurfaceBlend']=True
|
|
31
|
+
detail=image_array(doc,data,material['extras']['skinAtlasTexture'],mode='RGBA').copy()
|
|
32
|
+
source_report=json.loads((base/'materials/v2/report.json').read_text())
|
|
33
|
+
if source_report['output_sha256']!=hashlib.sha256(source.read_bytes()).hexdigest():raise ValueError('Skin source changed after its evidence report was written')
|
|
34
|
+
masks=(ROOT/source_report['maskReport']).parent
|
|
35
|
+
if json.loads((masks/'report.json').read_text())['sourceSha256']!=source_report['source_sha256']:raise ValueError('UV occupancy evidence belongs to another source')
|
|
36
|
+
occupied=np.load(masks/'material-0-surface.npz')['occupied']
|
|
37
|
+
if occupied.shape!=detail.shape[:2]:raise ValueError('UV occupancy does not match the atlas')
|
|
38
|
+
# UV gutters are not surface area. Give filtering samples outside each island
|
|
39
|
+
# its nearest occupied texel, without assigning any unknown surface texels.
|
|
40
|
+
distance,nearest=distance_transform_edt(~occupied,return_indices=True)
|
|
41
|
+
gutter=(~occupied)&(distance<=4)
|
|
42
|
+
detail[gutter]=detail[tuple(nearest[:,gutter])]
|
|
43
|
+
del distance,nearest
|
|
44
|
+
# Antialias only inside reviewed coverage: never grow skin over an unknown region.
|
|
45
|
+
inside=detail[:,:,3]>127
|
|
46
|
+
ramp=np.clip((distance_transform_edt(inside)-.5)/3,0,1)
|
|
47
|
+
detail[:,:,3]=np.round(255*ramp*ramp*(3-2*ramp)).astype(np.uint8)
|
|
48
|
+
pbr_report=None
|
|
49
|
+
if all_surfaces:
|
|
50
|
+
from surface_pbr_maps import bake_surface_pbr,surface_weights
|
|
51
|
+
labels=np.asarray(Image.open(base/'materials/v2/vision-labels.png'))
|
|
52
|
+
weights=surface_weights(labels,occupied)
|
|
53
|
+
detail[:,:,3]=np.round(weights['skin']*255).astype(np.uint8)
|
|
54
|
+
pbr_report=bake_surface_pbr(doc,data,material,labels,occupied,out,weights)
|
|
55
|
+
material['extras']['skinAtlasTexture']=append_texture(doc,data,detail,'skin-detail-blended',out)
|
|
56
|
+
material['pbrMetallicRoughness']['baseColorTexture']=copy.deepcopy(skin_material['pbrMetallicRoughness']['baseColorTexture'])
|
|
57
|
+
doc['materials'].append(material);primitive=copy.deepcopy(skin);primitive['material']=len(doc['materials'])-1
|
|
58
|
+
primitive['indices']=append_accessor(doc,data,ids.astype('<u4'),'SCALAR',5125)
|
|
59
|
+
doc['meshes'][0]['primitives']=[primitive]+[p for p in primitives if p not in combined]
|
|
60
|
+
report={'id':sid,'status':'mixed_surface_review_required','inputSha256':hashlib.sha256(source.read_bytes()).hexdigest(),'method':'authored UV skin coverage mixes native and skin response per fragment','limitations':['Hair, lip and ocular primitives remain separate','No new coverage inferred','WebGPU and WebGL visual acceptance required']}
|
|
61
|
+
report['uvGutterTexels']=int(gutter.sum())
|
|
62
|
+
report['triangle_counts']=source_report['triangle_counts']
|
|
63
|
+
report['viewerReady']=False
|
|
64
|
+
report['surfacePbr']=pbr_report
|
|
65
|
+
if all_surfaces:report['limitations'][0]='Ocular primitives remain separate; fiber flow and source defects still require review'
|
|
66
|
+
repair_profile=base/'materials/geometry-repair-profile.json'
|
|
67
|
+
if all_surfaces and repair_profile.is_file():
|
|
68
|
+
from geometry_repair import apply_geometry_repair
|
|
69
|
+
report['geometryRepair']=apply_geometry_repair(doc,data,repair_profile,out,source_report['source_sha256'])
|
|
70
|
+
report['outputSha256']=write_glb(doc,data,out/'character.glb');(out/'report.json').write_text(json.dumps(report,indent=2))
|
|
71
|
+
shutil.copyfile(base/'materials/v2/ATTRIBUTION.txt',out/'ATTRIBUTION.txt');print(json.dumps(report))
|
|
72
|
+
|
|
73
|
+
if __name__=='__main__':
|
|
74
|
+
parser=argparse.ArgumentParser();parser.add_argument('--id',required=True);parser.add_argument('--all-surfaces',action='store_true');args=parser.parse_args();build(args.id,args.all_surfaces)
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
"""Build a character material revision from reviewed vision labels and exact UV surfaces.
|
|
2
|
+
|
|
3
|
+
No anatomical classification by colour, height or character ID. Semantic masks are inputs.
|
|
4
|
+
Scan detail is attributed; glabrous ridges are explicitly authored, not captured data.
|
|
5
|
+
"""
|
|
6
|
+
import argparse,copy,hashlib,json,struct
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import numpy as np
|
|
9
|
+
from PIL import Image
|
|
10
|
+
from scipy.ndimage import distance_transform_edt,gaussian_filter,map_coordinates
|
|
11
|
+
from scipy.spatial import cKDTree
|
|
12
|
+
from scipy.spatial.transform import Rotation
|
|
13
|
+
from glb_arrays import read,accessor
|
|
14
|
+
from glb_output import append_accessor,append_texture,image_array
|
|
15
|
+
from face_atlas import sample
|
|
16
|
+
|
|
17
|
+
from workspace_paths import workspace_root
|
|
18
|
+
ROOT=workspace_root(__file__)
|
|
19
|
+
SKIN_IDS=(1,2,3,9)
|
|
20
|
+
|
|
21
|
+
def pad_unused_labels(labels,occupied,conflict):
|
|
22
|
+
"""Filtering gutters may fill unused UV space, never unknown occupied surface."""
|
|
23
|
+
result=labels.copy()
|
|
24
|
+
if not np.any(labels):return result
|
|
25
|
+
distance,nearest=distance_transform_edt(labels==0,return_indices=True)
|
|
26
|
+
padding=(labels==0)&~occupied&(distance<=2)&~conflict
|
|
27
|
+
result[padding]=labels[tuple(nearest[:,padding])]
|
|
28
|
+
return result
|
|
29
|
+
|
|
30
|
+
def sample_labels(labels,uv):
|
|
31
|
+
h,w=labels.shape;x=np.clip((uv[...,0]*w).astype(int),0,w-1);y=np.clip((uv[...,1]*h).astype(int),0,h-1)
|
|
32
|
+
return labels[y,x]
|
|
33
|
+
|
|
34
|
+
def source_world_points(doc,points,mesh_index=0):
|
|
35
|
+
"""glTF scene transforms, followed by the documented Y-up to Blender Z-up basis."""
|
|
36
|
+
parents={child:i for i,node in enumerate(doc['nodes']) for child in node.get('children',[])}
|
|
37
|
+
instances=[i for i,node in enumerate(doc['nodes']) if node.get('mesh')==mesh_index]
|
|
38
|
+
if len(instances)!=1:raise ValueError('Mesh instances require a per-instance packaging adapter')
|
|
39
|
+
index=instances[0];matrix=np.eye(4)
|
|
40
|
+
while True:
|
|
41
|
+
node=doc['nodes'][index]
|
|
42
|
+
if 'matrix' in node:local=np.array(node['matrix']).reshape(4,4).T
|
|
43
|
+
else:
|
|
44
|
+
local=np.eye(4);local[:3,:3]=Rotation.from_quat(node.get('rotation',[0,0,0,1])).as_matrix()@np.diag(node.get('scale',[1,1,1]));local[:3,3]=node.get('translation',[0,0,0])
|
|
45
|
+
matrix=local@matrix
|
|
46
|
+
if index not in parents:break
|
|
47
|
+
index=parents[index]
|
|
48
|
+
world=points@matrix[:3,:3].T+matrix[:3,3]
|
|
49
|
+
return world[:,[0,2,1]]*np.array([1,-1,1])
|
|
50
|
+
|
|
51
|
+
def triangle_categories(doc,data,prim,masks):
|
|
52
|
+
evidence=np.load(masks/'triangles.npz');ids=accessor(doc,data,prim['indices']).reshape(-1,3)
|
|
53
|
+
points=source_world_points(doc,accessor(doc,data,prim['attributes']['POSITION']))
|
|
54
|
+
centers=points[ids].mean(1);distance,index=cKDTree(evidence['centers']).query(centers)
|
|
55
|
+
tolerance=np.ptp(points,axis=0).max()*1e-6
|
|
56
|
+
if len(centers)!=len(evidence['centers']) or distance.max()>tolerance or len(np.unique(index))!=len(index):
|
|
57
|
+
raise ValueError('Triangle evidence does not bijectively match the source scene geometry')
|
|
58
|
+
return evidence['labels'][index],{'matchingTolerance':tolerance,'maxCenterError':float(distance.max()),'conflictingTriangles':int(evidence['conflict'].sum())}
|
|
59
|
+
|
|
60
|
+
def tile_sample(tile,u,v):
|
|
61
|
+
return map_coordinates(tile,[np.remainder(v,1)*(tile.shape[0]-1),np.remainder(u,1)*(tile.shape[1]-1)],order=1,mode='wrap')
|
|
62
|
+
|
|
63
|
+
def equalize_skin_seams(doc,data,prim,labels,out):
|
|
64
|
+
"""Match colour at coincident surface vertices across split UV islands.
|
|
65
|
+
|
|
66
|
+
Geometry supplies correspondence. The vision mask limits edits to skin; no colour
|
|
67
|
+
threshold identifies anatomy. Corrections taper inside a 24-texel neighbourhood.
|
|
68
|
+
"""
|
|
69
|
+
uv=accessor(doc,data,prim['attributes']['TEXCOORD_0']);p=accessor(doc,data,prim['attributes']['POSITION'])
|
|
70
|
+
mat=doc['materials'][prim['material']];source=image_array(doc,data,mat['pbrMetallicRoughness']['baseColorTexture']['index'])
|
|
71
|
+
srgb=source.astype(np.float32)/255;linear=np.where(srgb<=.04045,srgb/12.92,((srgb+.055)/1.055)**2.4)
|
|
72
|
+
extent=np.ptp(p,axis=0).max();_,group,counts=np.unique(np.round(p/(extent*1e-7)).astype(np.int64),axis=0,return_inverse=True,return_counts=True)
|
|
73
|
+
selected=np.isin(sample_labels(labels,uv),SKIN_IDS)&(counts[group]>1)
|
|
74
|
+
h,w=source.shape[:2];x=np.clip((uv[:,0]*w).astype(int),0,w-1);y=np.clip((uv[:,1]*h).astype(int),0,h-1)
|
|
75
|
+
values=linear[y,x];sums=np.zeros((len(counts),3),np.float64);number=np.zeros(len(counts),np.int32)
|
|
76
|
+
np.add.at(sums,group[selected],values[selected]);np.add.at(number,group[selected],1)
|
|
77
|
+
valid=selected&(number[group]>1);residual=sums[group[valid]]/number[group[valid],None]-values[valid]
|
|
78
|
+
correction=np.zeros((h,w,3),np.float32);hits=np.zeros((h,w),np.float32)
|
|
79
|
+
np.add.at(correction,(y[valid],x[valid]),residual);np.add.at(hits,(y[valid],x[valid]),1)
|
|
80
|
+
correction/=np.maximum(hits[:,:,None],1)
|
|
81
|
+
distance,nearest=distance_transform_edt(hits==0,return_indices=True)
|
|
82
|
+
field=correction[tuple(nearest)];field=gaussian_filter(field,(2,2,0));field*=np.clip(1-distance/24,0,1)[:,:,None]
|
|
83
|
+
skin=np.isin(labels,SKIN_IDS)
|
|
84
|
+
if skin.shape!=(h,w):skin=np.asarray(Image.fromarray(skin).resize((w,h),Image.Resampling.NEAREST))
|
|
85
|
+
corrected=np.clip(linear+field*skin[:,:,None],0,1)
|
|
86
|
+
encoded=np.where(corrected<=.0031308,12.92*corrected,1.055*corrected**(1/2.4)-.055)
|
|
87
|
+
encoded=np.round(encoded*255).astype(np.uint8);encoded[~skin]=source[~skin]
|
|
88
|
+
Image.fromarray(encoded).save(out/'skin-albedo-seams.png')
|
|
89
|
+
return encoded,{'coincidentSkinSamples':int(valid.sum()),'correctionP95Linear':float(np.percentile(abs(residual),95)) if len(residual) else 0,'method':'geometry-corresponding UV seam colour equalization; 24px taper, skin only'}
|
|
90
|
+
|
|
91
|
+
def verify_projection_contract(manifest,masks):
|
|
92
|
+
# Visibility bit i belongs to camera i, not merely to a source mesh hash.
|
|
93
|
+
# Dense requests archive the exact camera/material contract used by the bake.
|
|
94
|
+
request=masks/'dense-request.json'
|
|
95
|
+
if not request.is_file():
|
|
96
|
+
raise ValueError('Missing dense projection receipt; rebake the evidence')
|
|
97
|
+
archived=json.loads(request.read_text())
|
|
98
|
+
for key in ['sourceSha256','size','views','materials','worldBounds']:
|
|
99
|
+
if key not in archived or archived[key]!=manifest.get(key):
|
|
100
|
+
raise ValueError('Baked projection contract differs from evidence: '+key)
|
|
101
|
+
|
|
102
|
+
def build(sid,masks,evidence):
|
|
103
|
+
base=ROOT/'characters'/sid/'high/v1';out=base/'materials/v2';out.mkdir(exist_ok=True,parents=True)
|
|
104
|
+
manifest=json.loads((evidence/'manifest.json').read_text());report=json.loads((masks/'report.json').read_text());source=Path(manifest['source'])
|
|
105
|
+
verify_projection_contract(manifest,masks)
|
|
106
|
+
source_hash=hashlib.sha256(source.read_bytes()).hexdigest()
|
|
107
|
+
if source_hash!=manifest['sourceSha256'] or source_hash!=report['sourceSha256']:raise ValueError('Evidence/bake source mismatch')
|
|
108
|
+
doc,data=read(source);original=copy.deepcopy(doc);original_bytes=bytes(data)
|
|
109
|
+
if len(doc['meshes'])!=1 or len(doc['meshes'][0]['primitives'])!=1:raise ValueError('This packaging adapter currently requires a single source mesh primitive')
|
|
110
|
+
prim=doc['meshes'][0]['primitives'][0];uv=accessor(doc,data,prim['attributes']['TEXCOORD_0']);ids=accessor(doc,data,prim['indices']).reshape(-1,3)
|
|
111
|
+
labels=np.asarray(Image.open(masks/'material-0-labels.png')).copy();conflict=np.asarray(Image.open(masks/'material-0-conflicts.png'))>0
|
|
112
|
+
surface=np.load(masks/'material-0-surface.npz')
|
|
113
|
+
if 'visibilityBits' not in surface:
|
|
114
|
+
raise ValueError('Surface evidence predates geometric visibility: rebake with the current skin_views.py --surface-data. Annotation viewBits cannot substitute for visibilityBits.')
|
|
115
|
+
position=surface['position'];normal=surface['normal'];occupied=surface['occupied'];view_bits=surface['visibilityBits'];n=labels.shape[0]
|
|
116
|
+
# Texel-footprint padding only: never grow an unseen body part or resolve a conflict.
|
|
117
|
+
labels=pad_unused_labels(labels,occupied,conflict)
|
|
118
|
+
Image.fromarray(labels).save(out/'vision-labels.png')
|
|
119
|
+
bound=np.asarray(manifest['worldBounds']);unit_height=bound[1,2]-bound[0,2];center=(bound[0]+bound[1])/2
|
|
120
|
+
physical_height=json.loads((base/'output/viewer-profile.json').read_text())['height']
|
|
121
|
+
if not np.isfinite(physical_height) or physical_height<=0:raise ValueError('Character physical height must be positive finite metres')
|
|
122
|
+
physical=(position-center).astype(np.float32)*(physical_height/unit_height)
|
|
123
|
+
p=(position-center)/unit_height;p[:,:,2]+=.5
|
|
124
|
+
fp=json.loads((ROOT/'highquality/materials/faces'/f'{sid}.json').read_text());bounds=np.asarray(fp['bounds'])
|
|
125
|
+
independent_face=fp.get('independentFacialDetail',False)
|
|
126
|
+
registered=np.asarray(Image.open(out/'face-detail.png'),dtype=np.float32)/255
|
|
127
|
+
dynamic=np.asarray(Image.open(out/'face-expression.png'),dtype=np.float32)/255
|
|
128
|
+
band=np.load(out/'scan-band-height-m.npy')
|
|
129
|
+
# A clean forehead crop is mirrored and smoothed into a seamless repeat.
|
|
130
|
+
crop=band[round(.72*len(band)):round(.84*len(band)),round(.37*len(band)):round(.62*len(band))]
|
|
131
|
+
tile=np.concatenate([crop,crop[:,::-1]],axis=1);tile=np.concatenate([tile,tile[::-1]],axis=0);tile=gaussian_filter(tile,.7,mode='wrap')
|
|
132
|
+
skin=np.isin(labels,SKIN_IDS);glabrous=np.isin(labels,[2,3]);detail=np.zeros((n,n,4),np.uint8);expression=np.full((n,n,4),128,np.uint8);expression[:,:,3]=0
|
|
133
|
+
from authored_detail import AuthoredDetail
|
|
134
|
+
body_detail=AuthoredDetail(base/'materials/body-detail-profile.json',manifest,physical_height)
|
|
135
|
+
from authored_surface_patch import AuthoredSurfacePatch
|
|
136
|
+
surface_patch=AuthoredSurfacePatch(base/'materials/surface-patch-profile.json',manifest,physical_height,ROOT)
|
|
137
|
+
if surface_patch.independent_planes() and not independent_face:raise ValueError('Independent body detail requires the packed independent facial atlas path')
|
|
138
|
+
# Evaluate only occupied skin in chunks to keep peak memory bounded.
|
|
139
|
+
rows,cols=np.nonzero(skin&occupied);front_index=[v['name'] for v in manifest['views']].index('face')
|
|
140
|
+
for batch in np.array_split(np.arange(len(rows)),max(1,int(np.ceil(len(rows)/100000)))):
|
|
141
|
+
yy=rows[batch];xx=cols[batch];point=physical[yy,xx];norm=abs(normal[yy,xx]);weights=norm**4;weights/=np.maximum(weights.sum(1,keepdims=True),1e-8)
|
|
142
|
+
relief=(tile_sample(tile,point[:,1]/.06,point[:,2]/.06)*weights[:,0]+tile_sample(tile,point[:,0]/.06,point[:,2]/.06)*weights[:,1]+tile_sample(tile,point[:,0]/.06,point[:,1]/.06)*weights[:,2])*.40
|
|
143
|
+
palm=glabrous[yy,xx]
|
|
144
|
+
# No follicles on palms/soles. Fine low-amplitude ridges follow rest-space coordinates.
|
|
145
|
+
ridge=np.sin(point[:,0]*2*np.pi/.00045+np.sin(point[:,1]*85))*0.000003
|
|
146
|
+
relief=np.where(palm,ridge,relief)
|
|
147
|
+
coords=(p[yy,xx][:,[0,2]]-bounds[:2])/(bounds[2:]-bounds[:2]);face=sample(registered,coords);expr=sample(dynamic,coords)
|
|
148
|
+
visible=((view_bits[yy,xx]&(1<<front_index))!=0)&~palm
|
|
149
|
+
face_direction=np.asarray(manifest['views'][front_index]['direction'],float);face_direction/=np.linalg.norm(face_direction)
|
|
150
|
+
facing=np.abs(normal[yy,xx]@face_direction)
|
|
151
|
+
grazing=np.clip((facing-.2)/.25,0,1);grazing=grazing*grazing*(3-2*grazing)
|
|
152
|
+
face_weight=face[:,3]*visible*grazing;face_height=(face[:,0]-.5)*fp['detailHeightRangeM']
|
|
153
|
+
if independent_face:face_weight=np.zeros_like(face_weight)
|
|
154
|
+
height=relief*(1-face_weight)+face_height*face_weight
|
|
155
|
+
height+=body_detail.evaluate(point,normal[yy,xx],labels[yy,xx],view_bits[yy,xx])
|
|
156
|
+
height+=surface_patch.relief(point,normal[yy,xx],labels[yy,xx],view_bits[yy,xx],include_independent=False)
|
|
157
|
+
rough=np.where(palm,.66,.59)*(1-face_weight)+face[:,1]*face_weight
|
|
158
|
+
rough=surface_patch.evaluate(point,normal[yy,xx],labels[yy,xx],view_bits[yy,xx],rough,include_independent=False)
|
|
159
|
+
packed=np.stack([np.clip(.5+height/fp['detailHeightRangeM'],0,1),rough,.5+(face[:,2]-.5)*face_weight,np.ones(len(batch))],1)
|
|
160
|
+
detail[yy,xx]=np.round(packed*255).astype(np.uint8)
|
|
161
|
+
expr[:,:3]=.5+(expr[:,:3]-.5)*face_weight[:,None];expr[:,3]*=face_weight
|
|
162
|
+
expression[yy,xx]=np.round(expr*255).astype(np.uint8)
|
|
163
|
+
# Fill atlas gutter samples from the nearest real skin sample, but keep coverage zero outside the label.
|
|
164
|
+
valid=skin&occupied;dist,nearest=distance_transform_edt(~valid,return_indices=True);gutter=(~valid)&(dist<=3)
|
|
165
|
+
detail[gutter]=detail[tuple(nearest[:,gutter])];detail[:,:,3]=np.uint8(skin)*255
|
|
166
|
+
facial_attributes={};facial_metadata={};vertex_support_report=None
|
|
167
|
+
if independent_face:
|
|
168
|
+
# Retain the registered face map at its own resolution. Geometry-linked
|
|
169
|
+
# visibility is only a projection gate; body coverage still owns anatomy.
|
|
170
|
+
vertex_points=source_world_points(original,accessor(original,original_bytes,prim['attributes']['POSITION']))
|
|
171
|
+
vertex_world=vertex_points.copy()
|
|
172
|
+
basis=source_world_points(original,np.vstack([np.zeros(3),np.eye(3)]))
|
|
173
|
+
linear=(basis[1:]-basis[0]).T
|
|
174
|
+
vertex_normals=accessor(original,original_bytes,prim['attributes']['NORMAL'])@np.linalg.inv(linear)
|
|
175
|
+
vertex_normals/=np.maximum(np.linalg.norm(vertex_normals,axis=1,keepdims=True),1e-12)
|
|
176
|
+
vertex_physical=(vertex_points-center)*(physical_height/unit_height)
|
|
177
|
+
vertex_points=(vertex_points-center)/unit_height;vertex_points[:,2]+=.5
|
|
178
|
+
facial_uv=(vertex_points[:,[0,2]]-bounds[:2])/(bounds[2:]-bounds[:2])
|
|
179
|
+
vx=np.clip((uv[:,0]*n).astype(int),0,n-1);vy=np.clip((uv[:,1]*n).astype(int),0,n-1)
|
|
180
|
+
from vertex_detail_support import recover
|
|
181
|
+
vertex_labels,vertex_visibility,vertex_support_report=recover(source,manifest,masks,vertex_world,vertex_normals,labels[vy,vx],view_bits[vy,vx],~occupied[vy,vx],[front_index]+[p['index'] for p in surface_patch.independent_planes()],out)
|
|
182
|
+
facing=np.abs(vertex_normals@face_direction)
|
|
183
|
+
grazing=np.clip((facing-.2)/.25,0,1);grazing=grazing*grazing*(3-2*grazing)
|
|
184
|
+
gate=((vertex_visibility&(1<<front_index))!=0)&np.isin(vertex_labels,[1,9])
|
|
185
|
+
gate &= np.all((facial_uv>=0)&(facial_uv<=1),axis=1)
|
|
186
|
+
from regional_atlas import pack_regions
|
|
187
|
+
packed_detail,packed_expression,packed_binding,regional_metadata=pack_regions(registered,dynamic,np.column_stack([facial_uv,gate*grazing]),vertex_physical,vertex_normals,vertex_labels,vertex_visibility,surface_patch.independent_planes(),fp['detailHeightRangeM'],triangles=ids)
|
|
188
|
+
facial_attributes={'_FACIAL_DETAIL':append_accessor(doc,data,packed_binding.astype('<f4'),'VEC3',5126)}
|
|
189
|
+
facial_metadata={'skinFacialAtlasTexture':append_texture(doc,data,np.round(np.concatenate([packed_detail,packed_expression],axis=1)*255),'skin-facial-independent',out),
|
|
190
|
+
'skinFacialAtlasLayout':'detail-expression-halves-v1',**regional_metadata}
|
|
191
|
+
del physical,p,position,normal,nearest,dist
|
|
192
|
+
albedo,albedo_report=equalize_skin_seams(original,original_bytes,prim,labels,out)
|
|
193
|
+
albedo_index=append_texture(doc,data,albedo,'skin-albedo-seams',out)
|
|
194
|
+
atlas_index=append_texture(doc,data,detail,'skin-detail-uv',out);expression_index=append_texture(doc,data,expression,'skin-expression-uv',out)
|
|
195
|
+
face_uv=append_accessor(doc,data,uv.astype('<f4'),'VEC2',5126);weight_index=append_accessor(doc,data,np.ones(len(uv),dtype='<f4'),'SCALAR',5126);scatter_index=append_accessor(doc,data,np.full(len(uv),.12,dtype='<f4'),'SCALAR',5126)
|
|
196
|
+
categories,triangle_report=triangle_categories(original,original_bytes,prim,masks);kind=np.full(len(ids),'native',dtype='U8')
|
|
197
|
+
kind[np.isin(categories,SKIN_IDS)]='skin';kind[categories==6]='hair';kind[categories==4]='lips';kind[categories==5]='eyes';kind[categories==7]='nails'
|
|
198
|
+
mat=original['materials'][prim['material']];orm=image_array(original,original_bytes,mat['pbrMetallicRoughness']['metallicRoughnessTexture']['index'])
|
|
199
|
+
new_prims=[];counts={}
|
|
200
|
+
for name in ['native','skin','hair','lips','eyes','nails']:
|
|
201
|
+
selected=kind==name
|
|
202
|
+
if not selected.any():continue
|
|
203
|
+
q=copy.deepcopy(prim);q['indices']=append_accessor(doc,data,ids[selected].astype('<u4').reshape(-1),'SCALAR',5125);counts[name]=int(selected.sum())
|
|
204
|
+
if name!='native':
|
|
205
|
+
material=copy.deepcopy(mat);material['name']='Vision_'+name;material['extras']={'characterSurface':name,'surfaceVersion':2,'semanticSource':'local vision annotations'}
|
|
206
|
+
maps=orm.copy();maps[:,:,2]=0;rough={'skin':.59,'hair':.76,'lips':.38,'eyes':.14,'nails':.32}[name];maps[:,:,1]=round(rough*255)
|
|
207
|
+
material['pbrMetallicRoughness']['metallicFactor']=0;material['pbrMetallicRoughness']['roughnessFactor']=1
|
|
208
|
+
material['pbrMetallicRoughness']['metallicRoughnessTexture']={'index':append_texture(doc,data,maps,name+'-vision-orm',out)}
|
|
209
|
+
material['extensions']={'KHR_materials_specular':{'specularFactor':.65 if name=='skin' else .5,'specularColorFactor':[1,1,1]}}
|
|
210
|
+
if name=='skin':
|
|
211
|
+
material.pop('normalTexture',None)
|
|
212
|
+
material['pbrMetallicRoughness']['baseColorTexture']={'index':albedo_index}
|
|
213
|
+
material['extras'].update(skinDiffusionProfile=fp.get('diffusionProfile',[1,.55,.3]),skinMicroDetail=True,skinAtlasTexture=atlas_index,skinExpressionTexture=expression_index,skinAtlasHeightRangeM=fp['detailHeightRangeM'],skinBaseNormalMode='geometry plus authored UV detail; defective HY skin normal excluded',skinAtlasAttribution='Lee Perry-Smith / Infinite Realities; CC BY 3.0; see ATTRIBUTION.txt')
|
|
214
|
+
q['attributes'].update(_FACE_UV=face_uv,_FACE_WEIGHT=weight_index,_SCATTER=scatter_index)
|
|
215
|
+
q['attributes'].update(facial_attributes);material['extras'].update(facial_metadata)
|
|
216
|
+
if name=='hair':
|
|
217
|
+
material['extensions'].update(KHR_materials_anisotropy={'anisotropyStrength':.25,'anisotropyRotation':0},KHR_materials_sheen={'sheenColorFactor':[.025,.025,.025],'sheenRoughnessFactor':.8})
|
|
218
|
+
doc['materials'].append(material);q['material']=len(doc['materials'])-1
|
|
219
|
+
for extension in material['extensions']:
|
|
220
|
+
if extension not in doc.setdefault('extensionsUsed',[]):doc['extensionsUsed'].append(extension)
|
|
221
|
+
new_prims.append(q)
|
|
222
|
+
doc['meshes'][0]['primitives']=new_prims
|
|
223
|
+
assert sum(counts.values())==len(ids) and bytes(data[:len(original_bytes)])==original_bytes
|
|
224
|
+
assert doc['nodes']==original['nodes'] and doc.get('skins')==original.get('skins') and doc.get('animations')==original.get('animations')
|
|
225
|
+
repair_report=None
|
|
226
|
+
repair_profile=base/'materials/repair-profile.json'
|
|
227
|
+
if repair_profile.is_file():
|
|
228
|
+
from texture_repair import apply_reviewed_repair
|
|
229
|
+
repair_document=json.loads(repair_profile.read_text())
|
|
230
|
+
operations=repair_document.get('repairs',[repair_document])
|
|
231
|
+
if not isinstance(operations,list) or not operations or any(not isinstance(item,dict) or 'mask' not in item for item in operations):
|
|
232
|
+
raise ValueError('Repair profile requires a nonempty list of explicit repair selections')
|
|
233
|
+
repair_reports=[]
|
|
234
|
+
for authored in operations:
|
|
235
|
+
swatches=None
|
|
236
|
+
if authored.get('cleanSwatches'):
|
|
237
|
+
swatch_evidence=ROOT/authored['evidence'] if authored.get('evidence') else evidence
|
|
238
|
+
swatch_manifest=json.loads((swatch_evidence/'manifest.json').read_text())
|
|
239
|
+
if swatch_manifest['sourceSha256']!=source_hash:raise ValueError('Swatch evidence source mismatch')
|
|
240
|
+
donor_yx=[]
|
|
241
|
+
for swatch in authored['cleanSwatches']:
|
|
242
|
+
image_file=swatch_evidence/(swatch['view']+'.png')
|
|
243
|
+
if hashlib.sha256(image_file.read_bytes()).hexdigest()!=swatch_manifest['evidenceHashes'][image_file.name]:raise ValueError('Swatch image changed')
|
|
244
|
+
view_file=swatch_evidence/(swatch['view']+'.npz')
|
|
245
|
+
if hashlib.sha256(view_file.read_bytes()).hexdigest()!=swatch_manifest['evidenceHashes'][view_file.name]:raise ValueError('Swatch correspondence changed')
|
|
246
|
+
view=np.load(view_file)
|
|
247
|
+
for x,y in swatch['pixels']:
|
|
248
|
+
if view['material'][y,x]!=0:raise ValueError('Clean swatch is not on source material zero')
|
|
249
|
+
u,v=view['uv'][y,x];tx=min(labels.shape[1]-1,int(u*labels.shape[1]));ty=min(labels.shape[0]-1,int((1-v)*labels.shape[0]))
|
|
250
|
+
if labels[ty,tx] not in SKIN_IDS:raise ValueError('Clean swatch is outside independently reviewed skin')
|
|
251
|
+
donor_yx.append([ty,tx])
|
|
252
|
+
swatch_surface=np.load(masks/'material-0-surface.npz')
|
|
253
|
+
swatches={'positions':swatch_surface['position'],'occupied':swatch_surface['occupied'],'donorYX':donor_yx,'blendWidth':authored.get('blendWidthM',0)*np.ptp(np.asarray(swatch_manifest['worldBounds'])[:,2])/json.loads((base/'output/viewer-profile.json').read_text())['height']}
|
|
254
|
+
item_report=apply_reviewed_repair(doc,data,ROOT/authored['mask'],out,source_hash,allowed_mask=np.isin(labels,SKIN_IDS),fill_enclosed_holes_up_to=authored.get('fillEnclosedSelectionHolesUpToTexels',0),surface_swatches=swatches)
|
|
255
|
+
item_report['purpose']=authored.get('purpose','')
|
|
256
|
+
repair_reports.append(item_report)
|
|
257
|
+
repair_report=repair_reports[0] if 'repairs' not in repair_document else {'repairs':repair_reports,'status':'shaded_review_required'}
|
|
258
|
+
repair_report['profileSha256']=hashlib.sha256(repair_profile.read_bytes()).hexdigest()
|
|
259
|
+
doc['buffers'][0]['byteLength']=len(data);payload=json.dumps(doc,separators=(',',':')).encode();payload+=b' '*((-len(payload))%4);data.extend(b'\0'*((-len(data))%4))
|
|
260
|
+
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
|
|
261
|
+
(out/'character.part.glb').write_bytes(raw);(out/'character.part.glb').replace(out/'character.glb')
|
|
262
|
+
result={'id':sid,'status':'shaded_review_required','method':'vision labels and geometry-linked full UV detail','source_sha256':source_hash,'output_sha256':hashlib.sha256(raw).hexdigest(),'triangle_counts':counts,'geometry_skin_animation_bytes_unchanged':True,'evidence':str(evidence),'maskReport':str(masks/'report.json'),'annotationSha256':report['annotationSha256'],'detail':'landmark registered scan face; seamless reduced scan microrelief elsewhere; authored nonfollicular palm/sole ridges','sourceNormal':'excluded only on explicitly labelled skin','remainingConflictTexels':report['materials'][0]['conflictTexels'],'vertexDetailSupport':vertex_support_report,'albedoSeams':albedo_report,'sourceAlbedoRepair':repair_report,'triangleEvidence':triangle_report}
|
|
263
|
+
result['authoredBodyDetail']=body_detail.report
|
|
264
|
+
result['authoredSurfacePatches']=surface_patch.report
|
|
265
|
+
attribution=out/'ATTRIBUTION.txt';text=attribution.read_text()
|
|
266
|
+
for sha,notice in surface_patch.attributions.items():
|
|
267
|
+
marker='Surface patch adaptation SHA256 '+hashlib.sha256(notice.encode()).hexdigest()
|
|
268
|
+
if marker not in text:text+='\n'+marker+'\nSource license SHA256 '+sha+'\n'+notice+'\n'
|
|
269
|
+
attribution.write_text(text)
|
|
270
|
+
result['independentFacialDetail']={'enabled':independent_face,'atlasSize':list(registered.shape[:2]),'layout':facial_metadata.get('skinFacialAtlasLayout'),'coverageAuthority':'unchanged body atlas; facial projection never adds skin'}
|
|
271
|
+
if independent_face:
|
|
272
|
+
result['independentFacialDetail'].update({'packedTextureSize':[packed_detail.shape[1]*2,packed_detail.shape[0]],'faceRegion':facial_metadata.get('skinFacialAtlasRegion',[0,0,1,1]),'regionalPatches':facial_metadata.get('skinRegionalAtlasPatches',[])})
|
|
273
|
+
(out/'report.json').write_text(json.dumps(result,indent=2)+'\n');print(json.dumps(result))
|
|
274
|
+
|
|
275
|
+
if __name__=='__main__':
|
|
276
|
+
p=argparse.ArgumentParser();p.add_argument('--id',required=True);p.add_argument('--masks',required=True,type=Path);p.add_argument('--evidence',required=True,type=Path);a=p.parse_args();build(a.id,a.masks,a.evidence)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Explicit installed-workspace paths, with the existing playground layout retained."""
|
|
2
|
+
import os,shutil
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
def workspace_root(module_file):
|
|
6
|
+
configured=os.environ.get('BITMAGIC_CHARACTER_WORKSPACE')
|
|
7
|
+
if configured:return Path(configured).expanduser().resolve()
|
|
8
|
+
# Retain direct invocation in the source playground; installed callers use the runner.
|
|
9
|
+
file=Path(module_file).absolute()
|
|
10
|
+
if file.parent.name=='src' and file.parent.parent.name=='highquality':return file.parents[2]
|
|
11
|
+
return Path.cwd().resolve()
|
|
12
|
+
|
|
13
|
+
def blender_executable():
|
|
14
|
+
configured=os.environ.get('BITMAGIC_BLENDER')
|
|
15
|
+
found=configured or shutil.which('blender')
|
|
16
|
+
if not found and Path('/Applications/Blender.app/Contents/MacOS/Blender').is_file():found='/Applications/Blender.app/Contents/MacOS/Blender'
|
|
17
|
+
if not found:raise FileNotFoundError('Set BITMAGIC_BLENDER to the local Blender executable')
|
|
18
|
+
return found
|