UNPKG

@deck.gl-community/layers

Version:

Add-on layers for deck.gl

345 lines (291 loc) 10.2 kB
// 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>, }; @group(0) @binding(auto) var<uniform> app : appUniforms; @group(0) @binding(auto) var cubeTexture : texture_cube<f32>; @group(0) @binding(auto) var cubeTextureSampler : sampler; struct VertexInputs { @location(0) positions : vec3<f32>, }; struct VertexOutputs { @builtin(position) position : vec4<f32>, @location(0) direction : vec3<f32>, }; @vertex 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; } @fragment fn fragmentMain(inputs: VertexOutputs) -> @location(0) 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)); } `;