Skip to content

Commit 7fe512e

Browse files
authored
fix(arcgis): update arcgis module to use RenderNode instead of externalRenderers (#10257)
* fix(arcgis): Replace externalRenderers wth RenderNode * test(arcgis): ensure renderer does not fail * docs(arcgis): Replace externalRenderers references with RenderNode * fix(arcgis): update to work with luna v9.2.6 * docs(arcgis): updates related to luma 9.x.x * (arcgis) Improve integration * (arcgis) Inline RenderNode setup into constructor * (docs) Remove unneeded and potentially outdated docs * (arcgis) Fix high-tilt detachment * (docs) Update references to RenderNode * (test) Coverage for core changes * (arcgis) remove unused fbo * (arcgis) drop luma.gl patch for v9.3.3 * (chore) remove unhelpful entries * (chore) remove normalization hack * (test) restore accidentally removed tests * (chore) update loaders in website dependencies * (docs) update the upgrade guide to reflect changes * (docs) include loadScriptOptions info to the upgrade guide
1 parent 6b265fd commit 7fe512e

12 files changed

Lines changed: 518 additions & 238 deletions

File tree

docs/api-reference/arcgis/deck-renderer.md

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# DeckRenderer
22

3-
This class is an experimental implementation of the ArcGIS [ExternalRenderer](https://developers.arcgis.com/javascript/latest/api-reference/esri-views-3d-externalRenderers.html#ExternalRenderer) interface and can be added to 3D views of maps created with the ArcGIS
3+
This class is an experimental implementation of the ArcGIS [RenderNode](https://developers.arcgis.com/javascript/latest/api-reference/esri-views-3d-webgl-RenderNode.html) interface and can be added to 3D views of maps created with the ArcGIS
44
API for JavaScript.
55

66

@@ -11,7 +11,6 @@ import {DeckRenderer} from '@deck.gl/arcgis';
1111
import {ScatterplotLayer} from '@deck.gl/layers';
1212
import ArcGISMap from '@arcgis/core/Map';
1313
import SceneView from '@arcgis/core/views/SceneView';
14-
import * as externalRenderers from '@arcgis/core/views/3d/externalRenderers';
1514

1615
const sceneView = new SceneView({
1716
container: 'viewDiv',
@@ -38,8 +37,6 @@ const renderer = new DeckRenderer(sceneView, {
3837
})
3938
]
4039
});
41-
42-
externalRenderers.add(sceneView, renderer);
4340
```
4441

4542

@@ -49,7 +46,7 @@ externalRenderers.add(sceneView, renderer);
4946
new DeckRenderer(sceneView, props)
5047
```
5148

52-
- `sceneView` ([SceneView](https://developers.arcgis.com/javascript/latest/api-reference/esri-views-SceneView.html)) - the view to use this renderer with. `viewingMode` must be set to `'local'`.
49+
- `sceneView` ([SceneView](https://developers.arcgis.com/javascript/latest/api-reference/esri-views-SceneView.html)) - the view to use this renderer with. `viewingMode` must be set to `'local'`. `DeckRenderer` manages its internal deck.gl view state from the live `SceneView` camera and self-registers as a RenderNode; do not add it to `map.layers`.
5350
- `props` (object) - forwarded to a `Deck` instance. The following [Deck](../core/deck.md) props are supported:
5451

5552
- `layers`

docs/api-reference/arcgis/overview.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ that acts as an interface between deck.gl and ArcGIS.
88

99
2D integration with `MapView` is supported by the [DeckLayer](./deck-layer.md) class.
1010

11-
3D integration with `SceneView` is experimental: see the [DeckRenderer](./deck-renderer.md) class.
11+
3D integration with `SceneView` is experimental and currently targets `viewingMode: 'local'`: see the [DeckRenderer](./deck-renderer.md) class.
1212

1313
## Installation
1414

docs/developer-guide/base-maps/using-with-arcgis.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,4 @@ Starting with v8.1, deck.gl has support for ArcGIS with the [@deck.gl/arcgis](..
1010

1111
2D integration with `MapView` is supported by the [DeckLayer](../../api-reference/arcgis/deck-layer.md) class, see [pure JS example](https://github.com/visgl/deck.gl/tree/master/examples/get-started/pure-js/arcgis).
1212

13-
3D integration with `SceneView` is experimental: see the [DeckRenderer](../../api-reference/arcgis/deck-renderer.md) class.
13+
3D integration with `SceneView` is experimental and currently targets `viewingMode: 'local'`; `DeckRenderer` attaches as a RenderNode rather than a map layer. See the [DeckRenderer](../../api-reference/arcgis/deck-renderer.md) class.

docs/upgrade-guide.md

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,53 @@ new Deck({
5151
- The experimental `_renderLayersInGroups` prop has been removed from `MapboxOverlay`. In interleaved mode, layers are now always rendered in groups by `beforeId` or `slot`, enabling cross-layer extension handling (e.g. MaskExtension, CollisionFilterExtension) by default. If you were using `_renderLayersInGroups: true`, simply remove the prop.
5252
- Note: extensions that require shared rendering context (MaskExtension, CollisionFilterExtension) only work between layers in the same group. Ensure affected layers share the same `beforeId` or `slot` value.
5353

54+
### @deck.gl/arcgis
55+
56+
`DeckRenderer` now integrates with ArcGIS `SceneView` through `RenderNode` instead of `externalRenderers`.
57+
58+
If your app uses 3D ArcGIS integration, migrate as follows:
59+
60+
- Load `esri/views/3d/webgl/RenderNode` instead of `esri/views/3d/externalRenderers`.
61+
- If you pass a custom `esri` object to `loadArcGISModules`, make sure it exposes `esri.views['3d'].webgl.RenderNode`.
62+
- Remove `externalRenderers.add(sceneView, renderer)`. `DeckRenderer` now self-registers as a `RenderNode`.
63+
- `SceneView` integration still requires `viewingMode: 'local'`.
64+
- If you are loading ArcGIS via `esri-loader`, consider pinning the ArcGIS JS API script with `loadScriptOptions` (for example, `{url: 'https://js.arcgis.com/4.32/'}`) to avoid unintentional version drift.
65+
66+
```js
67+
// Before
68+
loadArcGISModules([
69+
'esri/Map',
70+
'esri/views/SceneView',
71+
'esri/views/3d/externalRenderers'
72+
]).then(({DeckRenderer, modules}) => {
73+
const [ArcGISMap, SceneView, externalRenderers] = modules;
74+
const sceneView = new SceneView({
75+
map: new ArcGISMap({basemap: 'dark-gray-vector'}),
76+
viewingMode: 'local'
77+
});
78+
79+
const renderer = new DeckRenderer(sceneView, {layers});
80+
externalRenderers.add(sceneView, renderer);
81+
});
82+
83+
// After
84+
loadArcGISModules([
85+
'esri/Map',
86+
'esri/views/SceneView',
87+
'esri/views/3d/webgl/RenderNode'
88+
], {
89+
url: 'https://js.arcgis.com/4.32/'
90+
}).then(({DeckRenderer, modules}) => {
91+
const [ArcGISMap, SceneView] = modules;
92+
const sceneView = new SceneView({
93+
map: new ArcGISMap({basemap: 'dark-gray-vector'}),
94+
viewingMode: 'local'
95+
});
96+
97+
new DeckRenderer(sceneView, {layers});
98+
});
99+
```
100+
54101
### Widgets
55102

56103
The following widgets have breaking changes in v9.3:
@@ -63,7 +110,6 @@ The following widgets have breaking changes in v9.3:
63110
- [ThemeWidget](./api-reference/widgets/theme-widget.md) - no longer experimental (removed underscore in export)
64111
- [LoadingWidget](./api-reference/widgets/loading-widget.md) - no longer experimental (removed underscore in export)
65112

66-
67113
## Upgrading to v9.1
68114

69115
### User input handling

modules/arcgis/src/commons.ts

Lines changed: 73 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ export type RenderResources = {
1919
texture: Texture;
2020
model: Model;
2121
fbo: Framebuffer;
22-
_externalFramebuffer?: {handle: WebGLFramebuffer; wrapper: Framebuffer};
2322
};
2423

2524
async function createDeckInstance(gl: WebGL2RenderingContext): Promise<{
@@ -87,16 +86,18 @@ out vec4 fragColor;
8786
8887
void main(void) {
8988
vec4 imageColor = texture(deckglTexture, v_texcoord);
90-
imageColor.rgb *= imageColor.a;
89+
// FBO stores premultiplied RGBA (rgb already multiplied by alpha).
90+
// The composite blend (ONE, ONE_MINUS_SRC_ALPHA) handles premultiplied
91+
// input correctly; multiplying again here would darken overlays.
9192
fragColor = imageColor;
9293
}
9394
`,
9495
bindings: {
9596
deckglTexture: texture
9697
},
9798
parameters: {
98-
depthWriteEnabled: true,
99-
depthCompare: 'less-equal',
99+
depthWriteEnabled: false,
100+
depthCompare: 'always',
100101
blendColorSrcFactor: 'one',
101102
blendColorDstFactor: 'one-minus-src-alpha',
102103
blendAlphaSrcFactor: 'one',
@@ -128,6 +129,18 @@ void main(void) {
128129

129130
_customRender: redrawReason => {
130131
if (redrawReason === 'arcgis') {
132+
// ArcGIS renders with alphaSrc=ZERO (preserves destination alpha for its
133+
// own compositing pipeline). Without resetting this, deck layers inherit
134+
// that blend state and write alpha=0 into the FBO, making the composite
135+
// shader output (0,0,0,0) everywhere. Reset to standard premultiplied-alpha
136+
// blend so the FBO stores correct RGBA for the composite pass.
137+
const glCtx = (device as WebGLDevice).gl;
138+
glCtx.blendFuncSeparate(
139+
glCtx.ONE,
140+
glCtx.ONE_MINUS_SRC_ALPHA,
141+
glCtx.ONE,
142+
glCtx.ONE_MINUS_SRC_ALPHA
143+
);
131144
deckInstance._drawLayers(redrawReason);
132145
} else {
133146
this.redraw();
@@ -149,44 +162,81 @@ export function render(
149162
altitude?: number;
150163
pitch: number;
151164
bearing: number;
165+
views?: unknown;
166+
viewState?: unknown;
152167
}
153168
) {
154169
const {model, deck, fbo} = resources;
155170
const device = model.device;
156171
if (device instanceof WebGLDevice) {
157-
const {width, height, ...viewState} = viewport;
172+
// @ts-ignore device.getParametersWebGL should return `any` not `void`?
173+
const rawScreenFbo: WebGLFramebuffer | null = device.getParametersWebGL(GL.FRAMEBUFFER_BINDING);
174+
const {width, height, views, viewState, ...defaultViewState} = viewport;
158175

159176
/* global window */
160177
const dpr = window.devicePixelRatio;
161178
const pixelWidth = Math.round(width * dpr);
162179
const pixelHeight = Math.round(height * dpr);
163180

164-
// Wrap the external framebuffer handle so luma.gl treats it as a proper Framebuffer resource.
165-
const externalFbo = device.getParametersWebGL(GL.FRAMEBUFFER_BINDING);
166-
let screenFbo: Framebuffer | null = null;
167-
if (externalFbo) {
168-
if (resources._externalFramebuffer?.handle !== externalFbo) {
169-
resources._externalFramebuffer?.wrapper.destroy();
170-
const wrapper = device.createFramebuffer({
171-
handle: externalFbo,
172-
width: pixelWidth,
173-
height: pixelHeight
174-
});
175-
resources._externalFramebuffer = {handle: externalFbo, wrapper};
176-
}
177-
screenFbo = resources._externalFramebuffer!.wrapper;
178-
}
179-
180181
fbo.resize({width: pixelWidth, height: pixelHeight});
181182

182-
deck.setProps({viewState});
183+
// luma's Framebuffer.resize() clones and destroys the color attachment texture when
184+
// dimensions change, leaving the cached texture reference and the model sampler binding
185+
// pointing at a destroyed GPU handle. Re-sync if the attachment was replaced.
186+
const currentTexture =
187+
(fbo.colorAttachments[0] as any).texture ?? (fbo.colorAttachments[0] as unknown as Texture);
188+
if (currentTexture !== resources.texture) {
189+
resources.texture = currentTexture;
190+
(model as any).setBindings({deckglTexture: currentTexture});
191+
}
192+
193+
// Pass CSS pixel dimensions — deck handles DPR internally. Passing physical
194+
// pixels would double-apply DPR and project layer geometry off-screen.
195+
// Without width/height, deck's viewport aspect diverges from ArcGIS's,
196+
// causing the overlay to drift off the ground plane under tilt/rotation.
197+
const deckProps: any = {
198+
width,
199+
height,
200+
viewState: viewState || defaultViewState
201+
};
202+
if (views) {
203+
deckProps.views = views;
204+
}
205+
deck.setProps(deckProps);
183206
// redraw deck immediately into deckFbo
184207
deck.redraw('arcgis');
185208

186209
// We overlay the texture on top of the map using the full-screen quad.
210+
const {gl} = device;
211+
212+
// drawBuffers is per-FBO state not tracked by luma's state cache;
213+
// restore it for the screen FBO after rendering to the offscreen FBO.
214+
if (rawScreenFbo) {
215+
gl.bindFramebuffer(GL.FRAMEBUFFER, rawScreenFbo);
216+
gl.drawBuffers([GL.COLOR_ATTACHMENT0]);
217+
} else {
218+
gl.drawBuffers([GL.BACK]);
219+
}
220+
221+
// The model sets blend factors but not blend:true, so
222+
// setDeviceParameters won't enable blending on its own.
223+
gl.enable(GL.BLEND);
224+
gl.blendFuncSeparate(GL.ONE, GL.ONE_MINUS_SRC_ALPHA, GL.ONE, GL.ONE_MINUS_SRC_ALPHA);
225+
gl.blendEquationSeparate(GL.FUNC_ADD, GL.FUNC_ADD);
226+
227+
// luma's WEBGLRenderPass duck-types the framebuffer prop, reading `.handle`, `.width`,
228+
// `.height`, and `.colorAttachments`. A raw WebGLFramebuffer has none of those, which
229+
// breaks viewport auto-setup and draw-buffer selection. Build a minimal adapter that
230+
// luma's state tracker accepts; `'handle' in obj` is true, so the raw FBO is bound.
231+
const screenFboAdapter = {
232+
handle: rawScreenFbo,
233+
width: pixelWidth,
234+
height: pixelHeight,
235+
colorAttachments: [null]
236+
};
187237

188238
const textureToScreenPass = device.beginRenderPass({
189-
framebuffer: screenFbo,
239+
framebuffer: screenFboAdapter as any,
190240
parameters: {viewport: [0, 0, pixelWidth, pixelHeight]},
191241
clearColor: false,
192242
clearDepth: false
@@ -204,5 +254,4 @@ export function finalizeResources(resources: RenderResources) {
204254
resources.model.destroy();
205255
resources.fbo.destroy();
206256
resources.texture.destroy();
207-
resources._externalFramebuffer?.wrapper.destroy();
208257
}

0 commit comments

Comments
 (0)