@deck.gl-community/layers
Version:
Add-on layers for deck.gl
345 lines (291 loc) • 10.2 kB
text/typescript
// deck.gl-community
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors
import {load} from '@loaders.gl/core';
import {
TextureCubeLoader,
type TextureCubeLoaderOptions,
type TextureCubeManifest
} from '@loaders.gl/textures';
import {Layer} from '@deck.gl/core';
import type {DefaultProps, LayerProps, UpdateParameters, Viewport} from '@deck.gl/core';
import {Matrix4} from '@math.gl/core';
import type {Device, RenderPipelineParameters} from '@luma.gl/core';
import {CubeGeometry, DynamicTexture, Model, ShaderInputs} from '@luma.gl/engine';
import type {TextureCubeData} from '@luma.gl/engine';
import type {ShaderModule} from '@luma.gl/shadertools';
import {convertLoadedCubemapToTextureData, createCubemapLoadOptions} from './cubemap-utils';
type AppUniforms = {
/** World transform for the unit cube used to draw the skybox. */
modelMatrix: any;
/** View transform with translation removed so the skybox stays camera-centered. */
viewMatrix: any;
/** Projection transform for the active viewport. */
projectionMatrix: any;
};
const app: ShaderModule<AppUniforms, AppUniforms> = {
name: 'app',
uniformTypes: {
modelMatrix: 'mat4x4<f32>',
viewMatrix: 'mat4x4<f32>',
projectionMatrix: 'mat4x4<f32>'
}
} as any;
const SKYBOX_PARAMETERS: RenderPipelineParameters = {
cullMode: 'front',
depthWriteEnabled: false,
depthCompare: 'less-equal'
};
const SKYBOX_SCALE = new Matrix4().scale([2, 2, 2]);
const defaultProps: DefaultProps<SkyboxLayerProps> = {
cubemap: null,
loadOptions: null,
orientation: 'default'
};
type _SkyboxLayerProps = {
/** Cubemap manifest URL or manifest object to load and render. */
cubemap: string | TextureCubeManifest | null;
/** Optional loaders.gl texture-cube load options. */
loadOptions?: TextureCubeLoaderOptions | null;
/**
* Declares how the cubemap faces are oriented relative to deck.gl's Z-up
* world. Use `y-up` for cubemaps authored for Y-up scenes where the source
* `+Y` face should align with deck.gl's vertical `+Z` axis.
*/
orientation?: 'default' | 'y-up';
};
export type SkyboxLayerProps = _SkyboxLayerProps & LayerProps;
type LoadedCubemapTexture = {
type: 'cube';
data: unknown[];
};
type SkyboxLayerState = {
/** Active GPU cubemap texture, if one has been loaded successfully. */
cubemapTexture: DynamicTexture | null;
/** Monotonic load token used to discard stale async cubemap loads. */
loadCount: number;
/** Backing model that renders the cube geometry. */
model?: Model;
/** Shader input manager for the skybox uniforms. */
shaderInputs?: ShaderInputs<any>;
};
/**
* Renders a camera-centered cubemap background for `MapView`, `GlobeView`,
* `FirstPersonView`, and other 3D-capable deck.gl views.
*/
export class SkyboxLayer<
ExtraProps extends Record<string, unknown> = Record<string, unknown>
> extends Layer<Required<_SkyboxLayerProps> & ExtraProps> {
static defaultProps = defaultProps;
static layerName = 'SkyboxLayer';
state: SkyboxLayerState = undefined!;
/** Initializes the cube model and starts loading the cubemap texture. */
initializeState(): void {
const attributeManager = this.getAttributeManager();
attributeManager?.remove(['instancePickingColors']);
const shaderInputs = new ShaderInputs(
createShaderInputModules(this.context.defaultShaderModules),
{
disableWarnings: true
}
);
const model = this._getModel(shaderInputs);
this.setState({
cubemapTexture: null,
loadCount: 0,
model,
shaderInputs
});
this._loadCubemap().catch(() => {});
}
/** Reloads the cubemap when its source manifest or load options change. */
updateState({props, oldProps}: UpdateParameters<this>): void {
if (props.cubemap !== oldProps.cubemap || props.loadOptions !== oldProps.loadOptions) {
this._loadCubemap().catch(() => {});
}
}
/** Releases GPU resources owned by the layer. */
finalizeState(): void {
this.state.cubemapTexture?.destroy();
this.state.model?.destroy();
}
/** Draws the skybox cube for the current viewport. */
draw(): void {
const {model, cubemapTexture, shaderInputs} = this.state;
if (!model || !cubemapTexture || !shaderInputs) {
return;
}
const viewport = this.context.viewport;
shaderInputs.setProps({
app: {
modelMatrix: getSkyboxModelMatrix(this.props.orientation),
viewMatrix: getSkyboxViewMatrix(viewport),
projectionMatrix: viewport.projectionMatrix
}
});
model.draw(this.context.renderPass);
}
/** Creates the luma.gl model used to render the skybox cube. */
protected _getModel(shaderInputs: ShaderInputs<any>): Model {
return new Model(this.context.device, {
...this.getShaders(),
id: this.props.id,
bufferLayout: this.getAttributeManager()?.getBufferLayouts() || [],
geometry: new CubeGeometry({indices: true}),
shaderInputs,
isInstanced: false,
parameters: SKYBOX_PARAMETERS
});
}
/** Returns the WGSL/GLSL shader pair used by the layer. */
getShaders() {
return {
source: SKYBOX_WGSL,
vs: SKYBOX_VS,
fs: SKYBOX_FS
};
}
/** Starts an asynchronous cubemap load for the current props. */
private async _loadCubemap(): Promise<void> {
const {cubemap, loadOptions} = this.props;
const nextLoadCount = this.state.loadCount + 1;
this.setState({loadCount: nextLoadCount});
if (!cubemap) {
this._setCubemapTexture(null);
return;
}
try {
const loadedTexture = await loadCubemapSource(cubemap, loadOptions);
if (this.state.loadCount !== nextLoadCount || !this.state.model) {
return;
}
const cubemapData = convertLoadedCubemapToTextureData(loadedTexture);
this._setCubemapTexture(createCubemapTexture(this.context.device, cubemapData));
} catch (error) {
if (this.state.loadCount === nextLoadCount) {
this.raiseError(error as Error, 'SkyboxLayer failed to load cubemap');
}
}
}
/** Swaps the active GPU cubemap texture and updates model bindings. */
private _setCubemapTexture(texture: DynamicTexture | null): void {
const {cubemapTexture, model} = this.state;
if (cubemapTexture === texture) {
return;
}
cubemapTexture?.destroy();
this.setState({cubemapTexture: texture});
if (texture && model) {
model.setBindings({cubeTexture: texture});
}
this.setNeedsRedraw();
}
}
/** Loads a cubemap manifest or manifest URL through loaders.gl. */
async function loadCubemapSource(
cubemap: string | TextureCubeManifest,
loadOptions?: TextureCubeLoaderOptions | null
): Promise<LoadedCubemapTexture> {
const normalizedLoadOptions = createCubemapLoadOptions(cubemap, loadOptions);
if (typeof cubemap === 'string') {
return (await load(cubemap, TextureCubeLoader, normalizedLoadOptions)) as LoadedCubemapTexture;
}
return (await TextureCubeLoader.parseText(
JSON.stringify(cubemap),
normalizedLoadOptions
)) as LoadedCubemapTexture;
}
/** Creates the runtime `DynamicTexture` instance used by the skybox model. */
function createCubemapTexture(device: Device, data: TextureCubeData): DynamicTexture {
return new DynamicTexture(device, {
dimension: 'cube',
data,
mipmaps: true,
sampler: {
addressModeU: 'clamp-to-edge',
addressModeV: 'clamp-to-edge',
addressModeW: 'clamp-to-edge',
magFilter: 'linear',
minFilter: 'linear',
mipmapFilter: 'linear'
}
});
}
/** Removes camera translation from the active view matrix for skybox rendering. */
function getSkyboxViewMatrix(viewport: Viewport): Matrix4 {
const viewMatrix = new Matrix4(viewport.viewMatrixUncentered || viewport.viewMatrix);
viewMatrix[12] = 0;
viewMatrix[13] = 0;
viewMatrix[14] = 0;
return viewMatrix;
}
/** Returns the skybox cube transform for the requested cubemap orientation. */
function getSkyboxModelMatrix(orientation: 'default' | 'y-up' = 'default'): Matrix4 {
if (orientation === 'y-up') {
return new Matrix4().rotateX(Math.PI / 2).scale([2, 2, 2]);
}
return new Matrix4(SKYBOX_SCALE);
}
/** Converts the current shader module list into a name-indexed dictionary. */
function createShaderInputModules(defaultShaderModules: ShaderModule[]): {
[moduleName: string]: ShaderModule;
} {
return Object.fromEntries([app, ...defaultShaderModules].map(module => [module.name, module]));
}
const SKYBOX_WGSL = /* wgsl */ `
struct appUniforms {
modelMatrix: mat4x4<f32>,
viewMatrix: mat4x4<f32>,
projectionMatrix: mat4x4<f32>,
};
var<uniform> app : appUniforms;
var cubeTexture : texture_cube<f32>;
var cubeTextureSampler : sampler;
struct VertexInputs {
positions : vec3<f32>,
};
struct VertexOutputs {
position : vec4<f32>,
direction : vec3<f32>,
};
fn vertexMain(inputs: VertexInputs) -> VertexOutputs {
var outputs : VertexOutputs;
let clipPosition =
app.projectionMatrix *
app.viewMatrix *
app.modelMatrix *
vec4<f32>(inputs.positions, 1.0);
outputs.position = vec4<f32>(clipPosition.x, clipPosition.y, clipPosition.w, clipPosition.w);
outputs.direction = inputs.positions;
return outputs;
}
fn fragmentMain(inputs: VertexOutputs) -> vec4<f32> {
return textureSample(cubeTexture, cubeTextureSampler, normalize(inputs.direction));
}
`;
const SKYBOX_VS = /* glsl */ `#version 300 es
in vec3 positions;
uniform appUniforms {
mat4 modelMatrix;
mat4 viewMatrix;
mat4 projectionMatrix;
} app;
out vec3 vDirection;
void main(void) {
vec4 clipPosition =
app.projectionMatrix * app.viewMatrix * app.modelMatrix * vec4(positions, 1.0);
gl_Position = clipPosition.xyww;
vDirection = positions;
}
`;
const SKYBOX_FS = /* glsl */ `#version 300 es
precision highp float;
uniform samplerCube cubeTexture;
in vec3 vDirection;
out vec4 fragColor;
void main(void) {
fragColor = texture(cubeTexture, normalize(vDirection));
}
`;