@navaramap/three 0.0.5 → 0.0.6

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 (58) hide show
  1. package/README.md +1 -1
  2. package/dist/assets/fontWorker-JfTCpd38.js +2 -0
  3. package/dist/assets/fontWorker-JfTCpd38.js.map +1 -0
  4. package/dist/assets/{index-TKhRxurv.js → index-B3U_DvcP.js} +2 -2
  5. package/dist/assets/index-B3U_DvcP.js.map +1 -0
  6. package/dist/assets/{navara_wasm_font_worker_bg-Kk06ArRB.wasm → navara_wasm_font_worker_bg-DympKYVX.wasm} +0 -0
  7. package/dist/assets/{navara_wasm_worker_bg-BZjLKf4i.wasm → navara_wasm_worker_bg-Bx_l45cW.wasm} +0 -0
  8. package/dist/index.d.ts +510 -374
  9. package/dist/index.js +6150 -5810
  10. package/dist/index.js.map +1 -1
  11. package/dist/index.umd.cjs +1188 -793
  12. package/dist/index.umd.cjs.map +1 -1
  13. package/package.json +12 -12
  14. package/src/core/DescRegistry.ts +3 -2
  15. package/src/core/EffectDesc.ts +10 -0
  16. package/src/core/EffectDescRegistry.ts +14 -0
  17. package/src/core/MeshDesc.ts +39 -2
  18. package/src/core/MeshDescWithSelectiveEffect.ts +14 -2
  19. package/src/core/ViewContext.ts +116 -7
  20. package/src/device.ts +4 -2
  21. package/src/index.ts +123 -11
  22. package/src/layer.ts +7 -5
  23. package/src/layers/effect/MRTPassEffectDesc.ts +34 -3
  24. package/src/layers/effect/SelectiveEffectDesc.ts +10 -2
  25. package/src/layers/effect/TransparentPassEffectDesc.ts +5 -4
  26. package/src/layersManager.ts +32 -1
  27. package/src/material/enhancer/model/modelBaseEnhancer/shader.ts +3 -2
  28. package/src/material/enhancer/polygon/polygonBaseEnhancer/index.ts +0 -6
  29. package/src/material/enhancer/polygon/polygonBaseEnhancer/mutates.ts +0 -6
  30. package/src/material/enhancer/polygon/polygonBaseEnhancer/shader.ts +4 -10
  31. package/src/material/enhancer/polygon/polygonBaseEnhancer/state.ts +1 -4
  32. package/src/material/enhancer/polygon/polygonBaseEnhancer/types.ts +0 -9
  33. package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/coverage.ts +31 -0
  34. package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/shader.test.ts +75 -0
  35. package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/shader.ts +26 -0
  36. package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/types.ts +1 -1
  37. package/src/material/gbufferLayout.test.ts +171 -0
  38. package/src/material/gbufferLayout.ts +298 -0
  39. package/src/material/index.ts +1 -0
  40. package/src/material/macro/tileShader.test.ts +35 -33
  41. package/src/material/macro/tileShader.ts +33 -10
  42. package/src/material/overrideMaterialsForMRT.ts +113 -71
  43. package/src/mesh/DrapedMesh.test.ts +57 -0
  44. package/src/mesh/DrapedMesh.ts +82 -0
  45. package/src/mesh/model.ts +3 -0
  46. package/src/mesh/polygon.ts +3 -2
  47. package/src/mesh/polyline.ts +3 -0
  48. package/src/mesh/sdfText/layout.ts +1 -3
  49. package/src/mesh/tile/index.ts +8 -4
  50. package/src/passes/CustomRenderPass.test.ts +143 -0
  51. package/src/passes/CustomRenderPass.ts +273 -30
  52. package/src/passes/RenderTargetCopyPass.ts +6 -0
  53. package/src/plugins/AttributionPlugin/index.ts +3 -2
  54. package/src/shaders/index.ts +16 -0
  55. package/src/type/index.ts +9 -51
  56. package/dist/assets/fontWorker-OwvKiQ8o.js +0 -2
  57. package/dist/assets/fontWorker-OwvKiQ8o.js.map +0 -1
  58. package/dist/assets/index-TKhRxurv.js.map +0 -1
