UNPKG

@deck.gl-community/layers

Version:

Add-on layers for deck.gl

248 lines (237 loc) 8.36 kB
// deck.gl-community // SPDX-License-Identifier: MIT // Copyright (c) vis.gl contributors import { load } from '@loaders.gl/core'; import { TextureCubeLoader } from '@loaders.gl/textures'; import { Layer } from '@deck.gl/core'; import { Matrix4 } from '@math.gl/core'; import { CubeGeometry, DynamicTexture, Model, ShaderInputs } from '@luma.gl/engine'; import { convertLoadedCubemapToTextureData, createCubemapLoadOptions } from "./cubemap-utils.js"; const app = { name: 'app', uniformTypes: { modelMatrix: 'mat4x4<f32>', viewMatrix: 'mat4x4<f32>', projectionMatrix: 'mat4x4<f32>' } }; const SKYBOX_PARAMETERS = { cullMode: 'front', depthWriteEnabled: false, depthCompare: 'less-equal' }; const SKYBOX_SCALE = new Matrix4().scale([2, 2, 2]); const defaultProps = { cubemap: null, loadOptions: null, orientation: 'default' }; /** * Renders a camera-centered cubemap background for `MapView`, `GlobeView`, * `FirstPersonView`, and other 3D-capable deck.gl views. */ export class SkyboxLayer extends Layer { static defaultProps = defaultProps; static layerName = 'SkyboxLayer'; state = undefined; /** Initializes the cube model and starts loading the cubemap texture. */ initializeState() { 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 }) { if (props.cubemap !== oldProps.cubemap || props.loadOptions !== oldProps.loadOptions) { this._loadCubemap().catch(() => { }); } } /** Releases GPU resources owned by the layer. */ finalizeState() { this.state.cubemapTexture?.destroy(); this.state.model?.destroy(); } /** Draws the skybox cube for the current viewport. */ draw() { 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. */ _getModel(shaderInputs) { 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. */ async _loadCubemap() { 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, 'SkyboxLayer failed to load cubemap'); } } } /** Swaps the active GPU cubemap texture and updates model bindings. */ _setCubemapTexture(texture) { 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, loadOptions) { const normalizedLoadOptions = createCubemapLoadOptions(cubemap, loadOptions); if (typeof cubemap === 'string') { return (await load(cubemap, TextureCubeLoader, normalizedLoadOptions)); } return (await TextureCubeLoader.parseText(JSON.stringify(cubemap), normalizedLoadOptions)); } /** Creates the runtime `DynamicTexture` instance used by the skybox model. */ function createCubemapTexture(device, data) { 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) { 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') { 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) { 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)); } `; //# sourceMappingURL=skybox-layer.js.map