@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.
- package/dist/adapter/manifest-interpreter.d.ts +59 -0
- package/dist/adapter/manifest-interpreter.d.ts.map +1 -0
- package/dist/adapter/manifest-interpreter.js +50 -0
- package/dist/adapter/native-debug-module.d.ts +45 -3
- package/dist/adapter/native-debug-module.d.ts.map +1 -1
- package/dist/adapter/native-debug-module.js +128 -10
- package/dist/adapter/renderer-config.d.ts +1 -1
- package/dist/adapter/renderer-config.js +1 -1
- package/dist/canvas-react/index.d.ts +5 -12
- package/dist/canvas-react/index.d.ts.map +1 -1
- package/dist/canvas-react/index.js +5 -12
- package/dist/canvas-react/pixi-react-root-factory.d.ts +34 -21
- package/dist/canvas-react/pixi-react-root-factory.d.ts.map +1 -1
- package/dist/canvas-react/pixi-react-root-factory.js +274 -36
- package/dist/core/countdown-timer.d.ts +118 -0
- package/dist/core/countdown-timer.d.ts.map +1 -0
- package/dist/core/countdown-timer.js +137 -0
- package/dist/core/deferred-commands.d.ts +127 -0
- package/dist/core/deferred-commands.d.ts.map +1 -0
- package/dist/core/deferred-commands.js +132 -0
- package/dist/core/sim-clock.d.ts +14 -2
- package/dist/core/sim-clock.d.ts.map +1 -1
- package/dist/core/sim-clock.js +14 -2
- package/dist/dev/instruments.d.ts +38 -11
- package/dist/dev/instruments.d.ts.map +1 -1
- package/dist/dev/instruments.js +81 -11
- package/dist/input/input-manager.d.ts +15 -0
- package/dist/input/input-manager.d.ts.map +1 -1
- package/dist/input/input-manager.js +15 -0
- package/dist/render/directional-shadow-fit.d.ts +88 -0
- package/dist/render/directional-shadow-fit.d.ts.map +1 -0
- package/dist/render/directional-shadow-fit.js +133 -0
- package/dist/render/environment-capture.d.ts +68 -0
- package/dist/render/environment-capture.d.ts.map +1 -0
- package/dist/render/environment-capture.js +90 -0
- package/dist/render/ibl-override-material.d.ts +97 -0
- package/dist/render/ibl-override-material.d.ts.map +1 -0
- package/dist/render/ibl-override-material.js +114 -0
- package/dist/runtime/game-input-seams.d.ts +51 -0
- package/dist/runtime/game-input-seams.d.ts.map +1 -0
- package/dist/runtime/game-input-seams.js +86 -0
- package/dist/runtime/game.d.ts +15 -0
- package/dist/runtime/game.d.ts.map +1 -1
- package/dist/runtime/game.js +40 -8
- package/dist/runtime/mount-game.d.ts.map +1 -1
- package/dist/runtime/mount-game.js +18 -2
- package/dist/runtime/mount-manifest.d.ts.map +1 -1
- package/dist/runtime/mount-manifest.js +4 -2
- package/dist/runtime/types.d.ts +1 -1
- package/dist/world3d-react/index.d.ts +15 -25
- package/dist/world3d-react/index.d.ts.map +1 -1
- package/dist/world3d-react/index.js +15 -25
- package/dist/world3d-react/r3f-root-factory.d.ts +19 -25
- package/dist/world3d-react/r3f-root-factory.d.ts.map +1 -1
- package/dist/world3d-react/r3f-root-factory.js +396 -50
- package/dist/world3d-react/rapier-physics-bridge.d.ts +24 -12
- package/dist/world3d-react/rapier-physics-bridge.d.ts.map +1 -1
- package/dist/world3d-react/rapier-physics-bridge.js +82 -49
- package/package.json +1 -1
- package/schemas/engine-capabilities.json +3 -3
- package/src/adapter/manifest-interpreter.ts +64 -0
- package/src/adapter/native-debug-module.ts +212 -15
- package/src/adapter/renderer-config.ts +1 -1
- package/src/canvas-react/index.ts +5 -21
- package/src/canvas-react/pixi-react-root-factory.tsx +301 -39
- package/src/core/countdown-timer.ts +188 -0
- package/src/core/deferred-commands.ts +174 -0
- package/src/core/sim-clock.ts +14 -2
- package/src/dev/instruments.ts +108 -11
- package/src/input/input-manager.ts +15 -0
- package/src/render/directional-shadow-fit.ts +156 -0
- package/src/render/environment-capture.ts +102 -0
- package/src/render/ibl-override-material.ts +170 -0
- package/src/runtime/game-input-seams.ts +108 -0
- package/src/runtime/game.ts +65 -7
- package/src/runtime/mount-game.ts +23 -1
- package/src/runtime/mount-manifest.ts +4 -2
- package/src/runtime/types.ts +1 -1
- package/src/world3d-react/index.ts +15 -31
- package/src/world3d-react/r3f-root-factory.tsx +444 -53
- package/src/world3d-react/rapier-physics-bridge.tsx +88 -49
- package/src/canvas-react/engine-bridge.ts +0 -59
- package/src/canvas-react/pixi-react-adapter.tsx +0 -356
- package/src/canvas-react/world-context.ts +0 -253
- package/src/world3d-react/engine-bridge.ts +0 -86
- package/src/world3d-react/r3f-adapter.tsx +0 -717
- 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"}
|