@vgai/engine 0.5.41 → 0.5.44

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.
Files changed (129) hide show
  1. package/dist/adapter/adapter-module.d.ts +28 -29
  2. package/dist/adapter/adapter-module.d.ts.map +1 -1
  3. package/dist/adapter/adapter-module.js +11 -34
  4. package/dist/adapter/authoring-seam-contract.d.ts +10 -0
  5. package/dist/adapter/authoring-seam-contract.d.ts.map +1 -1
  6. package/dist/adapter/authoring-seam-contract.js +2 -0
  7. package/dist/adapter/authoring.d.ts +31 -0
  8. package/dist/adapter/authoring.d.ts.map +1 -1
  9. package/dist/adapter/finders/finder-result.d.ts +2 -2
  10. package/dist/adapter/finders/finder-result.d.ts.map +1 -1
  11. package/dist/adapter/finders/index.d.ts +15 -2
  12. package/dist/adapter/finders/index.d.ts.map +1 -1
  13. package/dist/adapter/finders/index.js +38 -10
  14. package/dist/adapter/finders/prefabs-from-stories.d.ts.map +1 -1
  15. package/dist/adapter/finders/registry.d.ts +45 -0
  16. package/dist/adapter/finders/registry.d.ts.map +1 -0
  17. package/dist/adapter/finders/registry.js +56 -0
  18. package/dist/adapter/ingest/scene-capture.d.ts.map +1 -1
  19. package/dist/adapter/ingest/scene-capture.js +50 -20
  20. package/dist/adapter/system-adapter.d.ts +3 -1
  21. package/dist/adapter/system-adapter.d.ts.map +1 -1
  22. package/dist/canvas-react/pixi-react-root-factory.d.ts.map +1 -1
  23. package/dist/canvas-react/pixi-react-root-factory.js +21 -11
  24. package/dist/data/vite-plugin-data.d.ts +1 -1
  25. package/dist/data/vite-plugin-data.d.ts.map +1 -1
  26. package/dist/data/vite-plugin-data.js +2 -2
  27. package/dist/ecs/user-data.d.ts +6 -0
  28. package/dist/ecs/user-data.d.ts.map +1 -1
  29. package/dist/ecs/user-data.js +4 -0
  30. package/dist/index.d.ts +1 -1
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +1 -1
  33. package/dist/input/host-pointer.d.ts +18 -0
  34. package/dist/input/host-pointer.d.ts.map +1 -1
  35. package/dist/input/host-pointer.js +54 -5
  36. package/dist/input/input-manager.d.ts +83 -1
  37. package/dist/input/input-manager.d.ts.map +1 -1
  38. package/dist/input/input-manager.js +357 -8
  39. package/dist/manifest/configuration-kinds.d.ts +97 -0
  40. package/dist/manifest/configuration-kinds.d.ts.map +1 -0
  41. package/dist/manifest/configuration-kinds.js +148 -0
  42. package/dist/manifest/index.d.ts +2 -1
  43. package/dist/manifest/index.d.ts.map +1 -1
  44. package/dist/manifest/index.js +4 -3
  45. package/dist/manifest/kind-modules.d.ts +18 -0
  46. package/dist/manifest/kind-modules.d.ts.map +1 -0
  47. package/dist/manifest/kind-modules.js +58 -0
  48. package/dist/manifest/load-file.d.ts +3 -3
  49. package/dist/manifest/load-file.d.ts.map +1 -1
  50. package/dist/manifest/load-file.js +4 -4
  51. package/dist/manifest/load.d.ts +17 -12
  52. package/dist/manifest/load.d.ts.map +1 -1
  53. package/dist/manifest/load.js +29 -19
  54. package/dist/manifest/runtime-environment.d.ts +11 -0
  55. package/dist/manifest/runtime-environment.d.ts.map +1 -0
  56. package/dist/manifest/runtime-environment.js +44 -0
  57. package/dist/manifest/schema.d.ts +10 -9
  58. package/dist/manifest/schema.d.ts.map +1 -1
  59. package/dist/manifest/schema.js +32 -27
  60. package/dist/react/use-data.d.ts +1 -1
  61. package/dist/react/use-data.js +1 -1
  62. package/dist/render/environment-capture.d.ts +10 -1
  63. package/dist/render/environment-capture.d.ts.map +1 -1
  64. package/dist/render/environment-capture.js +25 -0
  65. package/dist/render/ibl-override-material.d.ts +11 -2
  66. package/dist/render/ibl-override-material.d.ts.map +1 -1
  67. package/dist/render/ibl-override-material.js +16 -2
  68. package/dist/render/matcap-texture.d.ts +5 -0
  69. package/dist/render/matcap-texture.d.ts.map +1 -0
  70. package/dist/render/matcap-texture.js +82 -0
  71. package/dist/render/soft-particle-depth.d.ts +14 -8
  72. package/dist/render/soft-particle-depth.d.ts.map +1 -1
  73. package/dist/render/soft-particle-depth.js +37 -14
  74. package/dist/render/viewport-shading.d.ts +11 -1
  75. package/dist/render/viewport-shading.d.ts.map +1 -1
  76. package/dist/render/viewport-shading.js +25 -0
  77. package/dist/runtime/create-runtime.js +6 -5
  78. package/dist/runtime/debug-bridge.d.ts.map +1 -1
  79. package/dist/runtime/debug-bridge.js +17 -3
  80. package/dist/runtime/mount-manifest.d.ts.map +1 -1
  81. package/dist/runtime/mount-manifest.js +11 -2
  82. package/dist/settings/schema.d.ts +99 -0
  83. package/dist/settings/schema.d.ts.map +1 -0
  84. package/dist/settings/schema.js +110 -0
  85. package/dist/world3d-react/r3f-root-factory.d.ts.map +1 -1
  86. package/dist/world3d-react/r3f-root-factory.js +34 -36
  87. package/dist/world3d-react/rapier-physics-bridge.d.ts.map +1 -1
  88. package/dist/world3d-react/rapier-physics-bridge.js +21 -6
  89. package/dist-config/config.js +1 -1
  90. package/dist-config/config.js.map +2 -2
  91. package/package.json +2 -1
  92. package/schemas/engine-capabilities.json +4 -4
  93. package/schemas/vgai-project.schema.json +41 -36
  94. package/schemas/vgai-settings.schema.json +112 -0
  95. package/src/adapter/adapter-module.ts +33 -51
  96. package/src/adapter/authoring-seam-contract.ts +2 -0
  97. package/src/adapter/authoring.ts +31 -0
  98. package/src/adapter/finders/finder-result.ts +2 -2
  99. package/src/adapter/finders/index.ts +68 -12
  100. package/src/adapter/finders/prefabs-from-stories.ts +2 -2
  101. package/src/adapter/finders/registry.ts +95 -0
  102. package/src/adapter/finders/scenes-from-entrypoint-selection.ts +4 -4
  103. package/src/adapter/ingest/scene-capture.ts +48 -19
  104. package/src/adapter/system-adapter.ts +3 -1
  105. package/src/canvas-react/pixi-react-root-factory.tsx +21 -11
  106. package/src/data/vite-plugin-data.ts +4 -2
  107. package/src/ecs/user-data.ts +6 -0
  108. package/src/index.ts +1 -1
  109. package/src/input/host-pointer.ts +84 -5
  110. package/src/input/input-manager.ts +404 -7
  111. package/src/manifest/configuration-kinds.ts +241 -0
  112. package/src/manifest/index.ts +4 -3
  113. package/src/manifest/kind-modules.ts +58 -0
  114. package/src/manifest/load-file.ts +11 -5
  115. package/src/manifest/load.ts +53 -22
  116. package/src/manifest/runtime-environment.ts +48 -0
  117. package/src/manifest/schema.ts +39 -33
  118. package/src/react/use-data.ts +1 -1
  119. package/src/render/environment-capture.ts +30 -1
  120. package/src/render/ibl-override-material.ts +32 -2
  121. package/src/render/matcap-texture.ts +92 -0
  122. package/src/render/soft-particle-depth.ts +43 -14
  123. package/src/render/viewport-shading.ts +33 -1
  124. package/src/runtime/create-runtime.ts +6 -5
  125. package/src/runtime/debug-bridge.ts +21 -3
  126. package/src/runtime/mount-manifest.ts +13 -2
  127. package/src/settings/schema.ts +127 -0
  128. package/src/world3d-react/r3f-root-factory.tsx +45 -39
  129. package/src/world3d-react/rapier-physics-bridge.tsx +22 -8
