tosijs-3d 0.11.3 → 0.11.4
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/CHANGELOG.md +5 -5
- package/changelog/v0.11.4.md +18 -0
- package/dist/character-builder.d.ts +33 -0
- package/dist/character-builder.d.ts.map +1 -0
- package/dist/character-builder.js +318 -0
- package/dist/character-builder.js.map +1 -0
- package/dist/hair-fit.d.ts +21 -0
- package/dist/hair-fit.d.ts.map +1 -0
- package/dist/hair-fit.js +169 -0
- package/dist/hair-fit.js.map +1 -0
- package/dist/human-face.d.ts +60 -0
- package/dist/human-face.d.ts.map +1 -0
- package/dist/human-face.js +348 -0
- package/dist/human-face.js.map +1 -0
- package/dist/human-hair.d.ts +63 -0
- package/dist/human-hair.d.ts.map +1 -0
- package/dist/human-hair.js +246 -0
- package/dist/human-hair.js.map +1 -0
- package/dist/human-morph.d.ts +41 -0
- package/dist/human-morph.d.ts.map +1 -0
- package/dist/human-morph.js +222 -0
- package/dist/human-morph.js.map +1 -0
- package/dist/human-pack.d.ts +79 -0
- package/dist/human-pack.d.ts.map +1 -0
- package/dist/human-pack.js +120 -0
- package/dist/human-pack.js.map +1 -0
- package/dist/human-rig.d.ts +37 -0
- package/dist/human-rig.d.ts.map +1 -0
- package/dist/human-rig.js +171 -0
- package/dist/human-rig.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/retarget.d.ts.map +1 -1
- package/dist/retarget.js +9 -1
- package/dist/retarget.js.map +1 -1
- package/dist/skin-tone.d.ts +32 -0
- package/dist/skin-tone.d.ts.map +1 -0
- package/dist/skin-tone.js +190 -0
- package/dist/skin-tone.js.map +1 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -12,16 +12,16 @@ Upgrading across a minor? Start with [Migration](/Migration/).
|
|
|
12
12
|
|
|
13
13
|
## Unreleased
|
|
14
14
|
|
|
15
|
-
## 0.11.
|
|
15
|
+
## 0.11.4
|
|
16
16
|
|
|
17
|
-
The latest release.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
bias defaults clear acne stripes. [Read 0.11.3](/v0.11.3/).
|
|
17
|
+
The latest release. A character creator on MakeHuman's CC0 body: shape a
|
|
18
|
+
person with sliders, give them skin, eyes and hair, and watch them walk with
|
|
19
|
+
UAL's clips. [Read 0.11.4](/v0.11.4/).
|
|
21
20
|
|
|
22
21
|
## Every version
|
|
23
22
|
|
|
24
23
|
<!-- toc -->
|
|
24
|
+
- [0.11.4](/v0.11.4/)
|
|
25
25
|
- [0.11.3](/v0.11.3/)
|
|
26
26
|
- [0.11.2](/v0.11.2/)
|
|
27
27
|
- [0.11.1](/v0.11.1/)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# 0.11.4
|
|
2
|
+
|
|
3
|
+
<!--{ "parent": "CHANGELOG", "order": 9988996 }-->
|
|
4
|
+
|
|
5
|
+
## Added
|
|
6
|
+
|
|
7
|
+
- **A character creator, on MakeHuman's CC0 body** (CHARACTER-DESIGN.md). Try it on the [character builder](/character-builder/) page: a person walking while you reshape them. The data is a **human pack** on the CDN (`makehuman/hm08/`), loaded with `loadHumanPack`.
|
|
8
|
+
- **Body**: `macroWeights` / `morphHuman` turn gender, age, muscle, weight, height, proportions and an ancestry mix into a body, in about a millisecond.
|
|
9
|
+
- **Skeleton**: `humanRig` places UAL's 65 joints from the body's own joint cubes, and bone heat binds the skin once per base, so UAL's clips play on any body shape (`buildRig`, `retargetClip`). `humanBody` draws a body unrigged.
|
|
10
|
+
- **Skin**: one dial from very fair to very dark through the Monk Skin Tone Scale, plus redness (`skinMaterial`, `skinAlbedo`); lips tinted from it over the pack's `lips` region, no UVs needed (`lipTint`, `lipAlbedo`).
|
|
11
|
+
- **Eyes**: `humanFace` adds eyes and eyelashes that fit every body (they are the base mesh's own helper vertices); the iris is drawn in code with a colour dial (`eyeTexture`, `irisColor`); `eyeGaze` aims the eyes at a world point with small saccades.
|
|
12
|
+
- **Hair**: Quaternius' CC0 hairstyles, beards and eyebrows, modelled for another head, fitted to any body (`loadHairLibrary`, `headScalp`, `humanHair`); tinted by `hairColor` (black to platinum, plus red). The pure fit is `fitToScalp` / `conformOutside`.
|
|
13
|
+
|
|
14
|
+
## Fixed
|
|
15
|
+
|
|
16
|
+
- **Retargeting no longer lines a character's collarbones up with the clip's.** A collarbone's rest direction is the body's shape, like the spine's; where two rigs differed (UAL's runs back to the shoulder, MakeHuman's lies flat), every clip shrugged the shoulders.
|
|
17
|
+
|
|
18
|
+
Size: barrel (min, packages external) 1266.7 KB raw / 433.9 KB gz, +1.67% on 0.11.3 (the character creator).
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import * as BABYLON from '@babylonjs/core';
|
|
2
|
+
import type { HumanPack } from './human-pack.js';
|
|
3
|
+
import type { HumanSkin } from './human-rig.js';
|
|
4
|
+
export interface HumanBody {
|
|
5
|
+
/** Move, turn and scale the body by this. */
|
|
6
|
+
root: BABYLON.TransformNode;
|
|
7
|
+
mesh: BABYLON.Mesh;
|
|
8
|
+
/** Reshape the body from new positions (xyz per vertex, the pack's frame). */
|
|
9
|
+
update(positions: Float32Array): void;
|
|
10
|
+
dispose(): void;
|
|
11
|
+
}
|
|
12
|
+
export declare function humanBody(scene: BABYLON.Scene, pack: HumanPack, name?: string): HumanBody;
|
|
13
|
+
export interface SkinMaterial extends BABYLON.PBRMaterial {
|
|
14
|
+
/** Re-colour: tone 0 (very fair) to 1 (very dark), redness 0.5 neutral. */
|
|
15
|
+
setSkin(tone: number, redness?: number): void;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* A skin material from [skin-tone](/skin-tone/)'s dials: dielectric, fairly
|
|
19
|
+
* rough, the albedo the tone gives. One flat colour for now: lips, nails and
|
|
20
|
+
* a texture's detail need the mesh's UVs, which the pack does not carry yet.
|
|
21
|
+
*/
|
|
22
|
+
export declare function skinMaterial(scene: BABYLON.Scene, options?: {
|
|
23
|
+
tone?: number;
|
|
24
|
+
redness?: number;
|
|
25
|
+
name?: string;
|
|
26
|
+
}): SkinMaterial;
|
|
27
|
+
/**
|
|
28
|
+
* Per-vertex colours (RGBA, the skin's vertices) that turn a skin material's
|
|
29
|
+
* colour into the lips' where `lips` (a pack region, every vertex) says so.
|
|
30
|
+
* They multiply the material's albedo, so they are the ratio lip / skin.
|
|
31
|
+
*/
|
|
32
|
+
export declare function lipTint(skin: HumanSkin, lips: Float32Array, tone: number, redness?: number, into?: Float32Array): Float32Array;
|
|
33
|
+
//# sourceMappingURL=character-builder.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"character-builder.d.ts","sourceRoot":"","sources":["../src/character-builder.ts"],"names":[],"mappings":"AA8NA,OAAO,KAAK,OAAO,MAAM,iBAAiB,CAAA;AAC1C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAEhD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAE/C,MAAM,WAAW,SAAS;IACxB,6CAA6C;IAC7C,IAAI,EAAE,OAAO,CAAC,aAAa,CAAA;IAC3B,IAAI,EAAE,OAAO,CAAC,IAAI,CAAA;IAClB,8EAA8E;IAC9E,MAAM,CAAC,SAAS,EAAE,YAAY,GAAG,IAAI,CAAA;IACrC,OAAO,IAAI,IAAI,CAAA;CAChB;AAED,wBAAgB,SAAS,CACvB,KAAK,EAAE,OAAO,CAAC,KAAK,EACpB,IAAI,EAAE,SAAS,EACf,IAAI,SAAU,GACb,SAAS,CAwDX;AAED,MAAM,WAAW,YAAa,SAAQ,OAAO,CAAC,WAAW;IACvD,2EAA2E;IAC3E,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAC9C;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAC1B,KAAK,EAAE,OAAO,CAAC,KAAK,EACpB,OAAO,GAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAO,GAC/D,YAAY,CAWd;AAED;;;;GAIG;AACH,wBAAgB,OAAO,CACrB,IAAI,EAAE,SAAS,EACf,IAAI,EAAE,YAAY,EAClB,IAAI,EAAE,MAAM,EACZ,OAAO,SAAM,EACb,IAAI,CAAC,EAAE,YAAY,GAClB,YAAY,CAgBd"}
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/*#
|
|
2
|
+
# character builder
|
|
3
|
+
|
|
4
|
+
**Make a person, and watch them walk.** The first steps of the character
|
|
5
|
+
creator (CHARACTER-DESIGN.md): MakeHuman's CC0 body
|
|
6
|
+
([human-pack](/human-pack/)), reshaped by sliders for gender, age, muscle,
|
|
7
|
+
weight, height and ancestry ([human-morph](/human-morph/)), with UAL's
|
|
8
|
+
skeleton placed from its own joint cubes ([human-rig](/human-rig/)) so it
|
|
9
|
+
plays UAL's clips at any shape.
|
|
10
|
+
|
|
11
|
+
## Demo
|
|
12
|
+
|
|
13
|
+
Open the settings (the owl): the body sliders reshape it
|
|
14
|
+
while it walks, since every body carries its own joints. The skin weights are
|
|
15
|
+
bone heat's, worked out from the mesh once when the page loads. The eyes
|
|
16
|
+
([human-face](/human-face/)) follow the camera, and the hair
|
|
17
|
+
([human-hair](/human-hair/)) is Quaternius' CC0 hairstyles, made for another
|
|
18
|
+
head and fitted to this one.
|
|
19
|
+
|
|
20
|
+
```js
|
|
21
|
+
import { b3d, b3dSun, b3dLight, slider3d, select3d, toggle3d, label3d, assetUrl, loadHumanPack, macroWeights, morphHuman, DEFAULT_HUMAN_BODY, humanRig, humanSkin, skinMaterial, lipTint, humanFace, eyeGaze, eyeTexture, eyeMaterial, lashMaterial, loadHairLibrary, headScalp, hairMaterial, humanHair, skinSegments, boneHeat, buildRig, readSkeleton, retargetClip } from 'tosijs-3d'
|
|
22
|
+
import { tosi, elements } from 'tosijs'
|
|
23
|
+
const { div } = elements
|
|
24
|
+
|
|
25
|
+
const status = div({ class: 'status' }, 'loading the animations…')
|
|
26
|
+
const { walker } = tosi({ walker: { ...DEFAULT_HUMAN_BODY, gender: 1, clip: 'Walk_Loop', tone: 0.3, redness: 0.5, iris: 0.2, hair: 'Hair_SimpleParted', beard: 'none', brows: 'Eyebrows_Regular', hairLight: 0.25, hairRed: 0.15, paused: false } })
|
|
27
|
+
let mat = null
|
|
28
|
+
let paint = () => {}
|
|
29
|
+
let eyes = null
|
|
30
|
+
let dress = () => {}
|
|
31
|
+
// Filled when the hair library loads.
|
|
32
|
+
const styles = { hair: ['none'], beard: ['none'], brows: ['none'] }
|
|
33
|
+
const clips = ['Idle_Loop', 'Walk_Loop', 'Jog_Fwd_Loop', 'Sprint_Loop', 'Dance_Loop', 'Crouch_Fwd_Loop']
|
|
34
|
+
let rebuild = () => {}
|
|
35
|
+
|
|
36
|
+
const slider = (label, key) =>
|
|
37
|
+
slider3d({ label, value: walker[key], min: 0, max: 1, step: 0.01 })
|
|
38
|
+
|
|
39
|
+
preview.append(
|
|
40
|
+
b3d(
|
|
41
|
+
{
|
|
42
|
+
clearColor: '#2a3340',
|
|
43
|
+
panelTitle: 'Walker',
|
|
44
|
+
scenePanel: () => [
|
|
45
|
+
select3d({ label: 'clip', value: walker.clip, options: clips }),
|
|
46
|
+
toggle3d({ label: 'pause', value: walker.paused }),
|
|
47
|
+
slider('gender', 'gender'),
|
|
48
|
+
slider('age', 'age'),
|
|
49
|
+
slider('muscle', 'muscle'),
|
|
50
|
+
slider('weight', 'weight'),
|
|
51
|
+
slider('height', 'height'),
|
|
52
|
+
label3d({ text: 'Skin' }),
|
|
53
|
+
slider3d({ label: 'tone', value: walker.tone, min: 0, max: 1, step: 0.01 }),
|
|
54
|
+
slider3d({ label: 'redness', value: walker.redness, min: 0, max: 1, step: 0.01 }),
|
|
55
|
+
slider3d({ label: 'eyes', value: walker.iris, min: 0, max: 1, step: 0.01 }),
|
|
56
|
+
label3d({ text: 'Hair' }),
|
|
57
|
+
select3d({ label: 'hair', value: walker.hair, options: styles.hair }),
|
|
58
|
+
select3d({ label: 'beard', value: walker.beard, options: styles.beard }),
|
|
59
|
+
select3d({ label: 'brows', value: walker.brows, options: styles.brows }),
|
|
60
|
+
slider3d({ label: 'light', value: walker.hairLight, min: 0, max: 1, step: 0.01 }),
|
|
61
|
+
slider3d({ label: 'red', value: walker.hairRed, min: 0, max: 1, step: 0.01 }),
|
|
62
|
+
label3d({ text: 'Ancestry' }),
|
|
63
|
+
slider('african', 'african'),
|
|
64
|
+
slider('asian', 'asian'),
|
|
65
|
+
slider('caucasian', 'caucasian'),
|
|
66
|
+
],
|
|
67
|
+
async sceneCreated(el, BABYLON) {
|
|
68
|
+
const scene = el.scene
|
|
69
|
+
const cam = new BABYLON.ArcRotateCamera('c', Math.PI / 2 - 0.5, 1.35, 4, new BABYLON.Vector3(0, 0.9, 0), scene)
|
|
70
|
+
cam.minZ = 0.05
|
|
71
|
+
cam.attachControl(el.querySelector('canvas'), true)
|
|
72
|
+
el.setActiveCamera(cam)
|
|
73
|
+
BABYLON.MeshBuilder.CreateGround('g', { width: 8, height: 8 }, scene)
|
|
74
|
+
|
|
75
|
+
// UAL's mannequin is only the clips' source: it stays hidden.
|
|
76
|
+
const ual = await BABYLON.LoadAssetContainerAsync(assetUrl('quaternius/UAL1_core.glb', 2), scene)
|
|
77
|
+
status.textContent = 'loading the body…'
|
|
78
|
+
ual.addAllToScene()
|
|
79
|
+
for (const g of ual.animationGroups) g.stop()
|
|
80
|
+
const ualRoot = ual.meshes.find((m) => m.name === '__root__')
|
|
81
|
+
const source = readSkeleton(ual.skeletons[0], ualRoot)
|
|
82
|
+
ualRoot.setEnabled(false)
|
|
83
|
+
|
|
84
|
+
const pack = await loadHumanPack(assetUrl('makehuman/hm08/hm08.json', 4)).catch((e) => {
|
|
85
|
+
status.textContent = `could not load the body: ${e.message}`
|
|
86
|
+
throw e
|
|
87
|
+
})
|
|
88
|
+
// Quaternius' hairstyles, made for its own head, fitted to each body.
|
|
89
|
+
const hairLib = await loadHairLibrary(scene, assetUrl('quaternius/libraries/hairstyles.glb'))
|
|
90
|
+
const adult = (n) => !n.endsWith('_Teen')
|
|
91
|
+
const facial = (n) => /Beard|Moustache|MuttonChops/.test(n)
|
|
92
|
+
styles.hair.push(...hairLib.names('hair').filter((n) => adult(n) && !facial(n)))
|
|
93
|
+
styles.beard.push(...hairLib.names('hair').filter((n) => adult(n) && facial(n)))
|
|
94
|
+
styles.brows.push(...hairLib.names('eyebrows').filter(adult))
|
|
95
|
+
const tints = new Map()
|
|
96
|
+
const tint = (part) => {
|
|
97
|
+
const from = part.material
|
|
98
|
+
if (!tints.has(from)) tints.set(from, hairMaterial(scene, from, `hair-${tints.size}`))
|
|
99
|
+
return tints.get(from)
|
|
100
|
+
}
|
|
101
|
+
status.textContent = 'binding the skin…'
|
|
102
|
+
// Let the message paint before the binding takes the thread.
|
|
103
|
+
await new Promise((r) => setTimeout(r, 50))
|
|
104
|
+
const skin = humanSkin(pack)
|
|
105
|
+
const [lips, eyePart, lashPart] = await Promise.all([pack.region('lips'), pack.part('eyes'), pack.part('eyelashes')])
|
|
106
|
+
eyes = eyeTexture(scene, { iris: walker.iris.value })
|
|
107
|
+
const faceMaterials = { eye: eyeMaterial(scene, eyes), lashes: lashMaterial(scene) }
|
|
108
|
+
let face = null
|
|
109
|
+
let scalp = null
|
|
110
|
+
let hairMeshes = []
|
|
111
|
+
dress = () => {
|
|
112
|
+
for (const m of tints.values()) m.setHair(walker.hairLight.value, walker.hairRed.value)
|
|
113
|
+
if (!built || !scalp) return
|
|
114
|
+
for (const m of hairMeshes) m.dispose()
|
|
115
|
+
hairMeshes = []
|
|
116
|
+
for (const key of ['hair', 'beard', 'brows']) {
|
|
117
|
+
const parts = hairLib.piece(walker[key].value)
|
|
118
|
+
if (!parts) continue
|
|
119
|
+
// Brows lie on the skin; hair stands a little off it.
|
|
120
|
+
const margin = key === 'brows' ? 0.0015 : 0.003
|
|
121
|
+
const centreY = key === 'brows' ? scalp.brow : undefined
|
|
122
|
+
hairMeshes.push(...humanHair(scene, built, built.rig, scalp, parts, tint, { margin, name: key, centreY }))
|
|
123
|
+
}
|
|
124
|
+
el.register({ meshes: hairMeshes })
|
|
125
|
+
}
|
|
126
|
+
for (const key of ['hair', 'beard', 'brows']) walker[key].observe(() => dress())
|
|
127
|
+
// Colours repaint whoever sets them: a slider, or code.
|
|
128
|
+
const recolour = () => { for (const m of tints.values()) m.setHair(walker.hairLight.value, walker.hairRed.value) }
|
|
129
|
+
walker.hairLight.observe(recolour)
|
|
130
|
+
walker.hairRed.observe(recolour)
|
|
131
|
+
for (const key of Object.keys(DEFAULT_HUMAN_BODY)) walker[key].observe(() => rebuild())
|
|
132
|
+
walker.tone.observe(() => paint())
|
|
133
|
+
walker.redness.observe(() => paint())
|
|
134
|
+
walker.iris.observe(() => eyes.setIris(walker.iris.value))
|
|
135
|
+
// The eyes find the camera.
|
|
136
|
+
eyeGaze(scene, { get eyes() { return face?.eyes ?? [] }, get forward() { return face?.forward ?? [] } }, () => cam.position)
|
|
137
|
+
const body = () => {
|
|
138
|
+
const b = {}
|
|
139
|
+
for (const k of Object.keys(DEFAULT_HUMAN_BODY)) b[k] = Number(walker[k].value)
|
|
140
|
+
return macroWeights(b, (n) => pack.has(n))
|
|
141
|
+
}
|
|
142
|
+
// The weights depend only on the topology: once, on the first body.
|
|
143
|
+
const first = await morphHuman(pack, body())
|
|
144
|
+
// The weights depend only on the topology: once, on the first body.
|
|
145
|
+
const rig0 = humanRig(pack.manifest, first)
|
|
146
|
+
const weights = boneHeat(skin.positions(first), skin.triangles, skinSegments(rig0))
|
|
147
|
+
|
|
148
|
+
mat = skinMaterial(scene, { tone: walker.tone.value, redness: walker.redness.value })
|
|
149
|
+
// The skin's colour, and the lips' as vertex colours over it.
|
|
150
|
+
paint = () => {
|
|
151
|
+
mat.setSkin(walker.tone.value, walker.redness.value)
|
|
152
|
+
if (lips && built) {
|
|
153
|
+
built.mesh.setVerticesData('color', lipTint(skin, lips, walker.tone.value, walker.redness.value))
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const stand = new BABYLON.TransformNode('stand', scene)
|
|
157
|
+
let built = null
|
|
158
|
+
let playing = null
|
|
159
|
+
let read = null
|
|
160
|
+
const play = () => {
|
|
161
|
+
playing?.dispose()
|
|
162
|
+
const clip = ual.animationGroups.find((g) => g.name === walker.clip.value)
|
|
163
|
+
playing = clip ? retargetClip(clip, source, read, { scene }) : null
|
|
164
|
+
playing?.start(true)
|
|
165
|
+
if (walker.paused.value) playing?.pause()
|
|
166
|
+
}
|
|
167
|
+
let busy = false
|
|
168
|
+
let again = false
|
|
169
|
+
rebuild = async () => {
|
|
170
|
+
if (busy) { again = true; return }
|
|
171
|
+
busy = true
|
|
172
|
+
const full = await morphHuman(pack, body())
|
|
173
|
+
const rig = humanRig(pack.manifest, full)
|
|
174
|
+
face?.dispose()
|
|
175
|
+
built?.dispose()
|
|
176
|
+
built = buildRig(scene, 'walker', skin.positions(full), skin.triangles, rig, weights)
|
|
177
|
+
face = humanFace(scene, built, rig, pack.manifest, full, { eyes: eyePart, eyelashes: lashPart }, faceMaterials)
|
|
178
|
+
built.rig = rig
|
|
179
|
+
scalp = headScalp(pack.manifest, full, pack.triangles, hairLib.reference)
|
|
180
|
+
dress()
|
|
181
|
+
built.mesh.material = mat
|
|
182
|
+
built.root.parent = stand
|
|
183
|
+
stand.position.y = -rig.at('root').y
|
|
184
|
+
el.register({ meshes: [built.mesh, ...face.meshes] })
|
|
185
|
+
paint()
|
|
186
|
+
read = readSkeleton(built.skeleton, built.root)
|
|
187
|
+
play()
|
|
188
|
+
busy = false
|
|
189
|
+
if (again) { again = false; rebuild() }
|
|
190
|
+
}
|
|
191
|
+
await rebuild()
|
|
192
|
+
status.remove()
|
|
193
|
+
walker.clip.observe(play)
|
|
194
|
+
walker.paused.observe(() => (walker.paused.value ? playing?.pause() : playing?.play(true)))
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
b3dSun({ timeOfDay: 15 }),
|
|
198
|
+
b3dLight({ intensity: 0.5 })
|
|
199
|
+
),
|
|
200
|
+
status
|
|
201
|
+
)
|
|
202
|
+
```
|
|
203
|
+
```css
|
|
204
|
+
tosi-b3d { width: 100%; height: 100%; }
|
|
205
|
+
.status { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(0,0,0,0.6); color: white; padding: 6px 12px; border-radius: 6px; font: 13px sans-serif; }
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
## humanBody
|
|
209
|
+
|
|
210
|
+
`humanBody(scene, pack)` draws a pack's body as one unrigged mesh, and
|
|
211
|
+
`update(positions)` reshapes it in place: for a body that only needs to be
|
|
212
|
+
looked at.
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
The pack is RIGHT-HANDED (as OBJ and glTF are), so the body sits under a
|
|
216
|
+
container that mirrors it the way glTF's `__root__` does, and stands with its
|
|
217
|
+
feet at the container's origin. Move the body by moving `root`. The mesh keeps
|
|
218
|
+
every vertex the targets move (helpers and joint cubes too), but draws only the
|
|
219
|
+
body's triangles.
|
|
220
|
+
*/
|
|
221
|
+
/*{ "parent": "b3d-biped", "order": 80 }*/
|
|
222
|
+
import * as BABYLON from '@babylonjs/core';
|
|
223
|
+
import { lipAlbedo, skinAlbedo } from './skin-tone.js';
|
|
224
|
+
export function humanBody(scene, pack, name = 'human') {
|
|
225
|
+
const root = new BABYLON.TransformNode(name, scene);
|
|
226
|
+
// The file's mirror, as glTF's `__root__` carries it.
|
|
227
|
+
const mirror = new BABYLON.TransformNode(`${name}-frame`, scene);
|
|
228
|
+
mirror.rotationQuaternion = new BABYLON.Quaternion(0, 1, 0, 0);
|
|
229
|
+
mirror.scaling = new BABYLON.Vector3(1, 1, -1);
|
|
230
|
+
mirror.parent = root;
|
|
231
|
+
const mesh = new BABYLON.Mesh(`${name}-body`, scene);
|
|
232
|
+
const indices = Array.from(pack.triangles);
|
|
233
|
+
const normals = new Float32Array(pack.positions.length);
|
|
234
|
+
const data = new BABYLON.VertexData();
|
|
235
|
+
data.positions = new Float32Array(pack.positions);
|
|
236
|
+
data.indices = indices;
|
|
237
|
+
data.normals = normals;
|
|
238
|
+
data.applyToMesh(mesh, true);
|
|
239
|
+
// As the glTF loader does for its mirrored root: the mirror turns the
|
|
240
|
+
// file's counter-clockwise faces clockwise, which is Babylon's front.
|
|
241
|
+
mesh.sideOrientation = BABYLON.Material.ClockWiseSideOrientation;
|
|
242
|
+
mesh.parent = mirror;
|
|
243
|
+
const material = new BABYLON.StandardMaterial(`${name}-grey`, scene);
|
|
244
|
+
material.diffuseColor = new BABYLON.Color3(0.62, 0.62, 0.64);
|
|
245
|
+
material.specularColor = new BABYLON.Color3(0.08, 0.08, 0.08);
|
|
246
|
+
mesh.material = material;
|
|
247
|
+
// Feet on the floor: the pack is centred near its middle.
|
|
248
|
+
const used = new Uint8Array(pack.manifest.vertexCount);
|
|
249
|
+
for (const i of pack.triangles)
|
|
250
|
+
used[i] = 1;
|
|
251
|
+
function update(positions) {
|
|
252
|
+
let foot = Infinity;
|
|
253
|
+
for (let i = 0; i < used.length; i++) {
|
|
254
|
+
if (used[i] && positions[i * 3 + 1] < foot)
|
|
255
|
+
foot = positions[i * 3 + 1];
|
|
256
|
+
}
|
|
257
|
+
mirror.position.y = -foot;
|
|
258
|
+
// Counter-clockwise, outward: the file's handedness, not Babylon's.
|
|
259
|
+
BABYLON.VertexData.ComputeNormals(positions, indices, normals, {
|
|
260
|
+
useRightHandedSystem: true,
|
|
261
|
+
});
|
|
262
|
+
mesh.updateVerticesData(BABYLON.VertexBuffer.PositionKind, positions);
|
|
263
|
+
mesh.updateVerticesData(BABYLON.VertexBuffer.NormalKind, normals);
|
|
264
|
+
mesh.refreshBoundingInfo();
|
|
265
|
+
}
|
|
266
|
+
update(pack.positions);
|
|
267
|
+
return {
|
|
268
|
+
root,
|
|
269
|
+
mesh,
|
|
270
|
+
update,
|
|
271
|
+
dispose() {
|
|
272
|
+
material.dispose();
|
|
273
|
+
mesh.dispose();
|
|
274
|
+
mirror.dispose();
|
|
275
|
+
root.dispose();
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* A skin material from [skin-tone](/skin-tone/)'s dials: dielectric, fairly
|
|
281
|
+
* rough, the albedo the tone gives. One flat colour for now: lips, nails and
|
|
282
|
+
* a texture's detail need the mesh's UVs, which the pack does not carry yet.
|
|
283
|
+
*/
|
|
284
|
+
export function skinMaterial(scene, options = {}) {
|
|
285
|
+
const m = new BABYLON.PBRMaterial(options.name ?? 'skin', scene);
|
|
286
|
+
m.metallic = 0;
|
|
287
|
+
m.roughness = 0.55;
|
|
288
|
+
m.setSkin = (tone, redness = 0.5) => {
|
|
289
|
+
const c = skinAlbedo(tone, redness);
|
|
290
|
+
// PBR's albedo is linear, as glTF's base colour is.
|
|
291
|
+
m.albedoColor = new BABYLON.Color3(c.r, c.g, c.b);
|
|
292
|
+
};
|
|
293
|
+
m.setSkin(options.tone ?? 0.3, options.redness ?? 0.5);
|
|
294
|
+
return m;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Per-vertex colours (RGBA, the skin's vertices) that turn a skin material's
|
|
298
|
+
* colour into the lips' where `lips` (a pack region, every vertex) says so.
|
|
299
|
+
* They multiply the material's albedo, so they are the ratio lip / skin.
|
|
300
|
+
*/
|
|
301
|
+
export function lipTint(skin, lips, tone, redness = 0.5, into) {
|
|
302
|
+
const s = skinAlbedo(tone, redness);
|
|
303
|
+
const l = lipAlbedo(tone, redness);
|
|
304
|
+
const ratio = [l.r / s.r, l.g / s.g, l.b / s.b];
|
|
305
|
+
const out = into ?? new Float32Array(skin.vertices.length * 4);
|
|
306
|
+
skin.vertices.forEach((v, i) => {
|
|
307
|
+
// The mask is how far the lip targets move a vertex, so the lips' own
|
|
308
|
+
// edges are faint in it: anything past half is all lip.
|
|
309
|
+
const m = Math.min(1, Math.max(0, (lips[v] - 0.05) / 0.45));
|
|
310
|
+
const t = m * m * (3 - 2 * m);
|
|
311
|
+
out[i * 4] = 1 + (ratio[0] - 1) * t;
|
|
312
|
+
out[i * 4 + 1] = 1 + (ratio[1] - 1) * t;
|
|
313
|
+
out[i * 4 + 2] = 1 + (ratio[2] - 1) * t;
|
|
314
|
+
out[i * 4 + 3] = 1;
|
|
315
|
+
});
|
|
316
|
+
return out;
|
|
317
|
+
}
|
|
318
|
+
//# sourceMappingURL=character-builder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"character-builder.js","sourceRoot":"","sources":["../src/character-builder.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface AxisFit {
|
|
2
|
+
scale: [number, number, number];
|
|
3
|
+
shift: [number, number, number];
|
|
4
|
+
}
|
|
5
|
+
/** `positions` (xyz) moved by a fit, into a new array or `into`. */
|
|
6
|
+
export declare function applyFit(positions: ArrayLike<number>, fit: AxisFit, into?: Float32Array): Float32Array;
|
|
7
|
+
/**
|
|
8
|
+
* The per-axis scale and shift that lay `reference` (xyz) onto `scalp` (xyz,
|
|
9
|
+
* the skin's points above the brow). Starts with the tops and centres
|
|
10
|
+
* aligned, then refines by nearest points.
|
|
11
|
+
*/
|
|
12
|
+
export declare function fitToScalp(reference: ArrayLike<number>, scalp: ArrayLike<number>, iterations?: number): AxisFit;
|
|
13
|
+
/** Area-weighted vertex normals, counter-clockwise outward. */
|
|
14
|
+
export declare function vertexNormals(positions: ArrayLike<number>, triangles: ArrayLike<number>): Float32Array;
|
|
15
|
+
/**
|
|
16
|
+
* Push every vertex of `positions` (xyz, in place) that is inside the skin,
|
|
17
|
+
* or nearer it than `margin`, out along the nearest skin point's normal.
|
|
18
|
+
* Returns how many moved.
|
|
19
|
+
*/
|
|
20
|
+
export declare function conformOutside(positions: Float32Array, scalp: ArrayLike<number>, normals: ArrayLike<number>, margin?: number): number;
|
|
21
|
+
//# sourceMappingURL=hair-fit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hair-fit.d.ts","sourceRoot":"","sources":["../src/hair-fit.ts"],"names":[],"mappings":"AA6BA,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;IAC/B,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;CAChC;AAED,oEAAoE;AACpE,wBAAgB,QAAQ,CACtB,SAAS,EAAE,SAAS,CAAC,MAAM,CAAC,EAC5B,GAAG,EAAE,OAAO,EACZ,IAAI,CAAC,EAAE,YAAY,GAClB,YAAY,CAQd;AAwBD;;;;GAIG;AACH,wBAAgB,UAAU,CACxB,SAAS,EAAE,SAAS,CAAC,MAAM,CAAC,EAC5B,KAAK,EAAE,SAAS,CAAC,MAAM,CAAC,EACxB,UAAU,SAAK,GACd,OAAO,CA0DT;AAED,+DAA+D;AAC/D,wBAAgB,aAAa,CAC3B,SAAS,EAAE,SAAS,CAAC,MAAM,CAAC,EAC5B,SAAS,EAAE,SAAS,CAAC,MAAM,CAAC,GAC3B,YAAY,CA4Bd;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAC5B,SAAS,EAAE,YAAY,EACvB,KAAK,EAAE,SAAS,CAAC,MAAM,CAAC,EACxB,OAAO,EAAE,SAAS,CAAC,MAAM,CAAC,EAC1B,MAAM,SAAQ,GACb,MAAM,CAoBR"}
|
package/dist/hair-fit.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/*#
|
|
2
|
+
# hair-fit
|
|
3
|
+
|
|
4
|
+
Hair made for one head, worn on another. A hairstyle library such as
|
|
5
|
+
Quaternius' is modelled around ITS base head, in world space; a
|
|
6
|
+
[character builder](/character-builder/) body has a different skull for
|
|
7
|
+
every slider setting. Two steps carry a style across, both pure:
|
|
8
|
+
|
|
9
|
+
1. **`fitToScalp(reference, scalp)`** fits a scale and an offset per axis that
|
|
10
|
+
lay a REFERENCE that hugs the library's scalp (a buzz cut) onto this body's
|
|
11
|
+
scalp, by iterated nearest points (ICP without rotation: both heads face
|
|
12
|
+
+Z, up +Y). The same fit then moves every style, eyebrows and beards too,
|
|
13
|
+
because they were all modelled on that one head.
|
|
14
|
+
2. **`conformOutside(positions, scalp, normals, margin)`** pushes any vertex
|
|
15
|
+
that ends up inside the skin (or closer than `margin` to it) back out along
|
|
16
|
+
the skin's normal, so a shape the fit cannot capture exactly (a longer
|
|
17
|
+
skull, a heavier brow) never shows skin through hair.
|
|
18
|
+
|
|
19
|
+
`vertexNormals(positions, triangles)` gives the skin's normals for the second
|
|
20
|
+
step (counter-clockwise outward, as OBJ and glTF wind).
|
|
21
|
+
|
|
22
|
+
```javascript
|
|
23
|
+
const fit = fitToScalp(buzzCut, scalp)
|
|
24
|
+
const placed = applyFit(styleVertices, fit)
|
|
25
|
+
conformOutside(placed, scalp, scalpNormals, 0.003)
|
|
26
|
+
```
|
|
27
|
+
*/
|
|
28
|
+
/*{ "parent": "character-builder", "order": 60 }*/
|
|
29
|
+
/** `positions` (xyz) moved by a fit, into a new array or `into`. */
|
|
30
|
+
export function applyFit(positions, fit, into) {
|
|
31
|
+
const out = into ?? new Float32Array(positions.length);
|
|
32
|
+
for (let i = 0; i < positions.length; i += 3) {
|
|
33
|
+
for (let k = 0; k < 3; k++) {
|
|
34
|
+
out[i + k] = positions[i + k] * fit.scale[k] + fit.shift[k];
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
/** The index of the point in `cloud` (xyz) nearest (x, y, z), and its squared distance. */
|
|
40
|
+
function nearest(cloud, x, y, z) {
|
|
41
|
+
let best = Infinity;
|
|
42
|
+
let at = 0;
|
|
43
|
+
for (let j = 0; j < cloud.length; j += 3) {
|
|
44
|
+
const dx = cloud[j] - x;
|
|
45
|
+
const dy = cloud[j + 1] - y;
|
|
46
|
+
const dz = cloud[j + 2] - z;
|
|
47
|
+
const d = dx * dx + dy * dy + dz * dz;
|
|
48
|
+
if (d < best) {
|
|
49
|
+
best = d;
|
|
50
|
+
at = j / 3;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return [at, best];
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The per-axis scale and shift that lay `reference` (xyz) onto `scalp` (xyz,
|
|
57
|
+
* the skin's points above the brow). Starts with the tops and centres
|
|
58
|
+
* aligned, then refines by nearest points.
|
|
59
|
+
*/
|
|
60
|
+
export function fitToScalp(reference, scalp, iterations = 12) {
|
|
61
|
+
const box = (p) => {
|
|
62
|
+
const lo = [Infinity, Infinity, Infinity];
|
|
63
|
+
const hi = [-Infinity, -Infinity, -Infinity];
|
|
64
|
+
for (let i = 0; i < p.length; i += 3) {
|
|
65
|
+
for (let k = 0; k < 3; k++) {
|
|
66
|
+
lo[k] = Math.min(lo[k], p[i + k]);
|
|
67
|
+
hi[k] = Math.max(hi[k], p[i + k]);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return { lo, hi };
|
|
71
|
+
};
|
|
72
|
+
const r = box(reference);
|
|
73
|
+
const s = box(scalp);
|
|
74
|
+
const fit = {
|
|
75
|
+
scale: [1, 1, 1],
|
|
76
|
+
shift: [
|
|
77
|
+
(s.lo[0] + s.hi[0] - r.lo[0] - r.hi[0]) / 2,
|
|
78
|
+
s.hi[1] - r.hi[1],
|
|
79
|
+
(s.lo[2] + s.hi[2] - r.lo[2] - r.hi[2]) / 2,
|
|
80
|
+
],
|
|
81
|
+
};
|
|
82
|
+
const n = reference.length / 3;
|
|
83
|
+
const target = new Float32Array(reference.length);
|
|
84
|
+
for (let it = 0; it < iterations; it++) {
|
|
85
|
+
for (let i = 0; i < n; i++) {
|
|
86
|
+
const [j] = nearest(scalp, reference[i * 3] * fit.scale[0] + fit.shift[0], reference[i * 3 + 1] * fit.scale[1] + fit.shift[1], reference[i * 3 + 2] * fit.scale[2] + fit.shift[2]);
|
|
87
|
+
target[i * 3] = scalp[j * 3];
|
|
88
|
+
target[i * 3 + 1] = scalp[j * 3 + 1];
|
|
89
|
+
target[i * 3 + 2] = scalp[j * 3 + 2];
|
|
90
|
+
}
|
|
91
|
+
// Least squares per axis: target ≈ reference · scale + shift.
|
|
92
|
+
for (let k = 0; k < 3; k++) {
|
|
93
|
+
let mr = 0;
|
|
94
|
+
let mt = 0;
|
|
95
|
+
for (let i = 0; i < n; i++) {
|
|
96
|
+
mr += reference[i * 3 + k];
|
|
97
|
+
mt += target[i * 3 + k];
|
|
98
|
+
}
|
|
99
|
+
mr /= n;
|
|
100
|
+
mt /= n;
|
|
101
|
+
let num = 0;
|
|
102
|
+
let den = 0;
|
|
103
|
+
for (let i = 0; i < n; i++) {
|
|
104
|
+
const a = reference[i * 3 + k] - mr;
|
|
105
|
+
num += a * (target[i * 3 + k] - mt);
|
|
106
|
+
den += a * a;
|
|
107
|
+
}
|
|
108
|
+
fit.scale[k] = den > 0 ? num / den : 1;
|
|
109
|
+
fit.shift[k] = mt - fit.scale[k] * mr;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return fit;
|
|
113
|
+
}
|
|
114
|
+
/** Area-weighted vertex normals, counter-clockwise outward. */
|
|
115
|
+
export function vertexNormals(positions, triangles) {
|
|
116
|
+
const out = new Float32Array(positions.length);
|
|
117
|
+
for (let t = 0; t < triangles.length; t += 3) {
|
|
118
|
+
const a = triangles[t] * 3;
|
|
119
|
+
const b = triangles[t + 1] * 3;
|
|
120
|
+
const c = triangles[t + 2] * 3;
|
|
121
|
+
const ux = positions[b] - positions[a];
|
|
122
|
+
const uy = positions[b + 1] - positions[a + 1];
|
|
123
|
+
const uz = positions[b + 2] - positions[a + 2];
|
|
124
|
+
const vx = positions[c] - positions[a];
|
|
125
|
+
const vy = positions[c + 1] - positions[a + 1];
|
|
126
|
+
const vz = positions[c + 2] - positions[a + 2];
|
|
127
|
+
const nx = uy * vz - uz * vy;
|
|
128
|
+
const ny = uz * vx - ux * vz;
|
|
129
|
+
const nz = ux * vy - uy * vx;
|
|
130
|
+
for (const v of [a, b, c]) {
|
|
131
|
+
out[v] += nx;
|
|
132
|
+
out[v + 1] += ny;
|
|
133
|
+
out[v + 2] += nz;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
for (let i = 0; i < out.length; i += 3) {
|
|
137
|
+
const l = Math.hypot(out[i], out[i + 1], out[i + 2]) || 1;
|
|
138
|
+
out[i] /= l;
|
|
139
|
+
out[i + 1] /= l;
|
|
140
|
+
out[i + 2] /= l;
|
|
141
|
+
}
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Push every vertex of `positions` (xyz, in place) that is inside the skin,
|
|
146
|
+
* or nearer it than `margin`, out along the nearest skin point's normal.
|
|
147
|
+
* Returns how many moved.
|
|
148
|
+
*/
|
|
149
|
+
export function conformOutside(positions, scalp, normals, margin = 0.003) {
|
|
150
|
+
let moved = 0;
|
|
151
|
+
for (let i = 0; i < positions.length; i += 3) {
|
|
152
|
+
const [j] = nearest(scalp, positions[i], positions[i + 1], positions[i + 2]);
|
|
153
|
+
const nx = normals[j * 3];
|
|
154
|
+
const ny = normals[j * 3 + 1];
|
|
155
|
+
const nz = normals[j * 3 + 2];
|
|
156
|
+
const out = (positions[i] - scalp[j * 3]) * nx +
|
|
157
|
+
(positions[i + 1] - scalp[j * 3 + 1]) * ny +
|
|
158
|
+
(positions[i + 2] - scalp[j * 3 + 2]) * nz;
|
|
159
|
+
if (out < margin) {
|
|
160
|
+
const push = margin - out;
|
|
161
|
+
positions[i] += nx * push;
|
|
162
|
+
positions[i + 1] += ny * push;
|
|
163
|
+
positions[i + 2] += nz * push;
|
|
164
|
+
moved++;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return moved;
|
|
168
|
+
}
|
|
169
|
+
//# sourceMappingURL=hair-fit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hair-fit.js","sourceRoot":"","sources":["../src/hair-fit.ts"],"names":[],"mappings":"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"}
|