@laplace.live/persona-sdk 1.8.1 → 1.9.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/dist/index.d.ts CHANGED
@@ -19,6 +19,7 @@ export * from './values/model-info.ts';
19
19
  export * from './values/scene-transition.ts';
20
20
  export * from './values/stage-info.ts';
21
21
  export * from './values/terms.ts';
22
+ export * from './values/volumetric-lighting.ts';
22
23
  export * from './values/vrm-bindings.ts';
23
24
  export * from './wire/envelope.ts';
24
25
  export * from './wire/errors.ts';
package/dist/index.js CHANGED
@@ -21,6 +21,7 @@ export * from "./values/model-info.js";
21
21
  export * from "./values/scene-transition.js";
22
22
  export * from "./values/stage-info.js";
23
23
  export * from "./values/terms.js";
24
+ export * from "./values/volumetric-lighting.js";
24
25
  export * from "./values/vrm-bindings.js";
25
26
  export * from "./wire/envelope.js";
26
27
  export * from "./wire/errors.js";
@@ -75,6 +75,11 @@ export interface ControllerMovementConfig {
75
75
  /** Turn a VRM avatar toward its movement direction. */
76
76
  faceMovement: boolean;
77
77
  }
78
+ export declare const CONTROLLER_MOVE_SPEED_MAX = 10;
79
+ export declare const CONTROLLER_TURN_SPEED_MAX = 720;
80
+ export declare const CONTROLLER_MOVEMENT_SMOOTHING_MAX = 0.5;
81
+ export declare const CONTROLLER_WALK_SPEED_MIN = 0.1;
82
+ export declare const CONTROLLER_WALK_SPEED_MAX = 10;
78
83
  export declare const DEFAULT_CONTROLLER_MOVEMENT: Readonly<ControllerMovementConfig>;
79
84
  /** Heal saved movement or a merged API patch using the same limits as the controls. */
80
85
  export declare function healControllerMovement(raw: unknown): ControllerMovementConfig;
@@ -114,9 +114,11 @@ export function controllerSlot(full) {
114
114
  ? parts.slot
115
115
  : null;
116
116
  }
117
- const CONTROLLER_MOVE_SPEED_MAX = 10;
118
- const CONTROLLER_TURN_SPEED_MAX = 720;
119
- const CONTROLLER_MOVEMENT_SMOOTHING_MAX = 0.5;
117
+ export const CONTROLLER_MOVE_SPEED_MAX = 10;
118
+ export const CONTROLLER_TURN_SPEED_MAX = 720;
119
+ export const CONTROLLER_MOVEMENT_SMOOTHING_MAX = 0.5;
120
+ export const CONTROLLER_WALK_SPEED_MIN = 0.1;
121
+ export const CONTROLLER_WALK_SPEED_MAX = 10;
120
122
  export const DEFAULT_CONTROLLER_MOVEMENT = {
121
123
  enabled: false,
122
124
  slot: 1,
@@ -140,7 +142,7 @@ export function healControllerMovement(raw) {
140
142
  smoothing: clamp(finiteOr(r.smoothing, d.smoothing), 0, CONTROLLER_MOVEMENT_SMOOTHING_MAX),
141
143
  walkEnabled: typeof r.walkEnabled === 'boolean' ? r.walkEnabled : d.walkEnabled,
142
144
  walkClip: nonEmptyString(r.walkClip),
143
- walkSpeed: clamp(finiteOr(r.walkSpeed, d.walkSpeed), 0.1, 10),
145
+ walkSpeed: clamp(finiteOr(r.walkSpeed, d.walkSpeed), CONTROLLER_WALK_SPEED_MIN, CONTROLLER_WALK_SPEED_MAX),
144
146
  faceMovement: typeof r.faceMovement === 'boolean' ? r.faceMovement : d.faceMovement,
145
147
  };
146
148
  }
@@ -4,7 +4,7 @@ export type GltfLoadPath = 'vrm' | 'gltf';
4
4
  export declare const LAPLACE_OUTFIT = "LAPLACE_outfit";
