@expofp/renderer 3.38.0 → 3.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/controls/camera-api.d.ts +28 -0
- package/dist/controls/camera-api.js +33 -10
- package/dist/controls/hooks.js +2 -0
- package/dist/core/InstanceUniforms.d.ts +15 -0
- package/dist/core/InstanceUniforms.js +32 -0
- package/dist/core/batch/BatchedMesh.js +6 -0
- package/dist/core/instanced/instanced-material.js +5 -1
- package/dist/core/lines/BatchedLines.d.ts +9 -7
- package/dist/core/lines/BatchedLines.js +9 -7
- package/dist/core/lines/{FrustumCulling.d.ts → FrustumCullingLines.d.ts} +7 -6
- package/dist/core/lines/{FrustumCulling.js → FrustumCullingLines.js} +2 -2
- package/dist/core/lines/{Raycasting.d.ts → RaycastingLines.d.ts} +7 -6
- package/dist/core/lines/{Raycasting.js → RaycastingLines.js} +2 -2
- package/dist/core/lines/lines-material.js +10 -1
- package/dist/core/sprites/BatchedSprites.d.ts +115 -0
- package/dist/core/sprites/BatchedSprites.js +205 -0
- package/dist/core/sprites/RaycastingSprites.d.ts +41 -0
- package/dist/core/sprites/RaycastingSprites.js +71 -0
- package/dist/core/sprites/sprite-material.d.ts +59 -0
- package/dist/core/sprites/sprite-material.js +192 -0
- package/dist/core/text/BatchedText.d.ts +6 -0
- package/dist/core/text/BatchedText.js +6 -0
- package/dist/core/text/MemberTexture.js +55 -1
- package/dist/core/text/Text.d.ts +16 -1
- package/dist/core/text/Text.js +15 -0
- package/dist/core/text/batched-text-material.js +4 -2
- package/dist/objects/containers/hooks.d.ts +7 -2
- package/dist/objects/containers/hooks.js +20 -5
- package/dist/objects/containers/mesh-writes.js +3 -3
- package/dist/objects/images/ImageInstance.js +103 -35
- package/dist/objects/images/Images.d.ts +8 -7
- package/dist/objects/images/Images.js +94 -28
- package/dist/objects/images/image-container.d.ts +14 -3
- package/dist/objects/images/image-container.js +19 -5
- package/dist/objects/layer/Layer.d.ts +7 -6
- package/dist/objects/layer/Layer.js +40 -30
- package/dist/objects/layer/layer-context.d.ts +41 -0
- package/dist/objects/layer/layer-context.js +45 -0
- package/dist/objects/materials/ColorMaterial.js +16 -3
- package/dist/objects/materials/LineMaterial.js +4 -2
- package/dist/objects/materials/TextureMaterial.js +4 -2
- package/dist/objects/materials/effects/color-dim.d.ts +1 -10
- package/dist/objects/materials/effects/color-dim.js +4 -12
- package/dist/objects/materials/effects/effects-context.d.ts +6 -5
- package/dist/objects/materials/shared.d.ts +18 -0
- package/dist/objects/materials/shared.js +21 -0
- package/dist/objects/models/Models.js +1 -8
- package/dist/objects/shapes/Shapes.js +2 -9
- package/dist/objects/text/TextInstance.js +1 -1
- package/dist/objects/text/text-layout.d.ts +13 -4
- package/dist/objects/text/text-layout.js +30 -4
- package/dist/space/Space.d.ts +15 -0
- package/dist/space/Space.js +16 -10
- package/dist/types/defs.d.ts +37 -0
- package/dist/utils/logging.d.ts +6 -3
- package/dist/utils/logging.js +15 -7
- package/dist/utils/math.d.ts +11 -0
- package/dist/utils/math.js +14 -0
- package/package.json +2 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type InstancedMeshImpl, type InstancedMeshImplParams } from '../../core/instanced/InstancedMesh';
|
|
2
2
|
import { type ImageDef } from '../../types/defs';
|
|
3
3
|
import { SlotContainer } from '../containers';
|
|
4
|
-
import { type
|
|
4
|
+
import { type PackedImage } from './atlas-packer';
|
|
5
5
|
/**
|
|
6
6
|
* The per-instance uniform each atlas mesh reads its texture region from. The container owns the
|
|
7
7
|
* name: it writes each slot's region here (`setUniformAt`) and hands it to the mesh's
|
|
@@ -42,9 +42,20 @@ export declare class ImageContainer extends SlotContainer<PackedImage, Instanced
|
|
|
42
42
|
* component renders `<instancedMesh args={container.args}>` with the geometry and material as
|
|
43
43
|
* children, and hands the mesh back through {@link SlotContainer.bind} — so R3F owns their
|
|
44
44
|
* lifetimes, including disposal on unmount and survival across StrictMode's simulated remount.
|
|
45
|
-
* @param
|
|
45
|
+
* @param capacity - How many instances to size the mesh for. This is the atlas's whole placement
|
|
46
|
+
* count, not one kind's share of it, so moving a def between the two meshes never resizes a
|
|
47
|
+
* buffer. The mesh grows past it on demand in any case.
|
|
46
48
|
*/
|
|
47
|
-
constructor(
|
|
49
|
+
constructor(capacity: number);
|
|
50
|
+
/**
|
|
51
|
+
* Whether the bound mesh draws its members facing the camera.
|
|
52
|
+
*
|
|
53
|
+
* Read off the mesh rather than stored, so one field cannot disagree with another. `screenSpace` is
|
|
54
|
+
* an ordinary def field, so flipping it re-renders nothing and only an instance can notice that it
|
|
55
|
+
* is drawing from the wrong mesh.
|
|
56
|
+
* @returns Whether this container holds a sprite batch.
|
|
57
|
+
*/
|
|
58
|
+
get facesCamera(): boolean;
|
|
48
59
|
protected addInstance(packed: PackedImage): number;
|
|
49
60
|
protected removeInstance(slot: number): void;
|
|
50
61
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Vector4 } from 'three';
|
|
2
|
+
import { isBatchedSprites } from '../../core/sprites/BatchedSprites';
|
|
2
3
|
import { SlotContainer } from '../containers';
|
|
3
4
|
import { DIM_SCHEMA } from '../materials/effects/color-dim';
|
|
4
5
|
/**
|
|
@@ -31,11 +32,24 @@ export class ImageContainer extends SlotContainer {
|
|
|
31
32
|
* component renders `<instancedMesh args={container.args}>` with the geometry and material as
|
|
32
33
|
* children, and hands the mesh back through {@link SlotContainer.bind} — so R3F owns their
|
|
33
34
|
* lifetimes, including disposal on unmount and survival across StrictMode's simulated remount.
|
|
34
|
-
* @param
|
|
35
|
+
* @param capacity - How many instances to size the mesh for. This is the atlas's whole placement
|
|
36
|
+
* count, not one kind's share of it, so moving a def between the two meshes never resizes a
|
|
37
|
+
* buffer. The mesh grows past it on demand in any case.
|
|
35
38
|
*/
|
|
36
|
-
constructor(
|
|
39
|
+
constructor(capacity) {
|
|
37
40
|
super({ schema: IMAGE_SCHEMA });
|
|
38
|
-
this.args = [{ capacity: Math.max(1,
|
|
41
|
+
this.args = [{ capacity: Math.max(1, capacity) }];
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Whether the bound mesh draws its members facing the camera.
|
|
45
|
+
*
|
|
46
|
+
* Read off the mesh rather than stored, so one field cannot disagree with another. `screenSpace` is
|
|
47
|
+
* an ordinary def field, so flipping it re-renders nothing and only an instance can notice that it
|
|
48
|
+
* is drawing from the wrong mesh.
|
|
49
|
+
* @returns Whether this container holds a sprite batch.
|
|
50
|
+
*/
|
|
51
|
+
get facesCamera() {
|
|
52
|
+
return isBatchedSprites(this.mesh);
|
|
39
53
|
}
|
|
40
54
|
addInstance(packed) {
|
|
41
55
|
// `addInstances` grows the buffers itself if the atlas ever overflows its sized capacity, so
|
|
@@ -65,6 +79,6 @@ export class ImageContainer extends SlotContainer {
|
|
|
65
79
|
const IMAGE_SCHEMA = {
|
|
66
80
|
vertex: { [UV_OFFSET_UNIFORM]: 'vec4', ...DIM_SCHEMA.vertex },
|
|
67
81
|
};
|
|
68
|
-
// Scratch for the one uv-region upload a claim performs, filled and read within a single
|
|
69
|
-
//
|
|
82
|
+
// Scratch for the one uv-region upload a claim performs, filled and read within a single synchronous
|
|
83
|
+
// statement.
|
|
70
84
|
const _uvOffset = new Vector4();
|
|
@@ -5,10 +5,10 @@ export interface LayerProps {
|
|
|
5
5
|
/** The layer definition to render, together with its descendants. */
|
|
6
6
|
layer: LayerDef;
|
|
7
7
|
/**
|
|
8
|
-
* Rendered inside the root group, before the tree's own content
|
|
9
|
-
*
|
|
10
|
-
* like any layer's, and its material exists before every layer's,
|
|
11
|
-
* on.
|
|
8
|
+
* Rendered inside the root group, before the tree's own content. This sits under the root
|
|
9
|
+
* layer's dim and overlay providers. The scene's background quad mounts here for that reason:
|
|
10
|
+
* the root's dim reaches it like any layer's, and its material exists before every layer's,
|
|
11
|
+
* which its draw order relies on.
|
|
12
12
|
*/
|
|
13
13
|
children?: ReactNode;
|
|
14
14
|
}
|
|
@@ -18,8 +18,9 @@ export interface LayerProps {
|
|
|
18
18
|
* This root component owns the tree-wide concerns — in debug mode the layer-visibility GUI, a
|
|
19
19
|
* "Main layers" folder for its direct child layers and a "Content layers" folder mirroring the
|
|
20
20
|
* scheduler's leaf queue (see `visibility-gui.ts`) — and seeds the recursion {@link LayerNode}
|
|
21
|
-
* carries
|
|
22
|
-
* `
|
|
21
|
+
* carries with `hidden`, at `false`. Dim and overlay need no seed: `LayerDimContext` and
|
|
22
|
+
* `LayerOverlayContext` already default to undimmed and plan tier, which is what "no layer above
|
|
23
|
+
* me" means for either one. The whole tree's draw order lives inside `[0, 1)`.
|
|
23
24
|
* @param props - The layer props.
|
|
24
25
|
* @returns The tree's root group.
|
|
25
26
|
*/
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Suspense
|
|
2
|
+
import { Suspense } from 'react';
|
|
3
3
|
import { useLayoutTargets } from '../../events/layout-targets';
|
|
4
4
|
import { usePointerProps } from '../../events/pointer';
|
|
5
5
|
import { isImageDefs, isLayerDefs, isLineDefs, isModelDefs, isShapeDefs, isTextDefs, } from '../../types/guards';
|
|
6
6
|
import { Images } from '../images';
|
|
7
7
|
import { Lines } from '../lines';
|
|
8
|
-
import { LayerDimContext } from '../materials/effects/color-dim';
|
|
9
8
|
import { Models } from '../models';
|
|
10
9
|
import { Shapes } from '../shapes';
|
|
11
10
|
import { Texts } from '../text';
|
|
11
|
+
import { LayerDimContext, LayerOverlayContext, useLayerDim, useLayerOverlay, } from './layer-context';
|
|
12
12
|
import { useStagedChildren } from './mount-scheduler';
|
|
13
13
|
import { useVisibilityGui } from './visibility-gui';
|
|
14
14
|
/**
|
|
@@ -17,15 +17,16 @@ import { useVisibilityGui } from './visibility-gui';
|
|
|
17
17
|
* This root component owns the tree-wide concerns — in debug mode the layer-visibility GUI, a
|
|
18
18
|
* "Main layers" folder for its direct child layers and a "Content layers" folder mirroring the
|
|
19
19
|
* scheduler's leaf queue (see `visibility-gui.ts`) — and seeds the recursion {@link LayerNode}
|
|
20
|
-
* carries
|
|
21
|
-
* `
|
|
20
|
+
* carries with `hidden`, at `false`. Dim and overlay need no seed: `LayerDimContext` and
|
|
21
|
+
* `LayerOverlayContext` already default to undimmed and plan tier, which is what "no layer above
|
|
22
|
+
* me" means for either one. The whole tree's draw order lives inside `[0, 1)`.
|
|
22
23
|
* @param props - The layer props.
|
|
23
24
|
* @returns The tree's root group.
|
|
24
25
|
*/
|
|
25
26
|
export function Layer(props) {
|
|
26
27
|
const { layer, children } = props;
|
|
27
28
|
useVisibilityGui(layer);
|
|
28
|
-
return (_jsx(LayerNode, { layer: layer,
|
|
29
|
+
return (_jsx(LayerNode, { layer: layer, hidden: false, orderBase: 0, orderStep: 1, children: children }));
|
|
29
30
|
}
|
|
30
31
|
/**
|
|
31
32
|
* One layer of the tree as a three.js `<group>`, recursing into its child layers.
|
|
@@ -43,45 +44,43 @@ export function Layer(props) {
|
|
|
43
44
|
* whose membership depends on the camera re-derives it. From then on a pixel-size change reaches
|
|
44
45
|
* those defs through the frame loop, with no render of this component involved.
|
|
45
46
|
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
* numbering the groups is all it takes to make array order the draw order. A nested group
|
|
49
|
-
* overwrites `groupOrder` for its own subtree, so the value that decides a mesh's place is its
|
|
50
|
-
* nearest enclosing layer's; the numbers on layers above it are overwritten and simply do not
|
|
51
|
-
* matter. Without this the order falls out of `material.id`, which ascends in mount order and
|
|
52
|
-
* therefore puts any material built after first paint — every model's, since they exist only once
|
|
53
|
-
* its GLB has loaded — last in the scene regardless of where its layer sits.
|
|
47
|
+
* Draw order is a pair: the tier, then the layer's own place. `overlay` adds a whole band above
|
|
48
|
+
* every plan-tier value. Array order still decides strictly within each band.
|
|
54
49
|
* @param props - The node props.
|
|
55
50
|
* @returns The `<group>` for a layer def.
|
|
56
51
|
*/
|
|
57
52
|
function LayerNode(props) {
|
|
58
|
-
const { layer, children: prelude,
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
const
|
|
53
|
+
const { layer, children: prelude, hidden: parentHidden, orderBase, orderStep } = props;
|
|
54
|
+
// What the nearest ancestor already resolved for dim and overlay. A layer cannot see its
|
|
55
|
+
// ancestors any other way.
|
|
56
|
+
const parentDim = useLayerDim();
|
|
57
|
+
const parentOverlay = useLayerOverlay();
|
|
58
|
+
// Once hidden or once in the overlay tier, a subtree stays that way. A descendant that dropped
|
|
59
|
+
// back into the plan tier would invert the nesting, and a descendant of a hidden layer is not on
|
|
60
|
+
// screen whatever it says about itself.
|
|
61
|
+
const hidden = parentHidden || layer.hidden === true;
|
|
62
|
+
const overlay = parentOverlay || layer.overlay === true;
|
|
63
|
+
// Nearest declaration wins, so only `undefined` inherits. A layer's own 0 un-dims it inside a
|
|
64
|
+
// dimmed ancestor.
|
|
65
|
+
const dim = layer.dim ?? parentDim;
|
|
66
|
+
// On screen only if nothing above hides it either. This is the value picking hangs off.
|
|
67
|
+
const visible = !hidden;
|
|
67
68
|
const { children, mounted } = useStagedChildren(layer);
|
|
68
69
|
useLayoutTargets(layer, children);
|
|
69
70
|
const pointerProps = usePointerProps(layer, visible);
|
|
70
71
|
let contents = null;
|
|
71
72
|
if (isLayerDefs(children)) {
|
|
72
73
|
const childStep = orderStep / children.length;
|
|
73
|
-
contents = children.map((child, index) => (_jsx(LayerNode, { layer: child,
|
|
74
|
+
contents = children.map((child, index) => (_jsx(LayerNode, { layer: child, hidden: hidden, orderBase: orderBase + index * childStep, orderStep: childStep }, child.name)));
|
|
74
75
|
}
|
|
75
76
|
else if (mounted > 0) {
|
|
76
77
|
contents = _jsx(LeafContent, { defs: children, mounted: mounted });
|
|
77
78
|
}
|
|
78
|
-
const group = (_jsxs("group", { name: layer.name, visible: !layer.hidden, renderOrder: orderBase, ...pointerProps, children: [prelude, contents] }));
|
|
79
|
-
// Always provided, never
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
|
|
83
|
-
// from not providing at all.
|
|
84
|
-
return _jsx(LayerDimContext, { value: dim, children: group });
|
|
79
|
+
const group = (_jsxs("group", { name: layer.name, visible: !layer.hidden, renderOrder: (overlay ? OVERLAY_TIER : PLAN_TIER) + orderBase, ...pointerProps, children: [prelude, contents] }));
|
|
80
|
+
// Always provided, never only when the layer declares one. Making a provider appear on first use
|
|
81
|
+
// would change this component's element type and remount the subtree, which releases and re-claims
|
|
82
|
+
// every GPU slot beneath it.
|
|
83
|
+
return (_jsx(LayerDimContext, { value: dim, children: _jsx(LayerOverlayContext, { value: overlay, children: group }) }));
|
|
85
84
|
}
|
|
86
85
|
/**
|
|
87
86
|
* The content of one leaf layer: the container for its defs' kind, inside a Suspense boundary of
|
|
@@ -112,3 +111,14 @@ function LeafContent(props) {
|
|
|
112
111
|
content = _jsx(Models, { defs: defs, mounted: mounted });
|
|
113
112
|
return _jsx(Suspense, { fallback: null, children: content });
|
|
114
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* Where the plan's own layers are numbered from. The tree subdivides `[0, 1)` depth first, so the
|
|
116
|
+
* mantissa stays free for nesting.
|
|
117
|
+
*/
|
|
118
|
+
const PLAN_TIER = 0;
|
|
119
|
+
/**
|
|
120
|
+
* Where an overlay subtree is numbered from, which puts it in `[1, 2)`. That is after every
|
|
121
|
+
* plan-tier layer, whatever renderOrder those layers hold. The whole cost is one bit of mantissa for
|
|
122
|
+
* subdivision.
|
|
123
|
+
*/
|
|
124
|
+
const OVERLAY_TIER = 1;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The dim amount a layer draws at, which every material beneath it reads.
|
|
3
|
+
* @returns The nearest provided amount, or `0` with no layer above.
|
|
4
|
+
*/
|
|
5
|
+
export declare function useLayerDim(): number;
|
|
6
|
+
/**
|
|
7
|
+
* Whether the current layer draws in the overlay tier.
|
|
8
|
+
* @returns `true` if this layer or an ancestor opted into the overlay tier, `false` otherwise.
|
|
9
|
+
*/
|
|
10
|
+
export declare function useLayerOverlay(): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* The dim amount the current layer hands down to its descendants.
|
|
13
|
+
*
|
|
14
|
+
* Context rather than a prop, because the value crosses the whole subtree and concerns none of the
|
|
15
|
+
* components between. A container hands its material nothing about layers, and a material that
|
|
16
|
+
* never asked about them still dims correctly. Threading it as a prop would put a `dim` on every
|
|
17
|
+
* material, every container, and every one of their inner components.
|
|
18
|
+
*
|
|
19
|
+
* Read through {@link useLayerDim} instead of directly. A `LayerNode` provides it, always, never
|
|
20
|
+
* only when it declares a `dim` of its own. See `Layer.tsx` for why.
|
|
21
|
+
*
|
|
22
|
+
* Defaults to `0`, undimmed, which is what "no layer above me" means for a component rendered
|
|
23
|
+
* outside a `Layer` tree. A fourth inherited field belongs in a context and a hook of its own here,
|
|
24
|
+
* not folded into this one.
|
|
25
|
+
*/
|
|
26
|
+
export declare const LayerDimContext: import("react").Context<number>;
|
|
27
|
+
/**
|
|
28
|
+
* Whether the current layer hands its overlay tier down to its descendants.
|
|
29
|
+
*
|
|
30
|
+
* Context rather than a prop, for the reason {@link LayerDimContext} is. It is also not a shading
|
|
31
|
+
* term, which is why `overlayParams`, the material parameters this tier unlocks, stays in
|
|
32
|
+
* `objects/materials/shared.ts` instead of moving here.
|
|
33
|
+
*
|
|
34
|
+
* Read through {@link useLayerOverlay} instead of directly. A `LayerNode` provides it, always,
|
|
35
|
+
* never only when it declares an `overlay` of its own. See `Layer.tsx` for why.
|
|
36
|
+
*
|
|
37
|
+
* Defaults to `false`, the plan tier, which is what "no layer above me" means for a component
|
|
38
|
+
* rendered outside a `Layer` tree.
|
|
39
|
+
*/
|
|
40
|
+
export declare const LayerOverlayContext: import("react").Context<boolean>;
|
|
41
|
+
//# sourceMappingURL=layer-context.d.ts.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { createContext, use } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* The dim amount a layer draws at, which every material beneath it reads.
|
|
4
|
+
* @returns The nearest provided amount, or `0` with no layer above.
|
|
5
|
+
*/
|
|
6
|
+
export function useLayerDim() {
|
|
7
|
+
return use(LayerDimContext);
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Whether the current layer draws in the overlay tier.
|
|
11
|
+
* @returns `true` if this layer or an ancestor opted into the overlay tier, `false` otherwise.
|
|
12
|
+
*/
|
|
13
|
+
export function useLayerOverlay() {
|
|
14
|
+
return use(LayerOverlayContext);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The dim amount the current layer hands down to its descendants.
|
|
18
|
+
*
|
|
19
|
+
* Context rather than a prop, because the value crosses the whole subtree and concerns none of the
|
|
20
|
+
* components between. A container hands its material nothing about layers, and a material that
|
|
21
|
+
* never asked about them still dims correctly. Threading it as a prop would put a `dim` on every
|
|
22
|
+
* material, every container, and every one of their inner components.
|
|
23
|
+
*
|
|
24
|
+
* Read through {@link useLayerDim} instead of directly. A `LayerNode` provides it, always, never
|
|
25
|
+
* only when it declares a `dim` of its own. See `Layer.tsx` for why.
|
|
26
|
+
*
|
|
27
|
+
* Defaults to `0`, undimmed, which is what "no layer above me" means for a component rendered
|
|
28
|
+
* outside a `Layer` tree. A fourth inherited field belongs in a context and a hook of its own here,
|
|
29
|
+
* not folded into this one.
|
|
30
|
+
*/
|
|
31
|
+
export const LayerDimContext = createContext(0);
|
|
32
|
+
/**
|
|
33
|
+
* Whether the current layer hands its overlay tier down to its descendants.
|
|
34
|
+
*
|
|
35
|
+
* Context rather than a prop, for the reason {@link LayerDimContext} is. It is also not a shading
|
|
36
|
+
* term, which is why `overlayParams`, the material parameters this tier unlocks, stays in
|
|
37
|
+
* `objects/materials/shared.ts` instead of moving here.
|
|
38
|
+
*
|
|
39
|
+
* Read through {@link useLayerOverlay} instead of directly. A `LayerNode` provides it, always,
|
|
40
|
+
* never only when it declares an `overlay` of its own. See `Layer.tsx` for why.
|
|
41
|
+
*
|
|
42
|
+
* Defaults to `false`, the plan tier, which is what "no layer above me" means for a component
|
|
43
|
+
* rendered outside a `Layer` tree.
|
|
44
|
+
*/
|
|
45
|
+
export const LayerOverlayContext = createContext(false);
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useState } from 'react';
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import { createLogger } from '../../utils/logging';
|
|
4
|
+
import { useLayerOverlay } from '../layer/layer-context';
|
|
3
5
|
import { applyAmbientOcclusion } from './effects/ao';
|
|
4
6
|
import { useDimPatch } from './effects/color-dim';
|
|
5
7
|
import { applyContactShadow } from './effects/contact-shadow';
|
|
6
8
|
import { useSceneEffects } from './effects/effects-context';
|
|
7
|
-
import { sharedParams } from './shared';
|
|
9
|
+
import { overlayParams, sharedParams } from './shared';
|
|
10
|
+
const logger = createLogger('color-material');
|
|
8
11
|
/**
|
|
9
12
|
* The material a shape batch draws with. It renders as a child of the mesh, so R3F attaches, updates
|
|
10
13
|
* and disposes it. It is also troika's base material for text, which never asks for `solid`. See
|
|
@@ -76,6 +79,16 @@ export function ColorMaterial(props) {
|
|
|
76
79
|
// skips the component in silence, so only the running app shows the failure.
|
|
77
80
|
const color = props.color ?? 'white';
|
|
78
81
|
const solid = props.solid === true;
|
|
82
|
+
const overlay = useLayerOverlay();
|
|
83
|
+
// Overlay is a contract for planar content. A volume that stops depth testing loses its own self
|
|
84
|
+
// occlusion, because its far faces then paint over its near ones in pack order. Making it work
|
|
85
|
+
// needs a depth clear between the tiers, so a second render pass, which external mode does not
|
|
86
|
+
// own. So a volume ignores the flag completely and says once that it did.
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
if (solid && overlay) {
|
|
89
|
+
logger.warn('overlay has no effect on a solid batch, so it is ignored.');
|
|
90
|
+
}
|
|
91
|
+
}, [solid, overlay]);
|
|
79
92
|
// Handed to both branches, because a volume dims the way flat content does. The patch runs on
|
|
80
93
|
// whatever `gl_FragColor` the material arrived at, which for a lit material is the shaded color.
|
|
81
94
|
const dimPatch = useDimPatch();
|
|
@@ -107,5 +120,5 @@ export function ColorMaterial(props) {
|
|
|
107
120
|
const translucent = props.translucent ?? false;
|
|
108
121
|
return (_jsx("meshLambertMaterial", { ...sharedParams, color: color, depthWrite: true, polygonOffset: false, transparent: translucent, forceSinglePass: !translucent, onBeforeCompile: onBeforeCompile }));
|
|
109
122
|
}
|
|
110
|
-
return _jsx("meshBasicMaterial", { ...sharedParams, color: color, onBeforeCompile: onBeforeCompile });
|
|
123
|
+
return (_jsx("meshBasicMaterial", { ...sharedParams, ...(overlay ? overlayParams : null), color: color, onBeforeCompile: onBeforeCompile }));
|
|
111
124
|
}
|
|
@@ -2,8 +2,9 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { extend } from '@react-three/fiber';
|
|
3
3
|
import { NotEqualStencilFunc, ReplaceStencilOp, } from 'three';
|
|
4
4
|
import { LineMaterial as LineMaterialBase } from 'three/addons/lines/LineMaterial.js';
|
|
5
|
+
import { useLayerOverlay } from '../layer/layer-context';
|
|
5
6
|
import { useDimPatch } from './effects/color-dim';
|
|
6
|
-
import { sharedParams } from './shared';
|
|
7
|
+
import { overlayParams, sharedParams } from './shared';
|
|
7
8
|
/**
|
|
8
9
|
* The stencil value a line layer stamps on every pixel it covers.
|
|
9
10
|
*
|
|
@@ -55,6 +56,7 @@ export function LineMaterial(props) {
|
|
|
55
56
|
const color = props.color ?? 'white';
|
|
56
57
|
const width = props.width ?? 1;
|
|
57
58
|
const onBeforeCompile = useDimPatch();
|
|
59
|
+
const overlay = useLayerOverlay();
|
|
58
60
|
/**
|
|
59
61
|
* Hands the batch about to draw a clean stencil to stamp.
|
|
60
62
|
*
|
|
@@ -68,5 +70,5 @@ export function LineMaterial(props) {
|
|
|
68
70
|
function onBeforeRender(renderer) {
|
|
69
71
|
renderer.clearStencil();
|
|
70
72
|
}
|
|
71
|
-
return (_jsx(LineMaterialElement, { ...sharedParams, color: color, linewidth: width, stencilWrite: true, stencilFunc: NotEqualStencilFunc, stencilZPass: ReplaceStencilOp, stencilRef: LINE_STENCIL_REF, onBeforeRender: onBeforeRender, onBeforeCompile: onBeforeCompile }));
|
|
73
|
+
return (_jsx(LineMaterialElement, { ...sharedParams, ...(overlay ? overlayParams : null), color: color, linewidth: width, stencilWrite: true, stencilFunc: NotEqualStencilFunc, stencilZPass: ReplaceStencilOp, stencilRef: LINE_STENCIL_REF, onBeforeRender: onBeforeRender, onBeforeCompile: onBeforeCompile }));
|
|
72
74
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { OneFactor } from 'three';
|
|
3
|
+
import { useLayerOverlay } from '../layer/layer-context';
|
|
3
4
|
import { useDimPatch } from './effects/color-dim';
|
|
4
|
-
import { sharedParams } from './shared';
|
|
5
|
+
import { overlayParams, sharedParams } from './shared';
|
|
5
6
|
/**
|
|
6
7
|
* The material used to render a texture, optionally through a per-instance uv-offset uniform so an
|
|
7
8
|
* instanced/batched mesh can pack many regions into one atlas.
|
|
@@ -16,6 +17,7 @@ import { sharedParams } from './shared';
|
|
|
16
17
|
export function TextureMaterial(props) {
|
|
17
18
|
const { map, uvOffset } = props;
|
|
18
19
|
const applyDim = useDimPatch();
|
|
20
|
+
const overlay = useLayerOverlay();
|
|
19
21
|
function onBeforeCompile(parameters) {
|
|
20
22
|
applyDim(parameters);
|
|
21
23
|
if (!uvOffset)
|
|
@@ -35,5 +37,5 @@ export function TextureMaterial(props) {
|
|
|
35
37
|
//
|
|
36
38
|
// The override lives here, not in `sharedParams`, because `ColorMaterial` and `LineMaterial`
|
|
37
39
|
// share that object and their colors are never premultiplied.
|
|
38
|
-
return (_jsx("meshBasicMaterial", { ...sharedParams, blendSrc: OneFactor, map: map, onBeforeCompile: onBeforeCompile }));
|
|
40
|
+
return (_jsx("meshBasicMaterial", { ...sharedParams, ...(overlay ? overlayParams : null), blendSrc: OneFactor, map: map, onBeforeCompile: onBeforeCompile }));
|
|
39
41
|
}
|
|
@@ -8,7 +8,7 @@ import { type RenderableDef } from '../../../types/defs';
|
|
|
8
8
|
*/
|
|
9
9
|
export type DimPatch = (parameters: WebGLProgramParametersWithUniforms) => void;
|
|
10
10
|
/**
|
|
11
|
-
* The dim patch every material installs, reading the layer amount from {@link
|
|
11
|
+
* The dim patch every material installs, reading the layer amount from {@link useLayerDim}.
|
|
12
12
|
*
|
|
13
13
|
* Dimming is deliberately **not** a material prop. It cuts across every material and every def type,
|
|
14
14
|
* so threading it as a prop would put a `dim` on each material, each container, and each of their
|
|
@@ -86,15 +86,6 @@ export declare function instanceDim(dim: number | undefined): number;
|
|
|
86
86
|
* @returns The amount, within `[0, 1]`.
|
|
87
87
|
*/
|
|
88
88
|
export declare function clampDim(dim: number): number;
|
|
89
|
-
/**
|
|
90
|
-
* The dim amount a layer draws at, which every material beneath it reads.
|
|
91
|
-
*
|
|
92
|
-
* Context rather than a prop because the value crosses the whole subtree and concerns none of the
|
|
93
|
-
* components between: a container hands its material nothing, and a material that never asked about
|
|
94
|
-
* layers still dims correctly. `Layer` provides it, and only when it declares a `dim` of its own —
|
|
95
|
-
* a layer without one is transparent to this, which is exactly what inheritance means.
|
|
96
|
-
*/
|
|
97
|
-
export declare const LayerDimContext: import("react").Context<number>;
|
|
98
89
|
/**
|
|
99
90
|
* The per-instance uniform a def's own dim is written to. Declared by every container that draws
|
|
100
91
|
* dimmable defs and read by {@link DIM_APPLY}; the material owns the name, the mesh only has to
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { useThree } from '@react-three/fiber';
|
|
2
|
-
import {
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
3
|
import { declareBeforeMain, sceneColorAnchor } from '../../../core/shader-source';
|
|
4
|
+
import { useLayerDim } from '../../layer/layer-context';
|
|
4
5
|
/**
|
|
5
|
-
* The dim patch every material installs, reading the layer amount from {@link
|
|
6
|
+
* The dim patch every material installs, reading the layer amount from {@link useLayerDim}.
|
|
6
7
|
*
|
|
7
8
|
* Dimming is deliberately **not** a material prop. It cuts across every material and every def type,
|
|
8
9
|
* so threading it as a prop would put a `dim` on each material, each container, and each of their
|
|
@@ -15,7 +16,7 @@ import { declareBeforeMain, sceneColorAnchor } from '../../../core/shader-source
|
|
|
15
16
|
* @returns The patch to call from the material's `onBeforeCompile`.
|
|
16
17
|
*/
|
|
17
18
|
export function useDimPatch() {
|
|
18
|
-
const dim =
|
|
19
|
+
const dim = useLayerDim();
|
|
19
20
|
const invalidate = useThree((state) => state.invalidate);
|
|
20
21
|
// A ref, not state: this object is written after render, every frame of an eased dim, and a ref is
|
|
21
22
|
// the container React sanctions that for. Its identity is what three holds onto, so the value can
|
|
@@ -125,15 +126,6 @@ export function instanceDim(dim) {
|
|
|
125
126
|
export function clampDim(dim) {
|
|
126
127
|
return Math.min(Math.max(dim, 0), 1);
|
|
127
128
|
}
|
|
128
|
-
/**
|
|
129
|
-
* The dim amount a layer draws at, which every material beneath it reads.
|
|
130
|
-
*
|
|
131
|
-
* Context rather than a prop because the value crosses the whole subtree and concerns none of the
|
|
132
|
-
* components between: a container hands its material nothing, and a material that never asked about
|
|
133
|
-
* layers still dims correctly. `Layer` provides it, and only when it declares a `dim` of its own —
|
|
134
|
-
* a layer without one is transparent to this, which is exactly what inheritance means.
|
|
135
|
-
*/
|
|
136
|
-
export const LayerDimContext = createContext(0);
|
|
137
129
|
/**
|
|
138
130
|
* The per-instance uniform a def's own dim is written to. Declared by every container that draws
|
|
139
131
|
* dimmable defs and read by {@link DIM_APPLY}; the material owns the name, the mesh only has to
|
|
@@ -25,11 +25,12 @@ import { type ContactShadow } from './contact-shadow';
|
|
|
25
25
|
* `contact-shadow.ts` uses for its three uniforms, published the way `SpaceContext` publishes a
|
|
26
26
|
* `Space` that is refitted in place.
|
|
27
27
|
*
|
|
28
|
-
* **`LayerDimContext`
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* frame write `.value` with no recompile.
|
|
32
|
-
* every layer boundary or flatten
|
|
28
|
+
* **`LayerDimContext` and `LayerOverlayContext` stay where they are and are not merged in.** Each
|
|
29
|
+
* carries one primitive of a layer's inherited state, is re-provided by every layer, and nests.
|
|
30
|
+
* This bundle is one object per renderer mount whose identity must never change, which is what
|
|
31
|
+
* lets a material capture it in `onBeforeCompile` and a frame write `.value` with no recompile.
|
|
32
|
+
* Merging either one in would either re-provide this object at every layer boundary or flatten
|
|
33
|
+
* that nesting.
|
|
33
34
|
*/
|
|
34
35
|
export type SceneEffects = AmbientOcclusion & ContactShadow;
|
|
35
36
|
/**
|
|
@@ -3,4 +3,22 @@ import { type MaterialParameters } from 'three';
|
|
|
3
3
|
* Material parameter defaults, shared between all types
|
|
4
4
|
*/
|
|
5
5
|
export declare const sharedParams: MaterialParameters;
|
|
6
|
+
/**
|
|
7
|
+
* What an overlay layer's planar materials add on top of {@link sharedParams}.
|
|
8
|
+
*
|
|
9
|
+
* Two obstacles, and render order clears neither.
|
|
10
|
+
*
|
|
11
|
+
* Depth. A solid shape sets `depthWrite: true`, so booth volumes fill the depth buffer. A planar def
|
|
12
|
+
* drawn later is still depth tested against them and loses. Turning the test off leaves order inside
|
|
13
|
+
* the tier as pure painter's order, because nothing in the tier writes depth.
|
|
14
|
+
*
|
|
15
|
+
* Pass. A translucent solid carries `transparent: true`, so it lives in three's transparent list.
|
|
16
|
+
* three draws that list after every opaque object, whatever render order says. An overlay that
|
|
17
|
+
* stayed in the opaque list would lose to a glass roof. So it opts back in. Inside the transparent
|
|
18
|
+
* list `reversePainterSortStable` compares `groupOrder` first, and the overlay tier's value of at
|
|
19
|
+
* least 1 beats every plan-tier value.
|
|
20
|
+
*
|
|
21
|
+
* `forceSinglePass` is not touched, so a double-sided overlay quad still blends once per pixel.
|
|
22
|
+
*/
|
|
23
|
+
export declare const overlayParams: MaterialParameters;
|
|
6
24
|
//# sourceMappingURL=shared.d.ts.map
|
|
@@ -49,3 +49,24 @@ export const sharedParams = {
|
|
|
49
49
|
polygonOffsetUnits: -FLAT_BIAS_UNITS,
|
|
50
50
|
forceSinglePass: true,
|
|
51
51
|
};
|
|
52
|
+
/**
|
|
53
|
+
* What an overlay layer's planar materials add on top of {@link sharedParams}.
|
|
54
|
+
*
|
|
55
|
+
* Two obstacles, and render order clears neither.
|
|
56
|
+
*
|
|
57
|
+
* Depth. A solid shape sets `depthWrite: true`, so booth volumes fill the depth buffer. A planar def
|
|
58
|
+
* drawn later is still depth tested against them and loses. Turning the test off leaves order inside
|
|
59
|
+
* the tier as pure painter's order, because nothing in the tier writes depth.
|
|
60
|
+
*
|
|
61
|
+
* Pass. A translucent solid carries `transparent: true`, so it lives in three's transparent list.
|
|
62
|
+
* three draws that list after every opaque object, whatever render order says. An overlay that
|
|
63
|
+
* stayed in the opaque list would lose to a glass roof. So it opts back in. Inside the transparent
|
|
64
|
+
* list `reversePainterSortStable` compares `groupOrder` first, and the overlay tier's value of at
|
|
65
|
+
* least 1 beats every plan-tier value.
|
|
66
|
+
*
|
|
67
|
+
* `forceSinglePass` is not touched, so a double-sided overlay quad still blends once per pixel.
|
|
68
|
+
*/
|
|
69
|
+
export const overlayParams = {
|
|
70
|
+
depthTest: false,
|
|
71
|
+
transparent: true,
|
|
72
|
+
};
|
|
@@ -139,15 +139,8 @@ function ModelPartMesh(props) {
|
|
|
139
139
|
// instead — but the hit resolution is not: `InstancedMeshImpl.raycast` walks the live slots,
|
|
140
140
|
// skipping deleted and hidden ones, and raycasts the real merged geometry, so a hit names the
|
|
141
141
|
// model's actual triangles and the slot that drew them.
|
|
142
|
-
const container = useContainer(() => new ModelContainer(asset.extents, defs.length, kind), defs);
|
|
142
|
+
const container = useContainer(() => new ModelContainer(asset.extents, defs.length, kind), defs, onKindMismatch);
|
|
143
143
|
const occluder = useOccluder();
|
|
144
|
-
// Assigned after mount rather than passed to the constructor: the container is built once and
|
|
145
|
-
// outlives every render, so it cannot close over a current callback. Nothing can fire before this
|
|
146
|
-
// runs — the split that placed these defs was computed from the same opacities they mount with.
|
|
147
|
-
useEffect(() => {
|
|
148
|
-
container.setKindMismatchHandler(onKindMismatch);
|
|
149
|
-
return () => container.setKindMismatchHandler(undefined);
|
|
150
|
-
}, [container, onKindMismatch]);
|
|
151
144
|
// The geometry belongs to the module-level cache and may be drawing in another layer, so it is
|
|
152
145
|
// mounted as a `primitive` with `dispose={null}` — R3F must not free it on unmount. Sharing it is
|
|
153
146
|
// safe because `InstancedMeshImpl` never writes to the geometry it is handed: its draw list lives in
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
2
|
+
import { useState } from 'react';
|
|
3
3
|
import { BatchedMesh } from '../../core/batch/BatchedMesh';
|
|
4
4
|
import { defKey, useContainer } from '../containers';
|
|
5
5
|
import { useOccluder } from '../lighting/occluder';
|
|
@@ -106,15 +106,8 @@ function ShapeBatch(props) {
|
|
|
106
106
|
//
|
|
107
107
|
// Stickiness costs this nothing: a batch mounts on the render that first fills it, so the defs
|
|
108
108
|
// sizing the container are a real membership rather than the empty one it may later stand at.
|
|
109
|
-
const container = useContainer(() => new ShapeContainer(defs, kind), defs);
|
|
109
|
+
const container = useContainer(() => new ShapeContainer(defs, kind), defs, onKindMismatch);
|
|
110
110
|
const occluder = useOccluder();
|
|
111
|
-
// Assigned after mount rather than passed to the constructor: the container is built once and
|
|
112
|
-
// outlives every render, so it cannot close over a current callback. Nothing can fire before this
|
|
113
|
-
// runs — the split that placed these defs was computed from the same colors they mount with.
|
|
114
|
-
useEffect(() => {
|
|
115
|
-
container.setKindMismatchHandler(onKindMismatch);
|
|
116
|
-
return () => container.setKindMismatchHandler(undefined);
|
|
117
|
-
}, [container, onKindMismatch]);
|
|
118
111
|
// Declares the incoming membership before React mounts the delta, so a change that outgrows the
|
|
119
112
|
// mesh resizes each buffer once, to exactly this size, instead of once per claiming instance.
|
|
120
113
|
// During render on purpose: instances claim in their mount effects, which run before any effect
|
|
@@ -77,7 +77,7 @@ function writeLine(mesh, lineSlot, text, line, placed, def) {
|
|
|
77
77
|
writeColorAndOpacity(text, line.stroke.color, 'outline');
|
|
78
78
|
text.outlineWidth = line.fontSize == 0 ? 0 : line.stroke.width / line.fontSize;
|
|
79
79
|
}
|
|
80
|
-
applyPlacement(text, placed, def.bounds);
|
|
80
|
+
applyPlacement(text, placed, def.bounds, def.screenSpace === true);
|
|
81
81
|
}
|
|
82
82
|
function writeColorAndOpacity(text, color, field) {
|
|
83
83
|
const colorFieldName = field == 'fill' ? 'color' : 'outlineColor';
|
|
@@ -61,16 +61,25 @@ export interface LaidOutLine {
|
|
|
61
61
|
*/
|
|
62
62
|
export declare function layoutText(def: TextDef): LaidOutLine[];
|
|
63
63
|
/**
|
|
64
|
-
* Writes a laid-out line onto its member: the bounds
|
|
64
|
+
* Writes a laid-out line onto its member: the bounds rotation and position applied to the local
|
|
65
65
|
* offset, the line's scale, and its clip rect.
|
|
66
66
|
*
|
|
67
|
-
* The post-transform half of
|
|
68
|
-
*
|
|
67
|
+
* The post-transform half of `layoutText`. `updateMatrix` is what reports the move to the batch,
|
|
68
|
+
* because members are not in the scene graph and nothing else would.
|
|
69
|
+
*
|
|
70
|
+
* A screen-space def also gets its sprite anchor. That is the def's bounds center, shared by every
|
|
71
|
+
* line: each line's member sits at its own plan position, and pivoting each about itself would
|
|
72
|
+
* compress the stack under tilt. The anchor carries no elevation, because every line already sits at
|
|
73
|
+
* the def's own elevation on `position`, and `MemberTexture` reads it from there instead.
|
|
74
|
+
* `MemberTexture` then composes the member per draw, so `matrixAutoUpdate` goes off and the
|
|
75
|
+
* position, rotation and scale written here become the inputs that compose reads rather than the
|
|
76
|
+
* matrix itself.
|
|
69
77
|
* @param text - The member to place.
|
|
70
78
|
* @param line - The line's local placement.
|
|
71
79
|
* @param bounds - The def's bounds, supplying the rotation, center and elevation.
|
|
80
|
+
* @param screenSpace - Whether the def faces the camera.
|
|
72
81
|
*/
|
|
73
|
-
export declare function applyPlacement(text: TextImpl, line: LaidOutLine, bounds: Rect): void;
|
|
82
|
+
export declare function applyPlacement(text: TextImpl, line: LaidOutLine, bounds: Rect, screenSpace: boolean): void;
|
|
74
83
|
/**
|
|
75
84
|
* The height one line's box occupies.
|
|
76
85
|
*
|