react-native-cesium 1.2.7 → 1.2.8

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.
@@ -62,7 +62,7 @@ struct OverlayParamsCPP {
62
62
  float wmTransX; // offset 48
63
63
  float wmTransY; // offset 52
64
64
  float wmScale; // offset 56
65
- float _pad1; // offset 60
65
+ float lodFade; // offset 60 — LOD dither threshold
66
66
  // RTC centre in absolute ECEF (float3). Fragment shader reconstructs world
67
67
  // position as: wp = in.localPos + rtcCentre (see shader).
68
68
  float rtcCenterX; // offset 64
@@ -125,7 +125,7 @@ struct OverlayParams {
125
125
  float wmWest, wmSouth, wmEast, wmNorth;
126
126
  float wmTransX, wmTransY;
127
127
  float wmScale;
128
- float _pad1;
128
+ float lodFade; // LOD dither threshold (was _pad1)
129
129
  // RTC centre in ECEF (float). wp = in.localPos + rtcCentre gives a world
130
130
  // position with ~0.75 m ULP (vs ~4 m for eyeRelPos + cameraEcef at altitude).
131
131
  float rtcCenterX, rtcCenterY, rtcCenterZ;
@@ -157,6 +157,20 @@ fragment float4 terrainFragment(
157
157
  texture2d<float> overlayTex [[texture(0)]],
158
158
  texture2d<float> waterMaskTex [[texture(1)]])
159
159
  {
160
+ #ifdef TERRAIN_DITHER
161
+ // ── LOD transition dither (IGN / Jorge Jimenez) ──────────────────────────
162
+ // Compiled only into the dither pipeline so the solid pipeline keeps Apple
163
+ // TBDR Hidden Surface Removal enabled at full speed. Interleaved Gradient
164
+ // Noise produces a fine-grained per-pixel threshold approximating blue noise
165
+ // without a texture lookup; the pattern is far less perceptible than Bayer.
166
+ // lodFade=1 → all pass; lodFade=0 → all discard.
167
+ {
168
+ float ign = fract(52.9829189f *
169
+ fract(0.06711056f * in.position.x + 0.00583715f * in.position.y));
170
+ if (ov.lodFade <= ign) { discard_fragment(); }
171
+ }
172
+ #endif
173
+
160
174
  constexpr sampler s(filter::linear, address::clamp_to_edge);
161
175
 
162
176
  // ── World position (RTC reconstruction) ──────────────────────────────────
@@ -190,14 +204,19 @@ fragment float4 terrainFragment(
190
204
  ? 1.0f
191
205
  : waterMaskTex.sample(s, wmUV).r;
192
206
 
193
- float3 lit;
194
-
195
207
  if (ov.hasOverlay != 0u) {
196
208
  float2 texUV = in.uv * float2(ov.scale[0], ov.scale[1])
197
209
  + float2(ov.translation[0], ov.translation[1]);
198
210
  texUV.y = 1.0f - texUV.y;
199
- lit = overlayTex.sample(s, texUV).rgb;
200
- } else {
211
+ // Return the overlay RGBA directly — preserves any transparency at tile
212
+ // edges and avoids incorrectly stamping the coastline outline on top of
213
+ // satellite imagery. The dither discard above fires before this return,
214
+ // so LOD fading still works correctly on overlay tiles.
215
+ return overlayTex.sample(s, texUV);
216
+ }
217
+
218
+ float3 lit;
219
+ {
201
220
  // ── Lighting ──────────────────────────────────────────────────────────
202
221
  // Normals from screen-space derivatives of tile-local position.
203
222
  // (Using localPos rather than eyeRelPos makes no difference for the
@@ -237,21 +256,28 @@ fragment float4 terrainFragment(
237
256
 
238
257
  // 1 km lat/lon grid — fades when sub-pixel to suppress Moiré.
239
258
  float gridLat = 1000.f / 6371000.f;
240
- float cosLat = max(cos(lat), 0.001f);
259
+ // cosLat from pxy (already computed above for the Bowring iteration) —
260
+ // avoids a second cos() transcendental. pxy/a_e is the geocentric cosine;
261
+ // the ~0.2 % geodetic difference is imperceptible at grid-line scale.
262
+ float cosLat = max(pxy / 6378137.f, 0.001f);
241
263
  float gridLon = gridLat / cosLat;
242
264
  float wLat = fwidth(lat) * 1.5f, wLon = fwidth(lon) * 1.5f;
243
265
  float gridVis = clamp(min(gridLat / max(wLat, 1e-9f),
244
266
  gridLon / max(wLon, 1e-9f)), 0.f, 1.f);
267
+ // MSL fmod() uses truncation (not floor), which returns negative values for
268
+ // negative latitudes (southern hemisphere) — use the floor formula.
245
269
  float latCell = lat - gridLat * floor(lat / gridLat);
246
270
  float lonCell = lon - gridLon * floor(lon / gridLon);
247
271
  float dLat = min(latCell, gridLat - latCell);
248
272
  float dLon = min(lonCell, gridLon - lonCell);
249
- float gridA = max(1.f - smoothstep(0.f, wLat, dLat),
250
- 1.f - smoothstep(0.f, wLon, dLon)) * gridVis;
273
+ // Linear ramp instead of smoothstep cubic — visually identical for a
274
+ // 1.5 px anti-aliased line, saves two multiplies per component.
275
+ float gridA = max(clamp(1.f - dLat / max(wLat, 1e-9f), 0.f, 1.f),
276
+ clamp(1.f - dLon / max(wLon, 1e-9f), 0.f, 1.f)) * gridVis;
251
277
  lit = mix(lit, float3(0.15f, 0.15f, 0.15f), gridA * 0.6f);
252
278
  }
253
279
 
254
- // Coastline outline from water mask (magenta for easy diagnostics).
280
+ // Coastline outline from water mask.
255
281
  if (ov.isEllipsoidFallback == 0u) {
256
282
  float dWater = fwidth(waterVal);
257
283
  float coastPx = abs(waterVal - 0.5f) / max(dWater * 0.5f, 0.01f);
@@ -373,7 +399,8 @@ namespace reactnativecesium {
373
399
 
374
400
  MetalBackend::MetalBackend()
375
401
  : device_(nullptr), commandQueue_(nullptr), metalLayer_(nullptr),
376
- terrainPipeline_(nullptr), skyPipeline_(nullptr),
402
+ terrainSolidPipeline_(nullptr), terrainDitherPipeline_(nullptr),
403
+ skyPipeline_(nullptr),
377
404
  terrainDepthState_(nullptr), skyDepthState_(nullptr),
378
405
  depthTexture_(nullptr), fallbackTexture_(nullptr),
379
406
  fallbackWaterMaskTexture_(nullptr),
@@ -383,6 +410,8 @@ MetalBackend::MetalBackend()
383
410
  for (int i = 0; i < kMaxFramesInFlight; ++i) {
384
411
  vtxBufs_[i] = idxBufs_[i] = uvBufs_[i] = altBufs_[i] = nullptr;
385
412
  vtxCaps_[i] = idxCaps_[i] = uvCaps_[i] = altCaps_[i] = 0;
413
+ uniformsBufs_[i] = nullptr;
414
+ uniformsCaps_[i] = 0;
386
415
  }
387
416
  }
388
417
 
@@ -418,40 +447,68 @@ void MetalBackend::buildRenderPipelines() {
418
447
  CAMetalLayer* layer = (__bridge CAMetalLayer*)metalLayer_;
419
448
  const MTLPixelFormat colorFmt = layer.pixelFormat;
420
449
 
421
- if (terrainPipeline_) {
422
- CFRelease(terrainPipeline_);
423
- terrainPipeline_ = nullptr;
450
+ if (terrainSolidPipeline_) {
451
+ CFRelease(terrainSolidPipeline_);
452
+ terrainSolidPipeline_ = nullptr;
453
+ }
454
+ if (terrainDitherPipeline_) {
455
+ CFRelease(terrainDitherPipeline_);
456
+ terrainDitherPipeline_ = nullptr;
424
457
  }
425
458
  if (skyPipeline_) {
426
459
  CFRelease(skyPipeline_);
427
460
  skyPipeline_ = nullptr;
428
461
  }
429
462
 
430
- // Use a separate NSError* per compile + a strict result check. Sharing a
431
- // single `err` across multiple operations conflated unrelated failures
432
- // (P2-error-handling).
433
- NSError* terrainLibErr = nil;
434
- id<MTLLibrary> tLib = [dev newLibraryWithSource:kTerrainShaderSrc
435
- options:nil error:&terrainLibErr];
436
- if (!tLib) {
437
- NSLog(@"[MetalBackend] terrain shader compilation FAILED: %@", terrainLibErr);
438
- return;
439
- }
463
+ // Build two terrain pipelines from a single MSL source:
464
+ // • Solid: compiled without TERRAIN_DITHER → no discard_fragment() in the
465
+ // shader, so Apple TBDR Hidden Surface Removal stays enabled.
466
+ // Used for every tile whose LOD-fade percentage is 1.0 (i.e. the
467
+ // overwhelming majority outside an active LOD transition).
468
+ // • Dither: compiled with TERRAIN_DITHER=1 → includes the IGN-based dither
469
+ // discard. Only used for tiles where DrawPrimitive::lodFade<1.
470
+ //
471
+ // Engineering trade-off documented in CesiumEngine::updateFrame: the draws
472
+ // are stable_partition-ed so all solid tiles come first; a single pipeline
473
+ // switch per frame is then enough to render the fading tail.
474
+ auto buildTerrainPipeline =
475
+ ^id<MTLRenderPipelineState>(NSDictionary<NSString*, NSObject*>* macros,
476
+ NSString* label) {
477
+ MTLCompileOptions* opts = [MTLCompileOptions new];
478
+ if (macros) opts.preprocessorMacros = macros;
479
+ NSError* libErr = nil;
480
+ id<MTLLibrary> lib = [dev newLibraryWithSource:kTerrainShaderSrc
481
+ options:opts error:&libErr];
482
+ if (!lib) {
483
+ NSLog(@"[MetalBackend] terrain %@ shader compilation FAILED: %@",
484
+ label, libErr);
485
+ return nil;
486
+ }
487
+ MTLRenderPipelineDescriptor* desc = [MTLRenderPipelineDescriptor new];
488
+ desc.label = [@"terrain-" stringByAppendingString:label];
489
+ desc.vertexFunction = [lib newFunctionWithName:@"terrainVertex"];
490
+ desc.fragmentFunction = [lib newFunctionWithName:@"terrainFragment"];
491
+ desc.colorAttachments[0].pixelFormat = colorFmt;
492
+ desc.depthAttachmentPixelFormat = MTLPixelFormatDepth32Float;
493
+ desc.rasterSampleCount = (NSUInteger)std::max(1, sampleCount_);
494
+ NSError* pipeErr = nil;
495
+ id<MTLRenderPipelineState> pipe =
496
+ [dev newRenderPipelineStateWithDescriptor:desc error:&pipeErr];
497
+ if (!pipe) {
498
+ NSLog(@"[MetalBackend] terrain %@ pipeline build FAILED: %@",
499
+ label, pipeErr);
500
+ }
501
+ return pipe;
502
+ };
440
503
 
441
- MTLRenderPipelineDescriptor* tDesc = [MTLRenderPipelineDescriptor new];
442
- tDesc.vertexFunction = [tLib newFunctionWithName:@"terrainVertex"];
443
- tDesc.fragmentFunction = [tLib newFunctionWithName:@"terrainFragment"];
444
- tDesc.colorAttachments[0].pixelFormat = colorFmt;
445
- tDesc.depthAttachmentPixelFormat = MTLPixelFormatDepth32Float;
446
- tDesc.rasterSampleCount = (NSUInteger)std::max(1, sampleCount_);
447
- NSError* terrainPipelineErr = nil;
448
- id<MTLRenderPipelineState> terrainPipeline =
449
- [dev newRenderPipelineStateWithDescriptor:tDesc error:&terrainPipelineErr];
450
- if (!terrainPipeline) {
451
- NSLog(@"[MetalBackend] terrain pipeline build FAILED: %@", terrainPipelineErr);
452
- return;
453
- }
454
- terrainPipeline_ = (__bridge_retained void*)terrainPipeline;
504
+ id<MTLRenderPipelineState> solidPipeline = buildTerrainPipeline(nil, @"solid");
505
+ if (!solidPipeline) return;
506
+ terrainSolidPipeline_ = (__bridge_retained void*)solidPipeline;
507
+
508
+ id<MTLRenderPipelineState> ditherPipeline = buildTerrainPipeline(
509
+ @{ @"TERRAIN_DITHER": @"1" }, @"dither");
510
+ if (!ditherPipeline) return;
511
+ terrainDitherPipeline_ = (__bridge_retained void*)ditherPipeline;
455
512
 
456
513
  NSError* skyLibErr = nil;
457
514
  id<MTLLibrary> sLib = [dev newLibraryWithSource:kSkyShaderSrc
@@ -562,12 +619,15 @@ void MetalBackend::shutdown() {
562
619
  if (idxBufs_[i]) { CFRelease(idxBufs_[i]); idxBufs_[i] = nullptr; }
563
620
  if (uvBufs_[i]) { CFRelease(uvBufs_[i]); uvBufs_[i] = nullptr; }
564
621
  if (altBufs_[i]) { CFRelease(altBufs_[i]); altBufs_[i] = nullptr; }
622
+ if (uniformsBufs_[i]) { CFRelease(uniformsBufs_[i]); uniformsBufs_[i] = nullptr; }
623
+ uniformsCaps_[i] = 0;
565
624
  vtxCaps_[i] = idxCaps_[i] = uvCaps_[i] = altCaps_[i] = 0;
566
625
  }
567
626
 
568
627
  if (device_) { CFRelease(device_); device_ = nullptr; }
569
628
  if (commandQueue_) { CFRelease(commandQueue_); commandQueue_ = nullptr; }
570
- if (terrainPipeline_) { CFRelease(terrainPipeline_); terrainPipeline_ = nullptr; }
629
+ if (terrainSolidPipeline_) { CFRelease(terrainSolidPipeline_); terrainSolidPipeline_ = nullptr; }
630
+ if (terrainDitherPipeline_) { CFRelease(terrainDitherPipeline_); terrainDitherPipeline_ = nullptr; }
571
631
  if (skyPipeline_) { CFRelease(skyPipeline_); skyPipeline_ = nullptr; }
572
632
  if (terrainDepthState_) { CFRelease(terrainDepthState_); terrainDepthState_ = nullptr; }
573
633
  if (skyDepthState_) { CFRelease(skyDepthState_); skyDepthState_ = nullptr; }
@@ -763,38 +823,75 @@ void MetalBackend::drawScene(const FrameResult& frame) {
763
823
  }
764
824
 
765
825
  // ── Terrain pass ──────────────────────────────────────────────────────────
766
- if (frame.draws.empty() || frame.localPositions.empty() || frame.indices.empty() ||
767
- !terrainPipeline_ || !terrainDepthState_) {
826
+ if (frame.draws.empty() ||
827
+ !terrainSolidPipeline_ || !terrainDitherPipeline_ || !terrainDepthState_) {
768
828
  return;
769
829
  }
770
830
 
771
831
  const int fi = frameIndex_;
772
832
 
773
- // Upload merged geometry to the current frame's persistent Metal buffers.
774
- const size_t vtxBytes = frame.localPositions.size() * sizeof(float);
775
- const size_t idxBytes = frame.indices.size() * sizeof(uint32_t);
776
- const size_t uvBytes = frame.uvs.size() * sizeof(float);
777
- const size_t altBytes = frame.altitudes.size() * sizeof(float);
778
-
779
- id<MTLBuffer> vtxBuf = ensureBuffer(&vtxBufs_[fi], &vtxCaps_[fi],
780
- vtxBytes, frame.localPositions.data(), dev);
781
- id<MTLBuffer> idxBuf = ensureBuffer(&idxBufs_[fi], &idxCaps_[fi],
782
- idxBytes, frame.indices.data(), dev);
783
- id<MTLBuffer> uvBuf = ensureBuffer(&uvBufs_[fi], &uvCaps_[fi],
784
- uvBytes, frame.uvs.data(), dev);
785
- id<MTLBuffer> altBuf = ensureBuffer(&altBufs_[fi], &altCaps_[fi],
786
- altBytes, frame.altitudes.data(), dev);
833
+ // check the per-slot geometry signature. When it matches the engine
834
+ // has intentionally skipped repopulating the merged CPU arrays — the
835
+ // persistent vtx/idx/uv/alt buffers from a previous frame already hold the
836
+ // right bytes for this signature.
837
+ const uint64_t sig = frame.geometrySignature;
838
+ const bool slotMatches = (sig != 0ULL) && (sig == geomSlotSig_[fi]);
839
+
840
+ if (!slotMatches && (frame.localPositions.empty() || frame.indices.empty())) {
841
+ return;
842
+ }
843
+
844
+ id<MTLBuffer> vtxBuf = nil;
845
+ id<MTLBuffer> idxBuf = nil;
846
+ id<MTLBuffer> uvBuf = nil;
847
+ id<MTLBuffer> altBuf = nil;
848
+
849
+ if (slotMatches) {
850
+ vtxBuf = (__bridge id<MTLBuffer>)vtxBufs_[fi];
851
+ idxBuf = (__bridge id<MTLBuffer>)idxBufs_[fi];
852
+ uvBuf = (__bridge id<MTLBuffer>)uvBufs_[fi];
853
+ altBuf = (__bridge id<MTLBuffer>)altBufs_[fi];
854
+ } else {
855
+ const size_t vtxBytes = frame.localPositions.size() * sizeof(float);
856
+ const size_t idxBytes = frame.indices.size() * sizeof(uint32_t);
857
+ const size_t uvBytes = frame.uvs.size() * sizeof(float);
858
+ const size_t altBytes = frame.altitudes.size() * sizeof(float);
859
+
860
+ vtxBuf = ensureBuffer(&vtxBufs_[fi], &vtxCaps_[fi],
861
+ vtxBytes, frame.localPositions.data(), dev);
862
+ idxBuf = ensureBuffer(&idxBufs_[fi], &idxCaps_[fi],
863
+ idxBytes, frame.indices.data(), dev);
864
+ uvBuf = ensureBuffer(&uvBufs_[fi], &uvCaps_[fi],
865
+ uvBytes, frame.uvs.data(), dev);
866
+ altBuf = ensureBuffer(&altBufs_[fi], &altCaps_[fi],
867
+ altBytes, frame.altitudes.data(), dev);
868
+
869
+ // Record what this slot now holds — set only after all four buffers
870
+ // have been refreshed (avoids the partial-update bug we hit on the
871
+ // previous attempt where the signature was bumped after the first
872
+ // buffer copy and the rest were skipped).
873
+ geomSlotSig_[fi] = sig;
874
+ }
787
875
 
788
876
  if (!vtxBuf || !idxBuf || !altBuf) return;
789
877
 
790
- [enc setRenderPipelineState:(__bridge id<MTLRenderPipelineState>)terrainPipeline_];
878
+ id<MTLRenderPipelineState> solidPS =
879
+ (__bridge id<MTLRenderPipelineState>)terrainSolidPipeline_;
880
+ id<MTLRenderPipelineState> ditherPS =
881
+ (__bridge id<MTLRenderPipelineState>)terrainDitherPipeline_;
882
+ // Start with the solid pipeline. CesiumEngine has stable_partition-ed draws so
883
+ // every lodFade==1 draw is first; we only switch to the dither pipeline once,
884
+ // when the first fading draw is reached.
885
+ id<MTLRenderPipelineState> currentPS = solidPS;
886
+ [enc setRenderPipelineState:currentPS];
791
887
  [enc setDepthStencilState:(__bridge id<MTLDepthStencilState>)terrainDepthState_];
792
888
  [enc setFrontFacingWinding:MTLWindingCounterClockwise];
793
889
  [enc setCullMode:MTLCullModeBack];
794
890
 
795
891
  // Set vertex buffers that are shared across all draws.
796
892
  // buffer(0): tile-local positions, buffer(1): altitudes, buffer(3): UVs.
797
- // buffer(2) is the per-draw MVP set in the loop below.
893
+ // buffer(2) is the per-draw VS uniforms (offset is updated per draw via the
894
+ // uniforms ring, fragment slot 0 likewise).
798
895
  [enc setVertexBuffer:vtxBuf offset:0 atIndex:0];
799
896
  [enc setVertexBuffer:altBuf offset:0 atIndex:1];
800
897
  if (uvBuf) [enc setVertexBuffer:uvBuf offset:0 atIndex:3];
@@ -802,55 +899,113 @@ void MetalBackend::drawScene(const FrameResult& frame) {
802
899
  id<MTLTexture> fallbackTex = (__bridge id<MTLTexture>)fallbackTexture_;
803
900
  id<MTLTexture> fallbackWMTex = (__bridge id<MTLTexture>)fallbackWaterMaskTexture_;
804
901
 
902
+ // ── build the per-draw uniforms ring ───────────────────────────────
903
+ // Layout: [draw0 VS][draw0 FS][draw1 VS][draw1 FS]…, each padded to
904
+ // kUniformAlign (256 B). One contiguous MTLBuffer per frame slot. We
905
+ // memcpy all per-draw structs in one pass on the CPU, then in the draw
906
+ // loop only update setVertexBufferOffset / setFragmentBufferOffset.
907
+ const size_t pad = kUniformAlign;
908
+ const size_t vsStride = ((sizeof(PerDrawVertexUniformsCPP) + pad - 1) / pad) * pad;
909
+ const size_t fsStride = ((sizeof(OverlayParamsCPP) + pad - 1) / pad) * pad;
910
+ const size_t perDraw = vsStride + fsStride;
911
+ const size_t needed = perDraw * frame.draws.size();
912
+
913
+ id<MTLBuffer> uniBuf = nil;
914
+ if (needed > 0) {
915
+ if (needed > uniformsCaps_[fi]) {
916
+ size_t newCap = std::max(needed, uniformsCaps_[fi] > 0 ? uniformsCaps_[fi] * 2 : needed);
917
+ newCap = (newCap + 4095UL) & ~4095UL;
918
+ if (uniformsBufs_[fi]) CFRelease(uniformsBufs_[fi]);
919
+ id<MTLBuffer> b = [dev newBufferWithLength:newCap
920
+ options:MTLResourceStorageModeShared];
921
+ uniformsBufs_[fi] = (__bridge_retained void*)b;
922
+ uniformsCaps_[fi] = newCap;
923
+ }
924
+ uniBuf = (__bridge id<MTLBuffer>)uniformsBufs_[fi];
925
+
926
+ uint8_t* base = static_cast<uint8_t*>(uniBuf.contents);
927
+ size_t off = 0;
928
+ for (const auto& draw : frame.draws) {
929
+ auto* vdu = reinterpret_cast<PerDrawVertexUniformsCPP*>(base + off);
930
+ const float* mp = glm::value_ptr(draw.mvpMatrix);
931
+ for (int i = 0; i < 16; ++i) vdu->mvpMatrix[i] = mp[i];
932
+
933
+ auto* ov = reinterpret_cast<OverlayParamsCPP*>(base + off + vsStride);
934
+ *ov = OverlayParamsCPP{};
935
+ ov->hasOverlay = (draw.overlayTexture && draw.hasUVs) ? 1u : 0u;
936
+ ov->isEllipsoidFallback = draw.isEllipsoidFallback ? 1u : 0u;
937
+ ov->isOnlyWater = draw.isOnlyWater ? 1u : 0u;
938
+ ov->hasWaterMask = draw.waterMaskTexture ? 1u : 0u;
939
+ ov->translation[0] = draw.overlayTranslation.x;
940
+ ov->translation[1] = draw.overlayTranslation.y;
941
+ ov->scale[0] = draw.overlayScale.x;
942
+ ov->scale[1] = draw.overlayScale.y;
943
+ ov->wmWest = draw.wmTileBounds.x;
944
+ ov->wmSouth = draw.wmTileBounds.y;
945
+ ov->wmEast = draw.wmTileBounds.z;
946
+ ov->wmNorth = draw.wmTileBounds.w;
947
+ ov->wmTransX = draw.wmTranslation.x;
948
+ ov->wmTransY = draw.wmTranslation.y;
949
+ ov->wmScale = draw.wmScale;
950
+ ov->lodFade = draw.lodFade;
951
+ ov->rtcCenterX = draw.rtcCenterEcef.x;
952
+ ov->rtcCenterY = draw.rtcCenterEcef.y;
953
+ ov->rtcCenterZ = draw.rtcCenterEcef.z;
954
+ ov->cameraTilespaceX = frame.cameraEcef.x - draw.rtcCenterEcef.x;
955
+ ov->cameraTilespaceY = frame.cameraEcef.y - draw.rtcCenterEcef.y;
956
+ ov->cameraTilespaceZ = frame.cameraEcef.z - draw.rtcCenterEcef.z;
957
+
958
+ off += perDraw;
959
+ }
960
+
961
+ // Bind once; per-draw we only rebind the offset (cheap encoder call).
962
+ [enc setVertexBuffer:uniBuf offset:0 atIndex:2];
963
+ [enc setFragmentBuffer:uniBuf offset:0 atIndex:0];
964
+ }
965
+
805
966
  if (fallbackWMTex) [enc setFragmentTexture:fallbackWMTex atIndex:1];
806
967
 
968
+ // track last-bound textures to dedupe redundant setFragmentTexture
969
+ // calls. The Vulkan path already dedupes its descriptor-set 1/2 binds via
970
+ // VkDescriptorSet identity; this matches that on Metal.
971
+ id<MTLTexture> lastTex0 = nil;
972
+ id<MTLTexture> lastTex1 = fallbackWMTex; // bound above
973
+
974
+ size_t drawOff = 0;
807
975
  for (const auto& draw : frame.draws) {
976
+ const size_t thisOff = drawOff;
977
+ drawOff += perDraw;
978
+
808
979
  if (draw.indexCount == 0) continue;
809
980
 
810
- // ── Per-draw vertex uniforms: the tile's RTC MVP matrix ───────────────
811
- PerDrawVertexUniformsCPP vdu{};
812
- const float* mp = glm::value_ptr(draw.mvpMatrix);
813
- for (int i = 0; i < 16; ++i) vdu.mvpMatrix[i] = mp[i];
814
- [enc setVertexBytes:&vdu length:sizeof(vdu) atIndex:2];
815
-
816
- // ── Per-draw fragment uniforms ─────────────────────────────────────────
817
- OverlayParamsCPP ov{};
818
- ov.hasOverlay = (draw.overlayTexture && draw.hasUVs) ? 1u : 0u;
819
- ov.isEllipsoidFallback = draw.isEllipsoidFallback ? 1u : 0u;
820
- ov.isOnlyWater = draw.isOnlyWater ? 1u : 0u;
821
- ov.hasWaterMask = draw.waterMaskTexture ? 1u : 0u;
822
- ov.translation[0] = draw.overlayTranslation.x;
823
- ov.translation[1] = draw.overlayTranslation.y;
824
- ov.scale[0] = draw.overlayScale.x;
825
- ov.scale[1] = draw.overlayScale.y;
826
- ov.wmWest = draw.wmTileBounds.x;
827
- ov.wmSouth = draw.wmTileBounds.y;
828
- ov.wmEast = draw.wmTileBounds.z;
829
- ov.wmNorth = draw.wmTileBounds.w;
830
- ov.wmTransX = draw.wmTranslation.x;
831
- ov.wmTransY = draw.wmTranslation.y;
832
- ov.wmScale = draw.wmScale;
833
- // RTC: tile centre and camera-in-tile-space for fragment world-position
834
- // reconstruction and view-direction lighting.
835
- ov.rtcCenterX = draw.rtcCenterEcef.x;
836
- ov.rtcCenterY = draw.rtcCenterEcef.y;
837
- ov.rtcCenterZ = draw.rtcCenterEcef.z;
838
- // cameraTilespace = cameraEcef − rtcCentre. Calculated here to keep the
839
- // fragment shader free of subtraction of two large float3 values.
840
- ov.cameraTilespaceX = frame.cameraEcef.x - draw.rtcCenterEcef.x;
841
- ov.cameraTilespaceY = frame.cameraEcef.y - draw.rtcCenterEcef.y;
842
- ov.cameraTilespaceZ = frame.cameraEcef.z - draw.rtcCenterEcef.z;
843
- [enc setFragmentBytes:&ov length:sizeof(ov) atIndex:0];
981
+ // ── Pipeline switch for LOD-fade dither ─────────────────────────────────
982
+ // Solid pipeline (no discard) for fully-visible tiles preserves Apple TBDR
983
+ // Hidden Surface Removal; dither pipeline only used for actively-fading
984
+ // tiles. Draws are partitioned so this switch fires at most once per frame.
985
+ id<MTLRenderPipelineState> wantedPS = (draw.lodFade < 1.0f) ? ditherPS : solidPS;
986
+ if (wantedPS != currentPS) {
987
+ currentPS = wantedPS;
988
+ [enc setRenderPipelineState:currentPS];
989
+ }
990
+
991
+ [enc setVertexBufferOffset:thisOff atIndex:2];
992
+ [enc setFragmentBufferOffset:thisOff + vsStride atIndex:0];
844
993
 
845
994
  id<MTLTexture> tex = (draw.overlayTexture && draw.hasUVs)
846
995
  ? (__bridge id<MTLTexture>)draw.overlayTexture
847
996
  : fallbackTex;
848
- if (tex) [enc setFragmentTexture:tex atIndex:0];
997
+ if (tex && tex != lastTex0) {
998
+ [enc setFragmentTexture:tex atIndex:0];
999
+ lastTex0 = tex;
1000
+ }
849
1001
 
850
1002
  id<MTLTexture> wmTex = draw.waterMaskTexture
851
1003
  ? (__bridge id<MTLTexture>)draw.waterMaskTexture
852
1004
  : fallbackWMTex;
853
- if (wmTex) [enc setFragmentTexture:wmTex atIndex:1];
1005
+ if (wmTex && wmTex != lastTex1) {
1006
+ [enc setFragmentTexture:wmTex atIndex:1];
1007
+ lastTex1 = wmTex;
1008
+ }
854
1009
 
855
1010
  [enc drawIndexedPrimitives:MTLPrimitiveTypeTriangle
856
1011
  indexCount:draw.indexCount
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-cesium",
3
- "version": "1.2.7",
3
+ "version": "1.2.8",
4
4
  "description": "Experimental Cesium rendering for React Native via Nitro (iOS/Metal today; Android planned)",
5
5
  "main": "./lib/commonjs/index.js",
6
6
  "module": "./lib/module/index.js",