@@ -0,0 +1,298 @@
1
+ import GBufferParsFragmentChunk from "@shaders/glsl/chunks/gbuffer_pars_fragment.glsl";
2
+ import {
3
+ HalfFloatType,
4
+ NearestFilter,
5
+ UnsignedByteType,
6
+ type WebGLRenderTarget,
7
+ } from "three";
8
+
9
+ /**
10
+ * TS side of the screen G-buffer (MRT) layout. Everything that touches the
11
+ * layout goes through this module so a change stays a single-file edit; the
12
+ * GLSL mirror is `shaders/glsl/chunks/gbuffer_pars_fragment.glsl`.
13
+ */
14
+
15
+ /** The only attachment that always exists. The rest are packed after it. */
16
+ export const GBUFFER_TEXTURE_INDEX = {
17
+ color: 0,
18
+ } as const;
19
+
20
+ /** Names an effect can declare in `requiredBuffers`. */
21
+ export type GBufferName =
22
+ "normal" | "selectiveEffect" | "emissive" | "shadow" | "globeNormal";
23
+
24
+ /**
25
+ * The subset backed by an MRT attachment. `globeNormal` is not one of them —
26
+ * it is a separate copy target — so it must not take an attachment slot, a
27
+ * `*_LOCATION` define, or a `MAX_DRAW_BUFFERS` slot.
28
+ */
29
+ export const GBUFFER_ATTACHMENT_NAMES = [
30
+ "normal",
31
+ "selectiveEffect",
32
+ "emissive",
33
+ "shadow",
34
+ ] as const satisfies readonly GBufferName[];
35
+
36
+ /**
37
+ * Per-view buffer configuration. Not set by the user - derived as the union
38
+ * of the active effects' `requiredBuffers`. A disabled buffer costs nothing:
39
+ * neither the storage nor the shader-side writes exist. Only
40
+ * {@link GBUFFER_ATTACHMENT_NAMES} are MRT attachments.
41
+ */
42
+ export type GBufferOptions = {
43
+ /**
44
+ * The view-space normal buffer (RG=octahedral normal, B=metalness or
45
+ * reflectivity, A=roughness *and* the blend factor). Required by every
46
+ * normal-reading effect (aerial perspective, SSR, fog light) and, via
47
+ * {@link globeNormal}, by draped meshes.
48
+ */
49
+ normal?: boolean;
50
+ /**
51
+ * The selective-effect id buffer (R=bitmask), required by buffer-based
52
+ * selective effects (`selectiveBloom`, `selectiveOutline`).
53
+ */
54
+ selectiveEffect?: boolean;
55
+ /**
56
+ * The HDR emissive buffer (RGB=emissive), required by `selectiveBloom`
57
+ * extraction.
58
+ */
59
+ emissive?: boolean;
60
+ /**
61
+ * The shadow buffer (R=directional/CSM shadow amount, 0=lit,
62
+ * 1=fully shadowed). No built-in effect requires it – declare it from a
63
+ * custom effect's `requiredBuffers` to read per-pixel shadowing.
64
+ */
65
+ shadow?: boolean;
66
+ /**
67
+ * The globe normal copy (the terrain's view-space normal, sampled in screen
68
+ * space). Kept at 1x1 unless a draped mesh needs it, so an effect reading
69
+ * `getGlobeNormalTexture()` has to declare it.
70
+ */
71
+ globeNormal?: boolean;
72
+ };
73
+
74
+ /** {@link GBufferOptions} with defaults applied. */
75
+ export type ResolvedGBufferOptions = Readonly<Required<GBufferOptions>>;
76
+
77
+ /** Applies defaults (all disabled) to a partial configuration. */
78
+ export function resolveGBufferOptions(
79
+ options?: GBufferOptions,
80
+ ): ResolvedGBufferOptions {
81
+ const globeNormal = options?.globeNormal ?? false;
82
+ return {
83
+ // The globe normal is a copy of the normal attachment, so it cannot be
84
+ // produced without one. Applied here so every resolved configuration is
85
+ // self-consistent, whichever path built it.
86
+ normal: (options?.normal ?? false) || globeNormal,
87
+ selectiveEffect: options?.selectiveEffect ?? false,
88
+ emissive: options?.emissive ?? false,
89
+ shadow: options?.shadow ?? false,
90
+ globeNormal,
91
+ };
92
+ }
93
+
94
+ /**
95
+ * Derives the buffer configuration for a set of effect requirements
96
+ * (`EffectDesc.requiredBuffers` of every active effect).
97
+ */
98
+ export function unionGBufferRequirements(
99
+ requirements: Iterable<readonly GBufferName[]>,
100
+ ): ResolvedGBufferOptions {
101
+ const enabled: Record<GBufferName, boolean> = {
102
+ normal: false,
103
+ selectiveEffect: false,
104
+ emissive: false,
105
+ shadow: false,
106
+ globeNormal: false,
107
+ };
108
+ for (const required of requirements) {
109
+ for (const name of required) {
110
+ enabled[name] = true;
111
+ }
112
+ }
113
+ return resolveGBufferOptions(enabled);
114
+ }
115
+
116
+ /**
117
+ * Attachment indices for one view. A disabled buffer is absent rather than a
118
+ * placeholder, so these shift — never hardcode them.
119
+ */
120
+ export type GBufferTextureIndex = {
121
+ color: typeof GBUFFER_TEXTURE_INDEX.color;
122
+ /** Present only when `buffers.normal` is enabled. */
123
+ normal?: number;
124
+ /** Present only when `buffers.selectiveEffect` is enabled. */
125
+ effectIds?: number;
126
+ /** Present only when `buffers.emissive` is enabled. */
127
+ emissive?: number;
128
+ /** Present only when `buffers.shadow` is enabled. */
129
+ shadow?: number;
130
+ };
131
+
132
+ /** Computes the attachment indices for resolved `buffers` options. */
133
+ export function computeGBufferTextureIndex(
134
+ buffers: ResolvedGBufferOptions,
135
+ ): GBufferTextureIndex {
136
+ const index: GBufferTextureIndex = { ...GBUFFER_TEXTURE_INDEX };
137
+ let next: number = GBUFFER_TEXTURE_INDEX.color + 1;
138
+ // Attachment order of the optional buffers. Extend here (and mirror in
139
+ // appendOptionalGBufferAttachments) when adding a new optional buffer.
140
+ // `globeNormal` is absent on purpose: it is not an attachment.
141
+ for (const [option, key] of [
142
+ ["normal", "normal"],
143
+ ["selectiveEffect", "effectIds"],
144
+ ["emissive", "emissive"],
145
+ ["shadow", "shadow"],
146
+ ] as const) {
147
+ if (buffers[option]) {
148
+ index[key] = next++;
149
+ }
150
+ }
151
+ return index;
152
+ }
153
+
154
+ /**
155
+ * Inject once near the top of any fragment shader rendering into the
156
+ * G-buffer. Raw `.glsl` shaders `#include` the chunk instead.
157
+ */
158
+ export const GBUFFER_PARS_FRAGMENT: string = GBufferParsFragmentChunk;
159
+
160
+ /** Output location of the normal buffer. Mirrored in the GLSL chunk. */
161
+ export const GBUFFER_NORMAL_LOCATION_DEFINE = "GBUFFER_NORMAL_LOCATION";
162
+ /** Enables the effectIds output. Mirrored in the GLSL chunk. */
163
+ export const USE_GBUFFER_SELECTIVE_EFFECT_DEFINE =
164
+ "USE_GBUFFER_SELECTIVE_EFFECT";
165
+ /** Output location of the effectIds buffer. Mirrored in the GLSL chunk. */
166
+ export const GBUFFER_EFFECT_ID_LOCATION_DEFINE = "GBUFFER_EFFECT_ID_LOCATION";
167
+ /** Enables the emissive output. Mirrored in the GLSL chunk. */
168
+ export const USE_GBUFFER_EMISSIVE_DEFINE = "USE_GBUFFER_EMISSIVE";
169
+ /** Output location of the emissive buffer. Mirrored in the GLSL chunk. */
170
+ export const GBUFFER_EMISSIVE_LOCATION_DEFINE = "GBUFFER_EMISSIVE_LOCATION";
171
+ /** Enables the shadow output. Mirrored in the GLSL chunk. */
172
+ export const USE_GBUFFER_SHADOW_DEFINE = "USE_GBUFFER_SHADOW";
173
+ /** Output location of the shadow buffer. Mirrored in the GLSL chunk. */
174
+ export const GBUFFER_SHADOW_LOCATION_DEFINE = "GBUFFER_SHADOW_LOCATION";
175
+
176
+ /** Stampers iterate this so defines of disabled buffers are cleared, not left stale. */
177
+ export const GBUFFER_DEFINE_NAMES = [
178
+ GBUFFER_NORMAL_LOCATION_DEFINE,
179
+ USE_GBUFFER_SELECTIVE_EFFECT_DEFINE,
180
+ GBUFFER_EFFECT_ID_LOCATION_DEFINE,
181
+ USE_GBUFFER_EMISSIVE_DEFINE,
182
+ GBUFFER_EMISSIVE_LOCATION_DEFINE,
183
+ USE_GBUFFER_SHADOW_DEFINE,
184
+ GBUFFER_SHADOW_LOCATION_DEFINE,
185
+ ] as const;
186
+
187
+ /** Defines enabling the optional outputs and carrying their locations. */
188
+ export function computeGBufferDefines(
189
+ buffers: ResolvedGBufferOptions,
190
+ ): Readonly<Record<string, number>> {
191
+ const index = computeGBufferTextureIndex(buffers);
192
+ const defines: Record<string, number> = {};
193
+ if (index.normal !== undefined) {
194
+ defines[GBUFFER_NORMAL_LOCATION_DEFINE] = index.normal;
195
+ }
196
+ if (index.effectIds !== undefined) {
197
+ defines[USE_GBUFFER_SELECTIVE_EFFECT_DEFINE] = 1;
198
+ defines[GBUFFER_EFFECT_ID_LOCATION_DEFINE] = index.effectIds;
199
+ }
200
+ if (index.emissive !== undefined) {
201
+ defines[USE_GBUFFER_EMISSIVE_DEFINE] = 1;
202
+ defines[GBUFFER_EMISSIVE_LOCATION_DEFINE] = index.emissive;
203
+ }
204
+ if (index.shadow !== undefined) {
205
+ defines[USE_GBUFFER_SHADOW_DEFINE] = 1;
206
+ defines[GBUFFER_SHADOW_LOCATION_DEFINE] = index.shadow;
207
+ }
208
+ return defines;
209
+ }
210
+
211
+ // Write snippets. Exported rather than inlined at the injection sites because
212
+ // enhancers and the tile mesh locate the injected code by exact string match,
213
+ // so both sides must share one constant.
214
+
215
+ /**
216
+ * `normalBuffer` write for physically-based materials (standard/physical).
217
+ * `modelBaseEnhancer` matches/replaces this exact string.
218
+ */
219
+ export const GBUFFER_NORMAL_WRITE_PHYSICAL =
220
+ "GBUFFER_WRITE_NORMAL(normal, metalnessFactor, roughnessFactor)";
221
+
222
+ /**
223
+ * `normalBuffer` write for non-physical materials (basic/lambert/phong).
224
+ * `polygonBaseEnhancer` matches/replaces this exact string.
225
+ */
226
+ export const GBUFFER_NORMAL_WRITE_BASIC =
227
+ "GBUFFER_WRITE_NORMAL(normal, reflectivity, roughnessFactor)";
228
+
229
+ /**
230
+ * Selective-effect writes for built-in `ShaderLib` materials. The tile mesh
231
+ * replaces this by exact string match with
232
+ * `TILE_EMISSIVE_EFFECT_BUFFER_REPLACEMENT`.
233
+ *
234
+ * Effect writes are always macro invocations: the macros own the alpha, which
235
+ * is the per-attachment blend factor rather than data (see the pars chunk).
236
+ */
237
+ export const GBUFFER_EFFECT_WRITE_BUILTIN = `GBUFFER_WRITE_EFFECT(uEffectIdsMask, diffuseColor.rgb * uEmissiveIntensity + emissive)`;
238
+
239
+ /**
240
+ * Selective-effect writes for custom `ShaderMaterial`
241
+ * (emissive = (fragColor + uEmissiveColor) × intensity).
242
+ */
243
+ export const GBUFFER_EFFECT_WRITE_SHADER_MATERIAL = `GBUFFER_WRITE_EFFECT(uEffectIdsMask, (gl_FragColor.rgb + uEmissiveColor) * uEmissiveIntensity)`;
244
+
245
+ /**
246
+ * Selective-effect writes for materials that support the effect-id mask but
247
+ * not emissive (LineMaterial, sprite, points).
248
+ */
249
+ export const GBUFFER_EFFECT_WRITE_ID_ONLY = `GBUFFER_WRITE_EFFECT_ID_ONLY(uEffectIdsMask)`;
250
+
251
+ /** Non-selective default — A=0.0, so a blended mesh keeps what is behind it. */
252
+ export const GBUFFER_EFFECT_WRITE_ZERO = `GBUFFER_WRITE_EFFECT_ZERO`;
253
+
254
+ /**
255
+ * Allocates the extra attachments in {@link computeGBufferTextureIndex} order.
256
+ * Disabled buffers get no placeholder texture.
257
+ */
258
+ export function createGBufferAttachments(
259
+ renderTarget: WebGLRenderTarget,
260
+ buffers: ResolvedGBufferOptions,
261
+ ): void {
262
+ appendOptionalGBufferAttachments(renderTarget, buffers);
263
+ }
264
+
265
+ /** Split out so `CustomRenderPass` can rebuild a target around a reused color. */
266
+ export function appendOptionalGBufferAttachments(
267
+ renderTarget: WebGLRenderTarget,
268
+ buffers: ResolvedGBufferOptions,
269
+ ): void {
270
+ if (buffers.normal) {
271
+ renderTarget.textures.push(renderTarget.texture.clone());
272
+ }
273
+
274
+ if (buffers.selectiveEffect) {
275
+ // Discrete bitmask data: HalfFloat is exact to 11 bits (0..2047), and
276
+ // interpolating between two masks would invent a third.
277
+ const effectIdsTexture = renderTarget.texture.clone();
278
+ effectIdsTexture.type = HalfFloatType;
279
+ effectIdsTexture.minFilter = NearestFilter;
280
+ effectIdsTexture.magFilter = NearestFilter;
281
+ effectIdsTexture.generateMipmaps = false;
282
+ renderTarget.textures.push(effectIdsTexture);
283
+ }
284
+
285
+ if (buffers.emissive) {
286
+ // HalfFloat keeps the HDR range bloom extraction thresholds against.
287
+ const emissiveTexture = renderTarget.texture.clone();
288
+ emissiveTexture.type = HalfFloatType;
289
+ renderTarget.textures.push(emissiveTexture);
290
+ }
291
+
292
+ if (buffers.shadow) {
293
+ // 8 bits is plenty for a shadow factor.
294
+ const shadowTexture = renderTarget.texture.clone();
295
+ shadowTexture.type = UnsignedByteType;
296
+ renderTarget.textures.push(shadowTexture);
297
+ }
298
+ }
@@ -1,3 +1,4 @@
1
1
  export * from "./enhancer";
