@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.
Files changed (64) hide show
  1. package/dist/controls/camera-api.d.ts +28 -0
  2. package/dist/controls/camera-api.js +33 -10
  3. package/dist/controls/hooks.js +2 -0
  4. package/dist/core/InstanceUniforms.d.ts +15 -0
  5. package/dist/core/InstanceUniforms.js +32 -0
  6. package/dist/core/batch/BatchedMesh.js +6 -0
  7. package/dist/core/instanced/instanced-material.js +5 -1
  8. package/dist/core/lines/BatchedLines.d.ts +9 -7
  9. package/dist/core/lines/BatchedLines.js +9 -7
  10. package/dist/core/lines/{FrustumCulling.d.ts → FrustumCullingLines.d.ts} +7 -6
  11. package/dist/core/lines/{FrustumCulling.js → FrustumCullingLines.js} +2 -2
  12. package/dist/core/lines/{Raycasting.d.ts → RaycastingLines.d.ts} +7 -6
  13. package/dist/core/lines/{Raycasting.js → RaycastingLines.js} +2 -2
  14. package/dist/core/lines/lines-material.js +10 -1
  15. package/dist/core/sprites/BatchedSprites.d.ts +115 -0
  16. package/dist/core/sprites/BatchedSprites.js +205 -0
  17. package/dist/core/sprites/RaycastingSprites.d.ts +41 -0
  18. package/dist/core/sprites/RaycastingSprites.js +71 -0
  19. package/dist/core/sprites/sprite-material.d.ts +59 -0
  20. package/dist/core/sprites/sprite-material.js +192 -0
  21. package/dist/core/text/BatchedText.d.ts +6 -0
  22. package/dist/core/text/BatchedText.js +6 -0
  23. package/dist/core/text/MemberTexture.js +55 -1
  24. package/dist/core/text/Text.d.ts +16 -1
  25. package/dist/core/text/Text.js +15 -0
  26. package/dist/core/text/batched-text-material.js +4 -2
  27. package/dist/events/LayoutDriver.d.ts +6 -6
  28. package/dist/events/LayoutDriver.js +9 -12
  29. package/dist/events/layout-targets.d.ts +35 -34
  30. package/dist/events/layout-targets.js +127 -107
  31. package/dist/objects/containers/hooks.d.ts +19 -4
  32. package/dist/objects/containers/hooks.js +101 -23
  33. package/dist/objects/containers/mesh-writes.js +3 -3
  34. package/dist/objects/images/ImageInstance.js +104 -36
  35. package/dist/objects/images/Images.d.ts +8 -7
  36. package/dist/objects/images/Images.js +94 -28
  37. package/dist/objects/images/image-container.d.ts +14 -3
  38. package/dist/objects/images/image-container.js +19 -5
  39. package/dist/objects/layer/Layer.d.ts +7 -6
  40. package/dist/objects/layer/Layer.js +77 -57
  41. package/dist/objects/layer/layer-context.d.ts +74 -0
  42. package/dist/objects/layer/layer-context.js +82 -0
  43. package/dist/objects/materials/ColorMaterial.js +16 -3
  44. package/dist/objects/materials/LineMaterial.js +4 -2
  45. package/dist/objects/materials/TextureMaterial.js +4 -2
  46. package/dist/objects/materials/effects/color-dim.d.ts +3 -11
  47. package/dist/objects/materials/effects/color-dim.js +17 -17
  48. package/dist/objects/materials/effects/effects-context.d.ts +6 -5
  49. package/dist/objects/materials/shared.d.ts +18 -0
  50. package/dist/objects/materials/shared.js +21 -0
  51. package/dist/objects/models/Models.js +1 -8
  52. package/dist/objects/shapes/ShapeInstance.js +1 -2
  53. package/dist/objects/shapes/Shapes.js +2 -9
  54. package/dist/objects/text/TextInstance.js +2 -2
  55. package/dist/objects/text/text-layout.d.ts +13 -4
  56. package/dist/objects/text/text-layout.js +30 -4
  57. package/dist/space/Space.d.ts +15 -0
  58. package/dist/space/Space.js +16 -10
  59. package/dist/types/defs.d.ts +37 -0
  60. package/dist/utils/logging.d.ts +6 -3
  61. package/dist/utils/logging.js +15 -7
  62. package/dist/utils/math.d.ts +11 -0
  63. package/dist/utils/math.js +14 -0
  64. package/package.json +2 -2
