@bitmagic/cli 0.1.66-dev.6 → 0.1.66-dev.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -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/commands/generate.d.ts +49 -0
- package/dist/commands/generate.js +168 -1
- package/dist/commands/generate.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/music.d.ts +4 -3
- package/dist/generate/music.js +6 -23
- package/dist/generate/music.js.map +1 -1
- package/dist/generate/stream.d.ts +13 -0
- package/dist/generate/stream.js +14 -1
- package/dist/generate/stream.js.map +1 -1
- package/dist/scaffold/asset-pipeline-skill-files.js +2 -0
- 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 +4 -4
- 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 +3 -1
- package/dist/scaffold/skill-files.js.map +1 -1
- package/dist/tools/bundles.js +8 -0
- package/dist/tools/bundles.js.map +1 -1
- package/package.json +4 -4
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `window.__lab` — the debug handle the shipped MetaHuman rigs drive (tools/metahuman/e2e: shot.mjs,
|
|
3
|
+
* eval.mjs, walk-probe.mjs and the probes under probes/). Install it once the actor has adopted the
|
|
4
|
+
* engine's player; add your own entries on top:
|
|
5
|
+
*
|
|
6
|
+
* window.__lab = { ...installMetaHumanLabHooks({ engine, actor, playerLoader, player }), mine: () => … };
|
|
7
|
+
*
|
|
8
|
+
* Nothing here is needed by the game itself — it is how a headless browser measures the character:
|
|
9
|
+
* pin the render camera (`freeCam`), freeze the procedural life (`setLife`), read the head position
|
|
10
|
+
* and facing for deterministic framing, tune the helper-joint policy, and dump the engine's retarget
|
|
11
|
+
* state (`retargetInfo`, `boneMap`) to see which rig bones are posed and corrected.
|
|
12
|
+
*/
|
|
13
|
+
import * as THREE from 'three';
|
|
14
|
+
import type { EngineLike } from 'types/game.js';
|
|
15
|
+
import type { HelperGroup, MetaHumanActor } from './MetaHumanActor.js';
|
|
16
|
+
|
|
17
|
+
/** The engine's ThirdPersonCamera, structurally (only what `orbitTo` touches). */
|
|
18
|
+
interface OrbitCamera {
|
|
19
|
+
setAutoFollow(on: boolean): void;
|
|
20
|
+
spherical: { theta: number; phi: number; radius: number };
|
|
21
|
+
targetSpherical: { theta: number; phi: number; radius: number };
|
|
22
|
+
distance: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface MetaHumanLabDeps {
|
|
26
|
+
engine: EngineLike;
|
|
27
|
+
actor: MetaHumanActor;
|
|
28
|
+
/** The engine's PlayerLoader: its private retarget maps are what `retargetInfo()` / `boneMap()` read. */
|
|
29
|
+
playerLoader: unknown;
|
|
30
|
+
/** The player object (capsule root) for `playerPosition()`. */
|
|
31
|
+
player?: THREE.Object3D | null;
|
|
32
|
+
/** The third-person camera, for `orbitTo` (optional). */
|
|
33
|
+
cameraController?: OrbitCamera | null;
|
|
34
|
+
/** The player controller, for `setPlayerYaw` (optional; needs `setFacingLock`). */
|
|
35
|
+
playerController?: { setFacingLock?: (rad: number | null) => void } | null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface Vec3Like { x: number; y: number; z: number }
|
|
39
|
+
|
|
40
|
+
export interface MetaHumanLabHooks {
|
|
41
|
+
actor: MetaHumanActor;
|
|
42
|
+
engine: () => EngineLike;
|
|
43
|
+
playerLoader: () => unknown;
|
|
44
|
+
camera: () => OrbitCamera | null;
|
|
45
|
+
playerPosition: () => Vec3Like | null;
|
|
46
|
+
headPosition: () => Vec3Like;
|
|
47
|
+
facing: () => Vec3Like;
|
|
48
|
+
setLife: (on: boolean) => void;
|
|
49
|
+
/** Snap the orbit camera: theta 0 = in front of the character (+Z), phi = polar angle from +Y. */
|
|
50
|
+
orbitTo: (theta: number, phi: number, distance?: number) => void;
|
|
51
|
+
/** Pin the render camera (applied right before render, after the camera controller). */
|
|
52
|
+
freeCam: (pos: Vec3Like, look: Vec3Like, fovDeg?: number) => void;
|
|
53
|
+
freeCamOff: () => void;
|
|
54
|
+
/** Turn the player to a yaw (null releases the lock). */
|
|
55
|
+
setPlayerYaw: (rad: number | null) => void;
|
|
56
|
+
/** Tune how much the corrective helper roots follow their parents (0 pinned … 1 rigid). */
|
|
57
|
+
setHelperFollow: (f: Partial<Record<HelperGroup, number>>) => Record<HelperGroup, number>;
|
|
58
|
+
/** The engine's memoised rig-bone → clip-bone name resolution (fills as frames run). */
|
|
59
|
+
boneMap: () => string[] | null;
|
|
60
|
+
/** Which rig bones the engine poses each frame, and which get a bind-difference correction. */
|
|
61
|
+
retargetInfo: () => { posed: string[] | null; deltas: string[] | null; bindCount: number | null };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function installMetaHumanLabHooks(deps: MetaHumanLabDeps): MetaHumanLabHooks {
|
|
65
|
+
let freeCamOff: (() => void) | null = null;
|
|
66
|
+
const hooks: MetaHumanLabHooks = {
|
|
67
|
+
actor: deps.actor,
|
|
68
|
+
engine: () => deps.engine,
|
|
69
|
+
playerLoader: () => deps.playerLoader,
|
|
70
|
+
camera: () => deps.cameraController ?? null,
|
|
71
|
+
playerPosition: () => { const p = deps.player?.position; return p ? { x: p.x, y: p.y, z: p.z } : null; },
|
|
72
|
+
headPosition: () => { const v = new THREE.Vector3(); deps.actor.getHeadWorldPosition(v); return { x: v.x, y: v.y, z: v.z }; },
|
|
73
|
+
facing: () => { const f = deps.actor.getFacing(); return { x: f.x, y: f.y, z: f.z }; },
|
|
74
|
+
setLife: (on) => deps.actor.setLifeEnabled(on),
|
|
75
|
+
orbitTo: (theta, phi, distance) => {
|
|
76
|
+
const tpc = deps.cameraController;
|
|
77
|
+
if (!tpc) return;
|
|
78
|
+
tpc.setAutoFollow(false);
|
|
79
|
+
tpc.spherical.theta = theta; tpc.targetSpherical.theta = theta;
|
|
80
|
+
tpc.spherical.phi = phi; tpc.targetSpherical.phi = phi;
|
|
81
|
+
if (distance !== undefined) { tpc.distance = distance; tpc.spherical.radius = distance; tpc.targetSpherical.radius = distance; }
|
|
82
|
+
},
|
|
83
|
+
freeCam: (pos, look, fovDeg) => {
|
|
84
|
+
freeCamOff?.();
|
|
85
|
+
const cam = deps.engine.getDefaultCamera();
|
|
86
|
+
const savedFov = cam.fov;
|
|
87
|
+
if (fovDeg !== undefined) { cam.fov = fovDeg; cam.updateProjectionMatrix(); }
|
|
88
|
+
const unregister = deps.engine.registerBeforeRender?.(() => {
|
|
89
|
+
cam.position.set(pos.x, pos.y, pos.z);
|
|
90
|
+
cam.lookAt(look.x, look.y, look.z);
|
|
91
|
+
cam.updateMatrixWorld(true);
|
|
92
|
+
});
|
|
93
|
+
freeCamOff = () => {
|
|
94
|
+
unregister?.();
|
|
95
|
+
cam.fov = savedFov; cam.updateProjectionMatrix();
|
|
96
|
+
freeCamOff = null;
|
|
97
|
+
};
|
|
98
|
+
},
|
|
99
|
+
freeCamOff: () => freeCamOff?.(),
|
|
100
|
+
setPlayerYaw: (rad) => deps.playerController?.setFacingLock?.(rad),
|
|
101
|
+
setHelperFollow: (f) => { Object.assign(deps.actor.helperFollow, f); return deps.actor.helperFollow; },
|
|
102
|
+
boneMap: () => {
|
|
103
|
+
const pl = deps.playerLoader as { mixamoNameMemo?: Map<string, string> };
|
|
104
|
+
return pl.mixamoNameMemo ? [...pl.mixamoNameMemo.entries()].map(([rig, clip]) => `${rig} → ${clip}`) : null;
|
|
105
|
+
},
|
|
106
|
+
retargetInfo: () => {
|
|
107
|
+
const pl = deps.playerLoader as {
|
|
108
|
+
skinnedRetargetDeltas?: Map<string, THREE.Quaternion> | null;
|
|
109
|
+
blendedPoseForBlock?: Map<string, unknown> | null;
|
|
110
|
+
skinnedBind?: { rotations: Map<string, THREE.Quaternion> } | null;
|
|
111
|
+
};
|
|
112
|
+
const deg = (q: THREE.Quaternion) => +(2 * Math.acos(Math.min(1, Math.abs(q.w))) * 180 / Math.PI).toFixed(1);
|
|
113
|
+
return {
|
|
114
|
+
posed: pl.blendedPoseForBlock ? [...pl.blendedPoseForBlock.keys()] : null,
|
|
115
|
+
deltas: pl.skinnedRetargetDeltas ? [...pl.skinnedRetargetDeltas.entries()].map(([k, q]) => `${k}:${deg(q)}°`) : null,
|
|
116
|
+
bindCount: pl.skinnedBind?.rotations.size ?? null,
|
|
117
|
+
};
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
(window as unknown as { __lab?: unknown }).__lab = hooks;
|
|
121
|
+
return hooks;
|
|
122
|
+
}
|
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MetaHuman material upgrades — renders an Epic MetaHuman GLB export (UE 5.7 →
|
|
3
|
+
* Blender → glTF, see assets/LICENSES.md) as faithfully as three.js allows,
|
|
4
|
+
* without touching the original geometry.
|
|
5
|
+
*
|
|
6
|
+
* WebGPU (the engine default) gets TSL node materials:
|
|
7
|
+
* - skin: roughness floor + a wrap-lighting subsurface term in a custom
|
|
8
|
+
* PhysicalLightingModel (light bleeding through ears / nose / lips)
|
|
9
|
+
* - hair & brow cards: the 4K MetaHuman card atlas (R = coverage, G = root→tip,
|
|
10
|
+
* B = per-strand seed) drives alpha-to-coverage, root darkening, per-strand
|
|
11
|
+
* tint & roughness variance and a normal-based self-occlusion
|
|
12
|
+
* - lashes: the 2K coverage mask, alpha-blended
|
|
13
|
+
* - eyes: wet cornea (near-mirror roughness + clearcoat)
|
|
14
|
+
* WebGL falls back to the plain glTF PBR materials with the same scalar tweaks.
|
|
15
|
+
*/
|
|
16
|
+
import * as THREE from 'three';
|
|
17
|
+
import { MeshPhysicalNodeMaterial, PhysicalLightingModel } from 'three/webgpu';
|
|
18
|
+
import type { NodeBuilder, LightingModelDirectInput, Node } from 'three/webgpu';
|
|
19
|
+
|
|
20
|
+
// @types/three 0.185 types TSL nodes by their GLSL-ish type parameter; the engine's
|
|
21
|
+
// lighting-model inputs are declared as bare `Node`, so we cast them to typed nodes.
|
|
22
|
+
type Vec3Node = Node<'vec3'>;
|
|
23
|
+
type FloatNode = Node<'float'>;
|
|
24
|
+
type Vec2Node = Node<'vec2'>;
|
|
25
|
+
import {
|
|
26
|
+
texture, uv, float, mix, smoothstep, abs, dot, uniform,
|
|
27
|
+
normalView, positionViewDirection, materialRoughness,
|
|
28
|
+
} from 'three/tsl';
|
|
29
|
+
import { isWebGpuActive } from 'engine/RendererType.js';
|
|
30
|
+
|
|
31
|
+
/** Which MetaHuman part a material belongs to, decided from its glTF name. */
|
|
32
|
+
export type MetaHumanPart =
|
|
33
|
+
| 'skin' | 'body' | 'hair' | 'brows' | 'lashes' | 'eye' | 'eyeshell' | 'teeth' | 'cloth' | 'hidden' | 'other';
|
|
34
|
+
|
|
35
|
+
export function classifyMaterial(name: string): MetaHumanPart {
|
|
36
|
+
const n = name.toLowerCase();
|
|
37
|
+
if (n.includes('eyeshell')) return 'eyeshell';
|
|
38
|
+
if (n === 'm_hide' || n.includes('lacrimal') || n.includes('eyeedge') || n.includes('saliva')) return 'hidden';
|
|
39
|
+
if (n.includes('face_skin') || n.includes('_face_') && n.includes('skin')) return 'skin';
|
|
40
|
+
if (n.includes('body_baked') || n.includes('mi_body')) return 'body';
|
|
41
|
+
if (n.includes('lashmat') || n.includes('eyelash')) return 'lashes';
|
|
42
|
+
if (n.includes('eyebrow') || n.includes('beard') || n.includes('mustache') || n.includes('goatee')) return 'brows';
|
|
43
|
+
if (n.includes('cardmat') || n.includes('hair_')) return 'hair';
|
|
44
|
+
if (n.includes('eyel_') || n.includes('eyer_') || n.includes('_eye_') || n.includes('mi_eye')) return 'eye';
|
|
45
|
+
if (n.includes('teeth')) return 'teeth';
|
|
46
|
+
if (n.includes('shirt') || n.includes('short') || n.includes('pants') || n.includes('hoodie') || n.includes('slacks') || n.includes('shoe') || n.includes('bodyshape')) return 'cloth';
|
|
47
|
+
return 'other';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface HairParams {
|
|
51
|
+
/** Hair colour (sRGB hex) — the atlas carries no albedo, only coverage/data. */
|
|
52
|
+
color: string;
|
|
53
|
+
roughness: number;
|
|
54
|
+
/** Gain applied to atlas coverage before alpha-to-coverage (MH atlases are authored for blending). */
|
|
55
|
+
density: number;
|
|
56
|
+
/** 0 = flat tone, 1 = fully black roots. */
|
|
57
|
+
rootDarkening: number;
|
|
58
|
+
/** Per-strand brightness variance amplitude. */
|
|
59
|
+
seedVariation: number;
|
|
60
|
+
roughnessFloor: number;
|
|
61
|
+
roughnessSeedAmp: number;
|
|
62
|
+
/** Extra alpha-blended fringe pass drawn over the A2C pass (0 = none). */
|
|
63
|
+
blendOpacity: number;
|
|
64
|
+
/** 'a2c' (needs MSAA), 'blend' (single sorted-ish pass), 'both'. */
|
|
65
|
+
mode: 'a2c' | 'blend' | 'both';
|
|
66
|
+
anisotropy: number;
|
|
67
|
+
anisotropyRotation: number;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface MetaHumanMaterialOptions {
|
|
71
|
+
hairAtlasUrl: string | null;
|
|
72
|
+
browAtlasUrl: string | null;
|
|
73
|
+
lashCoverageUrl: string | null;
|
|
74
|
+
hair: HairParams;
|
|
75
|
+
brows: HairParams;
|
|
76
|
+
/** Subsurface tint & strength for skin. */
|
|
77
|
+
sss: { color: string; strength: number; distortion: number; power: number; ambient: number };
|
|
78
|
+
skinRoughnessFloor: number;
|
|
79
|
+
skinSpecularIntensity: number;
|
|
80
|
+
/** 0 hides the MetaHuman eye shell (its UVs are not eye-centred in this export); >0 draws a dark socket ring. */
|
|
81
|
+
eyeShellOcclusion: number;
|
|
82
|
+
/** Whether the renderer really has MSAA (alpha-to-coverage is a no-op without it). */
|
|
83
|
+
msaa: boolean;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export const DEFAULT_HAIR: HairParams = {
|
|
87
|
+
color: '#090603', roughness: 0.55, density: 5.5, rootDarkening: 0.0, seedVariation: 0.36,
|
|
88
|
+
roughnessFloor: 0.56, roughnessSeedAmp: 0.08, blendOpacity: 0.22, mode: 'both',
|
|
89
|
+
anisotropy: 0.2, anisotropyRotation: Math.PI / 2,
|
|
90
|
+
};
|
|
91
|
+
export const DEFAULT_BROWS: HairParams = { ...DEFAULT_HAIR, mode: 'blend', blendOpacity: 0, anisotropy: 0 };
|
|
92
|
+
|
|
93
|
+
export const DEFAULT_METAHUMAN_MATERIAL_OPTIONS: MetaHumanMaterialOptions = {
|
|
94
|
+
hairAtlasUrl: null, browAtlasUrl: null, lashCoverageUrl: null,
|
|
95
|
+
hair: DEFAULT_HAIR, brows: DEFAULT_BROWS,
|
|
96
|
+
sss: { color: '#ff5a3c', strength: 0.22, distortion: 0.25, power: 2.5, ambient: 0.0 },
|
|
97
|
+
skinRoughnessFloor: 0.42,
|
|
98
|
+
skinSpecularIntensity: 0.8,
|
|
99
|
+
eyeShellOcclusion: 0,
|
|
100
|
+
msaa: true,
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
// Skin: physical material + wrap-lighting subsurface term (WebGPU / TSL)
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
interface SssNodes {
|
|
108
|
+
color: Vec3Node;
|
|
109
|
+
strength: FloatNode;
|
|
110
|
+
distortion: FloatNode;
|
|
111
|
+
power: FloatNode;
|
|
112
|
+
ambient: FloatNode;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* PhysicalLightingModel with an extra back-scattering diffuse term (the classic
|
|
117
|
+
* "fast subsurface" from the three.js SSS example): light arriving from behind
|
|
118
|
+
* the surface, bent by the normal, bleeds toward the viewer tinted by `color`.
|
|
119
|
+
*/
|
|
120
|
+
class SkinLightingModel extends PhysicalLightingModel {
|
|
121
|
+
constructor(
|
|
122
|
+
private readonly sss: SssNodes,
|
|
123
|
+
clearcoat: boolean, sheen: boolean, iridescence: boolean,
|
|
124
|
+
anisotropy: boolean, transmission: boolean, dispersion: boolean,
|
|
125
|
+
) {
|
|
126
|
+
super(clearcoat, sheen, iridescence, anisotropy, transmission, dispersion);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
override direct(input: LightingModelDirectInput, builder: NodeBuilder): void {
|
|
130
|
+
const lightDirection = input.lightDirection as Vec3Node;
|
|
131
|
+
const lightColor = input.lightColor as Vec3Node;
|
|
132
|
+
const half = lightDirection.add(normalView.mul(this.sss.distortion)).normalize();
|
|
133
|
+
const vDotH = positionViewDirection.dot(half.negate()).saturate();
|
|
134
|
+
const scatter = vDotH.pow(this.sss.power).add(this.sss.ambient).mul(this.sss.color).mul(this.sss.strength);
|
|
135
|
+
(input.reflectedLight.directDiffuse as Vec3Node).addAssign(scatter.mul(lightColor));
|
|
136
|
+
super.direct(input, builder);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
class MeshSkinNodeMaterial extends MeshPhysicalNodeMaterial {
|
|
141
|
+
sss: SssNodes | null = null;
|
|
142
|
+
|
|
143
|
+
override setupLightingModel(): PhysicalLightingModel {
|
|
144
|
+
if (!this.sss) return super.setupLightingModel();
|
|
145
|
+
return new SkinLightingModel(
|
|
146
|
+
this.sss, this.useClearcoat, this.useSheen, this.useIridescence,
|
|
147
|
+
this.useAnisotropy, this.useTransmission, this.useDispersion,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Standard/physical scalar + map properties worth carrying from a glTF material into a node material. */
|
|
153
|
+
const PBR_PROPS = [
|
|
154
|
+
'name', 'color', 'map', 'normalMap', 'normalScale', 'roughness', 'roughnessMap', 'metalness', 'metalnessMap',
|
|
155
|
+
'aoMap', 'aoMapIntensity', 'emissive', 'emissiveMap', 'emissiveIntensity', 'alphaMap',
|
|
156
|
+
'specularIntensity', 'specularIntensityMap', 'specularColor', 'specularColorMap', 'ior',
|
|
157
|
+
'side', 'transparent', 'opacity', 'alphaTest', 'depthWrite', 'envMapIntensity', 'clearcoat', 'clearcoatRoughness',
|
|
158
|
+
] as const;
|
|
159
|
+
|
|
160
|
+
function copyPbrProps(dst: THREE.Material, src: THREE.Material): void {
|
|
161
|
+
const d = dst as unknown as Record<string, unknown>;
|
|
162
|
+
const s = src as unknown as Record<string, unknown>;
|
|
163
|
+
for (const k of PBR_PROPS) {
|
|
164
|
+
if (!(k in s) || s[k] === undefined || !(k in d)) continue;
|
|
165
|
+
const v = s[k];
|
|
166
|
+
if (v && typeof v === 'object' && 'isColor' in (v as object)) (d[k] as THREE.Color).copy(v as THREE.Color);
|
|
167
|
+
else if (v && typeof v === 'object' && 'isVector2' in (v as object)) (d[k] as THREE.Vector2).copy(v as THREE.Vector2);
|
|
168
|
+
else d[k] = v;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function makeSkinMaterial(src: THREE.Material, opts: MetaHumanMaterialOptions, strengthScale: number): THREE.Material {
|
|
173
|
+
src.vertexColors = false; // MetaHuman COLOR_0/1 are rig masks, never albedo
|
|
174
|
+
if (!isWebGpuActive()) {
|
|
175
|
+
const m = src as THREE.MeshPhysicalMaterial;
|
|
176
|
+
if ('metalness' in m) m.metalness = 0;
|
|
177
|
+
if ('specularIntensity' in m) m.specularIntensity = opts.skinSpecularIntensity;
|
|
178
|
+
m.needsUpdate = true;
|
|
179
|
+
return m;
|
|
180
|
+
}
|
|
181
|
+
const mat = new MeshSkinNodeMaterial();
|
|
182
|
+
copyPbrProps(mat, src);
|
|
183
|
+
mat.vertexColors = false;
|
|
184
|
+
mat.metalness = 0;
|
|
185
|
+
mat.specularIntensity = opts.skinSpecularIntensity;
|
|
186
|
+
// Baked MH roughness has oily hotspots on forehead/nose; clamp up to a matte floor.
|
|
187
|
+
mat.roughnessNode = materialRoughness.max(float(opts.skinRoughnessFloor));
|
|
188
|
+
mat.sss = {
|
|
189
|
+
color: uniform(new THREE.Color(opts.sss.color)) as unknown as Vec3Node,
|
|
190
|
+
strength: uniform(opts.sss.strength * strengthScale) as unknown as FloatNode,
|
|
191
|
+
distortion: uniform(opts.sss.distortion) as unknown as FloatNode,
|
|
192
|
+
power: uniform(opts.sss.power) as unknown as FloatNode,
|
|
193
|
+
ambient: uniform(opts.sss.ambient) as unknown as FloatNode,
|
|
194
|
+
};
|
|
195
|
+
return mat;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ---------------------------------------------------------------------------
|
|
199
|
+
// Hair cards / brows / lashes
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
|
|
202
|
+
const textureCache = new Map<string, THREE.Texture>();
|
|
203
|
+
function loadDataTexture(url: string, like: THREE.Texture | null): THREE.Texture {
|
|
204
|
+
let tex = textureCache.get(url);
|
|
205
|
+
if (!tex) {
|
|
206
|
+
tex = new THREE.TextureLoader().load(url);
|
|
207
|
+
tex.colorSpace = THREE.NoColorSpace;
|
|
208
|
+
tex.flipY = false; // glTF UV convention
|
|
209
|
+
tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
|
|
210
|
+
tex.anisotropy = 8;
|
|
211
|
+
textureCache.set(url, tex);
|
|
212
|
+
}
|
|
213
|
+
if (like) {
|
|
214
|
+
// Mirror any KHR_texture_transform the glTF applied to the baked map.
|
|
215
|
+
tex.channel = like.channel;
|
|
216
|
+
tex.offset.copy(like.offset); tex.repeat.copy(like.repeat);
|
|
217
|
+
tex.rotation = like.rotation; tex.center.copy(like.center);
|
|
218
|
+
tex.matrixAutoUpdate = like.matrixAutoUpdate;
|
|
219
|
+
if (!like.matrixAutoUpdate) tex.matrix.copy(like.matrix);
|
|
220
|
+
}
|
|
221
|
+
return tex;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function hairMatrixUv(atlas: THREE.Texture): Vec2Node {
|
|
225
|
+
return uv(atlas.channel) as unknown as Vec2Node;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
interface HairBuild { primary: THREE.Material; blend: THREE.Material | null }
|
|
229
|
+
|
|
230
|
+
function makeHairMaterials(src: THREE.Material, atlasUrl: string | null, p: HairParams, msaa: boolean): HairBuild {
|
|
231
|
+
const base = new THREE.Color(p.color);
|
|
232
|
+
const srcMap = (src as THREE.MeshStandardMaterial).map ?? null;
|
|
233
|
+
const atlas = atlasUrl ? loadDataTexture(atlasUrl, srcMap) : null;
|
|
234
|
+
const useA2C = msaa && (p.mode === 'a2c' || p.mode === 'both');
|
|
235
|
+
|
|
236
|
+
if (!isWebGpuActive()) {
|
|
237
|
+
// WebGL fallback: alpha-tested + alpha-blended pair on the glTF material.
|
|
238
|
+
const m = src as THREE.MeshStandardMaterial;
|
|
239
|
+
m.vertexColors = false;
|
|
240
|
+
m.color.copy(base);
|
|
241
|
+
m.map = null;
|
|
242
|
+
m.roughness = p.roughness;
|
|
243
|
+
m.metalness = 0;
|
|
244
|
+
m.side = THREE.DoubleSide;
|
|
245
|
+
if (atlas) { m.alphaMap = atlas; }
|
|
246
|
+
m.transparent = true; m.alphaTest = 0.0; m.depthWrite = false;
|
|
247
|
+
m.polygonOffset = true; m.polygonOffsetFactor = -0.5; m.polygonOffsetUnits = -1;
|
|
248
|
+
const inner = m.clone();
|
|
249
|
+
inner.transparent = false; inner.alphaTest = 0.5; inner.depthWrite = true;
|
|
250
|
+
m.needsUpdate = true;
|
|
251
|
+
return { primary: inner, blend: m };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const build = (blendPass: boolean): THREE.Material => {
|
|
255
|
+
const mat = new MeshPhysicalNodeMaterial();
|
|
256
|
+
mat.name = (src.name || 'hair') + (blendPass ? '__blend' : '');
|
|
257
|
+
mat.side = THREE.DoubleSide;
|
|
258
|
+
mat.metalness = 0;
|
|
259
|
+
mat.roughness = p.roughness;
|
|
260
|
+
mat.specularIntensity = 0.45;
|
|
261
|
+
// A whisper of depth bias so cards flush with the scalp don't z-fight; anything bigger
|
|
262
|
+
// lets cards on the FAR side of the head win the depth test and show through the skin.
|
|
263
|
+
mat.polygonOffset = true; mat.polygonOffsetFactor = -0.5; mat.polygonOffsetUnits = -1;
|
|
264
|
+
if (p.anisotropy > 0) { mat.anisotropy = p.anisotropy; mat.anisotropyRotation = p.anisotropyRotation; }
|
|
265
|
+
|
|
266
|
+
const colorU = uniform(base);
|
|
267
|
+
const ndv = abs(dot(normalView, positionViewDirection));
|
|
268
|
+
const hairAO = mix(float(0.3), float(1.0), smoothstep(float(0.05), float(0.55), ndv));
|
|
269
|
+
|
|
270
|
+
if (atlas) {
|
|
271
|
+
const a = texture(atlas, hairMatrixUv(atlas));
|
|
272
|
+
const coverage = a.r.mul(float(p.density)).clamp(0.0, 1.0);
|
|
273
|
+
const rootT = a.g;
|
|
274
|
+
const seed = a.b;
|
|
275
|
+
const toneMul = mix(float(p.rootDarkening), float(1.0), rootT);
|
|
276
|
+
const strandMul = seed.sub(0.5).mul(2.0 * p.seedVariation).add(1.0);
|
|
277
|
+
mat.colorNode = colorU.mul(toneMul).mul(strandMul).mul(hairAO);
|
|
278
|
+
const rough = float(p.roughness)
|
|
279
|
+
.add(seed.sub(0.5).mul(2.0 * p.roughnessSeedAmp))
|
|
280
|
+
.add(rootT.oneMinus().mul(0.06))
|
|
281
|
+
.max(float(p.roughnessFloor)).clamp(0.0, 1.0);
|
|
282
|
+
mat.roughnessNode = rough;
|
|
283
|
+
mat.opacityNode = blendPass ? coverage.mul(float(p.blendOpacity)) : coverage;
|
|
284
|
+
} else {
|
|
285
|
+
mat.colorNode = colorU.mul(hairAO);
|
|
286
|
+
mat.alphaTest = 0.5;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (blendPass || !useA2C) {
|
|
290
|
+
mat.transparent = true; mat.depthWrite = false; mat.alphaTest = 0.0; mat.alphaToCoverage = false;
|
|
291
|
+
} else {
|
|
292
|
+
mat.transparent = false; mat.depthWrite = true; mat.alphaTest = 0.0; mat.alphaToCoverage = true;
|
|
293
|
+
}
|
|
294
|
+
return mat;
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
if (useA2C) {
|
|
298
|
+
return { primary: build(false), blend: p.mode === 'both' && p.blendOpacity > 0 ? build(true) : null };
|
|
299
|
+
}
|
|
300
|
+
// No MSAA: opaque alpha-clipped core + blended fringe (the classic two-pass hair).
|
|
301
|
+
const core = build(false);
|
|
302
|
+
core.transparent = false; core.depthWrite = true; core.alphaTest = 0.5;
|
|
303
|
+
const fringe = build(true);
|
|
304
|
+
(fringe as MeshPhysicalNodeMaterial).opacityNode = atlas
|
|
305
|
+
? texture(atlas, hairMatrixUv(atlas)).r.mul(float(p.density)).clamp(0.0, 1.0)
|
|
306
|
+
: null;
|
|
307
|
+
return { primary: core, blend: fringe };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function makeLashMaterial(src: THREE.Material, coverageUrl: string | null, color: string): THREE.Material {
|
|
311
|
+
const srcMap = (src as THREE.MeshStandardMaterial).map ?? null;
|
|
312
|
+
const cov = coverageUrl ? loadDataTexture(coverageUrl, srcMap) : null;
|
|
313
|
+
if (!isWebGpuActive()) {
|
|
314
|
+
const m = src as THREE.MeshStandardMaterial;
|
|
315
|
+
m.vertexColors = false; m.color.set(color); m.map = null; m.roughness = 0.55; m.metalness = 0;
|
|
316
|
+
m.side = THREE.DoubleSide; m.transparent = true; m.depthWrite = false; m.alphaTest = 0.0;
|
|
317
|
+
if (cov) m.alphaMap = cov;
|
|
318
|
+
m.needsUpdate = true;
|
|
319
|
+
return m;
|
|
320
|
+
}
|
|
321
|
+
const mat = new MeshPhysicalNodeMaterial();
|
|
322
|
+
mat.name = src.name || 'lashes';
|
|
323
|
+
mat.color.set(color); mat.roughness = 0.55; mat.metalness = 0; mat.side = THREE.DoubleSide;
|
|
324
|
+
mat.transparent = true; mat.depthWrite = false; mat.alphaTest = 0.0;
|
|
325
|
+
if (cov) mat.opacityNode = texture(cov, uv(cov.channel)).r;
|
|
326
|
+
return mat;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* MetaHuman's eye shell is shader-procedural in UE (a soft dark ring under the lids that
|
|
331
|
+
* sells socket depth); the export ships it fully transparent. Rebuild it as a radial
|
|
332
|
+
* gradient — clear over the iris, darkening toward the rim — drawn over the eyeball.
|
|
333
|
+
*/
|
|
334
|
+
let eyeOcclusionTex: THREE.Texture | null = null;
|
|
335
|
+
function eyeOcclusionTexture(): THREE.Texture {
|
|
336
|
+
if (eyeOcclusionTex) return eyeOcclusionTex;
|
|
337
|
+
const size = 256;
|
|
338
|
+
const canvas = document.createElement('canvas');
|
|
339
|
+
canvas.width = size; canvas.height = size;
|
|
340
|
+
const ctx = canvas.getContext('2d')!;
|
|
341
|
+
ctx.fillStyle = '#000'; ctx.fillRect(0, 0, size, size);
|
|
342
|
+
const grad = ctx.createRadialGradient(size / 2, size / 2, size * 0.10, size / 2, size / 2, size * 0.55);
|
|
343
|
+
grad.addColorStop(0.0, '#000'); grad.addColorStop(0.55, '#555'); grad.addColorStop(1.0, '#fff');
|
|
344
|
+
ctx.fillStyle = grad; ctx.fillRect(0, 0, size, size);
|
|
345
|
+
const tex = new THREE.CanvasTexture(canvas);
|
|
346
|
+
tex.wrapS = tex.wrapT = THREE.ClampToEdgeWrapping;
|
|
347
|
+
tex.flipY = false;
|
|
348
|
+
tex.colorSpace = THREE.NoColorSpace;
|
|
349
|
+
eyeOcclusionTex = tex;
|
|
350
|
+
return tex;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function makeEyeShellMaterial(src: THREE.Material, strength: number): THREE.Material {
|
|
354
|
+
const tex = eyeOcclusionTexture();
|
|
355
|
+
if (!isWebGpuActive()) {
|
|
356
|
+
const m = new THREE.MeshStandardMaterial({ color: 0x050302, roughness: 0.85, metalness: 0, transparent: true, depthWrite: false, alphaMap: tex });
|
|
357
|
+
m.name = src.name; m.vertexColors = false;
|
|
358
|
+
return m;
|
|
359
|
+
}
|
|
360
|
+
const mat = new MeshPhysicalNodeMaterial();
|
|
361
|
+
mat.name = src.name;
|
|
362
|
+
mat.color.set(0x050302); mat.roughness = 0.85; mat.metalness = 0;
|
|
363
|
+
mat.transparent = true; mat.depthWrite = false; mat.alphaTest = 0;
|
|
364
|
+
mat.opacityNode = texture(tex, uv(0) as unknown as Vec2Node).r.mul(float(strength));
|
|
365
|
+
return mat;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Clothing: same glTF PBR inputs, but as an explicit node material under WebGPU. The renderer's
|
|
370
|
+
* internal conversion of the loader's classic MeshPhysicalMaterial mis-skinned the outfit once the
|
|
371
|
+
* engine animated the rig (shards at the shoulders and hem), while node materials deform correctly.
|
|
372
|
+
*/
|
|
373
|
+
function makeClothMaterial(src: THREE.Material): THREE.Material {
|
|
374
|
+
src.vertexColors = false;
|
|
375
|
+
if (!isWebGpuActive()) {
|
|
376
|
+
src.needsUpdate = true;
|
|
377
|
+
return src;
|
|
378
|
+
}
|
|
379
|
+
const mat = new MeshPhysicalNodeMaterial();
|
|
380
|
+
copyPbrProps(mat, src);
|
|
381
|
+
mat.vertexColors = false;
|
|
382
|
+
mat.metalness = 0;
|
|
383
|
+
mat.sheen = 0.25; // a little fabric sheen on cotton
|
|
384
|
+
mat.sheenRoughness = 0.8;
|
|
385
|
+
mat.sheenColor.set(0xffffff);
|
|
386
|
+
return mat;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function makeEyeMaterial(src: THREE.Material): THREE.Material {
|
|
390
|
+
const m = src as THREE.MeshPhysicalMaterial;
|
|
391
|
+
m.vertexColors = false;
|
|
392
|
+
m.roughness = 0.05;
|
|
393
|
+
m.metalness = 0;
|
|
394
|
+
if ('clearcoat' in m) { m.clearcoat = 1.0; m.clearcoatRoughness = 0.03; }
|
|
395
|
+
if ('ior' in m) m.ior = 1.376;
|
|
396
|
+
if ('specularIntensity' in m) m.specularIntensity = 1.0;
|
|
397
|
+
m.needsUpdate = true;
|
|
398
|
+
return m;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// ---------------------------------------------------------------------------
|
|
402
|
+
// Entry point
|
|
403
|
+
// ---------------------------------------------------------------------------
|
|
404
|
+
|
|
405
|
+
export interface UpgradeReport {
|
|
406
|
+
byPart: Record<string, number>;
|
|
407
|
+
hiddenMeshes: string[];
|
|
408
|
+
blendClones: THREE.Mesh[];
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Walk a loaded MetaHuman glTF scene and swap in the upgraded materials.
|
|
413
|
+
* Returns the extra alpha-blend hair meshes it added (share geometry, no extra VRAM).
|
|
414
|
+
*/
|
|
415
|
+
export function upgradeMetaHumanMaterials(root: THREE.Object3D, options: Partial<MetaHumanMaterialOptions> = {}): UpgradeReport {
|
|
416
|
+
const opts: MetaHumanMaterialOptions = { ...DEFAULT_METAHUMAN_MATERIAL_OPTIONS, ...options };
|
|
417
|
+
const report: UpgradeReport = { byPart: {}, hiddenMeshes: [], blendClones: [] };
|
|
418
|
+
const blendJobs: Array<{ mesh: THREE.Mesh; material: THREE.Material }> = [];
|
|
419
|
+
|
|
420
|
+
root.traverse((obj) => {
|
|
421
|
+
const mesh = obj as THREE.Mesh;
|
|
422
|
+
if (!mesh.isMesh) return;
|
|
423
|
+
const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
|
|
424
|
+
materials.forEach((mat, i) => {
|
|
425
|
+
if (!mat) return;
|
|
426
|
+
const part = classifyMaterial(mat.name || '');
|
|
427
|
+
report.byPart[part] = (report.byPart[part] ?? 0) + 1;
|
|
428
|
+
let replacement: THREE.Material | null = null;
|
|
429
|
+
switch (part) {
|
|
430
|
+
case 'hidden':
|
|
431
|
+
mesh.visible = false;
|
|
432
|
+
report.hiddenMeshes.push(`${mesh.name}/${mat.name}`);
|
|
433
|
+
return;
|
|
434
|
+
case 'skin':
|
|
435
|
+
replacement = makeSkinMaterial(mat, opts, 1.0);
|
|
436
|
+
break;
|
|
437
|
+
case 'body':
|
|
438
|
+
replacement = makeSkinMaterial(mat, opts, 0.6);
|
|
439
|
+
break;
|
|
440
|
+
case 'hair': {
|
|
441
|
+
const built = makeHairMaterials(mat, opts.hairAtlasUrl, opts.hair, opts.msaa);
|
|
442
|
+
replacement = built.primary;
|
|
443
|
+
if (built.blend) blendJobs.push({ mesh, material: built.blend });
|
|
444
|
+
break;
|
|
445
|
+
}
|
|
446
|
+
case 'brows': {
|
|
447
|
+
const built = makeHairMaterials(mat, opts.browAtlasUrl, opts.brows, opts.msaa);
|
|
448
|
+
replacement = built.primary;
|
|
449
|
+
if (built.blend) blendJobs.push({ mesh, material: built.blend });
|
|
450
|
+
break;
|
|
451
|
+
}
|
|
452
|
+
case 'lashes':
|
|
453
|
+
replacement = makeLashMaterial(mat, opts.lashCoverageUrl, opts.hair.color);
|
|
454
|
+
break;
|
|
455
|
+
case 'eye':
|
|
456
|
+
replacement = makeEyeMaterial(mat);
|
|
457
|
+
break;
|
|
458
|
+
case 'eyeshell':
|
|
459
|
+
if (opts.eyeShellOcclusion <= 0) {
|
|
460
|
+
mesh.visible = false;
|
|
461
|
+
report.hiddenMeshes.push(`${mesh.name}/${mat.name}`);
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
replacement = makeEyeShellMaterial(mat, opts.eyeShellOcclusion);
|
|
465
|
+
mesh.renderOrder = 2; // after the eyeballs, before lashes
|
|
466
|
+
break;
|
|
467
|
+
case 'cloth':
|
|
468
|
+
replacement = makeClothMaterial(mat);
|
|
469
|
+
break;
|
|
470
|
+
case 'teeth':
|
|
471
|
+
case 'other':
|
|
472
|
+
default:
|
|
473
|
+
mat.vertexColors = false;
|
|
474
|
+
mat.needsUpdate = true;
|
|
475
|
+
replacement = mat;
|
|
476
|
+
break;
|
|
477
|
+
}
|
|
478
|
+
if (replacement && replacement !== mat) {
|
|
479
|
+
if (Array.isArray(mesh.material)) mesh.material[i] = replacement;
|
|
480
|
+
else mesh.material = replacement;
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
mesh.castShadow = true;
|
|
484
|
+
mesh.receiveShadow = true;
|
|
485
|
+
mesh.frustumCulled = false; // skinned + morphed meshes: bounds are stale, never cull
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
// Second pass: add the alpha-blend fringe clones (never mutate the graph while traversing).
|
|
489
|
+
for (const { mesh, material } of blendJobs) {
|
|
490
|
+
const clone = mesh.clone() as THREE.Mesh;
|
|
491
|
+
clone.material = material;
|
|
492
|
+
clone.name = mesh.name + '__blend';
|
|
493
|
+
clone.castShadow = false;
|
|
494
|
+
clone.receiveShadow = true;
|
|
495
|
+
clone.renderOrder = mesh.renderOrder + 1;
|
|
496
|
+
mesh.parent?.add(clone);
|
|
497
|
+
report.blendClones.push(clone);
|
|
498
|
+
}
|
|
499
|
+
return report;
|
|
500
|
+
}
|