Skip to content

Commit db5e27f

Browse files
authored
website: Add webgpu pointcloud example (#9706)
1 parent ae149c7 commit db5e27f

File tree

12 files changed

+257
-32
lines changed

12 files changed

+257
-32
lines changed

examples/website/point-cloud/app.tsx

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {PointCloudLayer} from '@deck.gl/layers';
1111

1212
import {LASWorkerLoader} from '@loaders.gl/las';
1313
import type {OrbitViewState} from '@deck.gl/core';
14+
import {Device, log} from '@luma.gl/core';
1415

1516
// TODO - export from loaders?
1617
type LASMesh = (typeof LASWorkerLoader)['dataType'];
@@ -31,9 +32,11 @@ const INITIAL_VIEW_STATE: OrbitViewState = {
3132
const transitionInterpolator = new LinearInterpolator(['rotationOrbit']);
3233

3334
export default function App({
34-
onLoad
35+
onLoad,
36+
device
3537
}: {
3638
onLoad?: (data: {count: number; progress: number}) => void;
39+
device?: Device;
3740
}) {
3841
const [viewState, updateViewState] = useState<OrbitViewState>(INITIAL_VIEW_STATE);
3942
const [isLoaded, setIsLoaded] = useState<boolean>(false);
@@ -56,6 +59,24 @@ export default function App({
5659

5760
const onDataLoad = useCallback((data: any) => {
5861
const header = (data as LASMesh).header!;
62+
const pos = (data as LASMesh).attributes.POSITION;
63+
// TODO: (kaapp) webgpu won't allow us to just downgrade
64+
// to Float32 and not supply the low value as webgl so kindly did.
65+
const f64Pos = new Float64Array(pos.value.length);
66+
for (let i = 0; i < pos.value.length; i++) {
67+
f64Pos[i] = pos.value[i];
68+
}
69+
(data as LASMesh).attributes.POSITION.value = f64Pos;
70+
// TODO: (kaapp) lack of transparency support for webgpu requires us to recolour the points
71+
// for now. We can remove this once we can create transparent webgpu canvas
72+
const color = (data as LASMesh).attributes.COLOR_0;
73+
for (let i = 0; i < color.value.length / 4; i++) {
74+
const index = i * 4;
75+
color.value[index] = 0xff; // r
76+
color.value[index + 1] = 0x00; // g
77+
color.value[index + 2] = 0x00; // b
78+
color.value[index + 3] = 0xff; // a
79+
}
5980
if (header.boundingBox) {
6081
const [mins, maxs] = header.boundingBox;
6182
// File contains bounding box info
@@ -83,12 +104,15 @@ export default function App({
83104
opacity: 0.5,
84105
pointSize: 0.5,
85106
// Additional format support can be added here
86-
loaders: [LASWorkerLoader]
107+
loaders: [LASWorkerLoader],
108+
// TODO (kaapp) currently webgpu requirement to ensure instanceColors are supplied
109+
pickable: true
87110
})
88111
];
89112

90113
return (
91114
<DeckGL
115+
device={device}
92116
views={new OrbitView({orbitAxis: 'Y', fovy: 50})}
93117
initialViewState={viewState}
94118
controller={true}

modules/core/src/lib/deck.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,25 @@ export default class Deck<ViewsT extends ViewOrViews = null> {
477477
viewState: this._getViewState()
478478
});
479479

480+
if (props.device && props.device.id !== this.device?.id) {
481+
this.animationLoop?.stop();
482+
if (this.canvas !== props.device.canvasContext?.canvas) {
483+
// remove old canvas if new one being used and de-register events
484+
// TODO (ck): We might not own this canvas depending it's source, so removing it from the
485+
// DOM here might be a bit unexpected but it should be ok for most users.
486+
this.canvas?.remove();
487+
this.eventManager?.destroy();
488+
489+
// ensure we will re-attach ourselves after createDevice callbacks
490+
this.canvas = null;
491+
}
492+
493+
log.log(`recreating animation loop for new device! id=${props.device.id}`);
494+
495+
this.animationLoop = this._createAnimationLoop(props.device, props);
496+
this.animationLoop.start();
497+
}
498+
480499
// Update the animation loop
481500
this.animationLoop?.setProps(resolvedProps);
482501

@@ -961,6 +980,11 @@ export default class Deck<ViewsT extends ViewOrViews = null> {
961980
// if external context...
962981
if (!this.canvas) {
963982
this.canvas = this.device.canvasContext?.canvas as HTMLCanvasElement;
983+
984+
// external canvas may not be in DOM
985+
if (!this.canvas.isConnected && this.props.parent) {
986+
this.props.parent.insertBefore(this.canvas, this.props.parent.firstChild);
987+
}
964988
// TODO v9
965989
// ts-expect-error - Currently luma.gl v9 does not expose these options
966990
// All WebGLDevice contexts are instrumented, but it seems the device

modules/core/src/shaderlib/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ export function getShaderAssembler(language: 'glsl' | 'wgsl'): ShaderAssembler {
3232
shaderAssembler.addDefaultModule(shaderModule);
3333
}
3434

35+
// if we're recreating the device we may have changed language
36+
// and must not inject hooks for the wrong language
37+
// shaderAssembler.resetShaderHooks();
38+
(shaderAssembler as any)._hookFunctions.length = 0;
39+
3540
// Add shader hooks based on language
3641
// TODO(ibgreen) - should the luma shader assembler support both sets of hooks?
3742
const shaderHooks = language === 'glsl' ? SHADER_HOOKS_GLSL : SHADER_HOOKS_WGSL;

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@
5555
"tap-spec": "^5.0.0",
5656
"tape-catch": "^1.0.6"
5757
},
58+
"resolutions": {
59+
"wgsl_reflect": "^1.2.0"
60+
},
5861
"pre-commit": [
5962
"test-fast",
6063
"bindings-precommit-tests"

website/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@
4141
"react-map-gl": "^8.0.0",
4242
"react-virtualized-auto-sizer": "^1.0.2",
4343
"styled-components": "^5.3.3",
44-
"supercluster": "^8.0.1"
44+
"supercluster": "^8.0.1",
45+
"zustand": "^5.0.5"
4546
},
4647
"devDependencies": {
4748
"@docusaurus/core": "^3.6.3",
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// deck.gl
2+
// SPDX-License-Identifier: MIT
3+
// Copyright (c) vis.gl contributors
4+
5+
import React from 'react';
6+
import BrowserOnly from '@docusaurus/BrowserOnly';
7+
8+
import {Tabs, Tab} from './tabs';
9+
import {useStore} from '../store/device-store';
10+
11+
const DEFAULT_DEVICE_TABS_PROPS = {
12+
devices: ['webgl2', 'webgpu']
13+
};
14+
15+
export const DeviceTabsPriv = (props) => {
16+
props = {...DEFAULT_DEVICE_TABS_PROPS, ...props};
17+
const deviceType = useStore(state => state.deviceType);
18+
const deviceError = useStore(state => state.deviceError);
19+
const setDeviceType = useStore(state => state.setDeviceType);
20+
21+
return (
22+
<Tabs selectedItem={deviceType} setSelectedItem={setDeviceType}>
23+
{props.devices.includes('webgl2') && (
24+
<Tab key="WebGL2" title="WebGL2" tag="webgl">
25+
{/* <img height="80" src="https://raw.github.com/visgl/deck.gl-data/master/images/whats-new/webgl2.jpg" />*/}
26+
{deviceError}
27+
</Tab>
28+
)}
29+
30+
{props.devices.includes('webgpu') && (
31+
<Tab key="WebGPU" title="WebGPU" tag="webgpu">
32+
{/* <img height="80" src="https://raw.githubusercontent.com/gpuweb/gpuweb/3b3a1632ff1ad6a573330a58710e341bb9d65576/logo/webgpu-horizontal.svg" /> */}
33+
{deviceError}
34+
</Tab>
35+
)}
36+
</Tabs>
37+
);
38+
};
39+
40+
export const DeviceTabs = (props) => (
41+
<BrowserOnly>{() => <DeviceTabsPriv {...props} />}</BrowserOnly>
42+
);

website/src/components/example/make-example.jsx

Lines changed: 31 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,20 @@
22
// SPDX-License-Identifier: MIT
33
// Copyright (c) vis.gl contributors
44

5-
import React, {useState, useEffect, useCallback} from 'react';
5+
import React, {useState, useEffect, useCallback, Children, isValidElement, cloneElement} from 'react';
66
import styled from 'styled-components';
77
import InfoPanel from '../info-panel';
88
import {loadData, joinPath} from '../../utils/data-utils';
99
import {normalizeParam} from '../../utils/format-utils';
1010
import {MAPBOX_STYLES} from '../../constants/defaults';
1111
import useBaseUrl from '@docusaurus/useBaseUrl';
12+
import { DeviceTabs } from '../device-tabs';
13+
import { useStore } from '../../store/device-store';
1214

1315
const DemoContainer = styled.div`
1416
height: 100%;
17+
position: relative;
18+
1519
.tooltip,
1620
.deck-tooltip {
1721
position: absolute;
@@ -52,6 +56,7 @@ export default function makeExample(DemoComponent, {isInteractive = true, style}
5256
const [data, setData] = useState(defaultData);
5357
const [params, setParams] = useState(defaultParams);
5458
const [meta, setMeta] = useState({});
59+
const device = useStore(state => state.device)
5560
const baseUrl = useBaseUrl('/');
5661

5762
const useParam = useCallback(newParameters => {
@@ -112,28 +117,32 @@ export default function makeExample(DemoComponent, {isInteractive = true, style}
112117
};
113118

114119
return (
115-
<DemoContainer style={style}>
116-
<DemoComponent
117-
data={data}
118-
mapStyle={mapStyle || MAPBOX_STYLES.BLANK}
119-
params={params}
120-
useParam={useParam}
121-
onStateChange={updateMeta}
122-
/>
123-
{isInteractive && (
124-
<InfoPanel
125-
title={DemoComponent.title}
120+
<>
121+
{DemoComponent.hasDeviceTabs && <DeviceTabs />}
122+
<DemoContainer style={style}>
123+
<DemoComponent
124+
device={device}
125+
data={data}
126+
mapStyle={mapStyle || MAPBOX_STYLES.BLANK}
126127
params={params}
127-
meta={meta}
128-
updateParam={updateParam}
129-
sourceLink={DemoComponent.code}
130-
>
131-
{DemoComponent.renderInfo(meta)}
132-
</InfoPanel>
133-
)}
134-
135-
{isInteractive && mapStyle && <MapTip>Hold down shift to rotate</MapTip>}
136-
</DemoContainer>
128+
useParam={useParam}
129+
onStateChange={updateMeta}
130+
/>
131+
{isInteractive && (
132+
<InfoPanel
133+
title={DemoComponent.title}
134+
params={params}
135+
meta={meta}
136+
updateParam={updateParam}
137+
sourceLink={DemoComponent.code}
138+
>
139+
{DemoComponent.renderInfo(meta)}
140+
</InfoPanel>
141+
)}
142+
143+
{isInteractive && mapStyle && <MapTip>Hold down shift to rotate</MapTip>}
144+
</DemoContainer>
145+
</>
137146
);
138147
};
139148
}

website/src/components/tabs.jsx

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// deck.gl
2+
// SPDX-License-Identifier: MIT
3+
// Copyright (c) vis.gl contributors
4+
5+
import React, {Children, useState} from 'react';
6+
import styled from 'styled-components';
7+
8+
const Header = styled.div`
9+
display: flex;
10+
flex-direction: row;
11+
12+
& > * + * {
13+
margin-left: 2px;
14+
}
15+
`;
16+
17+
const HeaderItem = styled.div(
18+
props => `
19+
cursor: pointer;
20+
padding: 10px 20px;
21+
font-weight: bold;
22+
&:hover {
23+
background-color: #eeefef;
24+
}
25+
${
26+
props.isSelected
27+
? `
28+
color: #276EF1;
29+
border-bottom: 4px solid #276EF1;
30+
`
31+
: ''
32+
}
33+
`
34+
);
35+
36+
const Body = styled.div`
37+
background-color: rgb(250, 248, 245);
38+
`;
39+
40+
export const Tabs = props => {
41+
const {children} = props;
42+
const tabs = Children.toArray(children);
43+
const [selectedItem, setSelectedItem] = useState(tabs[0]?.props.tag || tabs[0]?.props.title);
44+
let selected = props.selectedItem !== undefined ? props.selectedItem : selectedItem;
45+
const setSelected = props.setSelectedItem !== undefined ? props.setSelectedItem : setSelectedItem;
46+
// check if the selected tab even exists in the list
47+
if (!tabs.some(e => (e.props.tag || e.props.title) === selected)) {
48+
selected = selectedItem;
49+
}
50+
return (
51+
<>
52+
<Header>
53+
{tabs.map(tab => (
54+
<HeaderItem
55+
key={tab.props.title}
56+
isSelected={(tab.props.tag || tab.props.title) === selected}
57+
onClick={() => setSelected(tab.props.tag || tab.props.title)}
58+
>
59+
{tab.props.title}
60+
</HeaderItem>
61+
))}
62+
</Header>
63+
<Body>{tabs.find(tab => (tab.props.tag || tab.props.title) === selected)}</Body>
64+
</>
65+
);
66+
};
67+
68+
export const Tab = props => {
69+
const {children} = props;
70+
return <>{children}</>;
71+
};

website/src/examples/point-cloud-layer.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ class PointCloudDemo extends Component {
1414

1515
static code = `${GITHUB_TREE}/examples/website/point-cloud`;
1616

17+
static hasDeviceTabs = true;
18+
1719
static renderInfo(meta) {
1820
return (
1921
<div>
@@ -35,7 +37,7 @@ class PointCloudDemo extends Component {
3537

3638
render() {
3739
return <div style={{width: '100%', height: '100%', background: '#ecdbce'}}>
38-
<App onLoad={this._onLoad} />
40+
<App key={this.props.device?.type} onLoad={this._onLoad} device={this.props.device} />
3941
</div>;
4042
}
4143
}

website/src/store/device-store.jsx

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// deck.gl
2+
// SPDX-License-Identifier: MIT
3+
// Copyright (c) vis.gl contributors
4+
5+
import {create} from 'zustand';
6+
7+
import {luma, Device} from '@luma.gl/core';
8+
import {webgl2Adapter} from '@luma.gl/webgl';
9+
import {webgpuAdapter} from '@luma.gl/webgpu';
10+
11+
const cachedDevice = {};
12+
13+
export async function createDevice(type) {
14+
cachedDevice[type] =
15+
cachedDevice[type] ||
16+
luma.createDevice({
17+
adapters: [webgl2Adapter, webgpuAdapter],
18+
type,
19+
createCanvasContext: {
20+
container: 'deckgl-wrapper',
21+
useDevicePixels: true,
22+
autoResize: true,
23+
width: undefined,
24+
height: undefined,
25+
},
26+
});
27+
return await cachedDevice[type];
28+
}
29+
30+
export const useStore = create(set => ({
31+
deviceType: undefined,
32+
deviceError: undefined,
33+
device: undefined,
34+
setDeviceType: async (deviceType) => {
35+
let deviceError;
36+
let device;
37+
try {
38+
device = await createDevice(deviceType);
39+
} catch (error) {
40+
deviceError = error.message;
41+
}
42+
return set(state => ({deviceType, deviceError, device}));
43+
},
44+
}));

0 commit comments

Comments
 (0)