import { AddEquation, Camera, ClampToEdgeWrapping, CustomBlending, FloatType, GLSL3, HalfFloatType, LinearFilter, Matrix3, Mesh, NoBlending, NoColorSpace, OneFactor, PlaneGeometry, RawShaderMaterial, RGBAFormat, Scene, Uniform, Vector3, WebGL3DRenderTarget, WebGLRenderTarget, type ShaderMaterialParameters, type Texture, type WebGLRenderer } from 'three' import invariant from 'tiny-invariant' import { isFloatLinearSupported, resolveIncludes, type AnyFloatType } from '@takram/three-geospatial' import { AtmosphereParameters } from './AtmosphereParameters' import { IRRADIANCE_TEXTURE_HEIGHT, IRRADIANCE_TEXTURE_WIDTH, SCATTERING_TEXTURE_DEPTH, SCATTERING_TEXTURE_HEIGHT, SCATTERING_TEXTURE_MU_S_SIZE, SCATTERING_TEXTURE_MU_SIZE, SCATTERING_TEXTURE_NU_SIZE, SCATTERING_TEXTURE_R_SIZE, SCATTERING_TEXTURE_WIDTH, TRANSMITTANCE_TEXTURE_HEIGHT, TRANSMITTANCE_TEXTURE_WIDTH } from './constants' import { requestIdleCallback } from './helpers/requestIdleCallback' import type { PrecomputedTextures } from './types' import common from './shaders/bruneton/common.glsl?raw' import definitions from './shaders/bruneton/definitions.glsl?raw' import precompute from './shaders/bruneton/precompute.glsl?raw' import directIrradianceShader from './shaders/precompute/directIrradiance.frag?raw' import indirectIrradianceShader from './shaders/precompute/indirectIrradiance.frag?raw' import multipleScatteringShader from './shaders/precompute/multipleScattering.frag?raw' import scatteringDensityShader from './shaders/precompute/scatteringDensity.frag?raw' import singleScatteringShader from './shaders/precompute/singleScattering.frag?raw' import transmittanceShader from './shaders/precompute/transmittance.frag?raw' const vertexShader = /* glsl */ ` precision highp float; in vec2 position; void main() { gl_Position = vec4(position, 1.0, 1.0); } ` function createRenderTarget( type: AnyFloatType, width: number, height: number ): WebGLRenderTarget { const renderTarget = new WebGLRenderTarget(width, height, { depthBuffer: false, type, format: RGBAFormat }) const texture = renderTarget.texture texture.minFilter = LinearFilter texture.magFilter = LinearFilter texture.wrapS = ClampToEdgeWrapping texture.wrapT = ClampToEdgeWrapping texture.colorSpace = NoColorSpace return renderTarget } function create3DRenderTarget( type: AnyFloatType, width: number, height: number, depth: number ): WebGL3DRenderTarget { const renderTarget = new WebGL3DRenderTarget(width, height, depth, { depthBuffer: false, type, format: RGBAFormat }) const texture = renderTarget.texture texture.minFilter = LinearFilter texture.magFilter = LinearFilter texture.wrapS = ClampToEdgeWrapping texture.wrapT = ClampToEdgeWrapping texture.wrapR = ClampToEdgeWrapping texture.colorSpace = NoColorSpace return renderTarget } // eslint-disable-next-line @typescript-eslint/promise-function-async function iterateIdle(iterable: Iterable): Promise { const iterator = iterable[Symbol.iterator]() return new Promise((resolve, reject) => { const callback = (): void => { try { const { value, done } = iterator.next() if (done === true) { resolve(value) } else { requestIdleCallback(callback) } } catch (error: unknown) { reject(error instanceof Error ? error : new Error()) } } requestIdleCallback(callback) }) } async function readRenderTargetPixels( renderer: WebGLRenderer, renderTarget: WebGLRenderTarget, texture: Texture ): Promise { const { width, height } = renderTarget const imageData = texture.type === HalfFloatType ? new Uint16Array(width * height * 4) : new Float32Array(width * height * 4) await renderer.readRenderTargetPixelsAsync( renderTarget, 0, 0, renderTarget.width, renderTarget.height, imageData ) // eslint-disable-next-line require-atomic-updates texture.userData.imageData = imageData } class Context { lambdas = new Vector3() luminanceFromRadiance = new Matrix3() opticalDepth?: WebGLRenderTarget deltaIrradiance: WebGLRenderTarget deltaRayleighScattering: WebGL3DRenderTarget deltaMieScattering: WebGL3DRenderTarget deltaScatteringDensity: WebGL3DRenderTarget deltaMultipleScattering: WebGL3DRenderTarget constructor(type: AnyFloatType) { if (type === HalfFloatType) { this.opticalDepth = createRenderTarget( type, TRANSMITTANCE_TEXTURE_WIDTH, TRANSMITTANCE_TEXTURE_HEIGHT ) } this.deltaIrradiance = createRenderTarget( type, IRRADIANCE_TEXTURE_WIDTH, IRRADIANCE_TEXTURE_HEIGHT ) this.deltaRayleighScattering = create3DRenderTarget( type, SCATTERING_TEXTURE_WIDTH, SCATTERING_TEXTURE_HEIGHT, SCATTERING_TEXTURE_DEPTH ) this.deltaMieScattering = create3DRenderTarget( type, SCATTERING_TEXTURE_WIDTH, SCATTERING_TEXTURE_HEIGHT, SCATTERING_TEXTURE_DEPTH ) this.deltaScatteringDensity = create3DRenderTarget( type, SCATTERING_TEXTURE_WIDTH, SCATTERING_TEXTURE_HEIGHT, SCATTERING_TEXTURE_DEPTH ) // deltaMultipleScattering is only needed to compute scattering order 3 or // more, while deltaRayleighScattering and deltaMieScattering are only needed // to compute double scattering. Therefore, to save memory, we can store // deltaRayleighScattering and deltaMultipleScattering in the same GPU // texture. this.deltaMultipleScattering = this.deltaRayleighScattering } dispose(): void { this.opticalDepth?.dispose() this.deltaIrradiance.dispose() this.deltaRayleighScattering.dispose() this.deltaMieScattering.dispose() this.deltaScatteringDensity.dispose() } } class PrecomputeMaterial extends RawShaderMaterial { constructor(params: ShaderMaterialParameters) { super({ glslVersion: GLSL3, vertexShader, ...params, defines: { TRANSMITTANCE_TEXTURE_WIDTH: TRANSMITTANCE_TEXTURE_WIDTH.toFixed(0), TRANSMITTANCE_TEXTURE_HEIGHT: TRANSMITTANCE_TEXTURE_HEIGHT.toFixed(0), SCATTERING_TEXTURE_R_SIZE: SCATTERING_TEXTURE_R_SIZE.toFixed(0), SCATTERING_TEXTURE_MU_SIZE: SCATTERING_TEXTURE_MU_SIZE.toFixed(0), SCATTERING_TEXTURE_MU_S_SIZE: SCATTERING_TEXTURE_MU_S_SIZE.toFixed(0), SCATTERING_TEXTURE_NU_SIZE: SCATTERING_TEXTURE_NU_SIZE.toFixed(0), IRRADIANCE_TEXTURE_WIDTH: IRRADIANCE_TEXTURE_WIDTH.toFixed(0), IRRADIANCE_TEXTURE_HEIGHT: IRRADIANCE_TEXTURE_HEIGHT.toFixed(0), ...params.defines } }) } // eslint-disable-next-line accessor-pairs set additive(value: boolean) { this.transparent = value this.blending = value ? CustomBlending : NoBlending this.blendEquation = AddEquation this.blendEquationAlpha = AddEquation this.blendSrc = OneFactor this.blendDst = OneFactor this.blendSrcAlpha = OneFactor this.blendDstAlpha = OneFactor } setUniforms(context: Context): void { const uniforms = this.uniforms if (uniforms.luminanceFromRadiance != null) { uniforms.luminanceFromRadiance.value.copy(context.luminanceFromRadiance) } if (uniforms.singleRayleighScatteringTexture != null) { uniforms.singleRayleighScatteringTexture.value = context.deltaRayleighScattering.texture } if (uniforms.singleMieScatteringTexture != null) { uniforms.singleMieScatteringTexture.value = context.deltaMieScattering.texture } if (uniforms.multipleScatteringTexture != null) { uniforms.multipleScatteringTexture.value = context.deltaMultipleScattering.texture } if (uniforms.scatteringDensityTexture != null) { uniforms.scatteringDensityTexture.value = context.deltaScatteringDensity.texture } if (uniforms.irradianceTexture != null) { uniforms.irradianceTexture.value = context.deltaIrradiance.texture } } } export interface PrecomputedTexturesGeneratorOptions { type?: AnyFloatType combinedScattering?: boolean higherOrderScattering?: boolean } export class PrecomputedTexturesGenerator { readonly transmittanceRenderTarget: WebGLRenderTarget readonly scatteringRenderTarget: WebGL3DRenderTarget readonly irradianceRenderTarget: WebGLRenderTarget readonly singleMieScatteringRenderTarget?: WebGL3DRenderTarget readonly higherOrderScatteringRenderTarget?: WebGL3DRenderTarget readonly textures: PrecomputedTextures transmittanceMaterial = new PrecomputeMaterial({ fragmentShader: resolveIncludes(transmittanceShader, { bruneton: { common, definitions, precompute } }) }) directIrradianceMaterial = new PrecomputeMaterial({ fragmentShader: resolveIncludes(directIrradianceShader, { bruneton: { common, definitions, precompute } }), uniforms: { transmittanceTexture: new Uniform(null) } }) singleScatteringMaterial = new PrecomputeMaterial({ fragmentShader: resolveIncludes(singleScatteringShader, { bruneton: { common, definitions, precompute } }), uniforms: { luminanceFromRadiance: new Uniform(new Matrix3()), transmittanceTexture: new Uniform(null), layer: new Uniform(0) } }) scatteringDensityMaterial = new PrecomputeMaterial({ fragmentShader: resolveIncludes(scatteringDensityShader, { bruneton: { common, definitions, precompute } }), uniforms: { transmittanceTexture: new Uniform(null), singleRayleighScatteringTexture: new Uniform(null), singleMieScatteringTexture: new Uniform(null), multipleScatteringTexture: new Uniform(null), irradianceTexture: new Uniform(null), scatteringOrder: new Uniform(0), layer: new Uniform(0) } }) indirectIrradianceMaterial = new PrecomputeMaterial({ fragmentShader: resolveIncludes(indirectIrradianceShader, { bruneton: { common, definitions, precompute } }), uniforms: { luminanceFromRadiance: new Uniform(new Matrix3()), singleRayleighScatteringTexture: new Uniform(null), singleMieScatteringTexture: new Uniform(null), multipleScatteringTexture: new Uniform(null), scatteringOrder: new Uniform(0) } }) multipleScatteringMaterial = new PrecomputeMaterial({ fragmentShader: resolveIncludes(multipleScatteringShader, { bruneton: { common, definitions, precompute } }), uniforms: { luminanceFromRadiance: new Uniform(new Matrix3()), transmittanceTexture: new Uniform(null), scatteringDensityTexture: new Uniform(null), layer: new Uniform(0) } }) private readonly renderer: WebGLRenderer private readonly type: AnyFloatType private readonly mesh = new Mesh(new PlaneGeometry(2, 2)) private readonly scene = new Scene().add(this.mesh) private readonly camera = new Camera() private updating = false private disposeQueue: (() => void) | undefined constructor( renderer: WebGLRenderer, { type = isFloatLinearSupported(renderer) ? FloatType : HalfFloatType, combinedScattering = true, higherOrderScattering = true }: PrecomputedTexturesGeneratorOptions = {} ) { this.renderer = renderer this.type = type this.transmittanceRenderTarget = createRenderTarget( type, TRANSMITTANCE_TEXTURE_WIDTH, TRANSMITTANCE_TEXTURE_HEIGHT ) this.scatteringRenderTarget = create3DRenderTarget( type, SCATTERING_TEXTURE_WIDTH, SCATTERING_TEXTURE_HEIGHT, SCATTERING_TEXTURE_DEPTH ) this.irradianceRenderTarget = createRenderTarget( type, IRRADIANCE_TEXTURE_WIDTH, IRRADIANCE_TEXTURE_HEIGHT ) if (!combinedScattering) { this.singleMieScatteringRenderTarget = create3DRenderTarget( type, SCATTERING_TEXTURE_WIDTH, SCATTERING_TEXTURE_HEIGHT, SCATTERING_TEXTURE_DEPTH ) } if (higherOrderScattering) { this.higherOrderScatteringRenderTarget = create3DRenderTarget( type, SCATTERING_TEXTURE_WIDTH, SCATTERING_TEXTURE_HEIGHT, SCATTERING_TEXTURE_DEPTH ) } this.textures = { transmittanceTexture: this.transmittanceRenderTarget.texture, scatteringTexture: this.scatteringRenderTarget.texture, irradianceTexture: this.irradianceRenderTarget.texture, singleMieScatteringTexture: this.singleMieScatteringRenderTarget?.texture, higherOrderScatteringTexture: this.higherOrderScatteringRenderTarget?.texture } } private render3DRenderTarget( renderTarget: WebGL3DRenderTarget, material: PrecomputeMaterial ): void { for (let layer = 0; layer < renderTarget.depth; ++layer) { material.uniforms.layer.value = layer this.renderer.setRenderTarget(renderTarget, layer) this.renderer.render(this.scene, this.camera) } } private computeTransmittance(params: { renderTarget: WebGLRenderTarget }): void { const material = this.transmittanceMaterial delete material.defines.TRANSMITTANCE_PRECISION_LOG material.needsUpdate = true this.mesh.material = material this.renderer.setRenderTarget(params.renderTarget) this.renderer.render(this.scene, this.camera) } private computeOpticalDepth(params: { renderTarget: WebGLRenderTarget }): void { const material = this.transmittanceMaterial material.defines.TRANSMITTANCE_PRECISION_LOG = '1' material.needsUpdate = true this.mesh.material = material this.renderer.setRenderTarget(params.renderTarget) this.renderer.render(this.scene, this.camera) } private computeDirectIrradiance(params: { renderTarget: WebGLRenderTarget context: Context output: 'deltaIrradiance' | 'irradiance' additive: boolean }): void { const material = this.directIrradianceMaterial material.defines.OUTPUT = params.output material.additive = params.additive if (this.type === HalfFloatType) { material.defines.TRANSMITTANCE_PRECISION_LOG = '1' } else { delete material.defines.TRANSMITTANCE_PRECISION_LOG } material.needsUpdate = true const uniforms = material.uniforms uniforms.transmittanceTexture.value = params.context.opticalDepth?.texture ?? this.transmittanceRenderTarget.texture this.mesh.material = material this.renderer.setRenderTarget(params.renderTarget) this.renderer.render(this.scene, this.camera) } private computeSingleScattering(params: { renderTarget: WebGL3DRenderTarget context: Context output: 'deltaRayleigh' | 'deltaMie' | 'scattering' | 'singleMieScattering' additive: boolean }): void { const material = this.singleScatteringMaterial material.defines.OUTPUT = params.output material.additive = params.additive if (this.type === HalfFloatType) { material.defines.TRANSMITTANCE_PRECISION_LOG = '1' } else { delete material.defines.TRANSMITTANCE_PRECISION_LOG } material.needsUpdate = true const uniforms = material.uniforms uniforms.transmittanceTexture.value = params.context.opticalDepth?.texture ?? this.transmittanceRenderTarget.texture material.setUniforms(params.context) this.mesh.material = material this.render3DRenderTarget(params.renderTarget, material) } private computeScatteringDensity(params: { renderTarget: WebGL3DRenderTarget context: Context scatteringOrder: number }): void { const material = this.scatteringDensityMaterial if (this.type === HalfFloatType) { material.defines.TRANSMITTANCE_PRECISION_LOG = '1' } else { delete material.defines.TRANSMITTANCE_PRECISION_LOG } material.needsUpdate = true const uniforms = material.uniforms uniforms.transmittanceTexture.value = params.context.opticalDepth?.texture ?? this.transmittanceRenderTarget.texture uniforms.scatteringOrder.value = params.scatteringOrder material.setUniforms(params.context) this.mesh.material = material this.render3DRenderTarget(params.renderTarget, material) } private computeIndirectIrradiance(params: { renderTarget: WebGLRenderTarget context: Context scatteringOrder: number output: 'deltaIrradiance' | 'irradiance' additive: boolean }): void { const material = this.indirectIrradianceMaterial material.defines.OUTPUT = params.output material.additive = params.additive material.needsUpdate = true const uniforms = material.uniforms uniforms.scatteringOrder.value = params.scatteringOrder - 1 material.setUniforms(params.context) this.mesh.material = material this.renderer.setRenderTarget(params.renderTarget) this.renderer.render(this.scene, this.camera) } private computeMultipleScattering(params: { renderTarget: WebGL3DRenderTarget context: Context output: 'deltaMultipleScattering' | 'scattering' additive: boolean }): void { const material = this.multipleScatteringMaterial material.defines.OUTPUT = params.output material.additive = params.additive if (this.type === HalfFloatType) { material.defines.TRANSMITTANCE_PRECISION_LOG = '1' } else { delete material.defines.TRANSMITTANCE_PRECISION_LOG } material.needsUpdate = true const uniforms = material.uniforms uniforms.transmittanceTexture.value = params.context.opticalDepth?.texture ?? this.transmittanceRenderTarget.texture material.setUniforms(params.context) this.mesh.material = material this.render3DRenderTarget(params.renderTarget, material) } private *precompute(context: Context, additive: boolean): Iterable { // Note that we have to render the same materials multiple times where: // (1) different blending modes (2) rendering into 3D textures, because // MRT isn't supported in these situations. // Compute the transmittance, and store it in transmittanceTexture. this.computeTransmittance({ renderTarget: this.transmittanceRenderTarget }) // Compute the optical depth, and store it in opticalDepth. Avoid having // tiny transmittance values underflow to 0 due to half-float precision. if (this.type === HalfFloatType) { invariant(context.opticalDepth != null) this.computeOpticalDepth({ renderTarget: context.opticalDepth }) } // Compute the direct irradiance, store it in deltaIrradiance and, // depending on "additive", either initialize irradianceTexture with zeros // or leave it unchanged (we don't want the direct irradiance in // irradianceTexture, but only the irradiance from the sky). this.computeDirectIrradiance({ renderTarget: context.deltaIrradiance, context, output: 'deltaIrradiance', additive: false }) this.computeDirectIrradiance({ renderTarget: this.irradianceRenderTarget, context, output: 'irradiance', additive }) this.renderer.setRenderTarget(null) yield // Compute the rayleigh and mie single scattering, store them in // deltaRayleighScattering and deltaMieScattering, and either store them or // accumulate them in scatteringTexture and optional // mieScatteringTexture. this.computeSingleScattering({ renderTarget: context.deltaRayleighScattering, context, output: 'deltaRayleigh', additive: false }) this.computeSingleScattering({ renderTarget: context.deltaMieScattering, context, output: 'deltaMie', additive: false }) this.computeSingleScattering({ renderTarget: this.scatteringRenderTarget, context, output: 'scattering', additive }) if (this.singleMieScatteringRenderTarget != null) { this.computeSingleScattering({ renderTarget: this.singleMieScatteringRenderTarget, context, output: 'singleMieScattering', additive }) } this.renderer.setRenderTarget(null) yield // Compute the 2nd, 3rd and 4th order of scattering, in sequence. for (let scatteringOrder = 2; scatteringOrder <= 4; ++scatteringOrder) { // Compute the scattering density, and store it in deltaScatteringDensity. this.computeScatteringDensity({ renderTarget: context.deltaScatteringDensity, context, scatteringOrder }) // Compute the indirect irradiance, store it in deltaIrradiance and // accumulate it in irradianceTexture. this.computeIndirectIrradiance({ renderTarget: context.deltaIrradiance, context, scatteringOrder, output: 'deltaIrradiance', additive: false }) this.computeIndirectIrradiance({ renderTarget: this.irradianceRenderTarget, context, scatteringOrder, output: 'irradiance', additive: true }) // Compute the multiple scattering, store it in deltaMultipleScattering, // and accumulate it in scatteringTexture. this.computeMultipleScattering({ renderTarget: context.deltaMultipleScattering, context, output: 'deltaMultipleScattering', additive: false }) this.computeMultipleScattering({ renderTarget: this.scatteringRenderTarget, context, output: 'scattering', additive: true }) if (this.higherOrderScatteringRenderTarget != null) { this.computeMultipleScattering({ renderTarget: this.higherOrderScatteringRenderTarget, context, output: 'scattering', additive: true }) } this.renderer.setRenderTarget(null) yield } } async update( atmosphere = AtmosphereParameters.DEFAULT ): Promise { this.updating = true const atmosphereUniform = atmosphere.toUniform() this.transmittanceMaterial.uniforms.ATMOSPHERE = atmosphereUniform this.directIrradianceMaterial.uniforms.ATMOSPHERE = atmosphereUniform this.singleScatteringMaterial.uniforms.ATMOSPHERE = atmosphereUniform this.scatteringDensityMaterial.uniforms.ATMOSPHERE = atmosphereUniform this.indirectIrradianceMaterial.uniforms.ATMOSPHERE = atmosphereUniform this.multipleScatteringMaterial.uniforms.ATMOSPHERE = atmosphereUniform const renderer = this.renderer const context = new Context(this.type) context.lambdas.set(680, 550, 440) context.luminanceFromRadiance.identity() const autoClear = renderer.autoClear renderer.autoClear = false await iterateIdle(this.precompute(context, false)) renderer.autoClear = autoClear context.dispose() // Transmittance and irradiance textures needs access to the pixel data. await readRenderTargetPixels( this.renderer, this.transmittanceRenderTarget, this.transmittanceRenderTarget.texture ) await readRenderTargetPixels( this.renderer, this.irradianceRenderTarget, this.irradianceRenderTarget.texture ) this.updating = false this.disposeQueue?.() return this.textures } dispose(options: { textures?: boolean } = {}): void { if (this.updating) { this.disposeQueue = () => { this.dispose(options) this.disposeQueue = undefined } return } const { textures: disposeTextures = true } = options if (!disposeTextures) { this.transmittanceRenderTarget.textures.splice(0, 1) this.scatteringRenderTarget.textures.splice(0, 1) this.irradianceRenderTarget.textures.splice(0, 1) this.singleMieScatteringRenderTarget?.textures.splice(0, 1) this.higherOrderScatteringRenderTarget?.textures.splice(0, 1) } this.transmittanceRenderTarget.dispose() this.scatteringRenderTarget.dispose() this.irradianceRenderTarget.dispose() this.singleMieScatteringRenderTarget?.dispose() this.higherOrderScatteringRenderTarget?.dispose() this.transmittanceMaterial.dispose() this.directIrradianceMaterial.dispose() this.singleScatteringMaterial.dispose() this.scatteringDensityMaterial.dispose() this.indirectIrradianceMaterial.dispose() this.multipleScatteringMaterial.dispose() this.mesh.geometry.dispose() } }