5
5
  /** The app's room extension, read on props and sets — `renderer/vrm/gltf-extension.ts`. */
6
6
  export declare const LAPLACE_ENVIRONMENT = "LAPLACE_environment";
7
- /** `GLTFLoader` extensions that throw at parse without a decoder — and the app sets none. */
7
+ /** `GLTFLoader` extensions needing a decoder the app doesn't set. */
8
8
  export declare const DECODER_EXTENSIONS: string[];
9
9
  /** Per loader; the app's own extensions ride with the loader that reads them. */
10
10
  export declare const SUPPORTED_EXTENSIONS: Record<GltfLoadPath, ReadonlySet<string>>;
@@ -15,16 +15,12 @@ const VRM_EXTENSIONS = [
15
15
  'VRMC_materials_mtoon',
16
16
  'VRMC_materials_hdr_emissiveMultiplier',
17
17
  ];
18
- /** `GLTFLoader` extensions that throw at parse without a decoder — and the app sets none. */
19
- export const DECODER_EXTENSIONS = [
20
- 'KHR_draco_mesh_compression',
21
- 'KHR_texture_basisu',
22
- 'EXT_meshopt_compression',
23
- 'KHR_meshopt_compression',
24
- ];
18
+ /** `GLTFLoader` extensions needing a decoder the app doesn't set. */
19
+ export const DECODER_EXTENSIONS = ['KHR_texture_basisu'];
25
20
  /** The rest of `GLTFLoader`'s own `EXTENSIONS` table, which either loader renders as authored. */
