@vgai/engine 0.5.21 → 0.5.22

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 (87) hide show
  1. package/dist/adapter/manifest-interpreter.d.ts +59 -0
  2. package/dist/adapter/manifest-interpreter.d.ts.map +1 -0
  3. package/dist/adapter/manifest-interpreter.js +50 -0
  4. package/dist/adapter/native-debug-module.d.ts +45 -3
  5. package/dist/adapter/native-debug-module.d.ts.map +1 -1
  6. package/dist/adapter/native-debug-module.js +128 -10
  7. package/dist/adapter/renderer-config.d.ts +1 -1
  8. package/dist/adapter/renderer-config.js +1 -1
  9. package/dist/canvas-react/index.d.ts +5 -12
  10. package/dist/canvas-react/index.d.ts.map +1 -1
  11. package/dist/canvas-react/index.js +5 -12
  12. package/dist/canvas-react/pixi-react-root-factory.d.ts +34 -21
  13. package/dist/canvas-react/pixi-react-root-factory.d.ts.map +1 -1
  14. package/dist/canvas-react/pixi-react-root-factory.js +274 -36
  15. package/dist/core/countdown-timer.d.ts +118 -0
  16. package/dist/core/countdown-timer.d.ts.map +1 -0
  17. package/dist/core/countdown-timer.js +137 -0
  18. package/dist/core/deferred-commands.d.ts +127 -0
  19. package/dist/core/deferred-commands.d.ts.map +1 -0
  20. package/dist/core/deferred-commands.js +132 -0
  21. package/dist/core/sim-clock.d.ts +14 -2
  22. package/dist/core/sim-clock.d.ts.map +1 -1
  23. package/dist/core/sim-clock.js +14 -2
  24. package/dist/dev/instruments.d.ts +38 -11
  25. package/dist/dev/instruments.d.ts.map +1 -1
  26. package/dist/dev/instruments.js +81 -11
  27. package/dist/input/input-manager.d.ts +15 -0
  28. package/dist/input/input-manager.d.ts.map +1 -1
  29. package/dist/input/input-manager.js +15 -0
  30. package/dist/render/directional-shadow-fit.d.ts +88 -0
  31. package/dist/render/directional-shadow-fit.d.ts.map +1 -0
  32. package/dist/render/directional-shadow-fit.js +133 -0
  33. package/dist/render/environment-capture.d.ts +68 -0
  34. package/dist/render/environment-capture.d.ts.map +1 -0
  35. package/dist/render/environment-capture.js +90 -0
  36. package/dist/render/ibl-override-material.d.ts +97 -0
  37. package/dist/render/ibl-override-material.d.ts.map +1 -0
  38. package/dist/render/ibl-override-material.js +114 -0
  39. package/dist/runtime/game-input-seams.d.ts +51 -0
  40. package/dist/runtime/game-input-seams.d.ts.map +1 -0
  41. package/dist/runtime/game-input-seams.js +86 -0
  42. package/dist/runtime/game.d.ts +15 -0
  43. package/dist/runtime/game.d.ts.map +1 -1
  44. package/dist/runtime/game.js +40 -8
  45. package/dist/runtime/mount-game.d.ts.map +1 -1
  46. package/dist/runtime/mount-game.js +18 -2
  47. package/dist/runtime/mount-manifest.d.ts.map +1 -1
  48. package/dist/runtime/mount-manifest.js +4 -2
  49. package/dist/runtime/types.d.ts +1 -1
  50. package/dist/world3d-react/index.d.ts +15 -25
  51. package/dist/world3d-react/index.d.ts.map +1 -1
  52. package/dist/world3d-react/index.js +15 -25
  53. package/dist/world3d-react/r3f-root-factory.d.ts +19 -25
  54. package/dist/world3d-react/r3f-root-factory.d.ts.map +1 -1
  55. package/dist/world3d-react/r3f-root-factory.js +396 -50
  56. package/dist/world3d-react/rapier-physics-bridge.d.ts +24 -12
  57. package/dist/world3d-react/rapier-physics-bridge.d.ts.map +1 -1
  58. package/dist/world3d-react/rapier-physics-bridge.js +82 -49
  59. package/package.json +1 -1
  60. package/schemas/engine-capabilities.json +3 -3
  61. package/src/adapter/manifest-interpreter.ts +64 -0
  62. package/src/adapter/native-debug-module.ts +212 -15
  63. package/src/adapter/renderer-config.ts +1 -1
  64. package/src/canvas-react/index.ts +5 -21
  65. package/src/canvas-react/pixi-react-root-factory.tsx +301 -39
  66. package/src/core/countdown-timer.ts +188 -0
  67. package/src/core/deferred-commands.ts +174 -0
  68. package/src/core/sim-clock.ts +14 -2
  69. package/src/dev/instruments.ts +108 -11
  70. package/src/input/input-manager.ts +15 -0
  71. package/src/render/directional-shadow-fit.ts +156 -0
  72. package/src/render/environment-capture.ts +102 -0
  73. package/src/render/ibl-override-material.ts +170 -0
  74. package/src/runtime/game-input-seams.ts +108 -0
  75. package/src/runtime/game.ts +65 -7
  76. package/src/runtime/mount-game.ts +23 -1
  77. package/src/runtime/mount-manifest.ts +4 -2
  78. package/src/runtime/types.ts +1 -1
  79. package/src/world3d-react/index.ts +15 -31
  80. package/src/world3d-react/r3f-root-factory.tsx +444 -53
  81. package/src/world3d-react/rapier-physics-bridge.tsx +88 -49
  82. package/src/canvas-react/engine-bridge.ts +0 -59
  83. package/src/canvas-react/pixi-react-adapter.tsx +0 -356
  84. package/src/canvas-react/world-context.ts +0 -253
  85. package/src/world3d-react/engine-bridge.ts +0 -86
  86. package/src/world3d-react/r3f-adapter.tsx +0 -717
  87. package/src/world3d-react/world-context.ts +0 -358
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Fit a directional light's orthographic shadow box to the camera, for one
3
+ * frame — the geometry three does not supply.
4
+ *
5
+ * `DirectionalLightShadow` is an ordinary `OrthographicCamera` parented to the
6
+ * light, and it sits wherever the light's authored transform puts it, forever.
7
+ * That is fine for a scene the authored box contains and wrong for every game
8
+ * whose camera travels: the moment the player walks past the box, the casters
9
+ * out there stop having shadows, and no bias, map size or intensity fixes it
10
+ * because the geometry is simply outside the frustum. Three's own addon CSM is
11
+ * the multi-cascade answer to a different question and supplies neither of the
12
+ * two pieces below.
13
+ *
14
+ * ## The box is the frustum slice's BOUNDING SPHERE
15
+ *
16
+ * The standard fit, and the reason it cannot be authored as a constant: take
17
+ * the eight corners of the camera's frustum between its near plane and
18
+ * `maxDistance`, put the box centre at their centroid, and size the box to the
19
+ * largest distance from that centroid to a corner. That radius is decided by
20
+ * the camera's FIELD OF VIEW and aspect as much as by `maxDistance` — at a
21
+ * 70-degree vertical fov over 1024x600, a 40-unit slice has half-extents of
22
+ * 47.8 x 28.0 at its far plane and the sphere around it has radius ~58.9,
23
+ * nearly three times `maxDistance / 2`. A bounding SPHERE (rather than the
24
+ * tight box) is what makes the radius independent of camera yaw, so turning in
25
+ * place cannot resize the shadow.
26
+ *
27
+ * The corners are read through the camera's own `projectionMatrixInverse`, so
28
+ * a perspective and an orthographic camera are exact with no branch on type —
29
+ * and an asymmetric frustum (`setViewOffset`) is fitted over its real corners
30
+ * rather than an assumed centre.
31
+ *
32
+ * ## Texel snapping is not a polish detail
33
+ *
34
+ * A shadow box that follows a camera continuously re-rasterizes the same
35
+ * static geometry at sub-texel offsets, and the depth-test result flips along
36
+ * every shadow edge from frame to frame — the edges CRAWL, which is far more
37
+ * distracting than the missing shadows the fit is for. Quantizing the centre
38
+ * to whole shadow texels, in the LIGHT's own basis (the shadow map's u/v axes
39
+ * are the light's local x/y), makes the rasterized footprint of a static
40
+ * caster identical between frames. Depth (local z) is deliberately NOT snapped:
41
+ * it does not affect which texel a fragment lands in.
42
+ *
43
+ * ## What the caller keeps
44
+ *
45
+ * This is one pure function over three's own objects: it reads a camera and a
46
+ * quaternion and returns three numbers-and-a-vector. It moves nothing, and in
47
+ * particular it does not touch the light — how a fitted centre reaches the
48
+ * shadow camera is the caller's, because three derives the shadow camera's
49
+ * placement FROM the light every frame (`LightShadow.updateMatrices` copies the
50
+ * light's world position and looks at `light.target`), so the thing that has to
51
+ * move is the light NODE and only the caller knows how its light is parented,
52
+ * aimed and torn down. The depth range (`shadow.camera.near`/`far`) is the
53
+ * caller's for the same reason: it is what `shadow.bias` is normalized against.
54
+ *
55
+ * {@link DirectionalShadowFit.texelWorldSize} is published because everything
56
+ * sized in texels moves with the box — a receiver-side `shadow.normalBias`
57
+ * derived from a stale texel is wrong by exactly the factor the box grew, and
58
+ * it surfaces as self-shadow acne on curved casters rather than as anything a
59
+ * reviewer would call a shadow bug.
60
+ *
61
+ * Consumers today: `top-down-strategy`'s camera-fitted sun, and
62
+ * `godot-compat`'s Godot 3 `SHADOW_ORTHOGONAL` fit.
63
+ */
64
+ import { type Camera, Quaternion, Vector3 } from 'three';
65
+ /** The box a directional shadow is rendered through for one frame. */
66
+ export interface DirectionalShadowFit {
67
+ /** WORLD-space centre of the box, quantized to whole shadow texels in the light's basis. */
68
+ readonly center: Vector3;
69
+ /** Half-extent on both lateral axes — the frustum slice's bounding-sphere radius. */
70
+ readonly radius: number;
71
+ /** One shadow texel in world units at this radius: `2 * radius / mapSize`. */
72
+ readonly texelWorldSize: number;
73
+ }
74
+ /**
75
+ * The bounding sphere of `camera`'s frustum between its near plane and
76
+ * `maxDistance`, with the centre quantized to whole shadow texels in the
77
+ * LIGHT's basis.
78
+ *
79
+ * `camera` is read through its own `projectionMatrix`/`projectionMatrixInverse`/`matrixWorld`, so a
80
+ * caller must have brought those up to date (three does this for every rendered camera).
81
+ * `maxDistance` is the view depth the shadow covers, measured from the camera and clamped to its
82
+ * far plane — there is nothing to shadow past the geometry the camera draws.
83
+ * `lightOrientation` is the light's WORLD quaternion, whose local x/y are the shadow map's own axes
84
+ * and therefore the axes the snap quantizes along. A `mapSize` of zero or less disables snapping
85
+ * rather than dividing by it.
86
+ */
87
+ export declare function fitDirectionalShadowToCamera(camera: Camera, lightOrientation: Quaternion, maxDistance: number, mapSize: number): DirectionalShadowFit;
88
+ //# sourceMappingURL=directional-shadow-fit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"directional-shadow-fit.d.ts","sourceRoot":"","sources":["../../src/render/directional-shadow-fit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8DG;AACH,OAAO,EAAE,KAAK,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC;AAEzD,sEAAsE;AACtE,MAAM,WAAW,oBAAoB;IACnC,4FAA4F;IAC5F,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,qFAAqF;IACrF,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,8EAA8E;IAC9E,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;CACjC;AAoBD;;;;;;;;;;;;GAYG;AACH,wBAAgB,4BAA4B,CAC1C,MAAM,EAAE,MAAM,EACd,gBAAgB,EAAE,UAAU,EAC5B,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,MAAM,GACd,oBAAoB,CA4CtB"}
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Fit a directional light's orthographic shadow box to the camera, for one
3
+ * frame — the geometry three does not supply.
4
+ *
5
+ * `DirectionalLightShadow` is an ordinary `OrthographicCamera` parented to the
6
+ * light, and it sits wherever the light's authored transform puts it, forever.
7
+ * That is fine for a scene the authored box contains and wrong for every game
8
+ * whose camera travels: the moment the player walks past the box, the casters
9
+ * out there stop having shadows, and no bias, map size or intensity fixes it
10
+ * because the geometry is simply outside the frustum. Three's own addon CSM is
11
+ * the multi-cascade answer to a different question and supplies neither of the
12
+ * two pieces below.
13
+ *
14
+ * ## The box is the frustum slice's BOUNDING SPHERE
15
+ *
16
+ * The standard fit, and the reason it cannot be authored as a constant: take
17
+ * the eight corners of the camera's frustum between its near plane and
18
+ * `maxDistance`, put the box centre at their centroid, and size the box to the
19
+ * largest distance from that centroid to a corner. That radius is decided by
20
+ * the camera's FIELD OF VIEW and aspect as much as by `maxDistance` — at a
21
+ * 70-degree vertical fov over 1024x600, a 40-unit slice has half-extents of
22
+ * 47.8 x 28.0 at its far plane and the sphere around it has radius ~58.9,
23
+ * nearly three times `maxDistance / 2`. A bounding SPHERE (rather than the
24
+ * tight box) is what makes the radius independent of camera yaw, so turning in
25
+ * place cannot resize the shadow.
26
+ *
27
+ * The corners are read through the camera's own `projectionMatrixInverse`, so
28
+ * a perspective and an orthographic camera are exact with no branch on type —
29
+ * and an asymmetric frustum (`setViewOffset`) is fitted over its real corners
30
+ * rather than an assumed centre.
31
+ *
32
+ * ## Texel snapping is not a polish detail
33
+ *
34
+ * A shadow box that follows a camera continuously re-rasterizes the same
35
+ * static geometry at sub-texel offsets, and the depth-test result flips along
36
+ * every shadow edge from frame to frame — the edges CRAWL, which is far more
37
+ * distracting than the missing shadows the fit is for. Quantizing the centre
38
+ * to whole shadow texels, in the LIGHT's own basis (the shadow map's u/v axes
39
+ * are the light's local x/y), makes the rasterized footprint of a static
40
+ * caster identical between frames. Depth (local z) is deliberately NOT snapped:
41
+ * it does not affect which texel a fragment lands in.
42
+ *
43
+ * ## What the caller keeps
44
+ *
45
+ * This is one pure function over three's own objects: it reads a camera and a
46
+ * quaternion and returns three numbers-and-a-vector. It moves nothing, and in
47
+ * particular it does not touch the light — how a fitted centre reaches the
48
+ * shadow camera is the caller's, because three derives the shadow camera's
49
+ * placement FROM the light every frame (`LightShadow.updateMatrices` copies the
50
+ * light's world position and looks at `light.target`), so the thing that has to
51
+ * move is the light NODE and only the caller knows how its light is parented,
52
+ * aimed and torn down. The depth range (`shadow.camera.near`/`far`) is the
53
+ * caller's for the same reason: it is what `shadow.bias` is normalized against.
54
+ *
55
+ * {@link DirectionalShadowFit.texelWorldSize} is published because everything
56
+ * sized in texels moves with the box — a receiver-side `shadow.normalBias`
57
+ * derived from a stale texel is wrong by exactly the factor the box grew, and
58
+ * it surfaces as self-shadow acne on curved casters rather than as anything a
59
+ * reviewer would call a shadow bug.
60
+ *
61
+ * Consumers today: `top-down-strategy`'s camera-fitted sun, and
62
+ * `godot-compat`'s Godot 3 `SHADOW_ORTHOGONAL` fit.
63
+ */
64
+ import { Quaternion, Vector3 } from 'three';
65
+ const _inverseLight = new Quaternion();
66
+ const _corner = new Vector3();
67
+ const _centroid = new Vector3();
68
+ const _ndc = new Vector3();
69
+ /** The four lateral NDC corners; the slice's eight are these at two depths. */
70
+ const NDC_CORNERS = [
71
+ [-1, -1],
72
+ [-1, 1],
73
+ [1, -1],
74
+ [1, 1],
75
+ ];
76
+ /** View-space depth `-z` as this camera's NDC z, through the camera's OWN projection — so the
77
+ * slice is exact for a perspective and an orthographic camera alike, with no branch on type. */
78
+ function ndcDepthOf(camera, viewDepth) {
79
+ return _ndc.set(0, 0, -viewDepth).applyMatrix4(camera.projectionMatrix).z;
80
+ }
81
+ /**
82
+ * The bounding sphere of `camera`'s frustum between its near plane and
83
+ * `maxDistance`, with the centre quantized to whole shadow texels in the
84
+ * LIGHT's basis.
85
+ *
86
+ * `camera` is read through its own `projectionMatrix`/`projectionMatrixInverse`/`matrixWorld`, so a
87
+ * caller must have brought those up to date (three does this for every rendered camera).
88
+ * `maxDistance` is the view depth the shadow covers, measured from the camera and clamped to its
89
+ * far plane — there is nothing to shadow past the geometry the camera draws.
90
+ * `lightOrientation` is the light's WORLD quaternion, whose local x/y are the shadow map's own axes
91
+ * and therefore the axes the snap quantizes along. A `mapSize` of zero or less disables snapping
92
+ * rather than dividing by it.
93
+ */
94
+ export function fitDirectionalShadowToCamera(camera, lightOrientation, maxDistance, mapSize) {
95
+ const frustum = camera;
96
+ const camNear = typeof frustum.near === 'number' ? frustum.near : 0;
97
+ const camFar = typeof frustum.far === 'number' ? frustum.far : maxDistance;
98
+ // The two clamps are degenerate-input guards: far at least a hair past near, near never past far.
99
+ let far = Math.min(maxDistance, camFar);
100
+ far = Math.max(far, camNear + 0.001);
101
+ const near = Math.min(camNear, far);
102
+ const nearNdc = ndcDepthOf(camera, near);
103
+ const farNdc = ndcDepthOf(camera, far);
104
+ // The centroid of the eight corners. Both planes contribute four symmetric corners, so for an
105
+ // ordinary camera this is `(near + far) / 2` down -Z — but it is computed rather than assumed,
106
+ // because `setViewOffset` makes a frustum asymmetric and the fit is over the real corners.
107
+ _centroid.set(0, 0, 0);
108
+ for (const ndcZ of [nearNdc, farNdc]) {
109
+ for (const [x, y] of NDC_CORNERS) {
110
+ _centroid.add(_corner.set(x, y, ndcZ).applyMatrix4(camera.projectionMatrixInverse));
111
+ }
112
+ }
113
+ _centroid.multiplyScalar(1 / 8);
114
+ let radius = 0;
115
+ for (const ndcZ of [nearNdc, farNdc]) {
116
+ for (const [x, y] of NDC_CORNERS) {
117
+ radius = Math.max(radius, _corner.set(x, y, ndcZ).applyMatrix4(camera.projectionMatrixInverse).distanceTo(_centroid));
118
+ }
119
+ }
120
+ // One texel of margin, so the snapped box never clips its own outermost texel: the snap below
121
+ // moves the centre by up to half a texel on each lateral axis.
122
+ if (mapSize > 2)
123
+ radius *= mapSize / (mapSize - 2);
124
+ const center = _centroid.clone().applyMatrix4(camera.matrixWorld);
125
+ const texelWorldSize = mapSize > 0 ? (2 * radius) / mapSize : 0;
126
+ if (texelWorldSize > 0) {
127
+ center.applyQuaternion(_inverseLight.copy(lightOrientation).invert());
128
+ center.x = Math.round(center.x / texelWorldSize) * texelWorldSize;
129
+ center.y = Math.round(center.y / texelWorldSize) * texelWorldSize;
130
+ center.applyQuaternion(lightOrientation);
131
+ }
132
+ return { center, radius, texelWorldSize };
133
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Restricting a scene for the duration of ONE environment capture, and asking
3
+ * a renderer whether it can run one at all.
4
+ *
5
+ * A cube capture renders the live scene into an offscreen target, so whatever
6
+ * it must exclude has to be turned off and turned back on around that single
7
+ * render — never left flipped, and never flipped through a renderer-wide
8
+ * switch (see {@link withCaptureShadows} for the measured reason). Both
9
+ * helpers restore in a `finally`, so a capture that throws does not leave the
10
+ * game altered.
11
+ *
12
+ * Consumers today: the `reflections` capability's native probe system, and
13
+ * `godot-compat`'s Godot 3 `ReflectionProbe`.
14
+ */
15
+ import type { Scene, WebGLRenderer } from 'three';
16
+ /**
17
+ * Hide everything a capture's cull mask excludes, for exactly one capture.
18
+ *
19
+ * The object's side of the test is three's OWN `Object3D.layers.mask`, never a
20
+ * parallel `userData` copy. An engine whose own per-object visibility layers
21
+ * are a 32-bit mask (Godot's `VisualInstance.layers` is the worked case) writes
22
+ * the authored mask straight onto the object and this reads it back. Both
23
+ * default an unannotated object to `1`, which no default mask excludes, so an
24
+ * ordinary scene is untouched.
25
+ */
26
+ export declare function withCaptureMask(scene: Scene, mask: number, capture: () => void): void;
27
+ /**
28
+ * Suppress every shadow-casting light for the duration of ONE capture — as
29
+ * per-light `LightShadow.intensity`, never as `renderer.shadowMap.enabled`.
30
+ *
31
+ * **The distinction is not stylistic; the global flag silently deletes every
32
+ * shadow in the game.** `renderer.shadowMap.enabled` is a program PARAMETER
33
+ * (`WebGLPrograms.getParameters` → `shadowMapEnabled`), and
34
+ * `WebGLRenderer.setProgram`'s `needsProgramChange` list does NOT watch it —
35
+ * three requires a material version bump for a flip to reach a material that is
36
+ * already compiled. So flipping it around the capture does nothing for compiled
37
+ * materials and everything for the one material compiling INSIDE the window —
38
+ * and a probe system installing a shader override sets `material.needsUpdate =
39
+ * true` in the same `update()` call that runs the capture, so every patched
40
+ * material links with `USE_SHADOWMAP` absent and keeps that program for the
41
+ * rest of the run.
42
+ *
43
+ * Measured on the real Godot 3.6 binary against that lane's emitted port (a
44
+ * white ground, a floating box, one straight-down `DirectionalLight`,
45
+ * PanoramaSky ambient, Filmic white 6): with NO probe the port's shadow pixel
46
+ * is `92,109,181` against Godot's `92,109,181`; adding ONE `ReflectionProbe`
47
+ * moved the port's shadow pixel to `218,222,232` — byte-identical to its own
48
+ * LIT ground, i.e. the shadow was gone — while Godot's moved to `109,146,201`.
49
+ * Asking the capture not to touch shadows at all restored `108,146,200`, which
50
+ * is what named the mechanism. The defect was Godot-shaped; the mechanism is a
51
+ * three.js contract, which is why the fix lives here.
52
+ *
53
+ * `LightShadow.intensity` is a UNIFORM (`shadowmap_pars_fragment`:
54
+ * `mix(1.0, shadow, shadowIntensity)`), refreshed from the light on every
55
+ * `renderer.render()`, so zeroing it for the capture costs no recompile and
56
+ * cannot outlive the `finally` that restores it.
57
+ */
58
+ export declare function withCaptureShadows(scene: Scene, enabled: boolean, capture: () => void): void;
59
+ /**
60
+ * Whether this renderer can run an environment capture at all.
61
+ *
62
+ * The editor's still design-time tree and headless runtimes deliberately supply
63
+ * no GPU. They must keep the authored scene and gameplay; only the
64
+ * renderer-owned capture is unavailable, so a probe system asks this and skips
65
+ * the capture rather than refusing to mount.
66
+ */
67
+ export declare function canCaptureEnvironment(renderer: WebGLRenderer): boolean;
68
+ //# sourceMappingURL=environment-capture.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Hide everything a capture's cull mask excludes, for exactly one capture.
3
+ *
4
+ * The object's side of the test is three's OWN `Object3D.layers.mask`, never a
5
+ * parallel `userData` copy. An engine whose own per-object visibility layers
6
+ * are a 32-bit mask (Godot's `VisualInstance.layers` is the worked case) writes
7
+ * the authored mask straight onto the object and this reads it back. Both
8
+ * default an unannotated object to `1`, which no default mask excludes, so an
9
+ * ordinary scene is untouched.
10
+ */
11
+ export function withCaptureMask(scene, mask, capture) {
12
+ const hidden = [];
13
+ scene.traverse((object) => {
14
+ if ((object.layers.mask & mask) !== 0)
15
+ return;
16
+ hidden.push({ object, visible: object.visible });
17
+ object.visible = false;
18
+ });
19
+ try {
20
+ capture();
21
+ }
22
+ finally {
23
+ for (const entry of hidden)
24
+ entry.object.visible = entry.visible;
25
+ }
26
+ }
27
+ /**
28
+ * Suppress every shadow-casting light for the duration of ONE capture — as
29
+ * per-light `LightShadow.intensity`, never as `renderer.shadowMap.enabled`.
30
+ *
31
+ * **The distinction is not stylistic; the global flag silently deletes every
32
+ * shadow in the game.** `renderer.shadowMap.enabled` is a program PARAMETER
33
+ * (`WebGLPrograms.getParameters` → `shadowMapEnabled`), and
34
+ * `WebGLRenderer.setProgram`'s `needsProgramChange` list does NOT watch it —
35
+ * three requires a material version bump for a flip to reach a material that is
36
+ * already compiled. So flipping it around the capture does nothing for compiled
37
+ * materials and everything for the one material compiling INSIDE the window —
38
+ * and a probe system installing a shader override sets `material.needsUpdate =
39
+ * true` in the same `update()` call that runs the capture, so every patched
40
+ * material links with `USE_SHADOWMAP` absent and keeps that program for the
41
+ * rest of the run.
42
+ *
43
+ * Measured on the real Godot 3.6 binary against that lane's emitted port (a
44
+ * white ground, a floating box, one straight-down `DirectionalLight`,
45
+ * PanoramaSky ambient, Filmic white 6): with NO probe the port's shadow pixel
46
+ * is `92,109,181` against Godot's `92,109,181`; adding ONE `ReflectionProbe`
47
+ * moved the port's shadow pixel to `218,222,232` — byte-identical to its own
48
+ * LIT ground, i.e. the shadow was gone — while Godot's moved to `109,146,201`.
49
+ * Asking the capture not to touch shadows at all restored `108,146,200`, which
50
+ * is what named the mechanism. The defect was Godot-shaped; the mechanism is a
51
+ * three.js contract, which is why the fix lives here.
52
+ *
53
+ * `LightShadow.intensity` is a UNIFORM (`shadowmap_pars_fragment`:
54
+ * `mix(1.0, shadow, shadowIntensity)`), refreshed from the light on every
55
+ * `renderer.render()`, so zeroing it for the capture costs no recompile and
56
+ * cannot outlive the `finally` that restores it.
57
+ */
58
+ export function withCaptureShadows(scene, enabled, capture) {
59
+ if (enabled) {
60
+ capture();
61
+ return;
62
+ }
63
+ const restored = [];
64
+ scene.traverse((object) => {
65
+ const light = object;
66
+ if (!light.isLight || light.shadow === undefined)
67
+ return;
68
+ restored.push({ shadow: light.shadow, intensity: light.shadow.intensity });
69
+ light.shadow.intensity = 0;
70
+ });
71
+ try {
72
+ capture();
73
+ }
74
+ finally {
75
+ for (const entry of restored)
76
+ entry.shadow.intensity = entry.intensity;
77
+ }
78
+ }
79
+ /**
80
+ * Whether this renderer can run an environment capture at all.
81
+ *
82
+ * The editor's still design-time tree and headless runtimes deliberately supply
83
+ * no GPU. They must keep the authored scene and gameplay; only the
84
+ * renderer-owned capture is unavailable, so a probe system asks this and skips
85
+ * the capture rather than refusing to mount.
86
+ */
87
+ export function canCaptureEnvironment(renderer) {
88
+ const candidate = renderer;
89
+ return typeof candidate.compile === 'function' && candidate.coordinateSystem !== undefined;
90
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Replace Three's image-based-lighting functions on a `MeshStandardMaterial`,
3
+ * per fragment, without forking three.
4
+ *
5
+ * A local-probe system cannot express itself as `material.envMap`: one envMap
6
+ * per object picks ONE environment for the whole draw, and the decisive work —
7
+ * which probe volume contains this fragment, how it fades at the boundary,
8
+ * what happens where volumes overlap — is per-fragment and can differ across a
9
+ * single `InstancedMesh`. Every such system therefore does the same three
10
+ * things to three's own program, and this module is that shared mechanism:
11
+ *
12
+ * 1. take `ShaderChunk.envmap_physical_pars_fragment` and rename its two
13
+ * entry points, so the caller's replacement can still call them as its
14
+ * fallback ({@link baseIblFunctions});
15
+ * 2. publish the fragment's world position as a varying, spliced into
16
+ * `<worldpos_vertex>` under `USE_ENVMAP` ({@link IBL_WORLD_POSITION});
17
+ * 3. swap the fragment chunk for the caller's own GLSL and extend the
18
+ * program cache key so the variant is not shared with an unpatched
19
+ * material ({@link overrideMaterialIbl}).
20
+ *
21
+ * What the caller keeps is exactly what makes its system ITS system: the
22
+ * lighting model in the GLSL body, the uniforms behind it, and how probes are
23
+ * discovered, scheduled and captured. This module decides none of that — it
24
+ * hands back three's own shader object and gets out of the way.
25
+ *
26
+ * Consumers today: the `reflections` capability's native probe system, and
27
+ * `godot-compat`'s Godot 3 `ReflectionProbe`, whose per-fragment models are
28
+ * deliberately different renderers over this one splice.
29
+ */
30
+ import { type Mesh, type MeshStandardMaterial } from 'three';
31
+ /** The shader object three hands to `onBeforeCompile`. */
32
+ export type IblOverrideShader = Parameters<MeshStandardMaterial['onBeforeCompile']>[0];
33
+ /**
34
+ * The `varying vec3` the vertex splice publishes and the fragment body reads.
35
+ *
36
+ * It is ONE name across every consumer on purpose: the splice that declares it
37
+ * lives here, so a per-consumer name would be a string two files have to agree
38
+ * on with nothing checking that they do.
39
+ */
40
+ export declare const IBL_WORLD_POSITION = "vgaiIblWorldPosition";
41
+ /**
42
+ * Three's own IBL chunk with `getIBLIrradiance`/`getIBLRadiance` renamed to
43
+ * `getBaseIBLIrradiance`/`getBaseIBLRadiance`.
44
+ *
45
+ * A replacement chunk defines the original names itself; calling the renamed
46
+ * pair is how it falls back to the scene environment where no probe applies.
47
+ */
48
+ export declare function baseIblFunctions(): string;
49
+ /**
50
+ * Every material a mesh draws with that {@link overrideMaterialIbl} accepts.
51
+ *
52
+ * `isMeshStandardMaterial` is true for `MeshPhysicalMaterial` too, which is the
53
+ * intent: both compile the physical IBL chunk this module replaces.
54
+ */
55
+ export declare function standardMaterialsOf(mesh: Mesh): readonly MeshStandardMaterial[];
56
+ export interface IblOverrideOptions {
57
+ /**
58
+ * The GLSL that replaces `#include <envmap_physical_pars_fragment>`. Read at
59
+ * COMPILE time, not at install time, so a caller whose probe count is still
60
+ * settling does not have to reinstall to change it.
61
+ */
62
+ readonly fragment: () => string;
63
+ /**
64
+ * Appended to the material's own `customProgramCacheKey`, after a `|`. Must
65
+ * distinguish every shape of {@link IblOverrideOptions.fragment} the caller
66
+ * can produce — a probe COUNT belongs in here, because it changes the
67
+ * declared uniform array sizes.
68
+ */
69
+ readonly cacheKey: () => string;
70
+ /**
71
+ * Runs at the end of `onBeforeCompile`, with the shader three is compiling.
72
+ * Seed uniforms here: this is the only moment the caller is handed the
73
+ * object whose `uniforms` map the program will read.
74
+ */
75
+ readonly onCompile?: (shader: IblOverrideShader) => void;
76
+ }
77
+ export interface IblOverride {
78
+ readonly material: MeshStandardMaterial;
79
+ /**
80
+ * The shader three compiled this material with, or null before the first
81
+ * compile and after {@link IblOverride.restore}. Uniform writes go through
82
+ * `shader.uniforms`.
83
+ */
84
+ readonly shader: IblOverrideShader | null;
85
+ /** Put the material's own `onBeforeCompile`/`customProgramCacheKey` back. */
86
+ restore(): void;
87
+ }
88
+ /**
89
+ * Install the override. The returned handle is the only way to reach the
90
+ * compiled shader or to undo the patch; the material is otherwise untouched
91
+ * (in particular this never assigns `envMap`, which stays the caller's).
92
+ *
93
+ * The caller's own `onBeforeCompile` runs FIRST and its result is what gets
94
+ * spliced, so an override composes over a material that already had one.
95
+ */
96
+ export declare function overrideMaterialIbl(material: MeshStandardMaterial, options: IblOverrideOptions): IblOverride;
97
+ //# sourceMappingURL=ibl-override-material.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Replace Three's image-based-lighting functions on a `MeshStandardMaterial`,
3
+ * per fragment, without forking three.
4
+ *
5
+ * A local-probe system cannot express itself as `material.envMap`: one envMap
6
+ * per object picks ONE environment for the whole draw, and the decisive work —
7
+ * which probe volume contains this fragment, how it fades at the boundary,
8
+ * what happens where volumes overlap — is per-fragment and can differ across a
9
+ * single `InstancedMesh`. Every such system therefore does the same three
10
+ * things to three's own program, and this module is that shared mechanism:
11
+ *
12
+ * 1. take `ShaderChunk.envmap_physical_pars_fragment` and rename its two
13
+ * entry points, so the caller's replacement can still call them as its
14
+ * fallback ({@link baseIblFunctions});
15
+ * 2. publish the fragment's world position as a varying, spliced into
16
+ * `<worldpos_vertex>` under `USE_ENVMAP` ({@link IBL_WORLD_POSITION});
17
+ * 3. swap the fragment chunk for the caller's own GLSL and extend the
18
+ * program cache key so the variant is not shared with an unpatched
19
+ * material ({@link overrideMaterialIbl}).
20
+ *
21
+ * What the caller keeps is exactly what makes its system ITS system: the
22
+ * lighting model in the GLSL body, the uniforms behind it, and how probes are
23
+ * discovered, scheduled and captured. This module decides none of that — it
24
+ * hands back three's own shader object and gets out of the way.
25
+ *
26
+ * Consumers today: the `reflections` capability's native probe system, and
27
+ * `godot-compat`'s Godot 3 `ReflectionProbe`, whose per-fragment models are
28
+ * deliberately different renderers over this one splice.
29
+ */
30
+ import { ShaderChunk } from 'three';
31
+ /**
32
+ * The `varying vec3` the vertex splice publishes and the fragment body reads.
33
+ *
34
+ * It is ONE name across every consumer on purpose: the splice that declares it
35
+ * lives here, so a per-consumer name would be a string two files have to agree
36
+ * on with nothing checking that they do.
37
+ */
38
+ export const IBL_WORLD_POSITION = 'vgaiIblWorldPosition';
39
+ /**
40
+ * Three's own IBL chunk with `getIBLIrradiance`/`getIBLRadiance` renamed to
41
+ * `getBaseIBLIrradiance`/`getBaseIBLRadiance`.
42
+ *
43
+ * A replacement chunk defines the original names itself; calling the renamed
44
+ * pair is how it falls back to the scene environment where no probe applies.
45
+ */
46
+ export function baseIblFunctions() {
47
+ return ShaderChunk.envmap_physical_pars_fragment
48
+ .replaceAll('getIBLIrradiance', 'getBaseIBLIrradiance')
49
+ .replaceAll('getIBLRadiance', 'getBaseIBLRadiance');
50
+ }
51
+ /**
52
+ * Every material a mesh draws with that {@link overrideMaterialIbl} accepts.
53
+ *
54
+ * `isMeshStandardMaterial` is true for `MeshPhysicalMaterial` too, which is the
55
+ * intent: both compile the physical IBL chunk this module replaces.
56
+ */
57
+ export function standardMaterialsOf(mesh) {
58
+ const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
59
+ return materials.filter((material) => Boolean(material.isMeshStandardMaterial));
60
+ }
61
+ /**
62
+ * Install the override. The returned handle is the only way to reach the
63
+ * compiled shader or to undo the patch; the material is otherwise untouched
64
+ * (in particular this never assigns `envMap`, which stays the caller's).
65
+ *
66
+ * The caller's own `onBeforeCompile` runs FIRST and its result is what gets
67
+ * spliced, so an override composes over a material that already had one.
68
+ */
69
+ export function overrideMaterialIbl(material, options) {
70
+ const previousCompile = material.onBeforeCompile;
71
+ const previousCacheKey = material.customProgramCacheKey;
72
+ let compiled = null;
73
+ material.onBeforeCompile = (shader, renderer) => {
74
+ previousCompile.call(material, shader, renderer);
75
+ requireChunk(shader.vertexShader, '#include <worldpos_vertex>');
76
+ requireChunk(shader.fragmentShader, '#include <envmap_physical_pars_fragment>');
77
+ shader.vertexShader = `varying vec3 ${IBL_WORLD_POSITION};\n${shader.vertexShader.replace('#include <worldpos_vertex>', `#include <worldpos_vertex>
78
+ #if defined(USE_ENVMAP)
79
+ ${IBL_WORLD_POSITION} = worldPosition.xyz;
80
+ #endif`)}`;
81
+ shader.fragmentShader = `varying vec3 ${IBL_WORLD_POSITION};\n${shader.fragmentShader.replace('#include <envmap_physical_pars_fragment>', options.fragment())}`;
82
+ compiled = shader;
83
+ options.onCompile?.(shader);
84
+ };
85
+ material.customProgramCacheKey = () => `${previousCacheKey.call(material)}|${options.cacheKey()}`;
86
+ material.needsUpdate = true;
87
+ return {
88
+ material,
89
+ get shader() {
90
+ return compiled;
91
+ },
92
+ restore() {
93
+ material.onBeforeCompile = previousCompile;
94
+ material.customProgramCacheKey = previousCacheKey;
95
+ material.needsUpdate = true;
96
+ compiled = null;
97
+ },
98
+ };
99
+ }
100
+ /**
101
+ * A missing chunk is a NAMED error rather than a silent no-op.
102
+ *
103
+ * `String.replace` with no match returns the string unchanged, which would
104
+ * leave the varying declared and never assigned — a shader that links and
105
+ * renders the wrong thing. The only way to reach this today is a preceding
106
+ * `onBeforeCompile` in the same chain having already replaced the chunk (two
107
+ * IBL overrides stacked on one material), and that is worth a message naming
108
+ * the chunk instead of a black frame.
109
+ */
110
+ function requireChunk(source, chunk) {
111
+ if (source.includes(chunk))
112
+ return;
113
+ throw new Error(`overrideMaterialIbl needs three's ${chunk} shader chunk, and this material's program no longer contains it.`);
114
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * The GAME-SCOPED input wiring one world mount owns — HOST work performed from
3
+ * outside the world's own tree, shared verbatim by the three and canvas lanes
4
+ * (a project has ONE input map and one bot-input doctrine, whatever surface
5
+ * reads them).
6
+ *
7
+ * Two things happen here:
8
+ *
9
+ * 1. the project's input map is loaded through `Game.loadInputMap` (game-owned,
10
+ * load-once), so the actions the project declared exist;
11
+ * 2. this root's seams are registered on the game-scoped debug registry
12
+ * (`setVirtualInputTarget`/`setInputActionsSource`/`setInputTraceSource`),
13
+ * which is what makes `game.input.hold/tap/...` (the bot/`vgai eval` input
14
+ * doctrine) and the built-in `input.actions`/`input.trace` providers
15
+ * resolve to this world instead of throwing `DEBUG_INPUT_UNAVAILABLE`.
16
+ *
17
+ * A world's React tree receives no vgai context — this wiring is precisely the
18
+ * part that never needed one: every closure below reads GAME-scoped state the
19
+ * host owns (`game.input`, `game.loop.fixedDt`, the game's seed).
20
+ *
21
+ * `optionalInputMap` is the CONVENTIONAL-PATH probe used when the project
22
+ * never named a map: an absent file is the ordinary state of a brand-new
23
+ * project, so it resolves quietly instead of printing an error on every boot.
24
+ * A map that exists and fails to parse is as loud as ever.
25
+ *
26
+ * LIFETIME, honestly: `registry.strip(id)` runs on the mount's dispose, but it
27
+ * walks providers/commands only — the three seams set here survive it, keyed
28
+ * by this world's id, until a remount of the same id overwrites them. That
29
+ * residue is inert rather than hidden: every closure reads game-scoped state,
30
+ * so a stale entry actuates exactly what a live one would. What it can still
31
+ * do is name a dead world in `DEBUG_INPUT_WORLD_NOT_FOUND`'s registered list.
32
+ * Do not write "the strip clears the input seams" anywhere without changing
33
+ * `strip` to actually do it.
34
+ */
35
+ import type { DebugRegistry } from './debug-registry';
36
+ import type { GameInternal } from './game';
37
+ /** The conventional input-map location every scaffolded project ships
38
+ * (`public/inputmaps/default.inputmap.json`). */
39
+ export declare const DEFAULT_INPUT_MAP_PATH = "/inputmaps/default.inputmap.json";
40
+ /** The slice of a host context this wiring reads — satisfied structurally by
41
+ * both `ThreeHostContext` and `CanvasHostContext`. */
42
+ export interface GameInputSeamHost {
43
+ readonly game?: GameInternal | null | undefined;
44
+ readonly headless?: boolean | undefined;
45
+ }
46
+ export declare function wireGameInputSeams(host: GameInputSeamHost, registry: DebugRegistry, options: {
47
+ readonly id: string;
48
+ readonly inputMapPath?: string | null | undefined;
49
+ readonly optionalInputMap?: boolean | undefined;
50
+ }): Promise<void>;
51
+ //# sourceMappingURL=game-input-seams.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"game-input-seams.d.ts","sourceRoot":"","sources":["../../src/runtime/game-input-seams.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAGH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAE3C;kDACkD;AAClD,eAAO,MAAM,sBAAsB,qCAAqC,CAAC;AAEzE;uDACuD;AACvD,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,IAAI,CAAC,EAAE,YAAY,GAAG,IAAI,GAAG,SAAS,CAAC;IAChD,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CACzC;AAED,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,iBAAiB,EACvB,QAAQ,EAAE,aAAa,EACvB,OAAO,EAAE;IACP,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAClD,QAAQ,CAAC,gBAAgB,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CACjD,GACA,OAAO,CAAC,IAAI,CAAC,CAiDf"}