@@ -0,0 +1,205 @@
1
+ import { extend } from '@react-three/fiber';
2
+ import { Matrix4, PlaneGeometry, Vector2, Vector3, } from 'three';
3
+ import { InstancedMeshImpl } from '../instanced/InstancedMesh';
4
+ import { RaycastingSprites } from './RaycastingSprites';
5
+ import { ANCHOR_SCHEMA, ANCHOR_UNIFORM, patchSpriteMaterial, spriteReferenceDepth, } from './sprite-material';
6
+ /**
7
+ * Whether an instanced mesh is a sprite batch.
8
+ *
9
+ * three's own brand pattern, which `isLineMaterial` in `BatchedLines` already follows here. A holder
10
+ * that keeps one mesh field for several batch kinds narrows it with this, instead of keeping a
11
+ * second field that says which kind it stored.
12
+ * @param object - The instanced mesh to test.
13
+ * @returns Whether it is a sprite batch.
14
+ */
15
+ export function isBatchedSprites(object) {
16
+ return object.isBatchedSprites === true;
17
+ }
18
+ /**
19
+ * An instanced mesh whose every member faces the camera.
20
+ *
21
+ * The name means what it says, which is why this is a class and not a flag. A batch of sprites is
22
+ * homogeneous. One atlas is then drawn by two meshes over two disjoint subsets of its images,
23
+ * instead of one mesh learning about orientation, and the packing never hears about the distinction.
24
+ *
25
+ * What facing the camera changes, and nothing else does:
26
+ *
27
+ * - The projection, in the shader. See `sprite-material.ts`. Everything else an instanced mesh
28
+ * does applies unchanged: slots, visibility, growth, per-instance uniforms, the draw list that is
29
+ * also the paint order, and the material wiring.
30
+ * - The anchor, as a per-instance uniform rather than a fold into the matrix. A sprite pivots
31
+ * about its anchor, so the matrix translation has to be the anchor.
32
+ * - The bounds, which enclose the quad about that anchor. See `getBoundingSphereAt`.
33
+ * - Picking, which has to run three's own sprite algorithm through a scratch object. See
34
+ * `RaycastingSprites`.
35
+ *
36
+ * Members are ids, not `Sprite` objects. `BatchedTextImpl` takes `TextImpl` members because troika's
37
+ * layout is object shaped and asynchronous, and the batch has to pack what those objects produce. A
38
+ * sprite carries none of that. It is a matrix, an anchor and a color, and every one of those
39
+ * already lives in a data texture this mesh owns. Reusing three's type as the member representation
40
+ * would reinstate the object-per-instance layer the port dropped, and would import `SpriteMaterial`
41
+ * semantics this package does not implement. Three's type is reused where it costs nothing, as
42
+ * picking scratch.
43
+ *
44
+ * This class knows nothing about textures. A uv region belongs to `TextureMaterial` and to the layer
45
+ * that packs an atlas. A batch drawn with a plain color material works exactly as well, and tints
46
+ * its members through `setColorAt` like any instanced mesh.
47
+ *
48
+ * Render it with `BatchedSprites`.
49
+ */
50
+ export class BatchedSpritesImpl extends InstancedMeshImpl {
51
+ /** Marks this as a sprite batch. See `isBatchedSprites`. */
52
+ isBatchedSprites = true;
53
+ /**
54
+ * The quad every member is drawn from, shared by every sprite batch in the process. That is
55
+ * three's own arrangement, which builds one module-level geometry and lets every `Sprite` share it.
56
+ *
57
+ * Seeded here rather than mounted as a child element, because it is not a choice the way an atlas's
58
+ * quad is. A sprite is a quad, any z is flattened by the projection, and every non-quad
59
+ * silhouette is a matter of texture alpha. `InstancedMeshImpl` already seeds a throwaway geometry
60
+ * expecting R3F to replace it, and not rendering a replacement is what makes this one stand.
61
+ *
62
+ * Two consequences. Nothing may dispose it, so this class does not override `dispose`. Nothing may
63
+ * write its bounding sphere, so `getBoundingSphereAt` computes rather than inflating.
64
+ */
65
+ geometry = SPRITE_QUAD;
66
+ /** Three's own sprite algorithm, reached through a scratch object. See `RaycastingSprites`. */
67
+ raycasting = new RaycastingSprites();
68
+ /**
69
+ * @param params - Instance capacity. The buffers grow past it on demand.
70
+ */
71
+ constructor(params = {}) {
72
+ super(params);
73
+ // Declared by the class rather than by a caller, as `BatchedLinesImpl` declares its endpoints.
74
+ // The shader that reads this is the one this package writes, and keeping the name here is what
75
+ // stops a string contract crossing into `objects/`.
76
+ this.addUniforms(ANCHOR_SCHEMA);
77
+ }
78
+ /**
79
+ * A member's anchor. It is how far the pivot sits from the quad's middle, in unit-quad lengths, y
80
+ * down, and zero is the middle.
81
+ * @param id - The member to read.
82
+ * @param target - Receives the result.
83
+ * @returns The anchor.
84
+ */
85
+ getAnchorAt(id, target = _anchor) {
86
+ return this.getUniformAt(id, ANCHOR_UNIFORM, target);
87
+ }
88
+ /**
89
+ * Sets a member's anchor, and invalidates the bounds it decides.
90
+ * @param id - The member to write.
91
+ * @param x - How far right of the middle the pivot sits, in unit-quad lengths.
92
+ * @param y - How far below the middle the pivot sits, in unit-quad lengths.
93
+ */
94
+ setAnchorAt(id, x, y) {
95
+ this.setUniformAt(id, ANCHOR_UNIFORM, _anchor.set(x, y));
96
+ this.invalidateBounds();
97
+ }
98
+ /**
99
+ * Encloses one member: the sphere about its anchor that reaches the quad's furthest corner.
100
+ *
101
+ * The anchor is not folded into the matrix, so the matrix translation already is the center and
102
+ * only the radius needs correcting. That is the exact enclosing sphere for a quad pivoting about
103
+ * its anchor, up to the max-axis approximation `Sphere.applyMatrix4` already makes on the flat
104
+ * path. It also holds at every camera angle, which is what a sprite's bounds have to do.
105
+ *
106
+ * Two alternatives were rejected. Inflating the shared quad's own bounding sphere replaces a
107
+ * minimal sphere with a non-minimal one, needs a per-instance factor on a per-geometry field, and
108
+ * would propagate through every sprite batch in the process. Reading the anchor inside
109
+ * `InstancedMeshImpl.getBoundingSphereAt` is quad math in a class whose instances are arbitrary
110
+ * geometry, where `ModelContainer` is a live counter-example.
111
+ * @param id - The member.
112
+ * @param target - Receives the result.
113
+ * @returns The bounds.
114
+ */
115
+ getBoundingSphereAt(id, target) {
116
+ const matrix = this.getMatrixAt(id, _matrix);
117
+ target.center.setFromMatrixPosition(matrix);
118
+ target.radius = furthestCorner(this.getAnchorAt(id, _anchor)) * matrix.getMaxScaleOnAxis();
119
+ return target;
120
+ }
121
+ onBeforeRender(renderer, scene, camera, geometry, material, group) {
122
+ // Measured here rather than handed down from a scene component. The camera is what decides this
123
+ // value and three supplies it to this hook along with the material about to draw, so nothing has
124
+ // to thread a per-frame number through the components in between. `BatchedLinesImpl` writes its
125
+ // own resolution onto its material the same way, and for the same reason.
126
+ //
127
+ // Reading `matrixWorld` rather than a scene object keeps this class free of `space/`, and it
128
+ // answers under a host transform too, because whatever places the plan places this mesh with it.
129
+ spriteReferenceDepth(material).value = aimDepth(camera, this.matrixWorld);
130
+ super.onBeforeRender(renderer, scene, camera, geometry, material, group);
131
+ }
132
+ patchMaterial(material) {
133
+ // First, so the instancing patch wraps around it. At compile time the prologue the sprite
134
+ // patch reads has to be injected before that patch runs.
135
+ const unpatchSprite = patchSpriteMaterial(material);
136
+ const unpatchInstancing = super.patchMaterial(material);
137
+ return () => {
138
+ unpatchInstancing();
139
+ unpatchSprite();
140
+ };
141
+ }
142
+ /**
143
+ * Clears a member as it is handed out, so a reused slot keeps nothing of the one that held it. The
144
+ * anchor included, which a caller that writes only a matrix would otherwise inherit.
145
+ * @param id - The member being handed out.
146
+ */
147
+ resetInstance(id) {
148
+ super.resetInstance(id);
149
+ this.setAnchorAt(id, 0, 0);
150
+ }
151
+ }
152
+ /**
153
+ * Registers the class as an R3F element, by value so the generated key cannot collide with the
154
+ * `Sprite` three puts in the catalog when the canvas mounts.
155
+ */
156
+ export const BatchedSprites = extend(BatchedSpritesImpl);
157
+ /**
158
+ * How far the furthest corner of a unit quad lies from its anchor.
159
+ *
160
+ * The quad spans half a unit either side of its middle, and the anchor sits `anchor` away from that
161
+ * middle, so the furthest reach along each axis is half a unit plus that distance.
162
+ * @param anchor - The anchor's offset from the middle, in unit-quad lengths.
163
+ * @returns The reach, in unit-quad lengths.
164
+ */
165
+ function furthestCorner(anchor) {
166
+ return Math.hypot(0.5 + Math.abs(anchor.x), 0.5 + Math.abs(anchor.y));
167
+ }
168
+ /**
169
+ * How far the camera's own axis travels before it meets a batch's own plane, in world units.
170
+ *
171
+ * That is the depth a plan-unit size is exact at, so it is what the shader cancels the perspective
172
+ * divide against. It has to agree with the depth `measurePixelSize` in `space/layout-measure.ts`
173
+ * works at, because a caller's `onScale` rule sizes a def in plan units from the pixel size that
174
+ * function reports. Without controls mounted that function reads this same crossing point. With
175
+ * them it reads their distance to their target, and the two agree while the target sits on the
176
+ * plan, which is what orbiting a floor plan keeps true.
177
+ *
178
+ * The plane is the batch's own, taken from its world matrix, because a sprite batch draws at plan
179
+ * `z = 0` and whatever places the plan places the mesh with it.
180
+ * @param camera - The camera this draw uses.
181
+ * @param planToWorld - The batch's world matrix.
182
+ * @returns The distance in world units, or `0` for a camera that never meets the plane ahead of it.
183
+ */
184
+ function aimDepth(camera, planToWorld) {
185
+ // A camera looks down its own negative z.
186
+ const direction = _direction.setFromMatrixColumn(camera.matrixWorld, 2).normalize().negate();
187
+ const normal = _normal.setFromMatrixColumn(planToWorld, 2).normalize();
188
+ const facing = direction.dot(normal);
189
+ // The camera looks along the plane and never meets it.
190
+ if (facing === 0)
191
+ return 0;
192
+ const eye = _eye.setFromMatrixPosition(camera.matrixWorld);
193
+ const depth = _origin.setFromMatrixPosition(planToWorld).sub(eye).dot(normal) / facing;
194
+ // Behind the camera is no measurement, so the shader keeps its plan-unit sizing instead.
195
+ return depth > 0 ? depth : 0;
196
+ }
197
+ /** The quad every sprite batch draws. One per process. See `BatchedSpritesImpl.geometry`. */
198
+ const SPRITE_QUAD = new PlaneGeometry(1, 1);
199
+ // Scratch, each filled and consumed within one synchronous call.
200
+ const _anchor = new Vector2();
201
+ const _matrix = new Matrix4();
202
+ const _direction = new Vector3();
203
+ const _normal = new Vector3();
204
+ const _eye = new Vector3();
205
+ const _origin = new Vector3();
@@ -0,0 +1,41 @@
1
+ import { type Object3D, Vector2 } from 'three';
2
+ import { type RaycastHost, Raycasting } from '../instanced/Raycasting';
3
+ /** What this sweep reads off the batch it tests, beyond what every instanced mesh offers. */
4
+ interface RaycastHostSprites extends RaycastHost {
5
+ /**
6
+ * A member's anchor, as an offset from the quad's middle, y down.
7
+ * @param id - The member.
8
+ * @param target - Receives the result.
9
+ * @returns The anchor.
10
+ */
11
+ getAnchorAt(id: number, target: Vector2): Vector2;
12
+ }
13
+ /**
14
+ * Picking for a sprite batch: each member tested as the standing quad it actually draws.
15
+ *
16
+ * A sprite is picked by where it is drawn, not by the footprint it would have lying flat, and
17
+ * that quad is built after projection. So the test needs the camera. three has the math in
18
+ * `Sprite.raycast`, and this is how a batch reaches it: one scratch sprite, placed per member. That
19
+ * is the arrangement `core/lines/RaycastingLines.ts` already uses with its `_line`, and for the same
20
+ * reason.
21
+ *
22
+ * Only `prepareInstance` differs from the instanced sweep this inherits. The whole-batch early out,
23
+ * the frustum-culled candidates, the visibility test and naming the member each hit belongs to are
24
+ * the same questions for a sprite as for a flat quad. Answering them in world space is what lets
25
+ * this inherit them, because three's math projects the ray through the camera and needs it there.
26
+ *
27
+ * The camera comes from the raycaster, which three sets and R3F populates, and `Sprite.raycast` reads
28
+ * it itself. So nothing here captures it.
29
+ *
30
+ * Three things are written per test: the scratch's position and scale, its rotation, and its
31
+ * center. Together they make three draw the same quad the shader draws. They do not match the
32
+ * shader's formula term for term. Both `SPRITE_QUAD` and three's own sprite geometry use corners at
33
+ * plus or minus one half on each axis. That corner set stays the same when y is negated, so the
34
+ * scratch's per-corner math can differ from the shader's and still trace the same four corners. See
35
+ * the comments below for each piece.
36
+ */
37
+ export declare class RaycastingSprites extends Raycasting {
38
+ protected prepareInstance(host: RaycastHostSprites, objectIndex: number): Object3D;
39
+ }
40
+ export {};
41
+ //# sourceMappingURL=RaycastingSprites.d.ts.map
@@ -0,0 +1,71 @@
1
+ import { Matrix4, Sprite, SpriteMaterial, Vector2, Vector3 } from 'three';
2
+ import { axisLength } from '../../utils/math';
3
+ import { Raycasting } from '../instanced/Raycasting';
4
+ /**
5
+ * Picking for a sprite batch: each member tested as the standing quad it actually draws.
6
+ *
7
+ * A sprite is picked by where it is drawn, not by the footprint it would have lying flat, and
8
+ * that quad is built after projection. So the test needs the camera. three has the math in
9
+ * `Sprite.raycast`, and this is how a batch reaches it: one scratch sprite, placed per member. That
10
+ * is the arrangement `core/lines/RaycastingLines.ts` already uses with its `_line`, and for the same
11
+ * reason.
12
+ *
13
+ * Only `prepareInstance` differs from the instanced sweep this inherits. The whole-batch early out,
14
+ * the frustum-culled candidates, the visibility test and naming the member each hit belongs to are
15
+ * the same questions for a sprite as for a flat quad. Answering them in world space is what lets
16
+ * this inherit them, because three's math projects the ray through the camera and needs it there.
17
+ *
18
+ * The camera comes from the raycaster, which three sets and R3F populates, and `Sprite.raycast` reads
19
+ * it itself. So nothing here captures it.
20
+ *
21
+ * Three things are written per test: the scratch's position and scale, its rotation, and its
22
+ * center. Together they make three draw the same quad the shader draws. They do not match the
23
+ * shader's formula term for term. Both `SPRITE_QUAD` and three's own sprite geometry use corners at
24
+ * plus or minus one half on each axis. That corner set stays the same when y is negated, so the
25
+ * scratch's per-corner math can differ from the shader's and still trace the same four corners. See
26
+ * the comments below for each piece.
27
+ */
28
+ export class RaycastingSprites extends Raycasting {
29
+ prepareInstance(host, objectIndex) {
30
+ const matrix = host.getMatrixAt(objectIndex, _matrix);
31
+ // The shader takes the anchor from the instance matrix's translation and the scale from the
32
+ // model matrix, and a world rotation must not spin a sprite. So the scratch carries a
33
+ // translation and a scale and no rotation at all.
34
+ const planToWorld = axisLength(host.matrixWorld, 0);
35
+ _position.setFromMatrixPosition(matrix).applyMatrix4(host.matrixWorld);
36
+ _sprite.matrixWorld
37
+ .makeScale(planToWorld * axisLength(matrix, 0), planToWorld * axisLength(matrix, 1), 1)
38
+ .setPosition(_position);
39
+ // The plan-space rotation, read from the instance matrix alone. The shader reads this same
40
+ // angle from the same matrix. The rest of its formula differs from the scratch's, for the
41
+ // reason given in the class comment above. No separate negation here: the matrix already
42
+ // carries `Y_DOWN_FLIP` (see `writeImageMatrix` in `ImageInstance.tsx`), so this angle already
43
+ // reads the way three's y-up frame wants it.
44
+ const elements = matrix.elements;
45
+ _material.rotation = Math.atan2(elements[1], elements[0]);
46
+ // three's sprite center runs 0 to 1 with the middle at 0.5, and it is y up. Ours is an offset
47
+ // from the middle and it is y down. The flip cannot ride on a negative scale, because
48
+ // Sprite.raycast reads the scale through setFromMatrixScale, which returns magnitudes.
49
+ const anchor = host.getAnchorAt(objectIndex, _anchor);
50
+ _sprite.center.set(anchor.x + 0.5, 0.5 - anchor.y);
51
+ return _sprite;
52
+ }
53
+ }
54
+ /**
55
+ * The sprite every member is tested through.
56
+ *
57
+ * One object rather than one per member, for the reason the instanced sweep keeps a scratch mesh.
58
+ * What differs between members is the data, and three's raycast reads all of it off the object it is
59
+ * called on. It is never rendered, so neither its geometry nor its material holds anything to free,
60
+ * and its material is its own, so a test writes no state the batch is drawing with.
61
+ *
62
+ * `sizeAttenuation` stays at `SpriteMaterial`'s default of `true`, which is the behavior that leaves
63
+ * perspective alone to scale the quad. A sprite is sized in plan units, and constant pixel size
64
+ * is a caller's `onScale` rule rather than a renderer mode. Nothing exposes this flag, so the two
65
+ * halves cannot drift apart while that mode stays deferred.
66
+ */
67
+ const _material = new SpriteMaterial();
68
+ const _sprite = new Sprite(_material);
69
+ const _matrix = new Matrix4();
70
+ const _position = new Vector3();
71
+ const _anchor = new Vector2();
@@ -0,0 +1,59 @@
1
+ import { type IUniform, type Material, type WebGLProgramParametersWithUniforms } from 'three';
2
+ import { type UniformSchemaShader } from '../InstanceUniforms';
3
+ /**
4
+ * The per-instance anchor. It is how far the pivot sits from the quad's middle, in unit-quad
5
+ * lengths, y down. Zero is the middle and `(-0.5, -0.5)` is the top-left corner.
6
+ *
7
+ * An offset rather than three's `Sprite.center`, which runs 0 to 1 with the middle at 0.5. Zero then
8
+ * means centered, so a zero-filled uniform texture is already right and no default constant has to
9
+ * be kept in step with the shader.
10
+ *
11
+ * This file patches the material with free functions, as `instanced-material.ts` and
12
+ * `lines-material.ts` do. That keeps the patch out of a material component. A `TextureMaterial`
13
+ * must work as a flat quad or a sprite, and it must not need to know which.
14
+ */
15
+ export declare const ANCHOR_UNIFORM = "anchorOffset";
16
+ /**
17
+ * The per-instance uniform a sprite batch declares. There is no sprite flag beside it, because
18
+ * the class is the flag.
19
+ */
20
+ export declare const ANCHOR_SCHEMA: UniformSchemaShader;
21
+ /**
22
+ * The depth a material's sprites are drawn at, as the uniform the shader reads.
23
+ *
24
+ * One per material, created on first ask and kept for as long as the material lives. The identity
25
+ * is the whole point: {@link patchSpriteMaterial} binds this object into every program it compiles,
26
+ * and a draw writes `.value` through the same object with no recompile behind it. That is the
27
+ * currency `applyDim` and the scene effects already deal in.
28
+ *
29
+ * Owned here rather than handed in, so a mesh sets the depth on the material it is about to draw
30
+ * with, from its own render hook, instead of passing a value to a patch that runs at another time.
31
+ * @param material - The material to read the uniform of.
32
+ * @returns Its reference-depth uniform, in world units. Zero turns the correction off.
33
+ */
34
+ export declare function spriteReferenceDepth(material: Material): IUniform<number>;
35
+ /**
36
+ * Wraps a material so the programs it compiles draw their instances facing the camera.
37
+ *
38
+ * Installed inside the wrap an instanced draw puts on. By the time this runs, the prologue it reads
39
+ * is already in the shader, which is why the batch installs this one first.
40
+ *
41
+ * This also gives the material its own program cache key. three caches a compiled program under
42
+ * that key alone, with no way to compare shader source. A sprite compiles different GLSL than a
43
+ * flat draw of the same material, so without a key of its own, a sprite mesh can be handed a flat
44
+ * mesh's already-compiled program, and the patched shader above is silently dropped.
45
+ * @param material - The material the batch is about to draw with.
46
+ * @returns Restores the material exactly as it arrived. Call it once, after the draw.
47
+ */
48
+ export declare function patchSpriteMaterial(material: Material): () => void;
49
+ /**
50
+ * Replaces three's projection chunk with the sprite projection.
51
+ *
52
+ * Does nothing until a batch's prologue is in the shader. That is the guard `applyDim` and
53
+ * `patchLineShader` use, and for the same reason. A material may be compiled for something that is
54
+ * not a sprite batch at all, and the GLSL below reads names only the prologue brings into scope.
55
+ * @param shader - The program parameters to edit.
56
+ * @returns Whether the shader was patched.
57
+ */
58
+ export declare function patchSpriteShader(shader: WebGLProgramParametersWithUniforms): boolean;
59
+ //# sourceMappingURL=sprite-material.d.ts.map
@@ -0,0 +1,192 @@
1
+ import { replaceAnchor } from '../shader-source';
2
+ /**
3
+ * The per-instance anchor. It is how far the pivot sits from the quad's middle, in unit-quad
4
+ * lengths, y down. Zero is the middle and `(-0.5, -0.5)` is the top-left corner.
5
+ *
6
+ * An offset rather than three's `Sprite.center`, which runs 0 to 1 with the middle at 0.5. Zero then
7
+ * means centered, so a zero-filled uniform texture is already right and no default constant has to
8
+ * be kept in step with the shader.
9
+ *
10
+ * This file patches the material with free functions, as `instanced-material.ts` and
11
+ * `lines-material.ts` do. That keeps the patch out of a material component. A `TextureMaterial`
12
+ * must work as a flat quad or a sprite, and it must not need to know which.
13
+ */
14
+ export const ANCHOR_UNIFORM = 'anchorOffset';
15
+ /**
16
+ * The per-instance uniform a sprite batch declares. There is no sprite flag beside it, because
17
+ * the class is the flag.
18
+ */
19
+ export const ANCHOR_SCHEMA = { vertex: { [ANCHOR_UNIFORM]: 'vec2' } };
20
+ /**
21
+ * The depth a material's sprites are drawn at, as the uniform the shader reads.
22
+ *
23
+ * One per material, created on first ask and kept for as long as the material lives. The identity
24
+ * is the whole point: {@link patchSpriteMaterial} binds this object into every program it compiles,
25
+ * and a draw writes `.value` through the same object with no recompile behind it. That is the
26
+ * currency `applyDim` and the scene effects already deal in.
27
+ *
28
+ * Owned here rather than handed in, so a mesh sets the depth on the material it is about to draw
29
+ * with, from its own render hook, instead of passing a value to a patch that runs at another time.
30
+ * @param material - The material to read the uniform of.
31
+ * @returns Its reference-depth uniform, in world units. Zero turns the correction off.
32
+ */
33
+ export function spriteReferenceDepth(material) {
34
+ const existing = referenceDepths.get(material);
35
+ if (existing)
36
+ return existing;
37
+ const uniform = { value: 0 };
38
+ referenceDepths.set(material, uniform);
39
+ return uniform;
40
+ }
41
+ /**
42
+ * Wraps a material so the programs it compiles draw their instances facing the camera.
43
+ *
44
+ * Installed inside the wrap an instanced draw puts on. By the time this runs, the prologue it reads
45
+ * is already in the shader, which is why the batch installs this one first.
46
+ *
47
+ * This also gives the material its own program cache key. three caches a compiled program under
48
+ * that key alone, with no way to compare shader source. A sprite compiles different GLSL than a
49
+ * flat draw of the same material, so without a key of its own, a sprite mesh can be handed a flat
50
+ * mesh's already-compiled program, and the patched shader above is silently dropped.
51
+ * @param material - The material the batch is about to draw with.
52
+ * @returns Restores the material exactly as it arrived. Call it once, after the draw.
53
+ */
54
+ export function patchSpriteMaterial(material) {
55
+ // Saved raw rather than bound, for the reason patchInstancedMaterial spells out. Restoring a bound
56
+ // copy would put a different function back, and the next draw would bind that one in turn.
57
+ /* eslint-disable @typescript-eslint/unbound-method -- restored verbatim; called with `.call`. */
58
+ const baseOnBeforeCompile = material.onBeforeCompile;
59
+ const baseCacheKey = material.customProgramCacheKey;
60
+ /* eslint-enable @typescript-eslint/unbound-method */
61
+ // A constant suffix is enough: unlike the instancing key, "is a sprite" is fixed by the class,
62
+ // with no per-draw state to encode.
63
+ material.customProgramCacheKey = () => `efp_sprite_${baseCacheKey.call(material)}`;
64
+ material.onBeforeCompile = (shader, renderer) => {
65
+ // The material's own uniform, so the depth the draw wrote is already in the object this binds.
66
+ shader.uniforms[REFERENCE_DEPTH_UNIFORM] = spriteReferenceDepth(material);
67
+ patchSpriteShader(shader);
68
+ baseOnBeforeCompile.call(material, shader, renderer);
69
+ };
70
+ return () => {
71
+ material.onBeforeCompile = baseOnBeforeCompile;
72
+ material.customProgramCacheKey = baseCacheKey;
73
+ };
74
+ }
75
+ /**
76
+ * Replaces three's projection chunk with the sprite projection.
77
+ *
78
+ * Does nothing until a batch's prologue is in the shader. That is the guard `applyDim` and
79
+ * `patchLineShader` use, and for the same reason. A material may be compiled for something that is
80
+ * not a sprite batch at all, and the GLSL below reads names only the prologue brings into scope.
81
+ * @param shader - The program parameters to edit.
82
+ * @returns Whether the shader was patched.
83
+ */
84
+ export function patchSpriteShader(shader) {
85
+ const vertex = shader.vertexShader;
86
+ // The declaration the uniform prologue emits, not the bare name. A false positive here would
87
+ // rewrite a projection that has no anchor to read.
88
+ if (!vertex.includes(`vec2 ${ANCHOR_UNIFORM}`))
89
+ return false;
90
+ // Already patched, which is how a second compile of the same source stays harmless.
91
+ if (!vertex.includes(PROJECT_VERTEX))
92
+ return false;
93
+ // The uniform and the helper go above `main`, where GLSL needs them declared.
94
+ shader.vertexShader = replaceAnchor(`uniform float ${REFERENCE_DEPTH_UNIFORM};\n${DEPTH_SCALE}\n${vertex}`, PROJECT_VERTEX, SPRITE_PROJECT);
95
+ return true;
96
+ }
97
+ /**
98
+ * The depth a sprite's plan-unit size is exact at, in world units, as the shader names it.
99
+ *
100
+ * One value for the whole draw rather than one per instance, because it describes the camera and
101
+ * not a member. Zero leaves the perspective divide alone. Internal, because the uniform a caller
102
+ * writes is {@link spriteReferenceDepth} and no string has to cross with it. See {@link DEPTH_SCALE}.
103
+ */
104
+ const REFERENCE_DEPTH_UNIFORM = 'spriteReferenceDepth';
105
+ /**
106
+ * Every patched material's reference depth, keyed by the material. Weak, so a material that goes
107
+ * takes its uniform with it.
108
+ */
109
+ const referenceDepths = new WeakMap();
110
+ /**
111
+ * Three's projection chunk, which the sprite projection takes the place of. Replaced as an
112
+ * include, because three resolves includes after every `onBeforeCompile` has run.
113
+ */
114
+ const PROJECT_VERTEX = /* glsl */ `#include <project_vertex>`;
115
+ /**
116
+ * The sprite projection.
117
+ *
118
+ * A vertex is split into an anchor and a local 2D offset. The anchor is the instance matrix's own
119
+ * translation. three refuses to bake the anchor into the matrix for the same reason: a sprite
120
+ * pivots about its anchor, so the matrix translation has to be the anchor. The offset is the quad
121
+ * corner measured from that anchor, run through the instance's own linear map, then planted in the
122
+ * view basis instead of the plan basis.
123
+ *
124
+ * `w = 0` drops the translation, so `instanceMatrix` contributes exactly its rotation and scale.
125
+ * That is the quad's own 2D linear map. A flat draw applies that map in the world basis and a
126
+ * sprite applies the same map in the camera basis. The inner quantity is the same, so there is no
127
+ * ordering question between the two rotations and no angle to extract from a matrix.
128
+ *
129
+ * The anchor goes through `modelViewMatrix`, and that choice is about precision rather than about
130
+ * saving a multiply. three composes that matrix on the CPU in double precision, so its translation
131
+ * is the object measured from the camera, which is a small number. Multiplying `viewMatrix` by
132
+ * `modelMatrix` here instead would do the same work in float32 on two large, nearly cancelling
133
+ * translations. In external mode that is fatal: a MapLibre staticTransform places the plan in
134
+ * Mercator, where the whole world spans one unit, a venue spans about `5e-6`, and a float32 step
135
+ * near a coordinate of `0.5` is about `3e-8`. That leaves under two hundred distinct positions
136
+ * across the venue, which reads as a few pixels of jitter that moves as the camera moves. The flat
137
+ * path never had the problem, because three's own projection chunk uses `modelViewMatrix` too.
138
+ *
139
+ * The scale still comes from `modelMatrix`. A length has no cancellation to lose, so precision does
140
+ * not force the choice here. `modelViewMatrix` would give the same number, because the view matrix
141
+ * is rigid, so the view rotation is wasted work. That is three's own reasoning in `sprite.glsl.js`.
142
+ * The product with `instanceMatrix` would be wrong, because a MapLibre staticTransform rotates the
143
+ * plan in world space and the length of a rotated column would fold that rotation into the sprite.
144
+ * That transform forces uniform scale and no shear, so the first column's length is the whole
145
+ * conversion.
146
+ *
147
+ * This shader carries no sign for the plan's y-down convention. The instance matrix already
148
+ * carries it: `writeImageMatrix` in `objects/images/ImageInstance.tsx` composes `Y_DOWN_FLIP` from
149
+ * `space/Space.ts` into the linear part before it ever reaches this program. A sprite still has to
150
+ * look identical to the flat quad when the camera is straight overhead, which is what pins that
151
+ * flip's sign. A Storybook baseline pins the result, because this is the kind of sign that
152
+ * survives review and fails on screen.
153
+ *
154
+ * `mvPosition` keeps its name, because the chunks after this one read it.
155
+ */
156
+ const SPRITE_PROJECT = /* glsl */ `
157
+ vec4 mvPosition = modelViewMatrix * instanceMatrix[ 3 ];
158
+
159
+ vec3 local = vec3( position.xy - ${ANCHOR_UNIFORM}, 0.0 );
160
+ vec2 offset = ( instanceMatrix * vec4( local, 0.0 ) ).xy;
161
+ float planToWorld = length( modelMatrix[ 0 ].xyz );
162
+
163
+ mvPosition.xy += offset.xy * planToWorld * spriteDepthScale( mvPosition.z );
164
+ gl_Position = projectionMatrix * mvPosition;
165
+ `;
166
+ /**
167
+ * Cancels the perspective divide, so a sprite keeps the on-screen size its plan-unit bounds ask
168
+ * for wherever it sits in the frame.
169
+ *
170
+ * A sprite is sized in plan units and its quad is added in view space, so the projection divides it
171
+ * by its own depth and a distant sprite comes out smaller. Straight overhead that is invisible,
172
+ * because every sprite on the plan sits at one depth. Pitch the camera and the plan spreads across
173
+ * a range of depths, so one scene-wide pixel size can no longer be right for all of them.
174
+ *
175
+ * `referenceDepth` is the depth that pixel size was measured at, which is where the camera's own
176
+ * axis crosses the plan. Scaling by `depth / referenceDepth` makes the quad grow exactly as fast as
177
+ * the projection shrinks it, so the two cancel. At the reference depth the factor is one, which is
178
+ * what keeps `bounds.size` meaning the same plan units it means today.
179
+ *
180
+ * Two cases opt out and return one. A `referenceDepth` of zero is the uninitialized value, which
181
+ * leaves the old behavior in place rather than dividing by it. An orthographic projection performs
182
+ * no divide to cancel, and `projectionMatrix[2][3]` is what tells the two apart, exactly as three's
183
+ * own `isPerspectiveMatrix` does.
184
+ */
185
+ const DEPTH_SCALE = /* glsl */ `
186
+ float spriteDepthScale( const in float viewZ ) {
187
+ bool isPerspective = projectionMatrix[ 2 ][ 3 ] == -1.0;
188
+ if ( !isPerspective || ${REFERENCE_DEPTH_UNIFORM} <= 0.0 ) return 1.0;
189
+
190
+ return -viewZ / ${REFERENCE_DEPTH_UNIFORM};
191
+ }
192
+ `;
@@ -49,6 +49,12 @@ import { type TextImpl } from './Text';
49
49
  * Render it with {@link BatchedText}.
