@expofp/renderer 3.38.0 → 3.39.1
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/events/LayoutDriver.d.ts +6 -6
- package/dist/events/LayoutDriver.js +9 -12
- package/dist/events/layout-targets.d.ts +35 -34
- package/dist/events/layout-targets.js +127 -107
- package/dist/objects/containers/hooks.d.ts +19 -4
- package/dist/objects/containers/hooks.js +101 -23
- package/dist/objects/containers/mesh-writes.js +3 -3
- package/dist/objects/images/ImageInstance.js +104 -36
- 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 +77 -57
- package/dist/objects/layer/layer-context.d.ts +74 -0
- package/dist/objects/layer/layer-context.js +82 -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 +3 -11
- package/dist/objects/materials/effects/color-dim.js +17 -17
- 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/ShapeInstance.js +1 -2
- package/dist/objects/shapes/Shapes.js +2 -9
- package/dist/objects/text/TextInstance.js +2 -2
- 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,5 +1,7 @@
|
|
|
1
1
|
import { memo } from 'react';
|
|
2
|
-
import { Matrix4 } from 'three';
|
|
2
|
+
import { Matrix4, Vector2, Vector3 } from 'three';
|
|
3
|
+
import { isBatchedSprites } from '../../core/sprites/BatchedSprites';
|
|
4
|
+
import { Y_DOWN_FLIP } from '../../space/Space';
|
|
3
5
|
import { shapeToMatrix, useDefSync, writeMatrixIfChanged } from '../containers';
|
|
4
6
|
/**
|
|
5
7
|
* Keeps one image placement current: claims a slot on mount (where the container writes the fixed
|
|
@@ -18,66 +20,132 @@ import { shapeToMatrix, useDefSync, writeMatrixIfChanged } from '../containers';
|
|
|
18
20
|
*/
|
|
19
21
|
export const ImageInstance = memo(function ImageInstance(props) {
|
|
20
22
|
const { def, packed, container } = props;
|
|
21
|
-
useDefSync(container, def, packed, (slot) => {
|
|
22
|
-
|
|
23
|
+
useDefSync(container, def, () => packed, (slot) => {
|
|
24
|
+
const tile = tileRect(packed);
|
|
25
|
+
writeImageMatrix(container, slot, def, tile);
|
|
26
|
+
writeImageAnchor(container, slot, def, tile);
|
|
27
|
+
// Checked on every sync rather than behind the write, because screenSpace is an ordinary def
|
|
28
|
+
// field. Flipping it reaches this instance and stops there, so this is where a mesh learns that
|
|
29
|
+
// it holds something it should not. The atlas re-filters, which never touches the packing.
|
|
30
|
+
if (!!def.screenSpace !== container.facesCamera)
|
|
31
|
+
container.reportKindMismatch();
|
|
23
32
|
});
|
|
24
33
|
return null;
|
|
25
34
|
});
|
|
26
35
|
/**
|
|
27
|
-
*
|
|
36
|
+
* Composes an image placement's instance matrix, and skips the write when nothing moved.
|
|
28
37
|
*
|
|
29
|
-
* `bounds` is a rect, so the quad's placement composes exactly as a rect shape's does
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
38
|
+
* `bounds` is a rect, so the quad's placement composes exactly as a rect shape's does. The two
|
|
39
|
+
* kinds part over the anchor and the tile.
|
|
40
|
+
*
|
|
41
|
+
* A flat quad folds the anchor into the matrix. That shifts the quad within its own unit square so
|
|
42
|
+
* the anchor, not the quad's center, lands on the bounds center, which also makes the anchor the
|
|
43
|
+
* point rotation pivots about. A tile then shrinks the quad onto the fraction of the image it covers.
|
|
44
|
+
*
|
|
45
|
+
* A sprite cannot fold the anchor in. It pivots about its anchor, so the matrix translation has to
|
|
46
|
+
* be the bounds center and stay there for every tile of a split source. Only the tile's scale and
|
|
47
|
+
* the plan-to-view y mirror stay in the matrix here. The anchor itself is
|
|
48
|
+
* {@link writeImageAnchor}'s write, not this one's.
|
|
49
|
+
* @param container - The container holding the instance, which says which kind it draws.
|
|
35
50
|
* @param slot - The instance's slot.
|
|
36
51
|
* @param def - The image def supplying the transform.
|
|
37
|
-
* @param
|
|
52
|
+
* @param tile - This instance's tile within the source, or `undefined` for an unsplit image.
|
|
38
53
|
*/
|
|
39
|
-
function writeImageMatrix(
|
|
40
|
-
// The shared scratch `shapeToMatrix` returns, adjusted in place below and uploaded before any
|
|
41
|
-
// other write can claim it.
|
|
54
|
+
function writeImageMatrix(container, slot, def, tile) {
|
|
42
55
|
const matrix = shapeToMatrix(def.bounds);
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
56
|
+
if (container.facesCamera) {
|
|
57
|
+
if (tile)
|
|
58
|
+
matrix.scale(_tileScale.set(tile.width, tile.height, 1));
|
|
59
|
+
// Y_DOWN_FLIP is the plan-to-view y mirror for this quad's own 2D linear map. Order does not
|
|
60
|
+
// matter against the tile scale above: the flip lands on the left of the whole linear map
|
|
61
|
+
// either way. Premultiplying also flips the translation, so save it and put it back. The
|
|
62
|
+
// translation is the bounds center the sprite pivots about and must not move.
|
|
63
|
+
const centerY = matrix.elements[13];
|
|
64
|
+
matrix.premultiply(Y_DOWN_FLIP);
|
|
65
|
+
matrix.elements[13] = centerY;
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
const [originX, originY] = def.origin ?? DEFAULT_ORIGIN;
|
|
69
|
+
// The anchor as an offset from the quad's middle, which is the convention both paths use.
|
|
70
|
+
const anchorX = originX - 0.5;
|
|
71
|
+
const anchorY = originY - 0.5;
|
|
72
|
+
// Unit-plane axes run the same way as image axes, x right and y down, matching the atlas uv and
|
|
73
|
+
// the scene's y-down coordinates. So an origin of [0, 0], the top-left corner, shifts the quad
|
|
74
|
+
// by a positive half in both and puts its top-left corner on the bounds center.
|
|
75
|
+
matrix.multiply(_anchor.makeTranslation(-anchorX, -anchorY, 0));
|
|
76
|
+
if (tile) {
|
|
77
|
+
matrix.multiply(_tile.makeScale(tile.width, tile.height, 1).setPosition(tile.x, tile.y, 0));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
writeMatrixIfChanged(container.mesh, slot, matrix);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Writes an image placement's anchor uniform, for the one kind that has one, skipping the write when
|
|
84
|
+
* it did not move.
|
|
85
|
+
*
|
|
86
|
+
* Only a sprite pivots about its anchor. The flat path folds the anchor into the matrix instead (see
|
|
87
|
+
* {@link writeImageMatrix}) and has no anchor uniform to write. The mesh is narrowed here rather
|
|
88
|
+
* than asked through `facesCamera`, which answers the same question but cannot narrow the mesh's
|
|
89
|
+
* type for the two anchor calls. A tile shifts the anchor by the tile's own offset from the image's
|
|
90
|
+
* middle and rescales it into the tile's unit square, so the result still reads as an offset from
|
|
91
|
+
* the tile's own middle.
|
|
92
|
+
*
|
|
93
|
+
* `setAnchorAt` invalidates the mesh's bounds on every call, so the compare-and-skip is what keeps
|
|
94
|
+
* a def whose anchor did not move from paying for that invalidation on every sync, the same way
|
|
95
|
+
* `writeMatrixIfChanged` does for the matrix.
|
|
96
|
+
* @param container - The container holding the instance.
|
|
97
|
+
* @param slot - The instance's slot.
|
|
98
|
+
* @param def - The image def supplying the origin.
|
|
99
|
+
* @param tile - This instance's tile within the source, or `undefined` for an unsplit image.
|
|
100
|
+
*/
|
|
101
|
+
function writeImageAnchor(container, slot, def, tile) {
|
|
102
|
+
const mesh = container.mesh;
|
|
103
|
+
if (!isBatchedSprites(mesh))
|
|
104
|
+
return;
|
|
46
105
|
const [originX, originY] = def.origin ?? DEFAULT_ORIGIN;
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
106
|
+
// The anchor as an offset from the quad's middle, which is the convention both paths use.
|
|
107
|
+
let x = originX - 0.5;
|
|
108
|
+
let y = originY - 0.5;
|
|
109
|
+
if (tile) {
|
|
110
|
+
x = (x - tile.x) / tile.width;
|
|
111
|
+
y = (y - tile.y) / tile.height;
|
|
112
|
+
}
|
|
113
|
+
mesh.getAnchorAt(slot, _previousAnchor);
|
|
114
|
+
if (_previousAnchor.x === x && _previousAnchor.y === y)
|
|
115
|
+
return;
|
|
116
|
+
mesh.setAnchorAt(slot, x, y);
|
|
51
117
|
}
|
|
52
118
|
/**
|
|
53
|
-
*
|
|
54
|
-
* tiles reassemble into the whole image. The tile's fraction of the source, in source pixels, is
|
|
55
|
-
* also its fraction of the image. So the sub-quad is that same fraction of the unit plane. The
|
|
56
|
-
* code scales the sub-quad to the tile's size, then moves it so its center lands on that
|
|
57
|
-
* fraction's center point, in unit-plane coordinates.
|
|
58
|
-
* @param matrix - The image transform, multiplied in place.
|
|
119
|
+
* Where one tile sits inside the whole image, in unit-quad lengths.
|
|
59
120
|
* @param packed - This instance's placement, with the tile's region and the source size to divide
|
|
60
121
|
* by.
|
|
122
|
+
* @returns The tile's size and the offset of its center from the image's middle, or `undefined` for
|
|
123
|
+
* an unsplit placement or a source that has no size to measure against.
|
|
61
124
|
*/
|
|
62
|
-
function
|
|
125
|
+
function tileRect(packed) {
|
|
63
126
|
const { source, size } = packed;
|
|
64
127
|
if (!source || size.width <= 0 || size.height <= 0)
|
|
65
|
-
return;
|
|
128
|
+
return undefined;
|
|
66
129
|
const fx0 = source.x / size.width;
|
|
67
130
|
const fy0 = source.y / size.height;
|
|
68
131
|
const fx1 = (source.x + source.width) / size.width;
|
|
69
132
|
const fy1 = (source.y + source.height) / size.height;
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
133
|
+
return {
|
|
134
|
+
width: fx1 - fx0,
|
|
135
|
+
height: fy1 - fy0,
|
|
136
|
+
x: (fx0 + fx1) / 2 - 0.5,
|
|
137
|
+
y: (fy0 + fy1) / 2 - 0.5,
|
|
138
|
+
};
|
|
74
139
|
}
|
|
75
140
|
/**
|
|
76
141
|
* The anchor a def leaves unset: the image's center, so `bounds.center` is where the middle of the
|
|
77
142
|
* image sits. Matches {@link ImageDef.origin}'s documented default.
|
|
78
143
|
*/
|
|
79
144
|
const DEFAULT_ORIGIN = [0.5, 0.5];
|
|
80
|
-
// Scratch shared by every instance and every write
|
|
81
|
-
// synchronous statement, so an instance costs a texture write and no allocation.
|
|
82
|
-
|
|
145
|
+
// Scratch shared by every instance and every write. Each is filled and uploaded within a single
|
|
146
|
+
// synchronous statement, so an instance costs a texture write and no allocation. `_previousAnchor`
|
|
147
|
+
// is only ever read back and compared, so a caller still holding the matrix never collides with it.
|
|
148
|
+
const _anchor = new Matrix4();
|
|
83
149
|
const _tile = new Matrix4();
|
|
150
|
+
const _tileScale = new Vector3();
|
|
151
|
+
const _previousAnchor = new Vector2();
|
|
@@ -17,15 +17,16 @@ export interface ImagesProps extends DefsProps<ImageDef> {
|
|
|
17
17
|
createCanvas?: CreateAtlasCanvas;
|
|
18
18
|
}
|
|
19
19
|
/**
|
|
20
|
-
* Draws a collection of images as textured quads, packing their pixels into one or more atlases
|
|
21
|
-
*
|
|
22
|
-
* draw calls, not one per image.
|
|
20
|
+
* Draws a collection of images as textured quads, packing their pixels into one or more atlases.
|
|
21
|
+
* Each atlas draws through up to two meshes that share its texture, so a layer of many images still
|
|
22
|
+
* costs a handful of draw calls, not one per image.
|
|
23
23
|
*
|
|
24
24
|
* Unlike a shape layer, an image layer can span **several meshes**: bin-packing splits the images
|
|
25
|
-
* across as many atlases as their pixels need, and each atlas gets its own
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
25
|
+
* across as many atlases as their pixels need, and each atlas gets its own texture drawn through up
|
|
26
|
+
* to two meshes, a flat `InstancedMesh` and a camera-facing `BatchedSprites` (an
|
|
27
|
+
* {@link ImageAtlas}). An unmixed layer still mounts only one mesh. A def whose source is too big
|
|
28
|
+
* for one atlas is cut into tiles that may land on different atlases, so one def can drive several
|
|
29
|
+
* instances. The atlas iterates its packed placements, not the defs.
|
|
29
30
|
*
|
|
30
31
|
* Packing has no incremental path, so a change to the **set** of images **repacks from scratch**: a
|
|
31
32
|
* fresh atlas set under fresh keys, remounting every mesh and instance. A **reorder** of the same
|
|
@@ -3,25 +3,27 @@ import { useThree } from '@react-three/fiber';
|
|
|
3
3
|
import { use, useEffect, useRef, useState } from 'react';
|
|
4
4
|
import { CanvasTexture, SRGBColorSpace } from 'three';
|
|
5
5
|
import { InstancedMesh } from '../../core/instanced/InstancedMesh';
|
|
6
|
+
import { BatchedSprites } from '../../core/sprites/BatchedSprites';
|
|
6
7
|
import { createLogger } from '../../utils/logging';
|
|
7
8
|
import { defKey, useContainer } from '../containers';
|
|
8
9
|
import { TextureMaterial } from '../materials/TextureMaterial';
|
|
9
|
-
import { packAtlases } from './atlas-packer';
|
|
10
|
+
import { packAtlases, } from './atlas-packer';
|
|
10
11
|
import { ImageContainer, UV_OFFSET_UNIFORM } from './image-container';
|
|
11
12
|
import { ImageInstance } from './ImageInstance';
|
|
12
13
|
import { contextOf, createAtlasCanvas } from './pack-sources';
|
|
13
14
|
import { TextureBudgetContext, useResizeFactor } from './texture-budget';
|
|
14
15
|
const logger = createLogger('images');
|
|
15
16
|
/**
|
|
16
|
-
* Draws a collection of images as textured quads, packing their pixels into one or more atlases
|
|
17
|
-
*
|
|
18
|
-
* draw calls, not one per image.
|
|
17
|
+
* Draws a collection of images as textured quads, packing their pixels into one or more atlases.
|
|
18
|
+
* Each atlas draws through up to two meshes that share its texture, so a layer of many images still
|
|
19
|
+
* costs a handful of draw calls, not one per image.
|
|
19
20
|
*
|
|
20
21
|
* Unlike a shape layer, an image layer can span **several meshes**: bin-packing splits the images
|
|
21
|
-
* across as many atlases as their pixels need, and each atlas gets its own
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
22
|
+
* across as many atlases as their pixels need, and each atlas gets its own texture drawn through up
|
|
23
|
+
* to two meshes, a flat `InstancedMesh` and a camera-facing `BatchedSprites` (an
|
|
24
|
+
* {@link ImageAtlas}). An unmixed layer still mounts only one mesh. A def whose source is too big
|
|
25
|
+
* for one atlas is cut into tiles that may land on different atlases, so one def can drive several
|
|
26
|
+
* instances. The atlas iterates its packed placements, not the defs.
|
|
25
27
|
*
|
|
26
28
|
* Packing has no incremental path, so a change to the **set** of images **repacks from scratch**: a
|
|
27
29
|
* fresh atlas set under fresh keys, remounting every mesh and instance. A **reorder** of the same
|
|
@@ -107,26 +109,26 @@ export function Images(props) {
|
|
|
107
109
|
return (_jsx(_Fragment, { children: packed.atlases.map((atlas) => (_jsx(ImageAtlas, { atlas: atlas, defs: defs, mounted: mounted }, atlasKey(atlas, packed.generation)))) }));
|
|
108
110
|
}
|
|
109
111
|
/**
|
|
110
|
-
*
|
|
111
|
-
* `
|
|
112
|
-
*
|
|
113
|
-
* built once from state and released together on unmount. Paint order is not fixed: a reorder keeps
|
|
114
|
-
* this atlas and re-labels its instances through the effect below.
|
|
112
|
+
* Owns one atlas's texture and its flat/sprite split, and draws each non-empty side as its own
|
|
113
|
+
* `AtlasMesh`. The atlas's set of images is fixed for its lifetime. A repack remounts the whole
|
|
114
|
+
* component, so the texture is built once from state and released on unmount.
|
|
115
115
|
*
|
|
116
|
-
* The uploaded texture is not fixed
|
|
117
|
-
*
|
|
118
|
-
* {@link resizeTexture}). That is a texture re-upload, not a repack
|
|
119
|
-
* instances, and their normalized uv regions all stand,
|
|
120
|
-
* size.
|
|
116
|
+
* The uploaded texture is not fixed. A resizable atlas registers with the scene's texture budget and
|
|
117
|
+
* re-derives a downscaled copy of its canvas whenever the shared resize factor moves (see
|
|
118
|
+
* {@link resizeTexture}). That is a texture re-upload, not a repack. The meshes, their containers,
|
|
119
|
+
* their instances, and their normalized uv regions all stand, because uv does not depend on the
|
|
120
|
+
* canvas's pixel size.
|
|
121
|
+
*
|
|
122
|
+
* The flat/sprite split is a second kind of state, held here rather than derived every render. A
|
|
123
|
+
* def's `screenSpace` is an ordinary slot write that never re-renders this component, so an
|
|
124
|
+
* `AtlasMesh` calls `repartition` when it finds a member that no longer matches its kind.
|
|
121
125
|
* @param props - The atlas and the layer's images in paint order.
|
|
122
|
-
* @returns
|
|
126
|
+
* @returns One `AtlasMesh` per non-empty kind, sharing this atlas's texture.
|
|
123
127
|
*/
|
|
124
128
|
function ImageAtlas(props) {
|
|
125
129
|
const { atlas, defs } = props;
|
|
126
|
-
// Only the placements are staged; the container and the texture are built from the whole atlas.
|
|
127
130
|
const mounted = props.mounted ?? atlas.images.length;
|
|
128
131
|
const [texture] = useState(() => makeAtlasTexture(atlas.image));
|
|
129
|
-
const container = useContainer(() => new ImageContainer(atlas), defs);
|
|
130
132
|
const invalidate = useThree((state) => state.invalidate);
|
|
131
133
|
const budget = use(TextureBudgetContext);
|
|
132
134
|
const factor = useResizeFactor(budget, atlas);
|
|
@@ -157,15 +159,79 @@ function ImageAtlas(props) {
|
|
|
157
159
|
const scaled = scaledRef.current;
|
|
158
160
|
return () => closeScaled(scaled);
|
|
159
161
|
}, []);
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
+
// Held in state rather than derived each render, which is the shape `Shapes` uses for its own
|
|
163
|
+
// split. screenSpace is an ordinary slot write that never re-renders this component, so a def
|
|
164
|
+
// crossing the orientation boundary would otherwise stay in a mesh that no longer suits it.
|
|
165
|
+
// repartition is what an instance's reportKindMismatch runs. The atlas's placements never change
|
|
166
|
+
// identity, because a repack remounts this whole component, so nothing else triggers a re-split.
|
|
167
|
+
const [partition, setPartition] = useState(() => partitionImages(atlas.images));
|
|
168
|
+
/** Re-splits the atlas's placements, for a mesh that found a member of the wrong kind. */
|
|
169
|
+
function repartition() {
|
|
170
|
+
setPartition(() => partitionImages(atlas.images));
|
|
171
|
+
}
|
|
172
|
+
// One mesh per kind, keyed so React never reconciles a flat mesh element against a sprite one
|
|
173
|
+
// when the first kind empties out. An empty subset renders nothing, so an unmixed layer costs
|
|
174
|
+
// exactly what it costs today and a mixed layer costs one extra draw call per atlas.
|
|
175
|
+
//
|
|
176
|
+
// renderOrder is the kind's place in IMAGE_KINDS, which makes "flats, then sprites" an explicit
|
|
177
|
+
// rule within a layer. Today the atlas meshes carry no order at all and it falls out of
|
|
178
|
+
// material.id, which is mount order. What that costs is that a flat def and a sprite def in one
|
|
179
|
+
// layer can no longer interleave in array order. `overlay` is there for a caller who needs strict
|
|
180
|
+
// separation, and a camera-facing pin is generally meant to be on top anyway.
|
|
181
|
+
return (_jsx(_Fragment, { children: IMAGE_KINDS.map((kind, renderOrder) => {
|
|
182
|
+
const placements = partition[kind];
|
|
183
|
+
if (placements.length === 0)
|
|
184
|
+
return null;
|
|
185
|
+
return (_jsx(AtlasMesh, { kind: kind, placements: placements, defs: defs, mounted: mounted, capacity: atlas.images.length, texture: texture, renderOrder: renderOrder, onKindMismatch: repartition }, kind));
|
|
186
|
+
}) }));
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* One atlas mesh and its instances: the atlas's flat images or its camera-facing ones, never both.
|
|
190
|
+
*
|
|
191
|
+
* The material is a child and is genuinely per mesh. `InstancedMeshImpl` wraps whatever it draws with
|
|
192
|
+
* in a closure over its own textures, and two instanced meshes on one material would both render
|
|
193
|
+
* against the first one's. The texture is shared, and that is the whole of the two-mesh split: an
|
|
194
|
+
* atlas is a texture packing and a mesh is a draw call, and nothing forces them to be one thing.
|
|
195
|
+
*
|
|
196
|
+
* The sprite mesh mounts no plane geometry child. `BatchedSpritesImpl` seeds the shared quad
|
|
197
|
+
* itself, and not rendering a replacement is what makes the seeded one stand.
|
|
198
|
+
* @param props - The kind, its placements, and the shared texture.
|
|
199
|
+
* @returns The mesh and one instance component per placement.
|
|
200
|
+
*/
|
|
201
|
+
function AtlasMesh(props) {
|
|
202
|
+
const { kind, placements, defs, capacity, texture, renderOrder, onKindMismatch } = props;
|
|
203
|
+
// Handed to this mesh as a prefix of its own placements rather than apportioned between the two,
|
|
204
|
+
// as Shapes does for its batches. A slice can overshoot the scheduler's budget by a factor of two
|
|
205
|
+
// at worst, and the scheduler measures what a slice actually cost and sizes the next one from it.
|
|
206
|
+
const mounted = props.mounted ?? placements.length;
|
|
207
|
+
// Sized from the atlas's whole placement count, not this subset's, so a re-split never resizes a
|
|
208
|
+
// buffer. The container is bookkeeping only, so the copy StrictMode's double-invoked initializer
|
|
209
|
+
// discards is plain garbage.
|
|
210
|
+
const container = useContainer(() => new ImageContainer(capacity), defs, onKindMismatch);
|
|
211
|
+
// args is a frozen snapshot, so R3F never rebuilds the mesh. The ref arrow, rather than
|
|
212
|
+
// ref={container.bind}, keeps the React Compiler from inferring container itself as a ref and
|
|
162
213
|
// rejecting its render reads.
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
214
|
+
const mesh = kind === 'sprite' ? (_jsx(BatchedSprites, { args: container.args, ref: (instance) => container.bind(instance), sortObjects: true, customSort: container.customSort, perObjectFrustumCulled: true, renderOrder: renderOrder, children: _jsx(TextureMaterial, { map: texture, uvOffset: UV_OFFSET_UNIFORM }) })) : (_jsxs(InstancedMesh, { args: container.args, ref: (instance) => container.bind(instance), sortObjects: true, customSort: container.customSort, perObjectFrustumCulled: true, renderOrder: renderOrder, children: [_jsx("planeGeometry", {}), _jsx(TextureMaterial, { map: texture, uvOffset: UV_OFFSET_UNIFORM })] }));
|
|
215
|
+
return (_jsxs(_Fragment, { children: [mesh, placements.slice(0, mounted).map((packed) => (_jsx(ImageInstance, { def: packed.def, packed: packed, container: container }, `${defKey(packed.def)}:${packed.uv.u0}:${packed.uv.v0}`)))] }));
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Splits an atlas's placements by orientation.
|
|
219
|
+
*
|
|
220
|
+
* A filter over an array the component already holds. The packing, the canvas, the uv regions and the
|
|
221
|
+
* texture budget are all untouched, so nothing repacks. That is what makes flipping a def's
|
|
222
|
+
* screenSpace cheaper here than a shape's kind change, where a re-split rebuilds partitions.
|
|
223
|
+
* @param images - The atlas's placements, in packing order.
|
|
224
|
+
* @returns The two subsets, each still in packing order.
|
|
225
|
+
*/
|
|
226
|
+
function partitionImages(images) {
|
|
227
|
+
const flat = [];
|
|
228
|
+
const sprite = [];
|
|
229
|
+
for (const packed of images)
|
|
230
|
+
(packed.def.screenSpace === true ? sprite : flat).push(packed);
|
|
231
|
+
return { flat, sprite };
|
|
168
232
|
}
|
|
233
|
+
/** The atlas's two meshes, in draw order: flats first, then the camera-facing ones. */
|
|
234
|
+
const IMAGE_KINDS = ['flat', 'sprite'];
|
|
169
235
|
/**
|
|
170
236
|
* Whether two snapshots hold the same *set* of images, order aside — the question "does this need a
|
|
171
237
|
* repack". Order is excluded on purpose: packing reads only pixels, so a reorder produces the same
|
|
@@ -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
|
*/
|