@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.
- package/README.md +1 -1
- package/dist/assets/fontWorker-JfTCpd38.js +2 -0
- package/dist/assets/fontWorker-JfTCpd38.js.map +1 -0
- package/dist/assets/{index-TKhRxurv.js → index-B3U_DvcP.js} +2 -2
- package/dist/assets/index-B3U_DvcP.js.map +1 -0
- package/dist/assets/{navara_wasm_font_worker_bg-Kk06ArRB.wasm → navara_wasm_font_worker_bg-DympKYVX.wasm} +0 -0
- package/dist/assets/{navara_wasm_worker_bg-BZjLKf4i.wasm → navara_wasm_worker_bg-Bx_l45cW.wasm} +0 -0
- package/dist/index.d.ts +510 -374
- package/dist/index.js +6150 -5810
- package/dist/index.js.map +1 -1
- package/dist/index.umd.cjs +1188 -793
- package/dist/index.umd.cjs.map +1 -1
- package/package.json +12 -12
- package/src/core/DescRegistry.ts +3 -2
- package/src/core/EffectDesc.ts +10 -0
- package/src/core/EffectDescRegistry.ts +14 -0
- package/src/core/MeshDesc.ts +39 -2
- package/src/core/MeshDescWithSelectiveEffect.ts +14 -2
- package/src/core/ViewContext.ts +116 -7
- package/src/device.ts +4 -2
- package/src/index.ts +123 -11
- package/src/layer.ts +7 -5
- package/src/layers/effect/MRTPassEffectDesc.ts +34 -3
- package/src/layers/effect/SelectiveEffectDesc.ts +10 -2
- package/src/layers/effect/TransparentPassEffectDesc.ts +5 -4
- package/src/layersManager.ts +32 -1
- package/src/material/enhancer/model/modelBaseEnhancer/shader.ts +3 -2
- package/src/material/enhancer/polygon/polygonBaseEnhancer/index.ts +0 -6
- package/src/material/enhancer/polygon/polygonBaseEnhancer/mutates.ts +0 -6
- package/src/material/enhancer/polygon/polygonBaseEnhancer/shader.ts +4 -10
- package/src/material/enhancer/polygon/polygonBaseEnhancer/state.ts +1 -4
- package/src/material/enhancer/polygon/polygonBaseEnhancer/types.ts +0 -9
- package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/coverage.ts +31 -0
- package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/shader.test.ts +75 -0
- package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/shader.ts +26 -0
- package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/types.ts +1 -1
- package/src/material/gbufferLayout.test.ts +171 -0
- package/src/material/gbufferLayout.ts +298 -0
- package/src/material/index.ts +1 -0
- package/src/material/macro/tileShader.test.ts +35 -33
- package/src/material/macro/tileShader.ts +33 -10
- package/src/material/overrideMaterialsForMRT.ts +113 -71
- package/src/mesh/DrapedMesh.test.ts +57 -0
- package/src/mesh/DrapedMesh.ts +82 -0
- package/src/mesh/model.ts +3 -0
- package/src/mesh/polygon.ts +3 -2
- package/src/mesh/polyline.ts +3 -0
- package/src/mesh/sdfText/layout.ts +1 -3
- package/src/mesh/tile/index.ts +8 -4
- package/src/passes/CustomRenderPass.test.ts +143 -0
- package/src/passes/CustomRenderPass.ts +273 -30
- package/src/passes/RenderTargetCopyPass.ts +6 -0
- package/src/plugins/AttributionPlugin/index.ts +3 -2
- package/src/shaders/index.ts +16 -0
- package/src/type/index.ts +9 -51
- package/dist/assets/fontWorker-OwvKiQ8o.js +0 -2
- package/dist/assets/fontWorker-OwvKiQ8o.js.map +0 -1
- 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
|
+
}
|
package/src/material/index.ts
CHANGED
|
@@ -4,11 +4,16 @@ import {
|
|
|
4
4
|
generateTileCommonInjection,
|
|
5
5
|
generateTileMapFragment,
|
|
6
6
|
generateTileNormalFragmentMaps,
|
|
7
|
-
|
|
8
|
-
|
|
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(
|
|
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
|
-
|
|
261
|
+
GBUFFER_WRITE_NORMAL(finalNormal, tileReflectivity, tileRoughness)
|
|
238
262
|
`;
|
|
239
263
|
|
|
240
264
|
/**
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
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
|
-
|
|
248
|
-
emissiveBuffer = vec4(diffuseColor.rgb * tileEmissiveIntensity + tileEmissiveColor, 1.0);
|
|
272
|
+
GBUFFER_WRITE_EFFECT(tileEffectIdsMask, diffuseColor.rgb * tileEmissiveIntensity + tileEmissiveColor)
|
|
249
273
|
} else {
|
|
250
|
-
|
|
251
|
-
emissiveBuffer = vec4(0.0);
|
|
274
|
+
GBUFFER_WRITE_EFFECT_ZERO
|
|
252
275
|
}
|
|
253
276
|
`;
|
|
254
277
|
|