@starcried/starlite-vrm 0.1.0-alpha.2 → 0.1.0
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/BUNDLED_MOTIONS.json +63 -0
- package/BUNDLED_MOTIONS.md +41 -0
- package/README.en.md +65 -0
- package/README.md +31 -3
- package/THIRD_PARTY_NOTICES.md +2 -0
- package/dist/adjustments.d.ts +23 -0
- package/dist/adjustments.js +201 -0
- package/dist/builtin-motion-catalog.d.ts +16 -0
- package/dist/builtin-motion-catalog.js +10 -0
- package/dist/builtin-motion-data.d.ts +5 -0
- package/dist/builtin-motion-data.js +6 -0
- package/dist/builtin-motions.d.ts +3 -0
- package/dist/builtin-motions.js +10 -0
- package/dist/camera.d.ts +8 -0
- package/dist/camera.js +50 -0
- package/dist/config.cjs +71 -2
- package/dist/config.d.ts +4 -1
- package/dist/config.js +34 -3
- package/dist/environment-lighting.d.ts +12 -0
- package/dist/environment-lighting.js +82 -0
- package/dist/gaze.d.ts +2 -1
- package/dist/gaze.js +17 -6
- package/dist/index.d.ts +4 -1
- package/dist/index.js +240 -197
- package/dist/inspect.cjs +226 -74
- package/dist/inspect.d.ts +56 -9
- package/dist/inspect.js +259 -85
- package/dist/playback.js +25 -3
- package/dist/prepare.cjs +225 -77
- package/dist/prepare.js +4 -3
- package/dist/types.d.ts +22 -1
- package/package.json +6 -3
package/dist/config.cjs
CHANGED
|
@@ -19,11 +19,35 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
19
19
|
// src/config.ts
|
|
20
20
|
var config_exports = {};
|
|
21
21
|
__export(config_exports, {
|
|
22
|
+
BUILTIN_ANIMATIONS: () => BUILTIN_ANIMATIONS,
|
|
23
|
+
isBuiltinAnimation: () => isBuiltinAnimation,
|
|
22
24
|
validateCharacterProfile: () => validateCharacterProfile,
|
|
25
|
+
validateEnvironmentLighting: () => validateEnvironmentLighting,
|
|
26
|
+
validateModelAdjustments: () => validateModelAdjustments,
|
|
23
27
|
validateVrmWidgetConfig: () => validateVrmWidgetConfig
|
|
24
28
|
});
|
|
25
29
|
module.exports = __toCommonJS(config_exports);
|
|
26
30
|
|
|
31
|
+
// ../core/dist/locale.js
|
|
32
|
+
function validateLocale(value) {
|
|
33
|
+
if (value !== void 0 && value !== "zh-CN" && value !== "en") {
|
|
34
|
+
throw new Error("locale must be zh-CN or en");
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ../core/dist/environment-lighting.js
|
|
39
|
+
function validateEnvironmentLighting(input) {
|
|
40
|
+
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
41
|
+
throw new Error("environmentLighting must be an object");
|
|
42
|
+
const value = input;
|
|
43
|
+
if (Object.keys(value).some((key) => key !== "color" && key !== "strength" && key !== "brightness") || typeof value.color !== "string" || !/^#[0-9a-f]{6}$/iu.test(value.color))
|
|
44
|
+
throw new Error("environmentLighting.color must be #RRGGBB");
|
|
45
|
+
if (value.strength !== void 0 && (typeof value.strength !== "number" || !Number.isFinite(value.strength) || value.strength < 0 || value.strength > 2))
|
|
46
|
+
throw new Error("environmentLighting.strength must be between 0 and 2");
|
|
47
|
+
if (value.brightness !== void 0 && (typeof value.brightness !== "number" || !Number.isFinite(value.brightness) || value.brightness < 0.8 || value.brightness > 1.1))
|
|
48
|
+
throw new Error("environmentLighting.brightness must be between 0.8 and 1.1");
|
|
49
|
+
}
|
|
50
|
+
|
|
27
51
|
// ../core/dist/dialogue.js
|
|
28
52
|
function resolveDialogueSentences(input, animations, field) {
|
|
29
53
|
const resolve = (value, path) => {
|
|
@@ -61,12 +85,53 @@ function resolveDialogueSentences(input, animations, field) {
|
|
|
61
85
|
return { ...lines ? { lines } : {}, linesByAnimation };
|
|
62
86
|
}
|
|
63
87
|
|
|
88
|
+
// ../core/dist/performance.js
|
|
89
|
+
function validatePerformanceConfig(value) {
|
|
90
|
+
if (value === void 0)
|
|
91
|
+
return;
|
|
92
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).some((key) => key !== "show") || "show" in value && value.show !== void 0 && typeof value.show !== "boolean") {
|
|
93
|
+
throw new Error("performance must be an object with an optional boolean show");
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// src/builtin-motion-catalog.ts
|
|
98
|
+
var BUILTIN_ANIMATIONS = [
|
|
99
|
+
{ name: "StarLiteIdle", durationMs: 6e3 },
|
|
100
|
+
{ name: "StarLiteCuriousPeek", durationMs: 2100 },
|
|
101
|
+
{ name: "StarLiteHappyWave", durationMs: 4600 },
|
|
102
|
+
{ name: "StarLiteSleepyStartle", durationMs: 5500 }
|
|
103
|
+
];
|
|
104
|
+
function isBuiltinAnimation(name) {
|
|
105
|
+
return BUILTIN_ANIMATIONS.some((animation) => animation.name === name);
|
|
106
|
+
}
|
|
107
|
+
|
|
64
108
|
// src/config.ts
|
|
65
109
|
function finite(value, min, max) {
|
|
66
110
|
return typeof value === "number" && Number.isFinite(value) && value >= min && value <= max;
|
|
67
111
|
}
|
|
112
|
+
function validateModelAdjustments(input) {
|
|
113
|
+
const object = (value) => Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
114
|
+
if (!object(input) || Object.keys(input).some((key) => key !== "idle" && key !== "gaze")) throw new Error("invalid model adjustments");
|
|
115
|
+
const groups = {
|
|
116
|
+
idle: { armLiftDegrees: [-30, 45], palmTwistDegrees: [-45, 45], wristLiftDegrees: [-45, 45], handCurl: [0, 1], elbowBendDegrees: [-20, 45] },
|
|
117
|
+
gaze: { headStrength: [0, 2], eyeStrength: [0, 2], smoothingMs: [20, 1e3] }
|
|
118
|
+
};
|
|
119
|
+
for (const [group, limits] of Object.entries(groups)) {
|
|
120
|
+
const values = input[group];
|
|
121
|
+
if (values === void 0) continue;
|
|
122
|
+
if (!object(values)) throw new Error(`invalid model adjustments.${group}`);
|
|
123
|
+
for (const [key, value] of Object.entries(values)) {
|
|
124
|
+
const range = Object.hasOwn(limits, key) ? limits[key] : void 0;
|
|
125
|
+
if (!range || value !== void 0 && !finite(value, range[0], range[1])) throw new Error(`invalid model adjustments.${group}.${key}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
68
129
|
function validateVrmWidgetConfig(config) {
|
|
69
130
|
if (!config || config.backend !== void 0 && config.backend !== "vrm") throw new Error("invalid VRM backend");
|
|
131
|
+
validateLocale(config.locale);
|
|
132
|
+
if (config.adjustments !== void 0) validateModelAdjustments(config.adjustments);
|
|
133
|
+
validatePerformanceConfig(config.performance);
|
|
134
|
+
if (config.environmentLighting !== void 0) validateEnvironmentLighting(config.environmentLighting);
|
|
70
135
|
if (config.interactions?.passThrough !== void 0 && typeof config.interactions.passThrough !== "boolean") throw new Error("interactions.passThrough must be boolean");
|
|
71
136
|
if (config.interactions?.passThrough && config.interactions.drag) throw new Error("passThrough and drag cannot both be enabled");
|
|
72
137
|
for (const key of ["model", "fallback"]) if (typeof config[key] !== "string" || !config[key].trim()) throw new Error(`${key} is required`);
|
|
@@ -102,11 +167,11 @@ function validateVrmWidgetConfig(config) {
|
|
|
102
167
|
names.add(name);
|
|
103
168
|
}
|
|
104
169
|
for (const animation of animations) {
|
|
105
|
-
if (!animation || typeof animation.name !== "string" || !animation.name.trim() || animation.name.length > 128 || names.has(animation.name) || typeof animation.url !== "string" || !animation.url.trim()) throw new Error("invalid or duplicate animation");
|
|
170
|
+
if (!animation || typeof animation.name !== "string" || !animation.name.trim() || animation.name.length > 128 || names.has(animation.name) || animation.name !== "StarLiteIdle" && isBuiltinAnimation(animation.name) || typeof animation.url !== "string" || !animation.url.trim()) throw new Error("invalid or duplicate animation");
|
|
106
171
|
if (animation.durationMs !== void 0 && !finite(animation.durationMs, 1, 6e5)) throw new Error("invalid animation duration");
|
|
107
172
|
names.add(animation.name);
|
|
108
173
|
}
|
|
109
|
-
names.add(
|
|
174
|
+
for (const { name } of BUILTIN_ANIMATIONS) names.add(name);
|
|
110
175
|
for (const name of config.ambient?.animations ?? []) if (!names.has(name)) throw new Error(`unknown ambient animation: ${name}`);
|
|
111
176
|
if ((config.ambient?.animations?.length ?? 0) > 1) throw new Error("VRM supports one explicitly configured ambient clip");
|
|
112
177
|
if (config.neutralAnimation !== void 0 && !names.has(config.neutralAnimation)) throw new Error("unknown neutral animation");
|
|
@@ -147,6 +212,10 @@ function validateCharacterProfile(input) {
|
|
|
147
212
|
}
|
|
148
213
|
// Annotate the CommonJS export names for ESM import in node:
|
|
149
214
|
0 && (module.exports = {
|
|
215
|
+
BUILTIN_ANIMATIONS,
|
|
216
|
+
isBuiltinAnimation,
|
|
150
217
|
validateCharacterProfile,
|
|
218
|
+
validateEnvironmentLighting,
|
|
219
|
+
validateModelAdjustments,
|
|
151
220
|
validateVrmWidgetConfig
|
|
152
221
|
});
|
package/dist/config.d.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
export { validateEnvironmentLighting } from "@starcried/starlite-core";
|
|
2
|
+
export { BUILTIN_ANIMATIONS, isBuiltinAnimation } from "./builtin-motion-catalog.js";
|
|
3
|
+
import type { CharacterProfile, ModelAdjustments, VrmWidgetConfig } from "./types.js";
|
|
4
|
+
export declare function validateModelAdjustments(input: unknown): asserts input is ModelAdjustments;
|
|
2
5
|
export declare function validateVrmWidgetConfig(config: VrmWidgetConfig): void;
|
|
3
6
|
export declare function validateCharacterProfile(input: unknown): asserts input is CharacterProfile;
|
package/dist/config.js
CHANGED
|
@@ -1,10 +1,40 @@
|
|
|
1
|
-
|
|
1
|
+
export { validateEnvironmentLighting } from "@starcried/starlite-core";
|
|
2
|
+
import { validateEnvironmentLighting, resolveDialogueSentences, validatePerformanceConfig, validateLocale } from "@starcried/starlite-core";
|
|
3
|
+
import { BUILTIN_ANIMATIONS, isBuiltinAnimation } from "./builtin-motion-catalog.js";
|
|
4
|
+
export { BUILTIN_ANIMATIONS, isBuiltinAnimation } from "./builtin-motion-catalog.js";
|
|
2
5
|
function finite(value, min, max) {
|
|
3
6
|
return typeof value === "number" && Number.isFinite(value) && value >= min && value <= max;
|
|
4
7
|
}
|
|
8
|
+
export function validateModelAdjustments(input) {
|
|
9
|
+
const object = (value) => Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
10
|
+
if (!object(input) || Object.keys(input).some(key => key !== "idle" && key !== "gaze"))
|
|
11
|
+
throw new Error("invalid model adjustments");
|
|
12
|
+
const groups = {
|
|
13
|
+
idle: { armLiftDegrees: [-30, 45], palmTwistDegrees: [-45, 45], wristLiftDegrees: [-45, 45], handCurl: [0, 1], elbowBendDegrees: [-20, 45] },
|
|
14
|
+
gaze: { headStrength: [0, 2], eyeStrength: [0, 2], smoothingMs: [20, 1000] },
|
|
15
|
+
};
|
|
16
|
+
for (const [group, limits] of Object.entries(groups)) {
|
|
17
|
+
const values = input[group];
|
|
18
|
+
if (values === undefined)
|
|
19
|
+
continue;
|
|
20
|
+
if (!object(values))
|
|
21
|
+
throw new Error(`invalid model adjustments.${group}`);
|
|
22
|
+
for (const [key, value] of Object.entries(values)) {
|
|
23
|
+
const range = Object.hasOwn(limits, key) ? limits[key] : undefined;
|
|
24
|
+
if (!range || (value !== undefined && !finite(value, range[0], range[1])))
|
|
25
|
+
throw new Error(`invalid model adjustments.${group}.${key}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
5
29
|
export function validateVrmWidgetConfig(config) {
|
|
6
30
|
if (!config || (config.backend !== undefined && config.backend !== "vrm"))
|
|
7
31
|
throw new Error("invalid VRM backend");
|
|
32
|
+
validateLocale(config.locale);
|
|
33
|
+
if (config.adjustments !== undefined)
|
|
34
|
+
validateModelAdjustments(config.adjustments);
|
|
35
|
+
validatePerformanceConfig(config.performance);
|
|
36
|
+
if (config.environmentLighting !== undefined)
|
|
37
|
+
validateEnvironmentLighting(config.environmentLighting);
|
|
8
38
|
if (config.interactions?.passThrough !== undefined && typeof config.interactions.passThrough !== "boolean")
|
|
9
39
|
throw new Error("interactions.passThrough must be boolean");
|
|
10
40
|
if (config.interactions?.passThrough && config.interactions.drag)
|
|
@@ -46,13 +76,14 @@ export function validateVrmWidgetConfig(config) {
|
|
|
46
76
|
names.add(name);
|
|
47
77
|
}
|
|
48
78
|
for (const animation of animations) {
|
|
49
|
-
if (!animation || typeof animation.name !== "string" || !animation.name.trim() || animation.name.length > 128 || names.has(animation.name) || typeof animation.url !== "string" || !animation.url.trim())
|
|
79
|
+
if (!animation || typeof animation.name !== "string" || !animation.name.trim() || animation.name.length > 128 || names.has(animation.name) || (animation.name !== "StarLiteIdle" && isBuiltinAnimation(animation.name)) || typeof animation.url !== "string" || !animation.url.trim())
|
|
50
80
|
throw new Error("invalid or duplicate animation");
|
|
51
81
|
if (animation.durationMs !== undefined && !finite(animation.durationMs, 1, 600000))
|
|
52
82
|
throw new Error("invalid animation duration");
|
|
53
83
|
names.add(animation.name);
|
|
54
84
|
}
|
|
55
|
-
|
|
85
|
+
for (const { name } of BUILTIN_ANIMATIONS)
|
|
86
|
+
names.add(name);
|
|
56
87
|
for (const name of config.ambient?.animations ?? [])
|
|
57
88
|
if (!names.has(name))
|
|
58
89
|
throw new Error(`unknown ambient animation: ${name}`);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { DirectionalLight, HemisphereLight } from "three";
|
|
2
|
+
import { type EnvironmentLighting } from "@starcried/starlite-core";
|
|
3
|
+
/** Reuses both existing lights; no capture, material changes or extra render work. */
|
|
4
|
+
export declare class EnvironmentLight {
|
|
5
|
+
#private;
|
|
6
|
+
readonly light: HemisphereLight;
|
|
7
|
+
readonly key: DirectionalLight;
|
|
8
|
+
constructor();
|
|
9
|
+
set(value: EnvironmentLighting | null, immediate?: boolean): void;
|
|
10
|
+
snap(): void;
|
|
11
|
+
update(delta: number): void;
|
|
12
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { Color, DirectionalLight, HemisphereLight } from "three";
|
|
2
|
+
import { validateEnvironmentLighting } from "@starcried/starlite-core";
|
|
3
|
+
const luminance = (color) => .2126 * color.r + .7152 * color.g + .0722 * color.b;
|
|
4
|
+
const close = (a, b) => Math.abs(a.r - b.r) < 1e-6 && Math.abs(a.g - b.g) < 1e-6 && Math.abs(a.b - b.b) < 1e-6;
|
|
5
|
+
const encodeSrgb = (linear) => linear <= .0031308 ? 12.92 * linear : 1.055 * linear ** (1 / 2.4) - .055;
|
|
6
|
+
/** Reuses both existing lights; no capture, material changes or extra render work. */
|
|
7
|
+
export class EnvironmentLight {
|
|
8
|
+
light = new HemisphereLight(0xffffff, 0xb6c6d5, .65);
|
|
9
|
+
key = new DirectionalLight(0xffffff, 2);
|
|
10
|
+
#baseSky = this.light.color.clone();
|
|
11
|
+
#baseGround = this.light.groundColor.clone();
|
|
12
|
+
#sky = this.#baseSky.clone();
|
|
13
|
+
#ground = this.#baseGround.clone();
|
|
14
|
+
#settled = true;
|
|
15
|
+
#ambientIntensity = .65;
|
|
16
|
+
#keyIntensity = 2;
|
|
17
|
+
constructor() { this.key.position.set(-3, 4, 2); }
|
|
18
|
+
set(value, immediate = false) {
|
|
19
|
+
if (value !== null)
|
|
20
|
+
validateEnvironmentLighting(value);
|
|
21
|
+
this.#sky.copy(this.#baseSky);
|
|
22
|
+
this.#ground.copy(this.#baseGround);
|
|
23
|
+
this.#ambientIntensity = .65;
|
|
24
|
+
this.#keyIntensity = 2;
|
|
25
|
+
const strength = value?.strength ?? 1;
|
|
26
|
+
if (value && strength > 0) {
|
|
27
|
+
// Hex is sRGB; Three converts it to linear working space before arithmetic.
|
|
28
|
+
const color = new Color(value.color), maximum = Math.max(color.r, color.g, color.b);
|
|
29
|
+
// Preserve the accepted 0–100% curve exactly; higher values amplify
|
|
30
|
+
// linearly rather than extrapolating the easing polynomial.
|
|
31
|
+
const response = strength <= 1 ? 1 - (1 - strength) ** 3 : strength;
|
|
32
|
+
const perceived = Math.min(1, Math.max(0, encodeSrgb(luminance(color))));
|
|
33
|
+
const automatic = perceived <= .5 ? .8 + .4 * perceived : 1 + .2 * (perceived - .5);
|
|
34
|
+
const brightness = 1 + ((value.brightness ?? automatic) - 1) * response;
|
|
35
|
+
let transfer = 0;
|
|
36
|
+
if (maximum > 0) {
|
|
37
|
+
// Near-black compression/noise must not become a fully saturated colour.
|
|
38
|
+
const confidence = Math.min(1, maximum / .02);
|
|
39
|
+
const saturation = (maximum - Math.min(color.r, color.g, color.b)) / maximum;
|
|
40
|
+
if (saturation > 0) {
|
|
41
|
+
const hue = color.clone().multiplyScalar(1 / maximum);
|
|
42
|
+
// Unit-luminance chroma avoids double darkening. The accepted
|
|
43
|
+
// peak-2 cap stays exact through 100%, rising to 3 at 200%.
|
|
44
|
+
const peak = strength <= 1 ? 2 : 1 + strength;
|
|
45
|
+
const mix = Math.min(.65 * response * confidence, (1 - 1 / peak) / (1 - luminance(hue)), .9);
|
|
46
|
+
const filter = new Color(0xffffff).lerp(hue, mix);
|
|
47
|
+
filter.multiplyScalar(1 / luminance(filter));
|
|
48
|
+
this.#sky.multiply(filter);
|
|
49
|
+
this.#ground.multiply(filter).multiplyScalar(luminance(this.#baseGround) / luminance(this.#baseGround.clone().multiply(filter)));
|
|
50
|
+
transfer = .8 * response * confidence * saturation;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Rebalance a bounded amount of key energy into the existing fill. Its
|
|
54
|
+
// chromatic contribution is visible while the key colour stays white.
|
|
55
|
+
this.#ambientIntensity = (.65 + transfer) * brightness;
|
|
56
|
+
this.#keyIntensity = (2 - transfer) * brightness;
|
|
57
|
+
}
|
|
58
|
+
this.#settled = false;
|
|
59
|
+
if (immediate)
|
|
60
|
+
this.snap();
|
|
61
|
+
}
|
|
62
|
+
snap() {
|
|
63
|
+
this.light.color.copy(this.#sky);
|
|
64
|
+
this.light.groundColor.copy(this.#ground);
|
|
65
|
+
this.light.intensity = this.#ambientIntensity;
|
|
66
|
+
this.key.intensity = this.#keyIntensity;
|
|
67
|
+
this.#settled = true;
|
|
68
|
+
}
|
|
69
|
+
update(delta) {
|
|
70
|
+
if (this.#settled || !Number.isFinite(delta) || delta <= 0)
|
|
71
|
+
return;
|
|
72
|
+
// ~0.5 seconds to settle. No hidden elapsed time or extra animation frame.
|
|
73
|
+
const alpha = 1 - Math.exp(-Math.min(delta, .1) / .18);
|
|
74
|
+
this.light.color.lerp(this.#sky, alpha);
|
|
75
|
+
this.light.groundColor.lerp(this.#ground, alpha);
|
|
76
|
+
this.light.intensity += (this.#ambientIntensity - this.light.intensity) * alpha;
|
|
77
|
+
this.key.intensity += (this.#keyIntensity - this.key.intensity) * alpha;
|
|
78
|
+
if (close(this.light.color, this.#sky) && close(this.light.groundColor, this.#ground)
|
|
79
|
+
&& Math.abs(this.light.intensity - this.#ambientIntensity) < 1e-6 && Math.abs(this.key.intensity - this.#keyIntensity) < 1e-6)
|
|
80
|
+
this.snap();
|
|
81
|
+
}
|
|
82
|
+
}
|
package/dist/gaze.d.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { type Object3D, type PerspectiveCamera } from "three";
|
|
2
2
|
import type { VRMLookAt } from "@pixiv/three-vrm";
|
|
3
|
-
import type { VrmWidgetConfig } from "./types.js";
|
|
3
|
+
import type { ModelAdjustments, VrmWidgetConfig } from "./types.js";
|
|
4
4
|
/** Pointer projection and angle limits are independent of avatar scale and VRM version. */
|
|
5
5
|
export declare class PointerGaze {
|
|
6
6
|
#private;
|
|
7
7
|
readonly limits: NonNullable<VrmWidgetConfig["gaze"]>;
|
|
8
8
|
constructor(limits?: NonNullable<VrmWidgetConfig["gaze"]>);
|
|
9
|
+
setAdjustments(adjustments?: ModelAdjustments["gaze"]): void;
|
|
9
10
|
move(x: number, y: number, rect: {
|
|
10
11
|
left: number;
|
|
11
12
|
top: number;
|
package/dist/gaze.js
CHANGED
|
@@ -18,9 +18,11 @@ export class PointerGaze {
|
|
|
18
18
|
#tracking = false;
|
|
19
19
|
#yaw = 0;
|
|
20
20
|
#pitch = 0;
|
|
21
|
+
#adjustments = {};
|
|
21
22
|
constructor(limits = {}) {
|
|
22
23
|
this.limits = limits;
|
|
23
24
|
}
|
|
25
|
+
setAdjustments(adjustments = {}) { this.#adjustments = { ...adjustments }; }
|
|
24
26
|
move(x, y, rect, viewport = { width: rect.left + rect.width, height: rect.top + rect.height }) {
|
|
25
27
|
if (![x, y, rect.left, rect.top, rect.width, rect.height, viewport.width, viewport.height].every(Number.isFinite) || rect.width <= 0 || rect.height <= 0 || viewport.width <= 0 || viewport.height <= 0)
|
|
26
28
|
return;
|
|
@@ -51,16 +53,17 @@ export class PointerGaze {
|
|
|
51
53
|
/** Normalized humanoid correction precedes the single humanoid update. */
|
|
52
54
|
updateHead(head, camera, delta, authored) {
|
|
53
55
|
this.restoreHead();
|
|
54
|
-
if (!head || authored) {
|
|
56
|
+
if (!head || authored || this.#adjustments.headStrength === 0) {
|
|
55
57
|
this.#headYaw = 0;
|
|
56
58
|
this.#headPitch = 0;
|
|
57
59
|
return;
|
|
58
60
|
}
|
|
59
61
|
head.getWorldPosition(this.#head);
|
|
60
62
|
const offset = this.#offset(camera, this.#head);
|
|
61
|
-
const blend = 1 - Math.exp(-Math.max(0, delta) / ((this.limits.smoothingMs ?? 100) / 1000));
|
|
62
|
-
|
|
63
|
-
this.#
|
|
63
|
+
const blend = 1 - Math.exp(-Math.max(0, delta) / ((this.#adjustments.smoothingMs ?? this.limits.smoothingMs ?? 100) / 1000));
|
|
64
|
+
const yawLimit = this.limits.maxYawDegrees ?? 25, pitchLimit = this.limits.maxPitchDegrees ?? 15, strength = this.#adjustments.headStrength ?? 1;
|
|
65
|
+
this.#headYaw += (MathUtils.clamp(offset.x * yawLimit * .6 * strength, -yawLimit, yawLimit) - this.#headYaw) * blend;
|
|
66
|
+
this.#headPitch += (MathUtils.clamp(offset.y * pitchLimit * .6 * strength, -pitchLimit, pitchLimit) - this.#headPitch) * blend;
|
|
64
67
|
this.#headBase.copy(head.quaternion);
|
|
65
68
|
this.#posedHead = head;
|
|
66
69
|
this.#rotation.setFromAxisAngle(this.#scratch.setFromMatrixColumn(camera.matrixWorld, 1), MathUtils.degToRad(this.#headYaw));
|
|
@@ -80,12 +83,20 @@ export class PointerGaze {
|
|
|
80
83
|
this.#pitch = lookAt.pitch;
|
|
81
84
|
return;
|
|
82
85
|
}
|
|
86
|
+
if (this.#adjustments.eyeStrength === 0) {
|
|
87
|
+
this.#yaw = 0;
|
|
88
|
+
this.#pitch = 0;
|
|
89
|
+
lookAt.yaw = 0;
|
|
90
|
+
lookAt.pitch = 0;
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
83
93
|
lookAt.humanoid.getRawBoneNode("head")?.updateWorldMatrix(true, false);
|
|
84
94
|
lookAt.getLookAtWorldPosition(this.#head);
|
|
85
95
|
const yawLimit = this.limits.maxYawDegrees ?? 25, pitchLimit = this.limits.maxPitchDegrees ?? 15;
|
|
86
96
|
this.#target.copy(camera.position);
|
|
87
97
|
if (this.#tracking) {
|
|
88
|
-
const
|
|
98
|
+
const offset = this.#offset(camera, this.#head), strength = this.#adjustments.eyeStrength ?? 1;
|
|
99
|
+
const x = MathUtils.clamp(offset.x * strength, -1, 1), y = MathUtils.clamp(offset.y * strength, -1, 1);
|
|
89
100
|
const distance = camera.position.distanceTo(this.#head);
|
|
90
101
|
this.#target.addScaledVector(this.#scratch.setFromMatrixColumn(camera.matrixWorld, 0), distance * Math.tan(MathUtils.degToRad(x * yawLimit)));
|
|
91
102
|
this.#target.addScaledVector(this.#scratch.setFromMatrixColumn(camera.matrixWorld, 1), distance / Math.cos(MathUtils.degToRad(x * yawLimit)) * Math.tan(MathUtils.degToRad(y * pitchLimit)));
|
|
@@ -93,7 +104,7 @@ export class PointerGaze {
|
|
|
93
104
|
lookAt.lookAt(this.#target);
|
|
94
105
|
const yaw = Math.max(-yawLimit, Math.min(yawLimit, lookAt.yaw));
|
|
95
106
|
const pitch = Math.max(-pitchLimit, Math.min(pitchLimit, lookAt.pitch));
|
|
96
|
-
const blend = 1 - Math.exp(-Math.max(0, delta) / ((this.limits.smoothingMs ?? 100) / 1000));
|
|
107
|
+
const blend = 1 - Math.exp(-Math.max(0, delta) / ((this.#adjustments.smoothingMs ?? this.limits.smoothingMs ?? 100) / 1000));
|
|
97
108
|
this.#yaw += (yaw - this.#yaw) * blend;
|
|
98
109
|
this.#pitch += (pitch - this.#pitch) * blend;
|
|
99
110
|
lookAt.yaw = Math.max(-yawLimit, Math.min(yawLimit, this.#yaw));
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
export { resolveLocale, type StarLiteLocale } from "@starcried/starlite-core";
|
|
1
2
|
import { type MountOptions } from "@starcried/starlite-core";
|
|
3
|
+
export { BUILTIN_ANIMATIONS } from "./builtin-motion-catalog.js";
|
|
2
4
|
import type { VrmController, VrmWidgetConfig } from "./types.js";
|
|
3
5
|
export type * from "./types.js";
|
|
4
|
-
export {
|
|
6
|
+
export { QuickSettingsMenu, type QuickSettingsOptions, type QuickSettingItem } from "@starcried/starlite-core";
|
|
7
|
+
export { validateModelAdjustments, validateVrmWidgetConfig } from "./config.js";
|
|
5
8
|
export declare function mountModelWidget(host: HTMLElement, config: VrmWidgetConfig, options?: MountOptions): Promise<VrmController>;
|
|
6
9
|
export declare const mountVrmWidget: typeof mountModelWidget;
|