@expofp/renderer 3.45.10 → 3.45.12

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.
@@ -351,7 +351,9 @@ function closeScaled(scaled) {
351
351
  *
352
352
  * Mipmaps stay on. A floor plan is zoomed far out as often as it is zoomed far in, and without
353
353
  * mipmaps every image shimmers under minification. The cost is that neighbors can bleed through
354
- * the deeper mip levels, where the packer's one-pixel gutter no longer separates them.
354
+ * the deeper mip levels. The packer's gap and `TextureMaterial`'s region clamp keep the first few
355
+ * levels clean (see `PADDING` in `pack-sources.ts`), and past those a strip stays about one screen
356
+ * pixel wide.
355
357
  * @param image - The packed atlas image: a canvas, or a bitmap a worker transferred out of one.
356
358
  * @returns The texture.
357
359
  */
@@ -114,12 +114,22 @@ export function packSources(sources, maxDimension, createCanvas = createAtlasCan
114
114
  return { atlases, reports };
115
115
  }
116
116
  /**
117
- * The gap between neighboring images on an atlas. This gap stops linear filtering at one image's
118
- * edge from reaching into the next image's pixels. The packer applies this gap *between* rects
119
- * only, so atlas edges stay flush against the pixels. This is also why a tile exactly
120
- * `maxDimension` across still fits in a bin.
117
+ * The gap between neighboring images on an atlas. This gap stops filtering at one image's edge from
118
+ * reaching into the next image's pixels. The packer applies this gap *between* rects only, so atlas
119
+ * edges stay flush against the pixels. This is also why a tile exactly `maxDimension` across still
120
+ * fits in a bin.
121
+ *
122
+ * One pixel is enough for the full-size texture, but not for its mipmaps. Mip level L averages
123
+ * blocks of 2^L pixels, so a block at an image's edge takes in the gap and, once the gap is
124
+ * narrower than the block, the neighbor too. That shows as a thin strip of the neighbor's color
125
+ * along the edge when zoomed out. `TextureMaterial` clamps each sample inside its own region, and
126
+ * with that clamp a gap of 2^L keeps levels 0 to L clean. Sixteen pixels cover levels 0 to 4, where
127
+ * an image draws at a sixteenth of its pixel size or larger. Four pixels were measured too few: a
128
+ * 256 px booth logo zoomed out to about 40 screen pixels read level 3 and showed the strip. Past
129
+ * level 4, the strip can come back, still about one screen pixel wide, against an image under 16
130
+ * screen pixels for a 256 px logo.
121
131
  */
122
- const PADDING = 1;
132
+ const PADDING = 16;
123
133
  /**
124
134
  * Packs one kind of source, either all resizable or all fixed, into atlases. The function tags
125
135
  * each atlas with the group's kind and records each source's tile count in `reports`.
@@ -24,7 +24,8 @@ export interface TextureMaterialProps {
24
24
  * instanced/batched mesh can pack many regions into one atlas.
25
25
  *
26
26
  * When {@link TextureMaterialProps.uvOffset} is set, the patch overwrites `vMapUv` after
27
- * `<uv_vertex>`, replacing the stock chunk's value. Instanced meshes wrap this `onBeforeCompile`
27
+ * `<uv_vertex>`, replacing the stock chunk's value, and samples the map clamped to that region (see
28
+ * {@link SAMPLE_REGION}). Instanced meshes wrap this `onBeforeCompile`
28
29
  * and inject their uniform fetch at the top of `main()`, so the uniform is in scope here.
29
30
  * @param props - The material's texture and optional uv-offset uniform name.
30
31
  * @returns The material element.
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { OneFactor } from 'three';
3
+ import { declareBeforeMain, replaceAnchor } from '../../core/shader-source';
3
4
  import { useLayerOverlay } from '../layer/layer-context';
4
5
  import { useDimPatch } from './effects/color-dim';
5
6
  import { overlayParams, sharedParams } from './shared';
@@ -8,7 +9,8 @@ import { overlayParams, sharedParams } from './shared';
8
9
  * instanced/batched mesh can pack many regions into one atlas.
9
10
  *
10
11
  * When {@link TextureMaterialProps.uvOffset} is set, the patch overwrites `vMapUv` after
11
- * `<uv_vertex>`, replacing the stock chunk's value. Instanced meshes wrap this `onBeforeCompile`
12
+ * `<uv_vertex>`, replacing the stock chunk's value, and samples the map clamped to that region (see
13
+ * {@link SAMPLE_REGION}). Instanced meshes wrap this `onBeforeCompile`
12
14
  * and inject their uniform fetch at the top of `main()`, so the uniform is in scope here.
13
15
  * @param props - The material's texture and optional uv-offset uniform name.
14
16
  * @returns The material element.
@@ -20,11 +22,15 @@ export function TextureMaterial(props) {
20
22
  function onBeforeCompile(parameters) {
21
23
  applyDim(parameters);
22
24
  if (uvOffset) {
23
- parameters.vertexShader = parameters.vertexShader.replace('#include <uv_vertex>',
25
+ parameters.vertexShader = declareBeforeMain(parameters.vertexShader, MAP_REGION_VARYING).replace('#include <uv_vertex>',
24
26
  /*glsl*/ `
25
27
  #include <uv_vertex>
26
28
  vMapUv = uv * ${uvOffset}.zw + ${uvOffset}.xy;
29
+ vMapRegion = ${uvOffset};
27
30
  `.trim());