@@ -1 +1 @@
1
- {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/manifest/schema.ts"],"names":[],"mappings":"AAiBA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;GAGG;AACH,eAAO,MAAM,qBAAqB,EAAG,CAAU,CAAC;AAsFhD,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;qBA0H3B,CAAC;AACJ,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAM5D,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAqFnB,CAAC;AACZ,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAkB5D,eAAO,MAAM,eAAe;;;;;EAMzB,CAAC;AACJ,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAExD,eAAO,MAAM,qBAAqB;;;;EAEa,CAAC;AAChD,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAEpE,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;kBAqC7B,CAAC;AACJ,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAMhE,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBA8TyD,CAAC;AAEzF,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC"}
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/manifest/schema.ts"],"names":[],"mappings":"AAiBA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;GAGG;AACH,eAAO,MAAM,qBAAqB,EAAG,CAAU,CAAC;AAsFhD,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;qBA0H3B,CAAC;AACJ,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAM5D,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAqFnB,CAAC;AACZ,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAkB5D,eAAO,MAAM,eAAe;;;;;EAMzB,CAAC;AACJ,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAExD,eAAO,MAAM,qBAAqB;;;;EAEa,CAAC;AAChD,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAEpE,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;kBAqC7B,CAAC;AACJ,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAchE,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBA4TyD,CAAC;AAEzF,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC"}
@@ -328,6 +328,13 @@ export const LearnMetadataSchema = z
328
328
  // ---------------------------------------------------------------------------
329
329
  // Game manifest (vgai.project.json)
330
330
  // ---------------------------------------------------------------------------
331
+ /** The envelope of one run configuration; the kind's schema takes the rest. */
332
+ const ConfigurationEnvelopeSchema = z
333
+ .object({
334
+ id: z.string().min(1).describe('Configuration id, unique in the manifest'),
335
+ kind: z.string().min(1).describe('A registered run kind'),
336
+ })
337
+ .passthrough();
331
338
  export const GameManifestSchema = z
332
339
  .object({
333
340
  $schema: z
@@ -354,7 +361,6 @@ export const GameManifestSchema = z
354
361
  .describe('Engine version pin'),
355
362
  roots: z
356
363
  .array(AdapterRootSchema)
357
- .min(1)
358
364
  .superRefine((roots, ctx) => {
359
365
  // D-V6 (debt): root ids are described as unique above but that was
360
366
  // describe-only at the SCHEMA level — only load.ts's loader-level
@@ -410,7 +416,7 @@ export const GameManifestSchema = z
410
416
  });
411
417
  }
412
418
  })