2
+ export * from "./gbufferLayout";
2
3
  export * from "./macro";
3
4
  export * from "./overrideMaterialsForMRT";
@@ -4,11 +4,16 @@ import {
4
4
  generateTileCommonInjection,
5
5
  generateTileMapFragment,
6
6
  generateTileNormalFragmentMaps,
7
- TILE_EMISSIVE_EFFECT_BUFFER_REPLACEMENT,
8
- TILE_NORMAL_BUFFER_REPLACEMENT,
9
- TILE_PICK_FRAGMENT_OVERRIDE,
7
+ type TileShaderFeatures,
8
+ WATERMASK_OCEAN_REFLECTIVITY,
10
9
  } from "./tileShader";
11
10
 
11
+ const FEATURES_OFF: TileShaderFeatures = {
12
+ hasHillshade: false,
13
+ hasWater: false,
14
+ hasWatermask: false,
15
+ };
16
+
12
17
  describe("generateTileCommonInjection", () => {
13
18
  it("declares the three atlas samplers", () => {
14
19
  const src = generateTileCommonInjection(8);
@@ -26,7 +31,7 @@ describe("generateTileCommonInjection", () => {
26
31
  });
27
32
 
28
33
  describe("generateTileMapFragment", () => {
29
- const src = generateTileMapFragment(16);
34
+ const src = generateTileMapFragment(16, FEATURES_OFF);
30
35
 
31
36
  it("samples each atlas exactly once", () => {
32
37
  const colorMatches = src.match(/texture2D\(uColorAtlas/g) ?? [];
@@ -65,11 +70,34 @@ describe("generateTileMapFragment", () => {
65
70
  );
66
71
  expect(src).not.toMatch(/mix\(diffuseColor\.rgb,\s*atlasColor\.rgb/);
67
72
  });
73
+
74
+ it("omits the ocean reflectivity fallback when no watermask is present", () => {
75
+ expect(src).not.toContain("useWater && !hasSlotWaterParams");
76
+ });
77
+
78
+ it("overrides reflectivity/roughness on watermask water pixels", () => {
79
+ // Watermask water under a raster winner (or no winner – open ocean) must
80
+ // clear the SSR mask threshold; a texturized (vector) winner keeps its own
81
+ // per-slot params.
82
+ const masked = generateTileMapFragment(16, {
83
+ ...FEATURES_OFF,
84
+ hasWatermask: true,
85
+ });
86
+ expect(masked).toContain("if (useWater && !hasSlotWaterParams)");
87
+ expect(masked).toContain(
88
+ `tileReflectivity = ${WATERMASK_OCEAN_REFLECTIVITY.toFixed(4)};`,
89
+ );
90
+ // The override must land AFTER the per-slot reads so it wins.
91
+ expect(
92
+ masked.indexOf("if (useWater && !hasSlotWaterParams)"),
93
+ ).toBeGreaterThan(masked.indexOf("uReflectivities[winIdx]"));
94
+ });
68
95
  });
69
96
 
70
97
  describe("generateTileNormalFragmentMaps", () => {
71
98
  it("omits the hillshade lookup entirely when hasHillshade is off", () => {
72
99
  const src = generateTileNormalFragmentMaps({
100
+ ...FEATURES_OFF,
73
101
  hasHillshade: false,
74
102
  hasWater: false,
75
103
  });
@@ -79,6 +107,7 @@ describe("generateTileNormalFragmentMaps", () => {
79
107
 
80
108
  it("samples uNormalAtlas exactly once when hasHillshade is on", () => {
81
109
  const src = generateTileNormalFragmentMaps({
110
+ ...FEATURES_OFF,
82
111
  hasHillshade: true,
83
112
  hasWater: false,
84
113
  });
@@ -90,6 +119,7 @@ describe("generateTileNormalFragmentMaps", () => {
90
119
 
91
120
  it("omits water specular call when hasWater is off", () => {
92
121
  const src = generateTileNormalFragmentMaps({
122
+ ...FEATURES_OFF,
93
123
  hasHillshade: false,
94
124
  hasWater: false,
95
125
  });
@@ -99,38 +129,10 @@ describe("generateTileNormalFragmentMaps", () => {
99
129
 
100
130
  it("emits computeWaterSpecular when hasWater is on", () => {
101
131
  const src = generateTileNormalFragmentMaps({
132
+ ...FEATURES_OFF,
102
133
  hasHillshade: false,
103
134
  hasWater: true,
104
135
  });
105
136
  expect(src).toContain("computeWaterSpecular(");
106
137
  });
107
138
  });
108
-
109
- describe("MRT buffer replacements", () => {
110
- it("writes finalNormal into normalBuffer with reflectivity/roughness packed", () => {
111
- expect(TILE_NORMAL_BUFFER_REPLACEMENT).toContain("normalBuffer = vec4(");
112
- expect(TILE_NORMAL_BUFFER_REPLACEMENT).toContain("tileReflectivity");
113
- expect(TILE_NORMAL_BUFFER_REPLACEMENT).toContain("tileRoughness");
114
- });
115
-
116
- it("gates emissive/effect on isTexturizedLayer", () => {
117
- expect(TILE_EMISSIVE_EFFECT_BUFFER_REPLACEMENT).toContain(
118
- "if (isTexturizedLayer)",
119
- );
120
- expect(TILE_EMISSIVE_EFFECT_BUFFER_REPLACEMENT).toContain(
121
- "effectIdBuffer = vec4(0.0);",
122
- );
123
- });
124
-
125
- it("pick override writes nvr_pickColor straight to gl_FragColor", () => {
126
- // Output is the LAST write of the fragment shader so envmap/tonemapping/
127
- // colorspace/fog can't pollute the pick buffer with lit colours. The
128
- // value comes from the attr.g-masked atlas (see generateTileMapFragment)
129
- // — raster pixels become batchId=0 (no entity); vector pixels carry the
130
- // ID their own per-layer enhancer wrote during the per-layer RT pass.
131
- expect(TILE_PICK_FRAGMENT_OVERRIDE).toContain("if (uPickable > 0.)");
132
- expect(TILE_PICK_FRAGMENT_OVERRIDE).toContain(
133
- "gl_FragColor = vec4(nvr_pickColor, 1.0);",
134
- );
135
- });
136
- });
@@ -20,8 +20,16 @@ export type TileShaderFeatures = {
20
20
  hasHillshade: boolean;
21
21
  /** Some layer uses Water material → emit the water specular branch. */
22
22
  hasWater: boolean;
23
+ /**
24
+ * Quantized-mesh watermask is present (source requested `requestWaterMask`).
25
+ * Emits the ocean reflectivity fallback in the map fragment.
26
+ */
27
+ hasWatermask: boolean;
23
28
  };
24
29
 
30
+ export const WATERMASK_OCEAN_REFLECTIVITY = 0.02;
31
+ export const WATERMASK_OCEAN_ROUGHNESS = 0.2;
32
+
25
33
  /**
26
34
  * Goes after `#include <common>` in the fragment shader. Declares the three
27
35
  * atlas samplers + the per-slot uniform arrays we still index by winning
@@ -71,7 +79,23 @@ export function generateTileCommonInjection(numTextures: number): string {
71
79
  * generateTileEmissiveBuffer below to decide what gets written to the
72
80
  * emissive/effect MRT attachments).
73
81
  */
74
- export function generateTileMapFragment(maxTextures: number): string {
82
+ export function generateTileMapFragment(
83
+ maxTextures: number,
84
+ features: TileShaderFeatures,
85
+ ): string {
86
+ // Watermask ocean pixels: `useWater` fired from the tile-wide watermask but
87
+ // the winning slot (raster imagery, or none on open ocean) carries zeroed
88
+ // reflectivity/roughness – override them with the default ocean appearance
89
+ // so SSR and envmap reflections apply to the sea automatically. Mirrors the
90
+ // shininess/specularStrength fallback above. A texturized winner keeps its
91
+ // own params: a vector layer drawn over water should not turn reflective.
92
+ const watermaskReflectivityFallback = features.hasWatermask
93
+ ? `
94
+ if (useWater && !hasSlotWaterParams) {
95
+ tileReflectivity = ${WATERMASK_OCEAN_REFLECTIVITY.toFixed(4)};
96
+ tileRoughness = ${WATERMASK_OCEAN_ROUGHNESS.toFixed(4)};
97
+ }`
98
+ : "";
75
99
  return `
76
100
  vec4 atlasColor = texture2D(uColorAtlas, vOrigUv);
77
101
  vec4 atlasAttr = texture2D(uAttrAtlas, vOrigUv);
@@ -135,7 +159,7 @@ export function generateTileMapFragment(maxTextures: number): string {
135
159
  // letting the winner's own specular flag gate it — otherwise a draped raster
136
160
  // imagery slot (uSpeculars == false) suppresses the watermask water entirely.
137
161
  bool useSpecular = (!isTexturizedLayer && useWater) || (hasWinner && uSpeculars[winIdx]);
138
-
162
+ ${watermaskReflectivityFallback}
139
163
  float tileEmissiveIntensity = (hasWinner && isTexturizedLayer) ? uEmissiveIntensities[winIdx] : 0.0;
140
164
  vec3 tileEmissiveColor = (hasWinner && isTexturizedLayer) ? uEmissiveColors[winIdx] : vec3(0.0);
141
165
  float tileEffectIdsMask = (hasWinner && isTexturizedLayer) ? uEffectIdsMasks[winIdx] : 0.0;
@@ -234,21 +258,20 @@ export function generateTileNormalFragmentMaps(
234
258
  */
235
259
  export const TILE_NORMAL_BUFFER_REPLACEMENT = `
236
260
  vec3 finalNormal = mix(origNormal, normalize(origNormal * 0.7 + normal), applyWaterNormals);
237
- normalBuffer = vec4(packNormalToVec2(finalNormal), tileReflectivity, tileRoughness);
261
+ GBUFFER_WRITE_NORMAL(finalNormal, tileReflectivity, tileRoughness)
238
262
  `;
239
263
 
240
264
  /**
241
- * `effectIdBuffer = …; emissiveBuffer = …;` replacement. Only the
242
- * texturized-layer pixels write effect/emissive; raster pixels stay 0 so they
243
- * don't perturb downstream effect-mask passes.
265
+ * Effect/emissive write replacement, via the `GBUFFER_WRITE_EFFECT*` macros
266
+ * from the pars chunk (each buffer's write is compiled out when the view
267
+ * disables it). Only the texturized-layer pixels write effect/emissive;
268
+ * raster pixels stay 0 so they don't perturb downstream effect-mask passes.
244
269
  */
245
270
  export const TILE_EMISSIVE_EFFECT_BUFFER_REPLACEMENT = `
246
271
  if (isTexturizedLayer) {
247
- effectIdBuffer = vec4(tileEffectIdsMask, 0.0, 0.0, 1.0);
248
- emissiveBuffer = vec4(diffuseColor.rgb * tileEmissiveIntensity + tileEmissiveColor, 1.0);
272
+ GBUFFER_WRITE_EFFECT(tileEffectIdsMask, diffuseColor.rgb * tileEmissiveIntensity + tileEmissiveColor)
249
273
  } else {
250
- effectIdBuffer = vec4(0.0);
251
- emissiveBuffer = vec4(0.0);
274
+ GBUFFER_WRITE_EFFECT_ZERO
252
275
  }
253
276
  `;
254
277