@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,190 @@
|
|
|
1
|
+
# Projection contract
|
|
2
|
+
|
|
3
|
+
The portable helper produces evidence and UV labels. It does not fit a detail exemplar,
|
|
4
|
+
change a GLB, or claim a sparse projected label map is a production-ready skin mask.
|
|
5
|
+
Use its coverage report to drive further agent inspection.
|
|
6
|
+
|
|
7
|
+
Run with a local Python containing NumPy and Pillow. Blender supplies its own NumPy.
|
|
8
|
+
The lab uses `lowpoly/.venv/bin/python`; elsewhere choose the project's environment.
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
python <skill>/scripts/skin_views.py prepare --source character.glb --views views.json --output evidence-v1 --size 1024 --blender /path/to/blender
|
|
12
|
+
python <skill>/scripts/skin_views.py bake --evidence evidence-v1 --annotations annotations.json --output masks-v1 --resolution 4096 --dense --blender /path/to/blender
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Omit `--blender` when available on PATH. `prepare` uses Blender's imported mesh and
|
|
16
|
+
world transform, BVH visibility, interpolated UVs and unlit base-color image. It currently
|
|
17
|
+
supports directly linked Principled base-color textures and the active UV set. Complex
|
|
18
|
+
material graphs must first receive a faithful albedo bake; the helper fails instead of
|
|
19
|
+
guessing. Animated models are inspected in the imported pose. Validate orientation and
|
|
20
|
+
texture appearance against the original before annotating.
|
|
21
|
+
|
|
22
|
+
Evidence PNGs preserve sRGB byte textures as encoded values. Blender's non-float
|
|
23
|
+
`Image.pixels` exposes those byte values normalized to 0–1; applying an additional
|
|
24
|
+
linear-to-sRGB transfer would brighten the evidence incorrectly. Constant Principled
|
|
25
|
+
base colors are linear and receive exactly one transfer. Float images are rejected
|
|
26
|
+
until a faithful sRGB albedo bake is supplied. New manifests explicitly record
|
|
27
|
+
`evidenceColorSpace` and `byteImageTransfer`. The playground's known-color Blender
|
|
28
|
+
regression verifies both byte-texture and constant-color paths within one byte value.
|
|
29
|
+
|
|
30
|
+
Older evidence made before this correction may be too bright. Preserve those immutable
|
|
31
|
+
revisions and annotations, prepare a new color-correct revision, and visually review it.
|
|
32
|
+
Identical camera/UV correspondence can justify transferring polygon coordinates, but
|
|
33
|
+
never claim a new visual review merely because the geometry stayed unchanged. This
|
|
34
|
+
helper correction does not alter the runtime GLB texture color pipeline.
|
|
35
|
+
|
|
36
|
+
`views.json` is a list. `direction` points **from camera into the scene** in Blender world
|
|
37
|
+
coordinates, `up` defaults to +Z. `span` is square orthographic width divided by model
|
|
38
|
+
height. `targetOffset` is relative to bounding-box center, also in model-height units.
|
|
39
|
+
Directions are geometric, not assumptions about front/back or left/right anatomy.
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
[
|
|
43
|
+
{"name":"minus-y","direction":[0,1,0],"span":1.45},
|
|
44
|
+
{"name":"plus-y","direction":[0,-1,0],"span":1.45}
|
|
45
|
+
]
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
After the survey, choose offsets and tighter spans by inspecting the actual character.
|
|
49
|
+
For a top view, supply an up vector perpendicular to its direction. Each view yields a
|
|
50
|
+
PNG and NPZ containing material index, UV and viewing-angle confidence for every visible
|
|
51
|
+
pixel. Manifest records source hash, cameras, dimensions, material names and world bounds.
|
|
52
|
+
Rasterized sample coordinates refer to pixel centers; polygon coordinates are image
|
|
53
|
+
pixels from top-left, x right and y down. Use precisely the named image dimensions.
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
{
|
|
57
|
+
"sourceSha256":"copy the manifest hash",
|
|
58
|
+
"views":{
|
|
59
|
+
"minus-y":[
|
|
60
|
+
{"label":"skin","polygon":[[100,100],[140,100],[140,160],[100,160]]},
|
|
61
|
+
{"label":"eyes","polygon":[[110,110],[120,110],[120,120],[110,120]]}
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
These coordinates are format examples, not annotations for any character. Later polygons
|
|
68
|
+
overwrite earlier ones within a view. Across views, contradictory labels become unknown
|
|
69
|
+
and appear in the conflict map. Grazing samples are excluded deterministically. UVs outside
|
|
70
|
+
0–1 remain unclassified rather than being wrapped into a different region.
|
|
71
|
+
|
|
72
|
+
Outputs: per-material label, observed and conflict PNGs; per-view annotation overlays,
|
|
73
|
+
reprojected atlas overlays and raw reprojected labels; archived annotations; and a JSON report. Label IDs: unknown 0, skin 1, palm 2, sole 3, lips 4, eyes 5, hair 6, nails 7,
|
|
74
|
+
non_skin 8, dorsal_hand 9. Material ordering belongs to this evidence manifest, not a promised GLB index.
|
|
75
|
+
UV labels use top-left image rows (Blender UV V is inverted during baking).
|
|
76
|
+
|
|
77
|
+
The bake does not interpolate unknown texels or decide by majority vote: conflicting
|
|
78
|
+
observations require review. Low-resolution survey rays are insufficient to cover a 4K UV
|
|
79
|
+
atlas; use region close-ups and appropriate resolution for final maps. Do not count unknown
|
|
80
|
+
background texels as missed skin without examining the UV occupancy of the actual model.
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
## Dense bake and review (tested on three additional characters)
|
|
84
|
+
|
|
85
|
+
Use `--dense` for reviewed deliverables. The default sparse mode remains useful as a small
|
|
86
|
+
projection diagnostic. Dense mode rasterizes the source UV triangles, finds the 3D point
|
|
87
|
+
for each occupied texel, projects it into annotated cameras and ray-tests visibility against
|
|
88
|
+
the exact mesh. It does not interpolate semantic labels across unseen geometry. Distinct
|
|
89
|
+
surfaces sharing UV texels are reported as overlaps and remain unknown. Increasing atlas
|
|
90
|
+
resolution cannot add semantic accuracy beyond the annotation images.
|
|
91
|
+
|
|
92
|
+
Prepare hashes the PNG/NPZ evidence. Bake checks these hashes and the source mesh hash;
|
|
93
|
+
editing an image after annotation invalidates that evidence. Legacy manifests without
|
|
94
|
+
image hashes remain readable; prepare fresh evidence for new work. Preserve the original
|
|
95
|
+
manifest with its camera definitions. Evidence must not be resized or relit for annotation.
|
|
96
|
+
|
|
97
|
+
Review BOTH `<view>-overlay.png` (your polygons) and `<view>-reprojected.png` (the actual
|
|
98
|
+
baked atlas). All prepared views are reprojected, including views without annotations.
|
|
99
|
+
Palette: green skin, mint dorsal hand, cyan palm, blue eyes, purple hair, pink lips, pale
|
|
100
|
+
yellow nails, gray non-skin. Red means unknown; orange means conflict. Unknown clothing
|
|
101
|
+
is not missing skin. The raw `*-reprojected-labels.png` images retain categorical IDs.
|
|
102
|
+
|
|
103
|
+
The report distinguishes total visible pixels from annotated visible pixels. Use
|
|
104
|
+
`unresolvedAnnotatedPixels` and `disagreeingAnnotatedPixels` to detect projection losses;
|
|
105
|
+
these are NOT whole-character skin coverage or semantic accuracy scores. Low-resolution
|
|
106
|
+
atlases lose samples near small islands and seams even in dense mode. Match the source
|
|
107
|
+
atlas resolution, inspect the result, and keep unresolved seams visible. Do not hide them
|
|
108
|
+
with unrestricted dilation. A synthetic Blender regression exercises dense coverage,
|
|
109
|
+
occluded back surfaces and overlapping UVs.
|
|
110
|
+
|
|
111
|
+
Trials found source albedo spill on soldier b3843's palms, pale hair/ear ambiguity on b5588,
|
|
112
|
+
and missing side scalp coverage on b3658. These require different responses: record a
|
|
113
|
+
source-texture defect separately; correct hair/skin boundaries from multiple views; add
|
|
114
|
+
views/annotations for exposed surfaces. Indistinct nails require closer views. Do not
|
|
115
|
+
claim a material or skin-detail improvement from a semantic mask alone.
|
|
116
|
+
|
|
117
|
+
## Runtime packaging evidence
|
|
118
|
+
|
|
119
|
+
For the full-UV material adapter, add `--surface-data --triangle-labels` to the dense bake.
|
|
120
|
+
|
|
121
|
+
To investigate a seam or conflict, use `scripts/inspect_skin_point.py --evidence <folder>
|
|
122
|
+
--masks <dense-bake> --view <name> --x <pixel-x> --y <pixel-y>`. Coordinates belong to the
|
|
123
|
+
original evidence image. The read-only helper reports the UV texel and every visible
|
|
124
|
+
contributing view, its source pixel, authored label and geometric facing. It checks the
|
|
125
|
+
source, annotations and archived camera contract; it does not resolve the labels.
|
|
126
|
+
|
|
127
|
+
Add `--height-m <known-character-height>` to report surface distances to adjacent
|
|
128
|
+
occupied atlas texels in metres. Inspect individual neighbours: crossing a UV discontinuity
|
|
129
|
+
can produce a large distance. This diagnoses texture density, not anatomy or recovered
|
|
130
|
+
detail. If a face uses half-millimetre texels, increasing bump strength cannot recover
|
|
131
|
+
sub-texel pores. Preserve the registered detail in its own atlas instead of increasing
|
|
132
|
+
coverage or exaggerating relief to hide lost resolution.
|
|
133
|
+
|
|
134
|
+
Inspect the reported image pixels. A one-pixel garment-boundary mistake in a grazing
|
|
135
|
+
view can project into a much wider strip on the model. If that source boundary is
|
|
136
|
+
ambiguous and another view clearly shows the anatomy, explicitly mark the narrow
|
|
137
|
+
uncertain region in the ambiguous view as unknown and retain the reviewed clear view.
|
|
138
|
+
Do not automatically select a label by facing score or shrink all boundaries globally.
|
|
139
|
+
The first writes occupied texel positions, normals and geometric visibility bits in
|
|
140
|
+
Blender world space. The second classifies actual triangle centers with the same camera
|
|
141
|
+
annotations and exact visibility tests. Packaging matches those centers bijectively to the
|
|
142
|
+
GLB scene transform and fails if they differ; never assume Blender and GLB vertex ordering
|
|
143
|
+
match. This avoids assigning a whole triangle by a sparsely sampled UV center.
|
|
144
|
+
|
|
145
|
+
`triangles.npz` contains centers, labels and conflicts. `material-N-surface.npz` contains
|
|
146
|
+
position, normal, occupied, viewBits and visibilityBits. `viewBits` records views supporting
|
|
147
|
+
an annotation; `visibilityBits` independently records geometric visibility, including
|
|
148
|
+
unannotated views and unlabelled visible texels. Use the latter for projecting registered
|
|
149
|
+
detail, never an annotation's polygon boundary. Neither field assigns an anatomical label.
|
|
150
|
+
Older bakes without `visibilityBits` must be regenerated, not silently substituted.
|
|
151
|
+
Before consuming visibility bits, compare the archived `dense-request.json` source,
|
|
152
|
+
ordered cameras, image size, material ordering and world bounds against the supplied
|
|
153
|
+
manifest. A source hash alone cannot detect reordered camera bits. A color-only evidence
|
|
154
|
+
revision may preserve that geometric contract; changed cameras require a new bake.
|
|
155
|
+
These are build intermediates, not game textures.
|
|
156
|
+
The current visibility bit field supports up to 32 views in one revision. Blender workers
|
|
157
|
+
return a nonzero exit code on Python errors. Keep output revisions separate when changing
|
|
158
|
+
annotations; do not overwrite evidence to bypass a hash check.
|
|
159
|
+
|
|
160
|
+
Generic `skin` agrees with an observed `palm`, `sole` or `dorsal_hand` label. Two distinct
|
|
161
|
+
specific labels still conflict. This hierarchy combines semantic observations; it does not
|
|
162
|
+
recognize anatomy. A hard material boundary magnifies small polygon gaps: overlap enclosing
|
|
163
|
+
skin and its explicit hair/lip exclusions within the same view instead of leaving a narrow
|
|
164
|
+
unlabelled strip between them. Never fill an unseen nail or a contradictory observation.
|
|
165
|
+
|
|
166
|
+
### Thin unresolved folds
|
|
167
|
+
|
|
168
|
+
A closer view in the same direction does not necessarily resolve visibility gaps. The
|
|
169
|
+
b3658 rear-neck test retained a thin unobserved strip in a shallow fold until an oblique
|
|
170
|
+
view showed the recessed surface. Inspect source, coverage and shaded images separately.
|
|
171
|
+
Add that new view and annotate actual anatomy; never increase ray tolerance or fill unknown
|
|
172
|
+
regions merely to remove a line. Sample-level coverage improvements do not prove that all
|
|
173
|
+
residual albedo or filtering seams have disappeared. Keep those review results distinct.
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
### Source-color repair across UV islands
|
|
177
|
+
|
|
178
|
+
A harmonic image fill assumes the selected patch has clean boundaries. UV seams can split one anatomical patch into many islands whose boundaries remain contaminated. If a reviewed color repair remains unchanged, inspect the repaired albedo reprojected onto the exact mesh before extending semantic labels. For such defects, author clean color swatches on the same visible anatomical surface and interpolate in surface space, keeping the repair selection separate from anatomy. Check every donor against source hashes and independently reviewed skin. Record the swatch coordinates and inspect the final render. This is texture reconstruction, not segmentation or recovered source detail.
|
|
179
|
+
|
|
180
|
+
Designed features such as a nail plate absent from the source must be explicitly recorded as authored additions. They must not silently override contradictory anatomical observations; reconcile the views or keep the design unaccepted.
|
|
181
|
+
|
|
182
|
+
## Occluding silhouettes need precise annotations
|
|
183
|
+
|
|
184
|
+
A bottom-foot view may show the shin or clothing immediately beyond the heel and
|
|
185
|
+
between toes. A polygon around the apparent foot outline must exclude those visible
|
|
186
|
+
surfaces too; they are not necessarily empty background. In the b3662 trial, a few pixels
|
|
187
|
+
of excess sole annotation produced a curved sole-detail strip on the shin. The ray test
|
|
188
|
+
correctly saw that shin surface—the semantic polygon was wrong. Trace a bad rendered
|
|
189
|
+
point with `scripts/inspect_skin_point.py`, inspect the contributing source pixel, correct
|
|
190
|
+
that specific polygon, and reproject. Do not move a shader cutoff to conceal the artifact.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Regional detail helpers
|
|
2
|
+
|
|
3
|
+
The shipped `scripts/authored_detail.py` and `scripts/authored_surface_patch.py` are
|
|
4
|
+
Blender-independent NumPy/Pillow/SciPy libraries. They evaluate already-authored curves
|
|
5
|
+
and roughness patches. They do not recognize anatomy, load a rig, or export materials.
|
|
6
|
+
Keep profiles and licensed images in the character workspace, outside the refreshed skill.
|
|
7
|
+
|
|
8
|
+
Pass the exact evidence manifest and physical character height in metres. Surface points
|
|
9
|
+
must use the evidence's Blender world axes, centered on `worldBounds` and scaled by
|
|
10
|
+
physical height divided by the source bounds' Z extent. Normals use those same axes.
|
|
11
|
+
`visibility` is the dense bake's **geometric `visibilityBits`**, not annotation `viewBits`.
|
|
12
|
+
The label array is the reviewed semantic map. Both helpers leave other regions unchanged.
|
|
13
|
+
|
|
14
|
+
`AuthoredDetail(profile_path, manifest, height_m).evaluate(points, normals, labels,
|
|
15
|
+
visibility)` returns signed relief in metres. The profile contains `sourceSha256`,
|
|
16
|
+
`evidenceHashes`, `provenance`, and `curves`. Each curve specifies `view`, `region`,
|
|
17
|
+
`pointsPx`, `widthM`, and `depthM`. Regions supported here are `skin`, `palm`, `sole`,
|
|
18
|
+
and `dorsal_hand`. Coordinates belong to the exact source-view image. Record the curves
|
|
19
|
+
as designed enhancements, not measured anatomy.
|
|
20
|
+
|
|
21
|
+
`AuthoredSurfacePatch(profile_path, manifest, height_m, workspace_root).evaluate(points,
|
|
22
|
+
normals, labels, visibility, base_roughness)` returns blended roughness. The profile has
|
|
23
|
+
`sourceSha256`, `evidenceHashes`, and `patches`. Each patch contains:
|
|
24
|
+
|
|
25
|
+
- `view`, `region`, ordered `targetPixels` around the selected polygon.
|
|
26
|
+
- `roughnessTexture`, `textureSha256`, `sourceAnnotationImageSize`, and corresponding
|
|
27
|
+
`sourcePixels` from the independently inspected donor image.
|
|
28
|
+
- `featherM` and `roughnessRange` as an explicit authored response range.
|
|
29
|
+
- `provenance`: `title`, `url`, `licenseFile`, and `licenseSha256`.
|
|
30
|
+
|
|
31
|
+
Texture and license paths are relative to the supplied workspace root. The helper checks
|
|
32
|
+
the current evidence image, texture and license hashes. Retain its `report` and append
|
|
33
|
+
all `attributions` to the exported asset's notices. Exemplar roughness is not necessarily
|
|
34
|
+
measured: record its origin accurately. This helper transfers neither color nor normals.
|
|
35
|
+
Normal-map RGB is not height data; integrating normals requires a declared tangent
|
|
36
|
+
convention and surface metric, followed by independent relief review.
|
|
37
|
+
|
|
38
|
+
An optional `height` record supplies an already prepared signed-height NumPy array:
|
|
39
|
+
`path`, `sha256`, `boundsPx` in the donor annotation coordinates, explicit `scale`,
|
|
40
|
+
and `provenance`. Values are metres, not RGB channels. The separate
|
|
41
|
+
`relief(points, normals, labels, visibility)` method returns the feathered height
|
|
42
|
+
contribution, leaving roughness evaluation unchanged. Preserve the method, source
|
|
43
|
+
normal convention, assumed metric and any artistic amplitude change in provenance.
|
|
44
|
+
Use known or explicitly designed height; this helper does not integrate normal maps.
|
|
45
|
+
|
|
46
|
+
For a square, locally planar exemplar with a declared isotropic metric,
|
|
47
|
+
`scripts/normal_patch_height.py` exposes `normal_patch_height(rgb, width_m, normal_y)`.
|
|
48
|
+
RGB is the raw linear normal-map data in [0,1]; `normal_y` must explicitly be `up` or
|
|
49
|
+
`down`. It uses [Frankot/Chellappa discrete slope integration](https://doi.org/10.1109/34.3909),
|
|
50
|
+
a reflected periodic extension, and physically specified band limiting. Save its output
|
|
51
|
+
as a signed-metre NumPy array and inspect the reconstructed folds before transfer. It
|
|
52
|
+
does not recover an unknown donor metric, recognize a skin region, or make artist-authored
|
|
53
|
+
normal maps into measured scans. The playground's Hafnia palm test explicitly assumes an
|
|
54
|
+
80mm planar crop and records that assumption; it is not a universal scale for hand assets.
|
|
55
|
+
|
|
56
|
+
`sampleResolution` can increase field sampling independently of the annotation image.
|
|
57
|
+
Physical feather width stays unchanged. When the project's exporter supports regional
|
|
58
|
+
atlas packing, `independentDetail: true` exposes the patch through `independent_planes()`.
|
|
59
|
+
The exporter must then exclude it from the coarse body bake (`include_independent=False`)
|
|
60
|
+
and register its texture coordinates separately. Do not silently omit it in an exporter
|
|
61
|
+
that lacks this path. Overlapping independent bindings require a deliberately combined
|
|
62
|
+
patch; there is no automatic regional winner.
|
|
63
|
+
|
|
64
|
+
Inspect the transferred patch at region-sized framing under side and specular lighting.
|
|
65
|
+
A small central-palm experiment is not acceptance of the whole hand, fingers or nails.
|
|
66
|
+
If the full-body atlas cannot resolve the patch, address texel density before amplifying it.
|
|
67
|
+
|
|
68
|
+
For independent atlas slots, keep a transparent margin around the authored patch inside
|
|
69
|
+
the evidence view. The playground packer rejects edge-touching patches rather than
|
|
70
|
+
clamping visible relief into a seam. Widen/reframe the evidence and review the annotation
|
|
71
|
+
again. Packing a patch at higher resolution is not a visual acceptance: inspect its relief
|
|
72
|
+
at an ordinary region-sized view and with side light before claiming improved skin.
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""Attach the shared body rig without re-exporting authored materials or eye layers.
|
|
2
|
+
|
|
3
|
+
Original surface vertices match the dense bound mesh. Newly clipped eye vertices
|
|
4
|
+
use a unanimous rigid-head field or exact bound-triangle interpolation. Optical
|
|
5
|
+
rest fitting retains its pre-fit correspondence; off-surface evidence is rejected.
|
|
6
|
+
"""
|
|
7
|
+
import argparse,copy,hashlib,json,shutil
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import numpy as np
|
|
10
|
+
from scipy.spatial import cKDTree
|
|
11
|
+
from scipy.spatial.transform import Rotation
|
|
12
|
+
from glb_arrays import read,accessor
|
|
13
|
+
from glb_output import append_accessor
|
|
14
|
+
from vision_materials import source_world_points
|
|
15
|
+
from eye_assets import write_glb
|
|
16
|
+
|
|
17
|
+
from workspace_paths import workspace_root
|
|
18
|
+
ROOT=workspace_root(__file__)
|
|
19
|
+
|
|
20
|
+
def node_matrix(node):
|
|
21
|
+
if 'matrix' in node:return np.asarray(node['matrix']).reshape(4,4).T
|
|
22
|
+
matrix=np.eye(4);matrix[:3,:3]=Rotation.from_quat(node.get('rotation',[0,0,0,1])).as_matrix()@np.diag(node.get('scale',[1,1,1]));matrix[:3,3]=node.get('translation',[0,0,0]);return matrix
|
|
23
|
+
|
|
24
|
+
def world_matrices(doc):
|
|
25
|
+
parents={child:i for i,n in enumerate(doc['nodes']) for child in n.get('children',[])};cache={}
|
|
26
|
+
def world(i):
|
|
27
|
+
if i not in cache:cache[i]=(world(parents[i]) if i in parents else np.eye(4))@node_matrix(doc['nodes'][i])
|
|
28
|
+
return cache[i]
|
|
29
|
+
return [world(i) for i in range(len(doc['nodes']))]
|
|
30
|
+
|
|
31
|
+
def set_transform(node,matrix):
|
|
32
|
+
scale=np.linalg.norm(matrix[:3,:3],axis=0);rotation=matrix[:3,:3]/scale
|
|
33
|
+
if not np.allclose(rotation.T@rotation,np.eye(3),atol=1e-5) or np.linalg.det(rotation)<0:raise ValueError('Eye attachment requires a non-sheared positive transform')
|
|
34
|
+
node.pop('matrix',None);node.update(translation=matrix[:3,3].tolist(),rotation=Rotation.from_matrix(rotation).as_quat().tolist(),scale=scale.tolist())
|
|
35
|
+
|
|
36
|
+
def skin_points(points,joints,weights,matrices):
|
|
37
|
+
result=np.zeros_like(points,dtype=float);p=np.c_[points,np.ones(len(points))]
|
|
38
|
+
for slot in range(4):result+=np.einsum('nij,nj->ni',matrices[joints[:,slot]],p)[:,:3]*weights[:,slot,None]
|
|
39
|
+
return result
|
|
40
|
+
|
|
41
|
+
def surface_weight_transfer(query,points,faces,joints,weights,tolerance):
|
|
42
|
+
"""Exact closest-triangle interpolation; reject points off the bound surface."""
|
|
43
|
+
triangles=points[faces];centers=triangles.mean(1)
|
|
44
|
+
radius=np.linalg.norm(triangles-centers[:,None,:],axis=2).max()
|
|
45
|
+
tree=cKDTree(centers);vertices=cKDTree(points[np.unique(faces)])
|
|
46
|
+
result_j=[];result_w=[]
|
|
47
|
+
for point in query:
|
|
48
|
+
upper=vertices.query(point)[0]
|
|
49
|
+
ids=tree.query_ball_point(point,float(upper+radius+tolerance))
|
|
50
|
+
tri=triangles[ids];a=tri[:,0];u=tri[:,1]-a;v=tri[:,2]-a;p=point-a
|
|
51
|
+
uu=(u*u).sum(1);uv=(u*v).sum(1);vv=(v*v).sum(1);pu=(p*u).sum(1);pv=(p*v).sum(1)
|
|
52
|
+
determinant=uu*vv-uv*uv;safe=np.where(determinant>1e-24,determinant,1)
|
|
53
|
+
b=(vv*pu-uv*pv)/safe;c=(uu*pv-uv*pu)/safe
|
|
54
|
+
bary=np.stack([1-b-c,b,c],1);inside=(bary>=0).all(1)&(determinant>1e-24)
|
|
55
|
+
distance=np.linalg.norm(np.einsum('ni,nij->nj',bary,tri)-point,axis=1)
|
|
56
|
+
distance[~inside]=np.inf
|
|
57
|
+
for first,last in [(0,1),(1,2),(2,0)]:
|
|
58
|
+
edge=tri[:,last]-tri[:,first];length=(edge*edge).sum(1)
|
|
59
|
+
t=np.clip(((point-tri[:,first])*edge).sum(1)/np.maximum(length,1e-24),0,1)
|
|
60
|
+
candidate=np.linalg.norm(tri[:,first]+edge*t[:,None]-point,axis=1);better=candidate<distance
|
|
61
|
+
distance[better]=candidate[better];bary[better]=0;bary[better,first]=1-t[better];bary[better,last]=t[better]
|
|
62
|
+
closest=int(np.argmin(distance))
|
|
63
|
+
if distance[closest]>tolerance:raise ValueError('Binding provenance is not on the validated source surface')
|
|
64
|
+
corners=faces[ids[closest]];combined={}
|
|
65
|
+
for vertex,factor in zip(corners,bary[closest]):
|
|
66
|
+
for joint,weight in zip(joints[vertex],weights[vertex]):combined[int(joint)]=combined.get(int(joint),0)+float(factor*weight)
|
|
67
|
+
strongest=sorted(combined.items(),key=lambda item:item[1],reverse=True)[:4]
|
|
68
|
+
strongest += [(0,0)]*(4-len(strongest));total=sum(w for _,w in strongest)
|
|
69
|
+
if total<=0:raise ValueError('Bound triangle has no skin weights')
|
|
70
|
+
result_j.append([j for j,_ in strongest]);result_w.append([w/total for _,w in strongest])
|
|
71
|
+
return np.asarray(result_j),np.asarray(result_w)
|
|
72
|
+
|
|
73
|
+
def assemble(sid,relative_input,variant='eyes'):
|
|
74
|
+
base=ROOT/'characters'/sid/'high/v1';input_path=(base/relative_input).resolve()
|
|
75
|
+
if base.resolve() not in input_path.parents:raise ValueError('Input must be a revision inside this character')
|
|
76
|
+
doc,data=read(input_path);rig_path=base/'output/character.glb';rig,rig_data=read(rig_path);rig_report=json.loads((base/'rig-report.json').read_text())
|
|
77
|
+
if hashlib.sha256(rig_path.read_bytes()).hexdigest()!=rig_report['output_sha256']:raise ValueError('Body rig changed after validation')
|
|
78
|
+
if rig_report['geometry_displacement_max_m']>1e-6:raise ValueError('Reshaped sources require an explicit surface deformation transfer')
|
|
79
|
+
if len(rig.get('skins',[]))!=1 or doc.get('skins'):raise ValueError('This assembly adapter adds exactly one new body skin')
|
|
80
|
+
body_node=next(i for i,n in enumerate(doc['nodes']) if n.get('mesh')==0)
|
|
81
|
+
rig_body=next(i for i,n in enumerate(rig['nodes']) if n.get('skin')==0)
|
|
82
|
+
if any('mesh' in n for i,n in enumerate(rig['nodes']) if i!=rig_body):raise ValueError('Unexpected extra body geometry')
|
|
83
|
+
worlds=world_matrices(doc);rig_world=world_matrices(rig)
|
|
84
|
+
if not np.allclose(rig_world[rig_body],np.eye(4),atol=1e-6):raise ValueError('Bound mesh needs an explicit nonidentity bind-space adapter')
|
|
85
|
+
primitives=doc['meshes'][0]['primitives'];positions=accessor(doc,data,primitives[0]['attributes']['POSITION'])
|
|
86
|
+
if any(p['attributes']['POSITION']!=primitives[0]['attributes']['POSITION'] for p in primitives):raise ValueError('Expected a shared surface vertex pool')
|
|
87
|
+
rp=rig['meshes'][rig['nodes'][rig_body]['mesh']]['primitives'][0]
|
|
88
|
+
bound_positions=accessor(rig,rig_data,rp['attributes']['POSITION']);bound_joints=accessor(rig,rig_data,rp['attributes']['JOINTS_0']).astype(np.int32);bound_weights=accessor(rig,rig_data,rp['attributes']['WEIGHTS_0'])
|
|
89
|
+
if not np.issubdtype(bound_weights.dtype,np.floating):raise ValueError('Decode normalized integer weight attributes explicitly')
|
|
90
|
+
scale=rig_report['normalization']['scale'];center=np.asarray(rig_report['normalization']['center']);normalization=np.eye(4);normalization[:3,:3]*=scale;normalization[:3,3]=-center[[0,2,1]]*np.array([1,1,-1])*scale
|
|
91
|
+
transform=normalization@worlds[body_node];expected=positions@transform[:3,:3].T+transform[:3,3]
|
|
92
|
+
binding_index=primitives[0]['attributes'].get('_BIND_SOURCE_POSITION')
|
|
93
|
+
if any(p['attributes'].get('_BIND_SOURCE_POSITION')!=binding_index for p in primitives):raise ValueError('Expected shared source binding positions across material partitions')
|
|
94
|
+
binding_positions=positions if binding_index is None else accessor(doc,data,binding_index)
|
|
95
|
+
if binding_positions.shape!=positions.shape or not np.isfinite(binding_positions).all():raise ValueError('Invalid source binding positions')
|
|
96
|
+
binding_expected=binding_positions@transform[:3,:3].T+transform[:3,3]
|
|
97
|
+
distance,index=cKDTree(bound_positions).query(binding_expected);tolerance=max(np.ptp(bound_positions,axis=0))*2e-6
|
|
98
|
+
joints=bound_joints[index].copy();weights=bound_weights[index].copy();unmatched=distance>tolerance
|
|
99
|
+
source_skin=rig['skins'][0];head_indices=[k for k,i in enumerate(source_skin['joints']) if rig['nodes'][i].get('name') in ['Head','mixamorigHead','mixamorig:Head']]
|
|
100
|
+
if len(head_indices)!=1:raise ValueError('The shared rig must identify one Head joint')
|
|
101
|
+
head=head_indices[0]
|
|
102
|
+
barycentric_count=0
|
|
103
|
+
if unmatched.any():
|
|
104
|
+
# A clipped ocular vertex is safe only within the explicit rigid-head field.
|
|
105
|
+
d,near=cKDTree(bound_positions).query(binding_expected[unmatched],k=4)
|
|
106
|
+
head_weight=np.sum(bound_weights[near]*(bound_joints[near]==head),axis=2)
|
|
107
|
+
rigid=(d<=.003).all(1)&(head_weight>=.9999).all(1)
|
|
108
|
+
missing_ids=np.flatnonzero(unmatched);joints[missing_ids[rigid]]=head;weights[missing_ids[rigid]]=[1,0,0,0]
|
|
109
|
+
if not rigid.all():
|
|
110
|
+
selected=missing_ids[~rigid]
|
|
111
|
+
faces=np.concatenate([accessor(rig,rig_data,p['indices']).reshape(-1,3) for p in rig['meshes'][rig['nodes'][rig_body]['mesh']]['primitives']])
|
|
112
|
+
joints[selected],weights[selected]=surface_weight_transfer(binding_expected[selected],bound_positions,faces,bound_joints,bound_weights,tolerance)
|
|
113
|
+
barycentric_count=len(selected)
|
|
114
|
+
if not np.isfinite(weights).all() or np.max(abs(weights.sum(1)-1))>1e-5:raise ValueError('Invalid transferred weights')
|
|
115
|
+
original_length=len(data);mapping={}
|
|
116
|
+
for i,n in enumerate(rig['nodes']):
|
|
117
|
+
if i!=rig_body:mapping[i]=len(doc['nodes']);doc['nodes'].append(copy.deepcopy(n))
|
|
118
|
+
for old,new in mapping.items():
|
|
119
|
+
if 'children' in doc['nodes'][new]:doc['nodes'][new]['children']=[mapping[c] for c in rig['nodes'][old]['children'] if c!=rig_body]
|
|
120
|
+
roots=rig['scenes'][rig.get('scene',0)]['nodes'];doc['scenes'][doc.get('scene',0)]['nodes'] += [mapping[i] for i in roots if i!=rig_body]
|
|
121
|
+
inverse_bind=accessor(rig,rig_data,source_skin['inverseBindMatrices']).reshape(-1,4,4).transpose(0,2,1)
|
|
122
|
+
# Keep the rig's canonical inverse binds: Skeleton.pose() reconstructs its
|
|
123
|
+
# rest world matrices from them. Convert mesh coordinates instead of hiding
|
|
124
|
+
# a mesh-space transform inside the inverse binds.
|
|
125
|
+
coordinate_scale=np.linalg.norm(transform[:3,:3],axis=0)
|
|
126
|
+
if not np.allclose(coordinate_scale,coordinate_scale[0],rtol=1e-5):raise ValueError('Nonuniform bind-space conversion needs explicit normal handling')
|
|
127
|
+
rotation=transform[:3,:3]/coordinate_scale[0]
|
|
128
|
+
if not np.allclose(rotation.T@rotation,np.eye(3),atol=1e-5):raise ValueError('Sheared bind-space conversion is unsupported')
|
|
129
|
+
converted={}
|
|
130
|
+
def convert_attribute(index,kind,delta=False):
|
|
131
|
+
key=(index,kind,delta)
|
|
132
|
+
if key not in converted:
|
|
133
|
+
values=accessor(doc,data,index)
|
|
134
|
+
if kind=='POSITION':values=values@transform[:3,:3].T+(0 if delta else transform[:3,3])
|
|
135
|
+
elif kind=='TANGENT':values[:,:3]=values[:,:3]@rotation.T
|
|
136
|
+
else:values=values@rotation.T
|
|
137
|
+
converted[key]=append_accessor(doc,data,values.astype('<f4'),'VEC4' if kind=='TANGENT' else 'VEC3',5126)
|
|
138
|
+
if kind=='POSITION':doc['accessors'][converted[key]].update(min=values.min(0).tolist(),max=values.max(0).tolist())
|
|
139
|
+
return converted[key]
|
|
140
|
+
for p in primitives:
|
|
141
|
+
# Authoring correspondence is consumed here; it is not a renderer input.
|
|
142
|
+
p['attributes'].pop('_BIND_SOURCE_POSITION',None)
|
|
143
|
+
for kind in ['POSITION','NORMAL','TANGENT']:
|
|
144
|
+
if kind in p['attributes']:p['attributes'][kind]=convert_attribute(p['attributes'][kind],kind)
|
|
145
|
+
for target in p.get('targets',[]):
|
|
146
|
+
for kind in ['POSITION','NORMAL']:
|
|
147
|
+
if kind in target:target[kind]=convert_attribute(target[kind],kind,True)
|
|
148
|
+
adjusted=inverse_bind
|
|
149
|
+
pose_error=float(np.max(abs(np.linalg.inv(adjusted)-np.asarray([rig_world[i] for i in source_skin['joints']]))))
|
|
150
|
+
if pose_error>2e-5:raise ValueError('Inverse binds are incompatible with the engine skeleton pose reset')
|
|
151
|
+
ibm_index=append_accessor(doc,data,adjusted.transpose(0,2,1).reshape(-1,16).astype('<f4'),'MAT4',5126)
|
|
152
|
+
doc['skins']=[{'name':source_skin.get('name','SharedBodyRig'),'joints':[mapping[i] for i in source_skin['joints']],'inverseBindMatrices':ibm_index}];doc['nodes'][body_node]['skin']=0
|
|
153
|
+
ji=append_accessor(doc,data,joints.astype('<u2'),'VEC4',5123);wi=append_accessor(doc,data,weights.astype('<f4'),'VEC4',5126)
|
|
154
|
+
for p in primitives:p['attributes'].update(JOINTS_0=ji,WEIGHTS_0=wi)
|
|
155
|
+
# Optical roots and head-fixed tissue now follow the animated Head joint.
|
|
156
|
+
head_node=mapping[source_skin['joints'][head]];head_world=rig_world[source_skin['joints'][head]];rotation_changes={};attachments=list(doc['nodes'][body_node].get('children',[]))
|
|
157
|
+
for child in attachments:
|
|
158
|
+
node=doc['nodes'][child]
|
|
159
|
+
optical=node.get('extras',{}).get('characterEye') or ('mesh' in node and all(doc['materials'][p['material']].get('extras',{}).get('eyeLayer') for p in doc['meshes'][node['mesh']]['primitives']))
|
|
160
|
+
if not optical:raise ValueError('Nonocular body attachments require explicit parenting')
|
|
161
|
+
old_rotation=Rotation.from_quat(node.get('rotation',[0,0,0,1]));set_transform(node,np.linalg.inv(head_world)@normalization@worlds[child]);rotation_changes[child]=Rotation.from_quat(node['rotation'])*old_rotation.inv()
|
|
162
|
+
doc['nodes'][head_node].setdefault('children',[]).append(child)
|
|
163
|
+
doc['nodes'][body_node]['children']=[]
|
|
164
|
+
for i,node in enumerate(doc['nodes']):
|
|
165
|
+
if i!=body_node and body_node in node.get('children',[]):node['children'].remove(body_node)
|
|
166
|
+
set_transform(doc['nodes'][body_node],np.eye(4))
|
|
167
|
+
scene_nodes=doc['scenes'][doc.get('scene',0)]['nodes']
|
|
168
|
+
if body_node not in scene_nodes:scene_nodes.append(body_node)
|
|
169
|
+
for animation in doc.get('animations',[]):
|
|
170
|
+
for channel in animation['channels']:
|
|
171
|
+
target=channel['target']
|
|
172
|
+
if target.get('path')=='rotation' and target['node'] in rotation_changes:
|
|
173
|
+
sampler=animation['samplers'][channel['sampler']]
|
|
174
|
+
if sampler.get('interpolation','LINEAR')!='LINEAR':raise ValueError('Eye rotation retarget requires linear quaternion tracks')
|
|
175
|
+
q=accessor(doc,data,sampler['output']);q=(rotation_changes[target['node']]*Rotation.from_quat(q)).as_quat();sampler['output']=append_accessor(doc,data,q.astype('<f4'),'VEC4',5126)
|
|
176
|
+
copied={}
|
|
177
|
+
def copy_accessor(index):
|
|
178
|
+
if index not in copied:
|
|
179
|
+
original=rig['accessors'][index];copied[index]=append_accessor(doc,data,accessor(rig,rig_data,index),original['type'],original['componentType'])
|
|
180
|
+
for key in ['min','max','normalized']:
|
|
181
|
+
if key in original:doc['accessors'][copied[index]][key]=original[key]
|
|
182
|
+
return copied[index]
|
|
183
|
+
for animation in rig.get('animations',[]):
|
|
184
|
+
a=copy.deepcopy(animation)
|
|
185
|
+
for sampler in a['samplers']:sampler['input']=copy_accessor(sampler['input']);sampler['output']=copy_accessor(sampler['output'])
|
|
186
|
+
for channel in a['channels']:channel['target']['node']=mapping[channel['target']['node']]
|
|
187
|
+
doc.setdefault('animations',[]).append(a)
|
|
188
|
+
matrices=np.asarray([rig_world[i] for i in source_skin['joints']])@adjusted
|
|
189
|
+
maximum=0.
|
|
190
|
+
for batch in np.array_split(np.arange(len(positions)),max(1,len(positions)//100000)):
|
|
191
|
+
error=np.linalg.norm(skin_points(expected[batch],joints[batch],weights[batch],matrices)-expected[batch],axis=1);maximum=max(maximum,float(error.max()))
|
|
192
|
+
if maximum>2e-5:raise ValueError(f'Assembled bind pose does not preserve the source: {maximum}m')
|
|
193
|
+
out=base/'runtime/v1';out.mkdir(parents=True,exist_ok=True)
|
|
194
|
+
report={'id':sid,'status':'motion_review_required','viewerReady':False,'surfaceInput':relative_input,'surfaceSha256':hashlib.sha256(input_path.read_bytes()).hexdigest(),'rigSha256':rig_report['output_sha256'],'restPositionErrorM':maximum,'rigidHeadInsertedVertices':int(unmatched.sum()),'bodyBones':len(source_skin['joints']),'headAttachments':len(attachments),'clips':[a['name'] for a in doc.get('animations',[])],'sourceBufferBytesPreserved':bytes(data[:original_length])==bytes(read(input_path)[1]),'geometryChange':'Rigid uniform bind-space coordinate conversion of positions, normals and morph deltas; no surface reshaping','limitations':['Shared body rig currently has no independent finger bones','Motion and eye contact require visual acceptance']}
|
|
195
|
+
report['variant']=variant
|
|
196
|
+
report['barycentricWeightVertices']=barycentric_count
|
|
197
|
+
report['rigidHeadInsertedVertices']=int(unmatched.sum())-barycentric_count
|
|
198
|
+
report['poseResetMatrixError']=pose_error
|
|
199
|
+
report['outputSha256']=write_glb(doc,data,out/f'{variant}.glb');(out/f'{variant}-report.json').write_text(json.dumps(report,indent=2))
|
|
200
|
+
for name in ['ATTRIBUTION.txt','MakeHuman-LICENSE.md']:
|
|
201
|
+
if (input_path.parent/name).exists():shutil.copyfile(input_path.parent/name,out/name)
|
|
202
|
+
print(json.dumps(report))
|
|
203
|
+
|
|
204
|
+
if __name__=='__main__':
|
|
205
|
+
p=argparse.ArgumentParser();p.add_argument('--id',required=True);p.add_argument('--input',required=True);p.add_argument('--variant',choices=['eyes','surfaces'],default='eyes');a=p.parse_args();assemble(a.id,a.input,a.variant)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Bake agent-authored regional crease curves; never infer anatomy or landmarks."""
|
|
2
|
+
import hashlib,json
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
LABELS={'skin':1,'palm':2,'sole':3,'dorsal_hand':9}
|
|
6
|
+
|
|
7
|
+
def curve_distance(points,curve):
|
|
8
|
+
distance=np.full(len(points),np.inf)
|
|
9
|
+
for a,b in zip(curve[:-1],curve[1:]):
|
|
10
|
+
edge=b-a;length=np.dot(edge,edge)
|
|
11
|
+
if length<=0:raise ValueError('Crease curve has a repeated point')
|
|
12
|
+
t=np.clip((points-a)@edge/length,0,1)
|
|
13
|
+
distance=np.minimum(distance,np.linalg.norm(points-a-t[:,None]*edge,axis=1))
|
|
14
|
+
return distance
|
|
15
|
+
|
|
16
|
+
class AuthoredDetail:
|
|
17
|
+
def __init__(self,path,manifest,physical_height):
|
|
18
|
+
self.curves=[];self.report=None
|
|
19
|
+
if not path.exists():return
|
|
20
|
+
raw=path.read_bytes();profile=json.loads(raw)
|
|
21
|
+
if profile['sourceSha256']!=manifest['sourceSha256']:raise ValueError('Body detail profile belongs to another source')
|
|
22
|
+
for record in profile['curves']:
|
|
23
|
+
index=next(i for i,v in enumerate(manifest['views']) if v['name']==record['view'])
|
|
24
|
+
image=record['view']+'.png'
|
|
25
|
+
if profile['evidenceHashes'].get(image)!=manifest['evidenceHashes'].get(image):raise ValueError('Body detail coordinates belong to different view evidence')
|
|
26
|
+
view=manifest['views'][index];direction=np.asarray(view['direction'],float);direction/=np.linalg.norm(direction)
|
|
27
|
+
right=np.cross(direction,view.get('up',[0,0,1]));right/=np.linalg.norm(right);up=np.cross(right,direction)
|
|
28
|
+
curve=np.asarray(record['pointsPx'],float)
|
|
29
|
+
if curve.ndim!=2 or curve.shape[1]!=2 or len(curve)<2 or not np.isfinite(curve).all():raise ValueError('Invalid authored crease polyline')
|
|
30
|
+
width=record['widthM'];depth=record['depthM']
|
|
31
|
+
if not 0<width<=.003 or not 0<depth<=.00025:raise ValueError('Crease dimensions exceed the supported skin-detail range')
|
|
32
|
+
self.curves.append((index,np.asarray(view.get('targetOffset',[0,0,0]))*physical_height,right,up,direction,view['span']*physical_height,manifest['size'],curve,width,depth,LABELS[record['region']]))
|
|
33
|
+
self.report={'profileSha256':hashlib.sha256(raw).hexdigest(),'curves':len(self.curves),'provenance':profile['provenance'],'method':'agent-authored regional curves projected only onto visible independently labelled skin; physical width/depth'}
|
|
34
|
+
|
|
35
|
+
def evaluate(self,points,normals,labels,visibility):
|
|
36
|
+
result=np.zeros(len(points),np.float32)
|
|
37
|
+
for index,target,right,up,direction,span,size,curve,width,depth,region in self.curves:
|
|
38
|
+
selected=(labels==region)&((visibility&(1<<index))!=0)
|
|
39
|
+
if not selected.any():continue
|
|
40
|
+
local=points[selected]-target
|
|
41
|
+
xy=np.stack([(local@right/span+.5)*size,(.5-local@up/span)*size],1)
|
|
42
|
+
distance=curve_distance(xy,curve)*span/size
|
|
43
|
+
facing=np.clip((abs(normals[selected]@direction)-.2)/.3,0,1)
|
|
44
|
+
# A compact smooth groove avoids hard tube ends and adds no pigment.
|
|
45
|
+
relief=-depth*np.exp(-.5*(distance/width)**2)*facing*facing*(3-2*facing)
|
|
46
|
+
result[selected]=np.minimum(result[selected],relief)
|
|
47
|
+
return result
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Vision-registered roughness/height exemplars, limited by labels and visibility.
|
|
2
|
+
|
|
3
|
+
No inferred anatomy and no conversion of normal RGB into displacement.
|
|
4
|
+
"""
|
|
5
|
+
import hashlib,json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import numpy as np
|
|
8
|
+
from PIL import Image,ImageDraw
|
|
9
|
+
from scipy.interpolate import RBFInterpolator
|
|
10
|
+
from scipy.ndimage import distance_transform_edt,map_coordinates,gaussian_filter
|
|
11
|
+
from authored_detail import LABELS
|
|
12
|
+
|
|
13
|
+
class AuthoredSurfacePatch:
|
|
14
|
+
def __init__(self,path,manifest,physical_height,root):
|
|
15
|
+
self.patches=[];self.independent=[];self.report=None;self.attributions={}
|
|
16
|
+
if not path.exists():return
|
|
17
|
+
raw=path.read_bytes();profile=json.loads(raw)
|
|
18
|
+
if profile['sourceSha256']!=manifest['sourceSha256']:raise ValueError('Surface patch belongs to another source')
|
|
19
|
+
receipts=[]
|
|
20
|
+
for record in profile['patches']:
|
|
21
|
+
index=next(i for i,v in enumerate(manifest['views'])if v['name']==record['view']);view=manifest['views'][index];image=record['view']+'.png'
|
|
22
|
+
image_path=Path(manifest['output'])/image
|
|
23
|
+
if profile['evidenceHashes'].get(image)!=manifest['evidenceHashes'].get(image) or hashlib.sha256(image_path.read_bytes()).hexdigest()!=profile['evidenceHashes'][image]:raise ValueError('Surface patch evidence changed')
|
|
24
|
+
texture_path=root/record['roughnessTexture'];data=texture_path.read_bytes()
|
|
25
|
+
if hashlib.sha256(data).hexdigest()!=record['textureSha256']:raise ValueError('Surface patch exemplar changed')
|
|
26
|
+
provenance=record['provenance'];license_bytes=(root/provenance['licenseFile']).read_bytes()
|
|
27
|
+
if hashlib.sha256(license_bytes).hexdigest()!=provenance['licenseSha256']:raise ValueError('Surface patch license receipt changed')
|
|
28
|
+
self.attributions[provenance['licenseSha256']]=provenance['title']+'\n'+provenance['url']+'\nAdapted by vision registration and roughness remapping.\n'+license_bytes.decode()
|
|
29
|
+
donor=np.asarray(Image.open(texture_path).convert('L'),float)/255
|
|
30
|
+
source=np.asarray(record['sourcePixels'],float);target=np.asarray(record['targetPixels'],float)
|
|
31
|
+
if source.shape!=target.shape or source.ndim!=2 or source.shape[1]!=2 or len(source)<3 or not np.isfinite(source).all() or not np.isfinite(target).all():raise ValueError('Invalid surface patch correspondences')
|
|
32
|
+
size=manifest['size'];donor_size=record['sourceAnnotationImageSize'];resolution=record.get('sampleResolution',size)
|
|
33
|
+
if not isinstance(resolution,int) or not 16<=resolution<=4096:raise ValueError('Invalid patch sampling resolution')
|
|
34
|
+
if (target<0).any() or (target>=size).any() or (source<0).any() or (source>=donor_size).any():raise ValueError('Surface patch pixel outside evidence')
|
|
35
|
+
if not 0<record['featherM']<=.02:raise ValueError('Invalid surface patch feather')
|
|
36
|
+
low,high=record['roughnessRange']
|
|
37
|
+
if not 0<=low<high<=1:raise ValueError('Invalid surface patch roughness range')
|
|
38
|
+
target_mask=Image.new('L',(resolution,resolution));ImageDraw.Draw(target_mask).polygon([tuple(p*resolution/size)for p in target],fill=255);inside=np.asarray(target_mask)>0
|
|
39
|
+
yy,xx=np.nonzero(inside);mapped=RBFInterpolator(target/size,source/donor_size,kernel='thin_plate_spline',smoothing=0)(np.column_stack([xx,yy])/resolution)
|
|
40
|
+
sampled=map_coordinates(gaussian_filter(donor,1),[mapped[:,1]*(donor.shape[0]-1),mapped[:,0]*(donor.shape[1]-1)],order=1,mode='nearest')
|
|
41
|
+
# Preserve exemplar variation inside a declared art-directed physical response range.
|
|
42
|
+
rough=np.full((resolution,resolution),.5*(low+high));rough[yy,xx]=low+(high-low)*sampled
|
|
43
|
+
height=None
|
|
44
|
+
if record.get('height'):
|
|
45
|
+
h=record['height'];height_path=root/h['path']
|
|
46
|
+
if hashlib.sha256(height_path.read_bytes()).hexdigest()!=h['sha256']:raise ValueError('Surface height exemplar changed')
|
|
47
|
+
field=np.load(height_path,allow_pickle=False);bounds=np.asarray(h['boundsPx'],float)
|
|
48
|
+
if field.ndim!=2 or not np.isfinite(field).all() or np.max(abs(field))>.001 or bounds.shape!=(4,) or not np.isfinite(bounds).all() or np.any(bounds[2:]<=bounds[:2]) or not 0<h['scale']<=4 or not h.get('provenance'):
|
|
49
|
+
raise ValueError('Invalid physical surface height field or provenance')
|
|
50
|
+
coordinates=(mapped*donor_size-bounds[:2])/(bounds[2:]-bounds[:2])
|
|
51
|
+
height=np.zeros((resolution,resolution));height[yy,xx]=map_coordinates(field,[coordinates[:,1]*(field.shape[0]-1),coordinates[:,0]*(field.shape[1]-1)],order=1,mode='nearest')*h['scale']
|
|
52
|
+
self.attributions[provenance['licenseSha256']]+='\nHeight adaptation: '+json.dumps(h['provenance'])+'\n'
|
|
53
|
+
feather=np.clip(distance_transform_edt(inside)*view['span']*physical_height/resolution/record['featherM'],0,1);feather=feather*feather*(3-2*feather)
|
|
54
|
+
direction=np.asarray(view['direction'],float);direction/=np.linalg.norm(direction);right=np.cross(direction,view.get('up',[0,0,1]));right/=np.linalg.norm(right);up=np.cross(right,direction)
|
|
55
|
+
self.patches.append((index,np.asarray(view.get('targetOffset',[0,0,0]))*physical_height,right,up,direction,view['span']*physical_height,resolution,LABELS[record['region']],rough,feather,height))
|
|
56
|
+
self.independent.append(record.get('independentDetail',False))
|
|
57
|
+
receipts.append({'view':record['view'],'region':record['region'],'roughnessTexture':record['roughnessTexture'],'textureSha256':record['textureSha256'],'roughnessRange':record['roughnessRange'],'height':record.get('height'),'provenance':record['provenance'],'independentDetail':self.independent[-1],'sampleResolution':resolution})
|
|
58
|
+
self.report={'profileSha256':hashlib.sha256(raw).hexdigest(),'patches':receipts,'method':'independent vision correspondences; optional physical height; semantic labels and geometric visibility constrain application'}
|
|
59
|
+
|
|
60
|
+
def evaluate(self,points,normals,labels,visibility,base,include_independent=True):
|
|
61
|
+
return self._evaluate(points,normals,labels,visibility,base,False,include_independent)
|
|
62
|
+
|
|
63
|
+
def relief(self,points,normals,labels,visibility,include_independent=True):
|
|
64
|
+
return self._evaluate(points,normals,labels,visibility,np.zeros(len(points)),True,include_independent)
|
|
65
|
+
|
|
66
|
+
def _evaluate(self,points,normals,labels,visibility,base,use_height,include_independent):
|
|
67
|
+
total=np.zeros(len(points));value=np.zeros(len(points))
|
|
68
|
+
for independent,patch in zip(self.independent,self.patches):
|
|
69
|
+
if independent and not include_independent:continue
|
|
70
|
+
index,target,right,up,direction,span,size,region,rough,feather,height=patch
|
|
71
|
+
field=height if use_height else rough
|
|
72
|
+
if field is None:continue
|
|
73
|
+
selected=(labels==region)&((visibility&(1<<index))!=0)
|
|
74
|
+
if not selected.any():continue
|
|
75
|
+
local=points[selected]-target;xy=np.stack([(local@right/span+.5)*size,(.5-local@up/span)*size],1)
|
|
76
|
+
weight=map_coordinates(feather,[xy[:,1],xy[:,0]],order=1,mode='constant',cval=0)
|
|
77
|
+
facing=np.clip((abs(normals[selected]@direction)-.2)/.3,0,1);weight*=facing*facing*(3-2*facing)
|
|
78
|
+
value[selected]+=map_coordinates(field,[xy[:,1],xy[:,0]],order=1,mode='nearest')*weight;total[selected]+=weight
|
|
79
|
+
return base*(1-np.minimum(total,1))+value/np.maximum(total,1e-12)*np.minimum(total,1)
|
|
80
|
+
|
|
81
|
+
def independent_planes(self):
|
|
82
|
+
names=['index','target','right','up','direction','span','size','region','roughness','feather','height']
|
|
83
|
+
return [dict(zip(names,patch)) for independent,patch in zip(self.independent,self.patches) if independent]
|