31
+ // The region rides to the fragment shader as a varying, because the per-instance uniform
32
+ // fetch exists only in the vertex shader.
33
+ parameters.fragmentShader = replaceAnchor(declareBeforeMain(parameters.fragmentShader, `${MAP_REGION_VARYING}\n${SAMPLE_REGION}`), MAP_FRAGMENT, REGION_MAP_FRAGMENT);
28
34
  }
29
35
  if (opacityUniform) {
30
36
  // All four channels: the blend is premultiplied, so rgb already carries the texel's alpha and
@@ -46,3 +52,55 @@ export function TextureMaterial(props) {
46
52
  // premultiplied.
47
53
  return (_jsx("meshBasicMaterial", { ...sharedParams, ...(overlay ? overlayParams : null), blendSrc: OneFactor, map: map, onBeforeCompile: onBeforeCompile }));
48
54
  }
55
+ /**
56
+ * The atlas region an instance samples, carried from the vertex shader's per-instance uniform to the
57
+ * fragment shader. `xy` is the region's origin in uv space and `zw` its size. Flat, because the value
58
+ * is one per instance and needs no interpolation.
59
+ */
60
+ const MAP_REGION_VARYING = /* glsl */ `flat varying vec4 vMapRegion;`;
61
+ /**
62
+ * Samples an atlas without reading past the edge of one region.
63
+ *
64
+ * The GPU picks a mip level from how fast uv changes across the screen, and each level averages
65
+ * bigger blocks of the atlas. Near a region's edge, a sample can then take in the gap and the
66
+ * neighbor beyond it, which draws a thin strip of the neighbor's color. The function clamps uv to
67
+ * the region, inset by half a texel of the coarsest level the sample reads. Bilinear filtering at
68
+ * the clamped point then stays on blocks that hold this region's pixels. With trilinear filtering,
69
+ * the GPU blends the levels on both sides of the fractional level, so the inset uses the level
70
+ * above it.
71
+ *
72
+ * The clamp alone cannot keep a block from straddling the edge. That is what the packer's gap is
73
+ * for (see `PADDING` in `pack-sources.ts`), and the two together keep a level clean while the gap
74
+ * is at least one block wide.
75
+ *
76
+ * `textureGrad` takes the gradients of the unclamped uv. Gradients of the clamped uv fall to zero
77
+ * where the clamp holds, and the GPU would then read that edge at full resolution.
78
+ */
79
+ const SAMPLE_REGION = /* glsl */ `
80
+ vec4 sampleMapRegion( sampler2D atlas, vec2 uv, vec4 region ) {
81
+ vec2 size = vec2( textureSize( atlas, 0 ) );
82
+ vec2 gradX = dFdx( uv );
83
+ vec2 gradY = dFdy( uv );
84
+ vec2 texelsX = gradX * size;
85
+ vec2 texelsY = gradY * size;
86
+ float level = 0.5 * log2( max( dot( texelsX, texelsX ), dot( texelsY, texelsY ) ) );
87
+ vec2 inset = 0.5 * exp2( ceil( max( level, 0.0 ) ) ) / size;
88
+ // A region narrower than two insets clamps to its middle, because clamp() with a lower bound
89
+ // above its upper bound is undefined in GLSL.
90
+ vec2 middle = region.xy + 0.5 * region.zw;
91
+ vec2 low = min( region.xy + inset, middle );
92
+ vec2 high = max( region.xy + region.zw - inset, middle );
93
+ return textureGrad( atlas, clamp( uv, low, high ), gradX, gradY );
94
+ }
95
+ `;
96
+ /** Three's map chunk, which {@link REGION_MAP_FRAGMENT} takes the place of. */
97
+ const MAP_FRAGMENT = /* glsl */ `#include <map_fragment>`;
98
+ /**
99
+ * Three's `map_fragment`, with the sample taken through {@link SAMPLE_REGION}. The video decode
100
+ * branch is left out, because an atlas is drawn on a canvas and is never a video.
101
+ */
102
+ const REGION_MAP_FRAGMENT = /* glsl */ `
103
+ #ifdef USE_MAP
104
+ diffuseColor *= sampleMapRegion( map, vMapUv, vMapRegion );
105
+ #endif
106
+ `;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@expofp/renderer",
3
- "version": "3.45.10",
3
+ "version": "3.45.12",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/expofp/efp-app.git",
@@ -42,7 +42,7 @@
42
42
  "troika-three-text": "^0.52.5",
43
43
  "tslib": "^2.3.0",
44
44
  "webgl-memory": "^1.1.3",
45
- "@expofp/geometry": "3.45.10"
45
+ "@expofp/geometry": "3.45.12"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@react-three/test-renderer": "^9.1.0",