413
- .describe("The Game's explicit, non-empty adapter-root composition. There is no implicit root. " +
419
+ .describe("The project's explicit adapter-root composition ZERO OR MORE roots, and no implicit root: a project with no roots mounts nothing (a library of models, a folder of documents) and the editor derives its chrome from what IS declared. " +
414
420
  'Root ids are unique, and a project declares at most ONE world root per medium — ' +
415
421
  'one `three`, one `dom`, one canvas-surface root (first-party canvas, canvas ingest ' +
416
422
  'and canvas module are the same medium).'),
@@ -443,40 +449,39 @@ export const GameManifestSchema = z
443
449
  .strict()
444
450
  .optional()
445
451
  .describe('Editor-only authoring metadata; never runtime ownership'),
446
- server: z
452
+ configurations: z
453
+ .array(ConfigurationEnvelopeSchema)
454
+ .optional()
455
+ .describe("The project's run configurations — its ENTRYPOINTS beyond the host mount (`play`): each an " +
456
+ 'id and a registered kind (`process`, `compound`, and whatever a capability registers) with ' +
457
+ "that kind's own fields. The editor's transport, `vgai run <id>` and a harness start the same declaration."),
458
+ relay: z
447
459
  .object({
448
- room: z.string().describe('Colyseus room name'),
449
- endpoint: z
460
+ signalingUrl: z
450
461
  .string()
451
462
  .optional()
452
- .describe('WebSocket endpoint the editor reports for this room, e.g. "wss://rooms.example.com". ' +
453
- 'Omit to declare the scaffold default, `ws://<the page host>:2567` — the port ' +
454
- "`server/colyseus-setup.ts` listens on. Stated here because the endpoint is the game's " +
455
- 'own deployment fact, not something the editor may guess'),
456
- module: z
463
+ .describe('Managed relay signaling URL to prefer over the client default; omitted when the deploy did not pin one'),
464
+ grant: z
457
465
  .string()
458
466
  .optional()
459
- .describe('Module path for the Colyseus room bootstrap; defaults to the scaffolded server bootstrap'),
460
- relay: z
461
- .object({
462
- signalingUrl: z
463
- .string()
464
- .optional()
465
- .describe('Managed relay signaling URL to prefer over the client default; omitted when the deploy did not pin one'),
466
- accessToken: z
467
- .string()
468
- .describe('Room-scoped managed-relay access token minted at `vgai deploy` and read by the standalone client (main.ts)'),
469
- expiresAt: z
470
- .number()
471
- .describe('Token expiry as epoch milliseconds; the client falls back to a tokenless join once elapsed'),
472
- })
473
- .strict()
467
+ .describe('Room-scoped managed-relay GRANT minted at `vgai deploy`; players exchange it for a fresh token at `sessionUrl`'),
468
+ sessionUrl: z
469
+ .string()
470
+ .optional()
471
+ .describe('Public session endpoint where a deployed client exchanges the grant for a short-lived relay token'),
472
+ accessToken: z
473
+ .string()
474
+ .optional()
475
+ .describe('A baked relay token (older deploys); used directly while unexpired'),
476
+ expiresAt: z
477
+ .number()
474
478
  .optional()
475
- .describe('Deploy-baked managed-relay credentials. NEVER hand-authored in source — written only into the staged manifest by `vgai deploy` and fetched at runtime by the standalone client'),
479
+ .describe('Token expiry as epoch milliseconds; the client falls back to a tokenless join once elapsed'),
476
480
  })
477
481
  .strict()
478
482
  .optional()
479
- .describe('Multiplayer server config (Colyseus), if this game registers a room'),
483
+ .describe('Deploy-baked managed-relay credentials a DEPLOYMENT fact. NEVER hand-authored in source: written only into ' +
484
+ 'the staged manifest by `vgai deploy` and read by the deployed client at connect time.'),
480
485
  resolution: z
481
486
  .object({
482
487
  width: z.number().describe('Canvas width in pixels'),
@@ -48,7 +48,7 @@ export declare function useData<T>(handle: DataHandle<T>): T;
48
48
  * runtime state belongs in your game's own store, never written back into
49
49
  * data files. Outside a running editor there is no `/__editor` server, so
50
50
  * this rejects with a teaching error (a shipped game could never reach the
51
- * route anyway — builds strip `src/tools/` entirely, §4).
51
+ * route anyway — builds strip `src/contributions/` and `src/tools/` entirely, §4).
52
52
  *
53
53
  * The body is PARSED VALUES, never serialized text: the dev server's fold
54
54
  * (`packages/editor/server/data-file-serialize.ts`) owns key order and the
@@ -50,7 +50,7 @@ export function useData(handle) {
50
50
  * runtime state belongs in your game's own store, never written back into
51
51
  * data files. Outside a running editor there is no `/__editor` server, so
52
52
  * this rejects with a teaching error (a shipped game could never reach the
53
- * route anyway — builds strip `src/tools/` entirely, §4).
53
+ * route anyway — builds strip `src/contributions/` and `src/tools/` entirely, §4).
54
54
  *
55
55
  * The body is PARSED VALUES, never serialized text: the dev server's fold
56
56
  * (`packages/editor/server/data-file-serialize.ts`) owns key order and the
@@ -10,7 +10,7 @@
10
10
  * game altered.
11
11
  *
12
12
  * Consumers today: the `reflections` capability's native probe system, and
13
- * `godot-compat`'s Godot 3 `ReflectionProbe`.
13
+ * `godot-compat`'s versioned Godot `ReflectionProbe` binding.
14
14
  */
15
15
  import type { Scene, WebGLRenderer } from 'three';
16
16
  /**
@@ -56,6 +56,15 @@ export declare function withCaptureMask(scene: Scene, mask: number, capture: ()
56
56
  * cannot outlive the `finally` that restores it.
57
57
  */
58
58
  export declare function withCaptureShadows(scene: Scene, enabled: boolean, capture: () => void): void;
59
+ /**
60
+ * Include or suppress the scene's global sky/environment for one cube capture.
61
+ *
62
+ * This is renderer plumbing rather than a Godot policy: the caller decides whether the capture is
63
+ * interior. Clearing both properties is necessary because Three uses `background` for the cube's
64
+ * visible pixels and `environment` for the materials being photographed. Restoring only one makes
65
+ * a nominally isolated capture retain half of the outside lighting.
66
+ */
67
+ export declare function withCaptureSceneEnvironment(scene: Scene, enabled: boolean, capture: () => void): void;
59
68
  /**
60
69
  * Whether this renderer can run an environment capture at all.
61
70
  *
@@ -1 +1 @@
1
- {"version":3,"file":"environment-capture.d.ts","sourceRoot":"","sources":["../../src/render/environment-capture.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAmB,KAAK,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAEnE;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,IAAI,GAAG,IAAI,CAYrF;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,IAAI,GAAG,IAAI,CAiB5F;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,aAAa,GAAG,OAAO,CAGtE"}
1
+ {"version":3,"file":"environment-capture.d.ts","sourceRoot":"","sources":["../../src/render/environment-capture.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAmB,KAAK,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAEnE;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,IAAI,GAAG,IAAI,CAYrF;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,IAAI,GAAG,IAAI,CAiB5F;AAED;;;;;;;GAOG;AACH,wBAAgB,2BAA2B,CACzC,KAAK,EAAE,KAAK,EACZ,OAAO,EAAE,OAAO,EAChB,OAAO,EAAE,MAAM,IAAI,GAClB,IAAI,CAeN;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,aAAa,GAAG,OAAO,CAGtE"}
@@ -76,6 +76,31 @@ export function withCaptureShadows(scene, enabled, capture) {
76
76
  entry.shadow.intensity = entry.intensity;
77
77
  }
78
78
  }
79
+ /**
80
+ * Include or suppress the scene's global sky/environment for one cube capture.
81
+ *
82
+ * This is renderer plumbing rather than a Godot policy: the caller decides whether the capture is
83
+ * interior. Clearing both properties is necessary because Three uses `background` for the cube's
84
+ * visible pixels and `environment` for the materials being photographed. Restoring only one makes
85
+ * a nominally isolated capture retain half of the outside lighting.
86
+ */
87
+ export function withCaptureSceneEnvironment(scene, enabled, capture) {
88
+ if (enabled) {
89
+ capture();
90
+ return;
91
+ }
92
+ const background = scene.background;
93
+ const environment = scene.environment;
94
+ scene.background = null;
95
+ scene.environment = null;
96
+ try {
97
+ capture();
98
+ }
99
+ finally {
100
+ scene.background = background;
101
+ scene.environment = environment;
102
+ }
103
+ }
79
104
  /**
80
105
  * Whether this renderer can run an environment capture at all.
81
106
  *
@@ -24,10 +24,10 @@
24
24
  * hands back three's own shader object and gets out of the way.
25
25
  *
26
26
  * Consumers today: the `reflections` capability's native probe system, and
27
- * `godot-compat`'s Godot 3 `ReflectionProbe`, whose per-fragment models are
27
+ * `godot-compat`'s Godot `ReflectionProbe`, whose per-fragment models are
28
28
  * deliberately different renderers over this one splice.
29
29
  */
30
- import { type Mesh, type MeshStandardMaterial } from 'three';
30
+ import { type Mesh, type MeshStandardMaterial, type Texture } from 'three';
31
31
  /** The shader object three hands to `onBeforeCompile`. */
32
32
  export type IblOverrideShader = Parameters<MeshStandardMaterial['onBeforeCompile']>[0];
33
33
  /**
@@ -53,6 +53,15 @@ export declare function baseIblFunctions(): string;
53
53
  * intent: both compile the physical IBL chunk this module replaces.
54
54
  */
55
55
  export declare function standardMaterialsOf(mesh: Mesh): readonly MeshStandardMaterial[];
56
+ /**
57
+ * A black native texture whose only job is to compile Three's IBL branch.
58
+ *
59
+ * Local reflection systems can light a scene from captured probes when neither
60
+ * `scene.environment` nor a material `envMap` exists. Three removes both IBL
61
+ * entry points from that program unless one native input is present, so the
62
+ * caller temporarily assigns this neutral texture and restores what it found.
63
+ */
64
+ export declare function createIblSentinelTexture(): Texture;
56
65
  export interface IblOverrideOptions {
57
66
  /**
58
67
  * The GLSL that replaces `#include <envmap_physical_pars_fragment>`. Read at
@@ -1 +1 @@
1
- {"version":3,"file":"ibl-override-material.d.ts","sourceRoot":"","sources":["../../src/render/ibl-override-material.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,OAAO,EAAE,KAAK,IAAI,EAAE,KAAK,oBAAoB,EAAe,MAAM,OAAO,CAAC;AAE1E,0DAA0D;AAC1D,MAAM,MAAM,iBAAiB,GAAG,UAAU,CAAC,oBAAoB,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAEvF;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB,yBAAyB,CAAC;AAEzD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,CAIzC;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,IAAI,GAAG,SAAS,oBAAoB,EAAE,CAK/E;AAED,MAAM,WAAW,kBAAkB;IACjC;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,MAAM,CAAC;IAChC;;;;;OAKG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,MAAM,CAAC;IAChC;;;;OAIG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,iBAAiB,KAAK,IAAI,CAAC;CAC1D;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,QAAQ,EAAE,oBAAoB,CAAC;IACxC;;;;OAIG;IACH,QAAQ,CAAC,MAAM,EAAE,iBAAiB,GAAG,IAAI,CAAC;IAC1C,6EAA6E;IAC7E,OAAO,IAAI,IAAI,CAAC;CACjB;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,oBAAoB,EAC9B,OAAO,EAAE,kBAAkB,GAC1B,WAAW,CAsCb"}
1
+ {"version":3,"file":"ibl-override-material.d.ts","sourceRoot":"","sources":["../../src/render/ibl-override-material.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,OAAO,EAGL,KAAK,IAAI,EACT,KAAK,oBAAoB,EAGzB,KAAK,OAAO,EAEb,MAAM,OAAO,CAAC;AAEf,0DAA0D;AAC1D,MAAM,MAAM,iBAAiB,GAAG,UAAU,CAAC,oBAAoB,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAEvF;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB,yBAAyB,CAAC;AAEzD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,CAIzC;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,IAAI,GAAG,SAAS,oBAAoB,EAAE,CAK/E;AAED;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,IAAI,OAAO,CAWlD;AAED,MAAM,WAAW,kBAAkB;IACjC;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,MAAM,CAAC;IAChC;;;;;OAKG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,MAAM,CAAC;IAChC;;;;OAIG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,iBAAiB,KAAK,IAAI,CAAC;CAC1D;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,QAAQ,EAAE,oBAAoB,CAAC;IACxC;;;;OAIG;IACH,QAAQ,CAAC,MAAM,EAAE,iBAAiB,GAAG,IAAI,CAAC;IAC1C,6EAA6E;IAC7E,OAAO,IAAI,IAAI,CAAC;CACjB;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,oBAAoB,EAC9B,OAAO,EAAE,kBAAkB,GAC1B,WAAW,CAsCb"}
@@ -24,10 +24,10 @@
24
24
  * hands back three's own shader object and gets out of the way.
25
25
  *
26
26
  * Consumers today: the `reflections` capability's native probe system, and
27
- * `godot-compat`'s Godot 3 `ReflectionProbe`, whose per-fragment models are
27
+ * `godot-compat`'s Godot `ReflectionProbe`, whose per-fragment models are
28
28
  * deliberately different renderers over this one splice.
29
29
  */
30
- import { ShaderChunk } from 'three';
30
+ import { CubeUVReflectionMapping, DataTexture, RGBAFormat, ShaderChunk, UnsignedByteType, } from 'three';
31
31
  /**
32
32
  * The `varying vec3` the vertex splice publishes and the fragment body reads.
33
33
  *
@@ -58,6 +58,20 @@ export function standardMaterialsOf(mesh) {
58
58
  const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
59
59
  return materials.filter((material) => Boolean(material.isMeshStandardMaterial));
60
60
  }
61
+ /**
62
+ * A black native texture whose only job is to compile Three's IBL branch.
63
+ *
64
+ * Local reflection systems can light a scene from captured probes when neither
65
+ * `scene.environment` nor a material `envMap` exists. Three removes both IBL
66
+ * entry points from that program unless one native input is present, so the
67
+ * caller temporarily assigns this neutral texture and restores what it found.
68
+ */
69
+ export function createIblSentinelTexture() {
70
+ const texture = new DataTexture(new Uint8Array(16 * 16 * 4), 16, 16, RGBAFormat, UnsignedByteType);
71
+ texture.mapping = CubeUVReflectionMapping;
72
+ texture.needsUpdate = true;
73
+ return texture;
74
+ }
61
75
  /**
62
76
  * Install the override. The returned handle is the only way to reach the
63
77
  * compiled shader or to undo the patch; the material is otherwise untouched
@@ -0,0 +1,5 @@
1
+ import * as THREE from 'three';
2
+ export declare function drawNeutralMatcap(size?: number): HTMLCanvasElement;
3
+ /** The drawn sphere as a ready-to-sample texture. Callers own disposal. */
4
+ export declare function createNeutralMatcapTexture(size?: number): THREE.CanvasTexture;
5
+ //# sourceMappingURL=matcap-texture.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"matcap-texture.d.ts","sourceRoot":"","sources":["../../src/render/matcap-texture.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AA8B/B,wBAAgB,iBAAiB,CAAC,IAAI,SAAO,GAAG,iBAAiB,CAkDhE;AAED,2EAA2E;AAC3E,wBAAgB,0BAA0B,CAAC,IAAI,SAAO,GAAG,KAAK,CAAC,aAAa,CAQ3E"}
@@ -0,0 +1,82 @@
1
+ import * as THREE from 'three';
2
+ /**
3
+ * The neutral matcap sphere, DRAWN rather than shipped.
4
+ *
5
+ * A matcap is just a picture of a lit sphere sampled by view-space normal, so
6
+ * there is nothing to vendor: the same three numbers a shader would use
7
+ * (key, fill, rim) evaluated once per texel produce the image. Keeping it
8
+ * procedural means the diagnostic look has no binary asset, no license, no
9
+ * provenance row, and no way to go missing from a build.
10
+ *
11
+ * The look is deliberately CLAY: one warm key from the upper left, a cool
12
+ * fill from the lower right, a tight rim, and a broad soft highlight — the
13
+ * neutral sculpting material whose whole job is to let form read without
14
+ * colour, texture or authored lighting getting a vote.
15
+ */
16
+ const SIZE = 256;
17
+ /** sRGB transfer curve — the canvas holds display-referred bytes. */
18
+ function encode(value) {
19
+ const c = Math.min(1, Math.max(0, value));
20
+ return c <= 0.0031308 ? c * 12.92 : 1.055 * c ** (1 / 2.4) - 0.055;
21
+ }
22
+ function normalize(x, y, z) {
23
+ const length = Math.hypot(x, y, z) || 1;
24
+ return [x / length, y / length, z / length];
25
+ }
26
+ export function drawNeutralMatcap(size = SIZE) {
27
+ const canvas = document.createElement('canvas');
28
+ canvas.width = size;
29
+ canvas.height = size;
30
+ const context = canvas.getContext('2d');
31
+ if (!context)
32
+ return canvas;
33
+ const image = context.createImageData(size, size);
34
+ const key = normalize(-0.45, 0.62, 0.64);
35
+ const fill = normalize(0.65, -0.35, 0.5);
36
+ // View direction is +Z for a matcap: the sphere is drawn facing the camera.
37
+ const half = normalize(key[0], key[1], key[2] + 1);
38
+ const base = [0.7, 0.69, 0.68];
39
+ const fillColor = [0.34, 0.4, 0.52];
40
+ const rimColor = [0.85, 0.88, 1];
41
+ for (let py = 0; py < size; py++) {
42
+ for (let px = 0; px < size; px++) {
43
+ const nx = (px + 0.5) / size / 0.5 - 1;
44
+ const ny = 1 - (py + 0.5) / size / 0.5;
45
+ const r2 = nx * nx + ny * ny;
46
+ // Outside the unit disc a matcap is never sampled by a facing surface,
47
+ // but bilinear filtering reaches one texel past the silhouette. Clamp
48
+ // to the rim normal so the edge stays the rim colour instead of
49
+ // bleeding whatever happened to be there.
50
+ const clamped = r2 > 1;
51
+ const scale = clamped ? 1 / Math.sqrt(r2) : 1;
52
+ const x = nx * scale;
53
+ const y = ny * scale;
54
+ const z = clamped ? 0 : Math.sqrt(Math.max(0, 1 - r2));
55
+ const diffuse = Math.max(0, x * key[0] + y * key[1] + z * key[2]);
56
+ const fillTerm = Math.max(0, x * fill[0] + y * fill[1] + z * fill[2]);
57
+ const specular = Math.max(0, x * half[0] + y * half[1] + z * half[2]) ** 42 * 0.5;
58
+ const rim = (1 - z) ** 3.2 * 0.4;
59
+ const offset = (py * size + px) * 4;
60
+ for (let channel = 0; channel < 3; channel++) {
61
+ const lit = base[channel] * (0.16 + 0.8 * diffuse) +
62
+ fillColor[channel] * 0.3 * fillTerm +
63
+ rimColor[channel] * rim +
64
+ specular;
65
+ image.data[offset + channel] = Math.round(encode(lit) * 255);
66
+ }
67
+ image.data[offset + 3] = 255;
68
+ }
69
+ }
70
+ context.putImageData(image, 0, 0);
71
+ return canvas;
72
+ }
73
+ /** The drawn sphere as a ready-to-sample texture. Callers own disposal. */
74
+ export function createNeutralMatcapTexture(size = SIZE) {
75
+ const texture = new THREE.CanvasTexture(drawNeutralMatcap(size));
76
+ texture.colorSpace = THREE.SRGBColorSpace;
77
+ texture.minFilter = THREE.LinearFilter;
78
+ texture.magFilter = THREE.LinearFilter;
79
+ texture.generateMipmaps = false;
80
+ texture.needsUpdate = true;
81
+ return texture;
82
+ }
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Scene DEPTH for three.quarks' soft particles the pass that makes
3
- * `ParticlesDescriptor.softParticles` a real effect instead of a footgun.
2
+ * Opaque scene DEPTH for native consumers, including three.quarks soft particles and translated
3
+ * Godot `hint_depth_texture` shader uniforms.
4
4
  *
5
5
  * ## What three.quarks needs, and what it does when nobody supplies it
6
6
  *
@@ -61,12 +61,10 @@
61
61
  * with the armed particle renderers and every transparent surface hidden and a
62
62
  * colour-write-disabled override material in place.
63
63
  *
64
- * That cost is paid ONLY while something is armed. {@link armSoftParticleDepth}
65
- * is called from `particles-factory.ts`'s `registerParticleSystem` when the
66
- * system it is registering asked for `softParticles`; with the registry empty
67
- * {@link SoftParticleDepthPass.render} returns before it touches the scene, so a
68
- * world with no soft particles pays one `Set.size` read per frame and nothing
69
- * else. Armed, per frame per camera it costs: one scene traverse (to find the
64
+ * That cost is paid ONLY while something is armed. Soft particles enter through
65
+ * {@link armSoftParticleDepth}; translated depth-texture shader programs enter through
66
+ * {@link armSceneDepthTexture}. With both registries empty {@link SoftParticleDepthPass.render}
67
+ * returns before it touches the scene. Armed, per frame per camera it costs: one scene traverse (to find the
70
68
  * transparent surfaces to hide), one extra geometry-only scene draw with shadow
71
69
  * updates suppressed, and a drawing-buffer-sized depth target
72
70
  * (`UnsignedIntType` depth + an unread RGBA8 colour attachment ≈ 8
@@ -97,6 +95,10 @@
97
95
  */
98
96
  import * as THREE from 'three';
99
97
  import type { BatchedRenderer } from 'three.quarks';
98
+ /** Any native material/program that samples Godot's opaque-pass depth texture. */
99
+ export interface SceneDepthTextureConsumer {
100
+ setDepthTexture(texture: THREE.Texture): void;
101
+ }
100
102
  /**
101
103
  * A 1x1 texture whose red channel is 1.0 — "the depth buffer says nothing is in
102
104
  * front of anything".
@@ -120,6 +122,10 @@ export declare function armSoftParticleDepth(renderer: BatchedRenderer): void;
120
122
  * depth nobody is refreshing.
121
123
  */
122
124
  export declare function disarmSoftParticleDepth(renderer: BatchedRenderer): void;
125
+ /** Retain one consumer of the same opaque scene-depth image Godot exposes as hint_depth_texture. */
126
+ export declare function armSceneDepthTexture(consumer: SceneDepthTextureConsumer): void;
127
+ /** Release one retained Godot depth-texture consumer without leaving a disposed target bound. */
128
+ export declare function disarmSceneDepthTexture(consumer: SceneDepthTextureConsumer): void;
123
129
  /** Test/diagnostic read of the registry — how many renderers a pass would consider. */
124
130
  export declare function armedSoftParticleRendererCount(): number;
125
131
  /** The subset of `THREE.WebGLRenderer` this pass drives, so a host can supply its own. */
@@ -1 +1 @@
1
- {"version":3,"file":"soft-particle-depth.d.ts","sourceRoot":"","sources":["../../src/render/soft-particle-depth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgGG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAepD;;;;;;;;;;;;GAYG;AACH,wBAAgB,eAAe,IAAI,KAAK,CAAC,WAAW,CAOnD;AAED,8FAA8F;AAC9F,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,eAAe,GAAG,IAAI,CAEpE;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,eAAe,GAAG,IAAI,CAQvE;AAED,uFAAuF;AACvF,wBAAgB,8BAA8B,IAAI,MAAM,CAEvD;AAED,0FAA0F;AAC1F,MAAM,WAAW,yBAAyB;IACxC,oBAAoB,CAAC,MAAM,EAAE,KAAK,CAAC,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC;IAC3D,eAAe,IAAI,KAAK,CAAC,iBAAiB,GAAG,IAAI,CAAC;IAClD,eAAe,CAAC,MAAM,EAAE,KAAK,CAAC,iBAAiB,GAAG,IAAI,GAAG,IAAI,CAAC;IAC9D,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC;IAC1D,kGAAkG;IAClG,QAAQ,CAAC,SAAS,CAAC,EAAE;QAAE,UAAU,EAAE,OAAO,CAAC;QAAC,WAAW,EAAE,OAAO,CAAA;KAAE,CAAC;CACpE;AAED,MAAM,WAAW,qBAAqB;IACpC;;;;;;OAMG;IACH,MAAM,CAAC,QAAQ,EAAE,yBAAyB,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC;IAC5F,OAAO,IAAI,IAAI,CAAC;CACjB;AA0CD,wBAAgB,2BAA2B,IAAI,qBAAqB,CA0GnE"}
1
+ {"version":3,"file":"soft-particle-depth.d.ts","sourceRoot":"","sources":["../../src/render/soft-particle-depth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8FG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAapD,kFAAkF;AAClF,MAAM,WAAW,yBAAyB;IACxC,eAAe,CAAC,OAAO,EAAE,KAAK,CAAC,OAAO,GAAG,IAAI,CAAC;CAC/C;AAMD;;;;;;;;;;;;GAYG;AACH,wBAAgB,eAAe,IAAI,KAAK,CAAC,WAAW,CAOnD;AAED,8FAA8F;AAC9F,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,eAAe,GAAG,IAAI,CAEpE;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,eAAe,GAAG,IAAI,CAQvE;AAED,oGAAoG;AACpG,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,yBAAyB,GAAG,IAAI,CAG9E;AAED,iGAAiG;AACjG,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,yBAAyB,GAAG,IAAI,CAQjF;AAED,uFAAuF;AACvF,wBAAgB,8BAA8B,IAAI,MAAM,CAEvD;AAED,0FAA0F;AAC1F,MAAM,WAAW,yBAAyB;IACxC,oBAAoB,CAAC,MAAM,EAAE,KAAK,CAAC,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC;IAC3D,eAAe,IAAI,KAAK,CAAC,iBAAiB,GAAG,IAAI,CAAC;IAClD,eAAe,CAAC,MAAM,EAAE,KAAK,CAAC,iBAAiB,GAAG,IAAI,GAAG,IAAI,CAAC;IAC9D,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC;IAC1D,kGAAkG;IAClG,QAAQ,CAAC,SAAS,CAAC,EAAE;QAAE,UAAU,EAAE,OAAO,CAAC;QAAC,WAAW,EAAE,OAAO,CAAA;KAAE,CAAC;CACpE;AAED,MAAM,WAAW,qBAAqB;IACpC;;;;;;OAMG;IACH,MAAM,CAAC,QAAQ,EAAE,yBAAyB,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC;IAC5F,OAAO,IAAI,IAAI,CAAC;CACjB;AA0CD,wBAAgB,2BAA2B,IAAI,qBAAqB,CAiHnE"}
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Scene DEPTH for three.quarks' soft particles the pass that makes
3
- * `ParticlesDescriptor.softParticles` a real effect instead of a footgun.
2
+ * Opaque scene DEPTH for native consumers, including three.quarks soft particles and translated
3
+ * Godot `hint_depth_texture` shader uniforms.
4
4
  *
5
5
  * ## What three.quarks needs, and what it does when nobody supplies it
6
6
  *
@@ -61,12 +61,10 @@
61
61
  * with the armed particle renderers and every transparent surface hidden and a
62
62
  * colour-write-disabled override material in place.
63
63
  *
64
- * That cost is paid ONLY while something is armed. {@link armSoftParticleDepth}
65
- * is called from `particles-factory.ts`'s `registerParticleSystem` when the
66
- * system it is registering asked for `softParticles`; with the registry empty
67
- * {@link SoftParticleDepthPass.render} returns before it touches the scene, so a
68
- * world with no soft particles pays one `Set.size` read per frame and nothing
69
- * else. Armed, per frame per camera it costs: one scene traverse (to find the
64
+ * That cost is paid ONLY while something is armed. Soft particles enter through
65
+ * {@link armSoftParticleDepth}; translated depth-texture shader programs enter through
66
+ * {@link armSceneDepthTexture}. With both registries empty {@link SoftParticleDepthPass.render}
67
+ * returns before it touches the scene. Armed, per frame per camera it costs: one scene traverse (to find the
70
68
  * transparent surfaces to hide), one extra geometry-only scene draw with shadow
71
69
  * updates suppressed, and a drawing-buffer-sized depth target
72
70
  * (`UnsignedIntType` depth + an unread RGBA8 colour attachment ≈ 8
@@ -106,6 +104,7 @@ import * as THREE from 'three';
106
104
  * renderers actually parented under the scene it is rendering.
107
105
  */
108
106
  const armed = new Map();
107
+ const depthTextureConsumers = new Map();
109
108
  let farDepth;
110
109
  /**
111
110
  * A 1x1 texture whose red channel is 1.0 — "the depth buffer says nothing is in
@@ -148,6 +147,22 @@ export function disarmSoftParticleDepth(renderer) {
148
147
  return;
149
148
  renderer.setDepthTexture(farDepthTexture());
150
149
  }
150
+ /** Retain one consumer of the same opaque scene-depth image Godot exposes as hint_depth_texture. */
151
+ export function armSceneDepthTexture(consumer) {
152
+ depthTextureConsumers.set(consumer, (depthTextureConsumers.get(consumer) ?? 0) + 1);
153
+ consumer.setDepthTexture(farDepthTexture());
154
+ }
155
+ /** Release one retained Godot depth-texture consumer without leaving a disposed target bound. */
156
+ export function disarmSceneDepthTexture(consumer) {
157
+ const remaining = (depthTextureConsumers.get(consumer) ?? 0) - 1;
158
+ if (remaining > 0) {
159
+ depthTextureConsumers.set(consumer, remaining);
160
+ return;
161
+ }
162
+ if (!depthTextureConsumers.delete(consumer))
163
+ return;
164
+ consumer.setDepthTexture(farDepthTexture());
165
+ }
151
166
  /** Test/diagnostic read of the registry — how many renderers a pass would consider. */
152
167
  export function armedSoftParticleRendererCount() {
153
168
  return armed.size;
@@ -210,14 +225,14 @@ export function createSoftParticleDepthPass() {
210
225
  };
211
226
  return {
212
227
  render(renderer, scene, camera) {
213
- if (armed.size === 0)
228
+ if (armed.size === 0 && depthTextureConsumers.size === 0)
214
229
  return;
215
230
  const consumers = [];
216
231
  for (const candidate of armed.keys()) {
217
232
  if (isUnder(candidate, scene))
218
233
  consumers.push(candidate);
219
234
  }
220
- if (consumers.length === 0)
235
+ if (consumers.length === 0 && depthTextureConsumers.size === 0)
221
236
  return;
222
237
  // See REQUIRED_RENDERER_MEMBERS: a renderer that cannot swap render
223
238
  // targets cannot run a prepass, and the frame is worth more than the
@@ -227,9 +242,9 @@ export function createSoftParticleDepthPass() {
227
242
  if (!warnedIncapable) {
228
243
  warnedIncapable = true;
229
244
  // biome-ignore lint/suspicious/noConsole: deliberate loud degrade — the documented alternative to throwing out of the frame
230
- console.warn(`soft-particle depth prepass skipped: this renderer implements none of ${missing.join(', ')}. ` +
231
- `${String(consumers.length)} particle batch(es) keep the inert far-depth default, so they ` +
232
- 'draw unfaded the same pixels as a batch that never declared a proximity fade. A real ' +
245
+ console.warn(`opaque scene-depth prepass skipped: this renderer is missing ${missing.join(', ')}. ` +
246
+ `${String(consumers.length)} particle batch(es) and ${String(depthTextureConsumers.size)} ` +
247
+ 'shader consumer(s) keep the inert far-depth default. A real ' +
233
248
  'WebGLRenderer has these; a design-time settle stand-in does not.');
234
249
  }
235
250
  return;
@@ -284,14 +299,22 @@ export function createSoftParticleDepthPass() {
284
299
  object.visible = true;
285
300
  hidden.length = 0;
286
301
  }
302
+ const renderedDepth = rt.depthTexture;
303
+ if (renderedDepth === null) {
304
+ throw new Error('opaque scene-depth prepass lost its authored depth texture.');
305
+ }
287
306
  for (const consumer of consumers)
288
- consumer.setDepthTexture(rt.depthTexture);
307
+ consumer.setDepthTexture(renderedDepth);
308
+ for (const consumer of depthTextureConsumers.keys())
309
+ consumer.setDepthTexture(renderedDepth);
289
310
  },
290
311
  dispose() {
291
312
  // Every consumer this pass fed now points at a texture that is about to
292
313
  // go away. Hand them the inert default back rather than a disposed one.
293
314
  for (const candidate of armed.keys())
294
315
  candidate.setDepthTexture(farDepthTexture());
316
+ for (const consumer of depthTextureConsumers.keys())
317
+ consumer.setDepthTexture(farDepthTexture());
295
318
  target?.dispose();
296
319
  target = null;
297
320
  override.dispose();
@@ -1,6 +1,6 @@
1
1
  import * as THREE from 'three';
2
2
  /** Temporary developer-facing shading modes. These never become scene data. */
3
- export type ViewportShadingMode = 'solid' | 'clay' | 'unlit' | 'wireframe' | 'normals' | 'overdraw';
3
+ export type ViewportShadingMode = 'solid' | 'clay' | 'unlit' | 'wireframe' | 'matcap' | 'normals' | 'overdraw';
4
4
  /**
5
5
  * Applies a render-only material view and restores every native material in a
6
6
  * `finally` block. Gameplay and authoring therefore always observe the real
@@ -9,6 +9,16 @@ export type ViewportShadingMode = 'solid' | 'clay' | 'unlit' | 'wireframe' | 'no
9
9
  export declare class ViewportShadingRenderer {
10
10
  private readonly _derived;
11
11
  private readonly _normals;
12
+ /**
13
+ * The clay/sculpt look. Built on FIRST USE, not at construction: the matcap
14
+ * sphere is drawn into a 2D canvas, and every ViewportShadingRenderer that
15
+ * only ever draws `solid` would otherwise pay for a texture nobody samples.
16
+ * `toneMapped: false` for the same reason the overdraw material sets it —
17
+ * a diagnostic look must read exactly as authored, not as the document's
18
+ * exposure and tone curve happen to grade it.
19
+ */
20
+ private _matcap;
21
+ private _matcapTexture;
12
22
  private readonly _overdraw;
13
23
  render(scene: THREE.Scene, mode: ViewportShadingMode, draw: () => void, include?: (mesh: THREE.Mesh) => boolean): void;
14
24
  dispose(): void;
@@ -1 +1 @@
1
- {"version":3,"file":"viewport-shading.d.ts","sourceRoot":"","sources":["../../src/render/viewport-shading.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,+EAA+E;AAC/E,MAAM,MAAM,mBAAmB,GAAG,OAAO,GAAG,MAAM,GAAG,OAAO,GAAG,WAAW,GAAG,SAAS,GAAG,UAAU,CAAC;AAqBpG;;;;GAIG;AACH,qBAAa,uBAAuB;IAClC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA2C;IACpE,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAkC;IAC3D,OAAO,CAAC,QAAQ,CAAC,SAAS,CASvB;IAEH,MAAM,CACJ,KAAK,EAAE,KAAK,CAAC,KAAK,EAClB,IAAI,EAAE,mBAAmB,EACzB,IAAI,EAAE,MAAM,IAAI,EAChB,OAAO,GAAE,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,KAAK,OAAoB,GAClD,IAAI;IA0BP,OAAO,IAAI,IAAI;IAWf,OAAO,CAAC,YAAY;CA6CrB"}
1
+ {"version":3,"file":"viewport-shading.d.ts","sourceRoot":"","sources":["../../src/render/viewport-shading.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAG/B,+EAA+E;AAC/E,MAAM,MAAM,mBAAmB,GAC3B,OAAO,GACP,MAAM,GACN,OAAO,GACP,WAAW,GACX,QAAQ,GACR,SAAS,GACT,UAAU,CAAC;AAqBf;;;;GAIG;AACH,qBAAa,uBAAuB;IAClC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA2C;IACpE,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAkC;IAC3D;;;;;;;OAOG;IACH,OAAO,CAAC,OAAO,CAAyC;IACxD,OAAO,CAAC,cAAc,CAA8B;IACpD,OAAO,CAAC,QAAQ,CAAC,SAAS,CASvB;IAEH,MAAM,CACJ,KAAK,EAAE,KAAK,CAAC,KAAK,EAClB,IAAI,EAAE,mBAAmB,EACzB,IAAI,EAAE,MAAM,IAAI,EAChB,OAAO,GAAE,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,KAAK,OAAoB,GAClD,IAAI;IA0BP,OAAO,IAAI,IAAI;IAef,OAAO,CAAC,YAAY;CAuDrB"}
@@ -1,4 +1,5 @@
1
1
  import * as THREE from 'three';
2
+ import { createNeutralMatcapTexture } from './matcap-texture';
2
3
  function materialColor(material) {
3
4
  const color = material.color;
4
5
  return color?.clone() ?? new THREE.Color(0xbec7d1);
@@ -14,6 +15,16 @@ function textureOf(material, key) {
14
15
  export class ViewportShadingRenderer {
15
16
  _derived = new Map();
16
17
  _normals = new THREE.MeshNormalMaterial();
18
+ /**
19
+ * The clay/sculpt look. Built on FIRST USE, not at construction: the matcap
20
+ * sphere is drawn into a 2D canvas, and every ViewportShadingRenderer that
21
+ * only ever draws `solid` would otherwise pay for a texture nobody samples.
22
+ * `toneMapped: false` for the same reason the overdraw material sets it —
23
+ * a diagnostic look must read exactly as authored, not as the document's
24
+ * exposure and tone curve happen to grade it.
25
+ */
26
+ _matcap = null;
27
+ _matcapTexture = null;
17
28
  _overdraw = new THREE.MeshBasicMaterial({
18
29
  color: 0xffffff,
19
30
  transparent: true,
@@ -56,12 +67,26 @@ export class ViewportShadingRenderer {
56
67
  this._derived.clear();
57
68
  this._normals.dispose();
58
69
  this._overdraw.dispose();
70
+ this._matcap?.dispose();
71
+ this._matcapTexture?.dispose();
72
+ this._matcap = null;
73
+ this._matcapTexture = null;
59
74
  }
60
75
  _materialFor(material, mode) {
61
76
  if (mode === 'normals')
62
77
  return this._normals;
63
78
  if (mode === 'overdraw')
64
79
  return this._overdraw;
80
+ if (mode === 'matcap') {
81
+ if (!this._matcap) {
82
+ this._matcapTexture = createNeutralMatcapTexture();
83
+ this._matcap = new THREE.MeshMatcapMaterial({
84
+ matcap: this._matcapTexture,
85
+ toneMapped: false,
86
+ });
87
+ }
88
+ return this._matcap;
89
+ }
65
90
  let pair = this._derived.get(material);
66
91
  if (!pair) {
67
92
  const common = {
@@ -112,14 +112,15 @@ export async function createGameRuntime(config) {
112
112
  // Universal root host
113
113
  // ---------------------------------------------------------------------------
114
114
  /** A stand-in `THREE.WebGLRenderer` for headless (`headless:true`) three
115
- * roots: a headless mount never calls a render-phase method on
116
- * `host.renderer` at all, so
117
- * this only needs to satisfy the handful of calls THIS file itself makes
115
+ * roots: draw submission is inert while native scene/component evaluation
116
+ * still runs. This satisfies Fiber's custom-renderer gate and the calls
118
117
  * (`setPixelRatio`/`setClearColor`/`setSize` at mount, `dispose`/
119
118
  * `forceContextLoss` at teardown) — never a real GL call. Node conformance
120
119
  * tests only; never used when `headless` is left `false`/absent. */
121
- function createHeadlessRendererStub() {
120
+ function createHeadlessRendererStub(canvas) {
122
121
  return {
122
+ domElement: canvas,
123
+ render() { },
123
124
  setPixelRatio() { },
124
125
  setClearColor() { },
125
126
  setSize() { },
@@ -174,7 +175,7 @@ function disposeMountedRoot(entry, container) {
174
175
  async function mountOneThreeRoot(spec, ctx) {
175
176
  const { game, canvas, w, h, dpr, isBottom, headless, assets, antialias } = ctx;
176
177
  const renderer = headless
177
- ? createHeadlessRendererStub()
178
+ ? createHeadlessRendererStub(canvas)
178
179
  : createHostRenderer(canvas, w, h,
179
180
  // The manifest's `rendering.antialias` reaches the WebGL context here and NOWHERE else:
180
181
  // `createHostRenderer` passes it straight to the constructor, and the sample count is