50
50
  */
51
51
  export declare class BatchedTextImpl extends TroikaBatchedText {
52
+ /**
53
+ * Marks this as a text batch. three's brand pattern, which `BatchedMeshImpl` inherits from three
54
+ * and `InstancedMeshImpl` declares for itself. troika ships no such flag on its own `BatchedText`,
55
+ * so this one is ours.
56
+ */
57
+ readonly isBatchedText = true;
52
58
  /** Per-instance uniform store, shared with {@link BatchedMeshImpl} and {@link InstancedMeshImpl}. */
53
59
  private readonly uniforms;
54
60
  /** The per-member transform and style rows the batch's shader draws from. */
@@ -51,6 +51,12 @@ import { MemberTexture } from './MemberTexture';
51
51
  * Render it with {@link BatchedText}.
52
52
  */
53
53
  export class BatchedTextImpl extends TroikaBatchedText {
54
+ /**
55
+ * Marks this as a text batch. three's brand pattern, which `BatchedMeshImpl` inherits from three
56
+ * and `InstancedMeshImpl` declares for itself. troika ships no such flag on its own `BatchedText`,
57
+ * so this one is ours.
58
+ */
59
+ isBatchedText = true;
54
60
  /** Per-instance uniform store, shared with {@link BatchedMeshImpl} and {@link InstancedMeshImpl}. */
55
61
  uniforms = new InstanceUniforms();
56
62
  /** The per-member transform and style rows the batch's shader draws from. */