26
21
  const RENDERABLE = [
27
22
  'KHR_binary_glTF',
23
+ 'KHR_draco_mesh_compression',
28
24
  'KHR_lights_punctual',
29
25
  'KHR_materials_clearcoat',
30
26
  'KHR_materials_dispersion',
@@ -42,6 +38,8 @@ const RENDERABLE = [
42
38
  'EXT_materials_bump',
43
39
  'EXT_texture_webp',
44
40
  'EXT_texture_avif',
41
+ 'EXT_meshopt_compression',
42
+ 'KHR_meshopt_compression',
45
43
  'EXT_mesh_gpu_instancing',
46
44
  ];
47
45
  /** Per loader; the app's own extensions ride with the loader that reads them. */
@@ -6,7 +6,7 @@ export declare function clamp01(v: number): number;
6
6
  export declare function wrapAngle(a: number): number;
7
7
  export declare const DEFAULT_LIVE2D_PLACEMENT: ScreenPlacement;
8
8
  export declare const DEFAULT_VRM_PLACEMENT: VrmPlacement;
9
- /** Scene colors heal to 6-digit hex. */
9
+ /** Scene colors heal to 6-digit hex; both cases are spelled out because JSON Schema patterns carry no flags. */
10
10
  export declare const SCENE_COLOR_RE: RegExp;
11
11
  /** The value when it is a 6-digit hex color, else `fallback` — validated, never clamped. */
12
12
  export declare function hexColorOr(v: unknown, fallback: string): string;
@@ -14,13 +14,24 @@ export declare function hexColorOr(v: unknown, fallback: string): string;
14
14
  export declare function opaqueHex(hex: string): string;
15
15
  export declare const SCENE_LIGHT_INTENSITY_MAX = 2;
16
16
  /**
17
- * Point lights decay physically (1/d²), so reach rides on power the way Blender's
18
- * does: 20 stays visible to ~10 m where 2 self-extinguishes by ~5. The other
19
- * types have no falloff to overcome — 20 would just blow the stage out.
17
+ * Point lights need headroom against inverse-square decay; directional and ambient
18
+ * lights have no falloff and would wash out the stage at the same intensity.
20
19
  */
21
20
  export declare const SCENE_LIGHT_POINT_INTENSITY_MAX = 20;
22
- /** Intensity ceiling for a light of `type` — one source for the slider and scene healing. */
21
+ export declare const SCENE_LIGHT_AREA_SIZE_MIN = 0.01;
22
+ export declare const SCENE_LIGHT_AREA_SIZE_MAX = 20;
23
+ export declare const DEFAULT_AREA_LIGHT_SIZE = 1;
24
+ export declare const SCENE_LIGHT_SPOT_ANGLE_MIN = 1;
25
+ /** A 90° half-angle would make the spot shadow camera's 180° projection singular. */
26
+ export declare const SCENE_LIGHT_SPOT_ANGLE_MAX = 89;
27
+ export declare const DEFAULT_SPOT_ANGLE_DEG = 30;
28
+ export declare const DEFAULT_SPOT_PENUMBRA = 0.3;
29
+ /** Positional lights need falloff headroom; the slider and scene healing share this ceiling. */
23
30
  export declare function sceneLightIntensityMax(type: SceneLightType): number;
31
+ /** Whether a light of `type` aims with azimuth/elevation. */
32
+ export declare function sceneLightIsAimed(type: SceneLightType): boolean;
33
+ /** Whether a light of `type` can cast shadows; ambient and area lights never do. */
34
+ export declare function sceneLightCastsShadows(type: SceneLightType): boolean;
24
35
  export declare const LIVE2D_SCALE_MIN = 0.1;
25
36
  export declare const LIVE2D_SCALE_MAX = 24;
26
37
  export declare const VRM_SCALE_MIN = 0.05;
@@ -30,6 +41,8 @@ export declare const SCENE_LIGHT_RANGE_MAX = 20;
30
41
  export declare const SCENE_LIGHT_SHADOW_RADIUS_MAX = 16;
31
42
  /** The point disk's 32 fixed taps stay dense across a far wider penumbra than 5 dithered ones. */
32
43
  export declare const SCENE_LIGHT_POINT_SHADOW_RADIUS_MAX = 32;
44
+ export declare const SCENE_LIGHT_SHADOW_SOURCE_SIZE_MAX = 10;
45
+ export declare const DEFAULT_LIGHT_SHADOW_SOURCE_SIZE = 0.1;
33
46
  /** Shadow softness (PCF disk radius, in shadow-map texels) ceiling for a light of `type`. */
34
47
  export declare function sceneLightShadowRadiusMax(type: SceneLightType): number;
35
48
  /**
@@ -107,8 +120,9 @@ export declare function environmentLookIsUntouched(env: SceneEnvironment, refere
107
120
  *
108
121
  * {@link SceneEnvironment.iblAssetId} and {@link SceneEnvironment.modelAssetId} are a
109
122
  * single slot — a scene wears an equirect map or a 3D set — so this clears whichever
110
- * the pick is not. `bakeFromModel` goes with the set: it outlives the id it names, and
111
- * it wins over a picked map, so a stale one would leave a fresh map lighting nothing.
123
+ * the pick is not. `bakeFromModel` goes with the set: on for a set arriving where none was,
124
+ * kept across a swap, dropped otherwise — it wins over a picked map, so a stale one would
125
+ * leave a fresh map lighting nothing.
112
126
  * Light overrides index the set's own lamps, so a different set — or none — drops them.
113
127
  * `modelLook` stays put: the stage replaces it as the next set parses, and the desktop
114
128
  * reads the outgoing one as what the scene wears now when deciding to adopt the next.
@@ -18,8 +18,8 @@ export function wrapAngle(a) {
18
18
  }
19
19
  export const DEFAULT_LIVE2D_PLACEMENT = { x: 0, y: 0, scale: 1, rotation: 0 };
20
20
  export const DEFAULT_VRM_PLACEMENT = { x: 0, y: 0, z: 0, rotX: 0, rotY: 0, rotZ: 0, scale: 1 };
21
- /** Scene colors heal to 6-digit hex. */
22
- export const SCENE_COLOR_RE = /^#[0-9a-f]{6}$/i;
21
+ /** Scene colors heal to 6-digit hex; both cases are spelled out because JSON Schema patterns carry no flags. */
22
+ export const SCENE_COLOR_RE = /^#[0-9a-fA-F]{6}$/;
23
23
  /** The value when it is a 6-digit hex color, else `fallback` — validated, never clamped. */
24
24
  export function hexColorOr(v, fallback) {
25
25
  return typeof v === 'string' && SCENE_COLOR_RE.test(v) ? v : fallback;
@@ -30,14 +30,29 @@ export function opaqueHex(hex) {
30
30
  }
31
31
  export const SCENE_LIGHT_INTENSITY_MAX = 2;
32
32
  /**
33
- * Point lights decay physically (1/d²), so reach rides on power the way Blender's
34
- * does: 20 stays visible to ~10 m where 2 self-extinguishes by ~5. The other
35
- * types have no falloff to overcome — 20 would just blow the stage out.
33
+ * Point lights need headroom against inverse-square decay; directional and ambient
34
+ * lights have no falloff and would wash out the stage at the same intensity.
36
35
  */
37
36
  export const SCENE_LIGHT_POINT_INTENSITY_MAX = 20;
38
- /** Intensity ceiling for a light of `type` — one source for the slider and scene healing. */
37
+ export const SCENE_LIGHT_AREA_SIZE_MIN = 0.01;
38
+ export const SCENE_LIGHT_AREA_SIZE_MAX = 20;
39
+ export const DEFAULT_AREA_LIGHT_SIZE = 1;
40
+ export const SCENE_LIGHT_SPOT_ANGLE_MIN = 1;
41
+ /** A 90° half-angle would make the spot shadow camera's 180° projection singular. */
42
+ export const SCENE_LIGHT_SPOT_ANGLE_MAX = 89;
43
+ export const DEFAULT_SPOT_ANGLE_DEG = 30;
44
+ export const DEFAULT_SPOT_PENUMBRA = 0.3;
45
+ /** Positional lights need falloff headroom; the slider and scene healing share this ceiling. */
39
46
  export function sceneLightIntensityMax(type) {
40
- return type === 'point' ? SCENE_LIGHT_POINT_INTENSITY_MAX : SCENE_LIGHT_INTENSITY_MAX;
47
+ return type === 'directional' || type === 'ambient' ? SCENE_LIGHT_INTENSITY_MAX : SCENE_LIGHT_POINT_INTENSITY_MAX;
48
+ }
49
+ /** Whether a light of `type` aims with azimuth/elevation. */
50
+ export function sceneLightIsAimed(type) {
51
+ return type === 'directional' || type === 'spot' || type === 'area';
52
+ }
53
+ /** Whether a light of `type` can cast shadows; ambient and area lights never do. */
54
+ export function sceneLightCastsShadows(type) {
55
+ return type !== 'ambient' && type !== 'area';
41
56
  }
42
57
  // Live2D bounds are the zoom clamp (zoom.ts re-exports these); VRM scales a world-space root.
43
58
  export const LIVE2D_SCALE_MIN = 0.1;
@@ -51,6 +66,8 @@ export const SCENE_LIGHT_RANGE_MAX = 20;
51
66
  export const SCENE_LIGHT_SHADOW_RADIUS_MAX = 16;
52
67
  /** The point disk's 32 fixed taps stay dense across a far wider penumbra than 5 dithered ones. */
53
68
  export const SCENE_LIGHT_POINT_SHADOW_RADIUS_MAX = 32;
69
+ export const SCENE_LIGHT_SHADOW_SOURCE_SIZE_MAX = 10;
70
+ export const DEFAULT_LIGHT_SHADOW_SOURCE_SIZE = 0.1;
54
71
  /** Shadow softness (PCF disk radius, in shadow-map texels) ceiling for a light of `type`. */
55
72
  export function sceneLightShadowRadiusMax(type) {
56
73
  return type === 'point' ? SCENE_LIGHT_POINT_SHADOW_RADIUS_MAX : SCENE_LIGHT_SHADOW_RADIUS_MAX;
@@ -116,16 +133,23 @@ export function defaultSceneLightOf(type) {
116
133
  color: '#ffffff',
117
134
  // Ambient is a flat albedo floor with no shading — 1.0 alone washes the model out.
118
135
  intensity: type === 'ambient' ? 0.4 : 1,
119
- azimuth: DEFAULT_LIGHT_AZIMUTH_DEG,
120
- elevation: DEFAULT_LIGHT_ELEVATION_DEG,
136
+ azimuth: type === 'area' || type === 'spot' ? 0 : DEFAULT_LIGHT_AZIMUTH_DEG,
137
+ elevation: type === 'area' || type === 'spot' ? 0 : DEFAULT_LIGHT_ELEVATION_DEG,
121
138
  x: 0,
122
139
  y: 1.4,
123
140
  z: 1,
124
141
  range: 10,
142
+ angle: DEFAULT_SPOT_ANGLE_DEG,
143
+ penumbra: DEFAULT_SPOT_PENUMBRA,
144
+ width: DEFAULT_AREA_LIGHT_SIZE,
145
+ height: DEFAULT_AREA_LIGHT_SIZE,
146
+ roll: 0,
125
147
  // A point light shadow is six cube faces, so it stays opt-in where a
126
148
  // directional light's single depth pass can be on by default.
127
149
  shadowQuality: type === 'directional' ? 'high' : 'off',
128
150
  shadowRadius: 6,
151
+ shadowFilter: 'pcf',
152
+ shadowSourceSize: DEFAULT_LIGHT_SHADOW_SOURCE_SIZE,
129
153
  };
130
154
  }
131
155
  /**
@@ -190,8 +214,9 @@ export function environmentLookIsUntouched(env, reference) {
190
214
  *
191
215
  * {@link SceneEnvironment.iblAssetId} and {@link SceneEnvironment.modelAssetId} are a
192
216
  * single slot — a scene wears an equirect map or a 3D set — so this clears whichever
193
- * the pick is not. `bakeFromModel` goes with the set: it outlives the id it names, and
194
- * it wins over a picked map, so a stale one would leave a fresh map lighting nothing.
217
+ * the pick is not. `bakeFromModel` goes with the set: on for a set arriving where none was,
218
+ * kept across a swap, dropped otherwise — it wins over a picked map, so a stale one would
219
+ * leave a fresh map lighting nothing.
195
220
  * Light overrides index the set's own lamps, so a different set — or none — drops them.
196
221
  * `modelLook` stays put: the stage replaces it as the next set parses, and the desktop
197
222
  * reads the outgoing one as what the scene wears now when deciding to adopt the next.
@@ -204,7 +229,7 @@ export function environmentWithAsset(env, asset) {
204
229
  ...env,
205
230
  iblAssetId: asset !== null && !isSet ? asset.id : null,
206
231
  modelAssetId,
207
- bakeFromModel: isSet && env.bakeFromModel,
232
+ bakeFromModel: isSet && (env.modelAssetId === null || env.bakeFromModel),
208
233
  modelLightOverrides: modelAssetId === env.modelAssetId ? env.modelLightOverrides : {},
209
234
  };
210
235
  }
@@ -0,0 +1,42 @@
1
+ import { type SceneEnvironment, type SceneVolumetricLighting } from '../wire/types.ts';
2
+ /** Slider and healing metadata for the haze's numeric settings. */
3
+ export declare const SCENE_VOLUMETRIC_LIGHTING_SPECS: {
4
+ readonly density: {
5
+ readonly default: 1;
6
+ readonly min: 0;
7
+ readonly max: 10;
8
+ readonly step: 0.05;
9
+ readonly digits: 2;
10
+ };
11
+ readonly intensity: {
12
+ readonly default: 1;
13
+ readonly min: 0;
14
+ readonly max: 10;
15
+ readonly step: 0.05;
16
+ readonly digits: 2;
17
+ };
18
+ readonly range: {
19
+ readonly default: 20;
20
+ readonly min: 1;
21
+ readonly max: 100;
22
+ readonly step: 1;
23
+ readonly digits: 0;
24
+ };
25
+ readonly speed: {
26
+ readonly default: 0.2;
27
+ readonly min: 0;
28
+ readonly max: 5;
29
+ readonly step: 0.05;
30
+ readonly digits: 2;
31
+ };
32
+ };
33
+ /** Existing scenes retain clear air until volumetric lighting is enabled. */
34
+ export declare function defaultSceneVolumetricLighting(): SceneVolumetricLighting;
35
+ /** Restore saved haze settings and clamp them to the shared editor bounds. */
36
+ export declare function healSceneVolumetricLighting(raw: unknown): SceneVolumetricLighting;
37
+ /** An environment edit whose haze settings arrive as leaves, so queued edits never replace one another. */
38
+ export type EnvironmentPatch = Omit<Partial<SceneEnvironment>, 'volumetricLighting'> & {
39
+ volumetricLighting?: Partial<SceneVolumetricLighting>;
40
+ };
41
+ /** Merge `patch` into `target` in place; haze leaves land in the settings object `target` already holds. */
42
+ export declare function mergeEnvironmentPatch(target: EnvironmentPatch, { volumetricLighting, ...fields }: EnvironmentPatch): void;
@@ -0,0 +1,43 @@
1
+ import { EFFECTS_QUALITY_LEVELS } from "../wire/types.js";
2
+ import { finiteOr, isOneOf, isRecord } from "./guards.js";
3
+ import { clamp } from "./limits.js";
4
+ /** Slider and healing metadata for the haze's numeric settings. */
5
+ export const SCENE_VOLUMETRIC_LIGHTING_SPECS = {
6
+ density: { default: 1, min: 0, max: 10, step: 0.05, digits: 2 },
7
+ intensity: { default: 1, min: 0, max: 10, step: 0.05, digits: 2 },
8
+ range: { default: 20, min: 1, max: 100, step: 1, digits: 0 },
9
+ speed: { default: 0.2, min: 0, max: 5, step: 0.05, digits: 2 },
10
+ };
11
+ /** Existing scenes retain clear air until volumetric lighting is enabled. */
12
+ export function defaultSceneVolumetricLighting() {
13
+ const specs = SCENE_VOLUMETRIC_LIGHTING_SPECS;
14
+ return {
15
+ enabled: false,
16
+ density: specs.density.default,
17
+ intensity: specs.intensity.default,
18
+ range: specs.range.default,
19
+ quality: 'medium',
20
+ speed: specs.speed.default,
21
+ };
22
+ }
23
+ /** Restore saved haze settings and clamp them to the shared editor bounds. */
24
+ export function healSceneVolumetricLighting(raw) {
25
+ const d = defaultSceneVolumetricLighting();
26
+ if (!isRecord(raw))
27
+ return d;
28
+ const specs = SCENE_VOLUMETRIC_LIGHTING_SPECS;
29
+ return {
30
+ enabled: typeof raw.enabled === 'boolean' ? raw.enabled : d.enabled,
31
+ density: clamp(finiteOr(raw.density, d.density), specs.density.min, specs.density.max),
32
+ intensity: clamp(finiteOr(raw.intensity, d.intensity), specs.intensity.min, specs.intensity.max),
33
+ range: clamp(finiteOr(raw.range, d.range), specs.range.min, specs.range.max),
34
+ quality: isOneOf(raw.quality, EFFECTS_QUALITY_LEVELS) ? raw.quality : d.quality,
35
+ speed: clamp(finiteOr(raw.speed, d.speed), specs.speed.min, specs.speed.max),
36
+ };
37
+ }
38
+ /** Merge `patch` into `target` in place; haze leaves land in the settings object `target` already holds. */
39
+ export function mergeEnvironmentPatch(target, { volumetricLighting, ...fields }) {
40
+ Object.assign(target, fields);
41
+ if (volumetricLighting)
42
+ target.volumetricLighting = Object.assign(target.volumetricLighting ?? {}, volumetricLighting);
43
+ }
@@ -330,7 +330,8 @@ export interface SceneCamera {
330
330
  * the clip: applying a pose leaves whatever camera motion the scene has running.
331
331
  */
332
332
  export type CameraPose = Pick<SceneCamera, 'orbit' | 'fov' | 'roll' | 'projection'>;
333
- export type SceneLightType = 'directional' | 'point' | 'ambient';
333
+ export declare const SCENE_LIGHT_TYPES: readonly ["directional", "point", "ambient", "area", "spot"];
334
+ export type SceneLightType = (typeof SCENE_LIGHT_TYPES)[number];
334
335
  /**
335
336
  * Per-light shadow tier. `off` is the old `castShadow: false`; the rest raise
336
337
  * shadow-map resolution. A point light pays 6 cube faces for the same tier a
@@ -338,11 +339,15 @@ export type SceneLightType = 'directional' | 'point' | 'ambient';
338
339
  */
339
340
  export declare const SHADOW_QUALITY_LEVELS: readonly ["off", "low", "medium", "high", "ultra"];
340
341
  export type ShadowQuality = (typeof SHADOW_QUALITY_LEVELS)[number];
342
+ export declare const SHADOW_FILTERS: readonly ["pcf", "pcss"];
343
+ export type ShadowFilter = (typeof SHADOW_FILTERS)[number];
341
344
  /**
342
345
  * One scene light. Angles are degrees. Directional lights aim with
343
346
  * azimuth/elevation and sit at x/y/z (which moves their handle and shadow
344
347
  * coverage, not their parallel shading), point lights use x/y/z + range,
345
- * ambient uses none — all fields stay so a type switch keeps them.
348
+ * spot adds azimuth/elevation, angle and penumbra to point's fields;
349
+ * area uses x/y/z, azimuth/elevation/roll and width/height; ambient uses none.
350
+ * All fields stay so a type switch keeps them. Area lights affect PBR and MToon materials.
346
351
  */
347
352
  export interface SceneLight {
348
353
  id: string;
@@ -357,10 +362,23 @@ export interface SceneLight {
357
362
  y: number;
358
363
  z: number;
359
364
  range: number;
360
- /** Ambient light is directionless, so it never casts whatever this says. */
365
+ /** Spot cone half-angle in degrees, 1–89; omitted values default to 30. */
366
+ angle?: number;
367
+ /** Spot cone edge softness, 0–1; omitted values default to 0.3. */
368
+ penumbra?: number;
369
+ /** Rectangular area light dimensions in metres; omitted values default to 1. */
370
+ width?: number;
371
+ height?: number;
372
+ /** Area light rotation in its rectangle's plane, in degrees; defaults to 0. */
373
+ roll?: number;
374
+ /** Ambient and area lights never cast shadows. */
361
375
  shadowQuality: ShadowQuality;
362
- /** Penumbra width in shadow-map texels — a stylistic dial; the filter widens the edge uniformly. */
376
+ /** PCF disk radius in shadow-map texels; retained while PCSS is selected. */
363
377
  shadowRadius: number;
378
+ /** Per-light filter; defaults to PCF. Gate editing on the `shadow-filters` app capability. */
379
+ shadowFilter?: ShadowFilter;
380
+ /** PCSS source diameter: degrees for directional lights, metres for point/spot; 0–10, default 0.1. */
381
+ shadowSourceSize?: number;
364
382
  }
365
383
  /** Distance haze. Only geometry fogs — empty space keeps the window's transparency. */
366
384
  export interface SceneFog {
@@ -369,6 +387,16 @@ export interface SceneFog {
369
387
  /** Exponential-squared falloff; the useful band is well under 1 at avatar scale. */
370
388
  density: number;
371
389
  }
390
+ /** Illuminated haze in the 3D stage; rendered only by perspective cameras. */
391
+ export interface SceneVolumetricLighting {
392
+ enabled: boolean;
393
+ density: number;
394
+ intensity: number;
395
+ /** Distance from the camera in scene units. */
396
+ range: number;
397
+ quality: EffectsQuality;
398
+ speed: number;
399
+ }
372
400
  /** Display transform applied after the scene renders. `none` keeps colors exactly as authored. */
373
401
  export type SceneToneMapping = 'none' | 'neutral' | 'aces' | 'agx';
374
402
  /** Highlight glow in normal, streak, or star mode; color selection applies only to layer effects. */
@@ -849,6 +877,10 @@ export interface SceneEnvironment {
849
877
  /** The set's suggested look, captured when the model was picked; null when it carries none. */
850
878
  modelLook: EnvironmentLook | null;
851
879
  fog: SceneFog;
880
+ /** Omitted by hosts without volumetric lighting support. */
881
+ volumetricLighting?: SceneVolumetricLighting;
882
+ /** Optimize unshadowed point lights on supported renderers; omitted by older hosts. */
883
+ clusteredLighting?: boolean;
852
884
  effects: SceneEffects;
853
885
  /**
854
886
  * Built-in effects added as layers, in registry order — one keeps its row and tuning while
@@ -908,7 +940,7 @@ export interface ScenePatch {
908
940
  * App-level features a client gates on (never version-sniff): `hello` and
909
941
  * `app.info` report them — the per-app mirror of {@link InstanceRuntime.capabilities}.
910
942
  */
911
- export declare const APP_CAPABILITIES: readonly ["storage", "speech", "automations", "controllers", "model-editing", "asset-inspection", "layer-effects", "scene-transitions"];
943
+ export declare const APP_CAPABILITIES: readonly ["storage", "speech", "automations", "controllers", "model-editing", "asset-inspection", "layer-effects", "scene-transitions", "area-lights", "spot-lights", "shadow-filters"];
912
944
  export type AppCapability = (typeof APP_CAPABILITIES)[number];
913
945
  export declare function isAppCapability(v: unknown): v is AppCapability;
914
946
  /** What a loaded model instance can do; absent capabilities answer `unsupported-for-format`. */
@@ -18,12 +18,14 @@ export function isAssetKind(v) {
18
18
  }
19
19
  // ---- Objects -------------------------------------------------------------------
20
20
  export const OBJECT_SPACES = ['2d', '3d'];
21
+ export const SCENE_LIGHT_TYPES = ['directional', 'point', 'ambient', 'area', 'spot'];
21
22
  /**
22
23
  * Per-light shadow tier. `off` is the old `castShadow: false`; the rest raise
23
24
  * shadow-map resolution. A point light pays 6 cube faces for the same tier a
24
25
  * directional light covers with one map, so it gets the smaller of the pair.
25
26
  */
26
27
  export const SHADOW_QUALITY_LEVELS = ['off', 'low', 'medium', 'high', 'ultra'];
28
+ export const SHADOW_FILTERS = ['pcf', 'pcss'];
27
29
  // ---- Runtime state -------------------------------------------------------------
28
30
  /**
29
31
  * App-level features a client gates on (never version-sniff): `hello` and
@@ -38,6 +40,9 @@ export const APP_CAPABILITIES = [
38
40
  'asset-inspection',
39
41
  'layer-effects',
40
42
  'scene-transitions',
43
+ 'area-lights',
44
+ 'spot-lights',
45
+ 'shadow-filters',
41
46
  ];
42
47
  export function isAppCapability(v) {
43
48
  return isOneOf(v, APP_CAPABILITIES);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@laplace.live/persona-sdk",
3
- "version": "1.8.1",
3
+ "version": "1.9.0",
4
4
  "description": "TypeScript SDK and wire schema for the LAPLACE Persona plugin API",
5
5
  "license": "MIT",
6
6
  "type": "module",