@takram/three-clouds
Version:
A Three.js and R3F implementation of geospatial volumetric clouds
495 lines (436 loc) • 15.7 kB
text/typescript
import {
GLSL3,
Matrix4,
Uniform,
Vector2,
Vector3,
Vector4,
type BufferGeometry,
type Camera,
type Data3DTexture,
type DataArrayTexture,
type Group,
type Object3D,
type OrthographicCamera,
type PerspectiveCamera,
type Scene,
type Texture,
type WebGLRenderer
} from 'three'
import {
AtmosphereMaterialBase,
AtmosphereParameters,
type AtmosphereMaterialBaseUniforms
} from '@takram/three-atmosphere'
import {
common,
definitions,
runtime
} from '@takram/three-atmosphere/shaders/bruneton'
import {
define,
defineExpression,
defineFloat,
defineInt,
Geodetic,
reinterpretType,
resolveIncludes,
unrollLoops
} from '@takram/three-geospatial'
import {
cascadedShadowMaps,
depth,
generators,
interleavedGradientNoise,
math,
raySphereIntersection,
turbo,
vogelDisk
} from '@takram/three-geospatial/shaders'
import { bayerOffsets } from './bayer'
import { defaults } from './qualityPresets'
import type {
AtmosphereUniforms,
CloudLayerUniforms,
CloudParameterUniforms
} from './uniforms'
import fragmentShader from './shaders/clouds.frag?raw'
import clouds from './shaders/clouds.glsl?raw'
import vertexShader from './shaders/clouds.vert?raw'
import parameters from './shaders/parameters.glsl?raw'
import types from './shaders/types.glsl?raw'
const vectorScratch = /*#__PURE__*/ new Vector3()
const geodeticScratch = /*#__PURE__*/ new Geodetic()
export interface CloudsMaterialParameters {
parameterUniforms: CloudParameterUniforms
layerUniforms: CloudLayerUniforms
atmosphereUniforms: AtmosphereUniforms
}
export interface CloudsMaterialUniforms
extends CloudParameterUniforms,
CloudLayerUniforms,
AtmosphereUniforms {
depthBuffer: Uniform<Texture | null>
viewMatrix: Uniform<Matrix4>
inverseProjectionMatrix: Uniform<Matrix4>
inverseViewMatrix: Uniform<Matrix4>
reprojectionMatrix: Uniform<Matrix4>
viewReprojectionMatrix: Uniform<Matrix4>
resolution: Uniform<Vector2>
cameraNear: Uniform<number>
cameraFar: Uniform<number>
cameraHeight: Uniform<number>
frame: Uniform<number>
temporalJitter: Uniform<Vector2>
targetUvScale: Uniform<Vector2>
mipLevelScale: Uniform<number>
stbnTexture: Uniform<Data3DTexture | null>
// Scattering
skyLightScale: Uniform<number>
groundBounceScale: Uniform<number>
powderScale: Uniform<number>
powderExponent: Uniform<number>
// Primary raymarch
maxIterationCount: Uniform<number>
minStepSize: Uniform<number>
maxStepSize: Uniform<number>
maxRayDistance: Uniform<number>
perspectiveStepScale: Uniform<number>
minDensity: Uniform<number>
minExtinction: Uniform<number>
minTransmittance: Uniform<number>
// Secondary raymarch
maxIterationCountToSun: Uniform<number>
maxIterationCountToGround: Uniform<number>
minSecondaryStepSize: Uniform<number>
secondaryStepScale: Uniform<number>
// Beer shadow map
shadowBuffer: Uniform<DataArrayTexture | null>
shadowTexelSize: Uniform<Vector2>
shadowIntervals: Uniform<Vector2[]>
shadowMatrices: Uniform<Matrix4[]>
shadowFar: Uniform<number>
maxShadowFilterRadius: Uniform<number>
// Shadow length
maxShadowLengthIterationCount: Uniform<number>
minShadowLengthStepSize: Uniform<number>
maxShadowLengthRayDistance: Uniform<number>
// Haze
hazeDensityScale: Uniform<number>
hazeExponent: Uniform<number>
hazeScatteringCoefficient: Uniform<number>
hazeAbsorptionCoefficient: Uniform<number>
}
export class CloudsMaterial extends AtmosphereMaterialBase {
declare uniforms: AtmosphereMaterialBaseUniforms & CloudsMaterialUniforms
temporalUpscale = true
private previousProjectionMatrix?: Matrix4
private previousViewMatrix?: Matrix4
constructor(
{
parameterUniforms,
layerUniforms,
atmosphereUniforms
}: CloudsMaterialParameters,
atmosphere = AtmosphereParameters.DEFAULT
) {
super(
{
name: 'CloudsMaterial',
glslVersion: GLSL3,
vertexShader: resolveIncludes(vertexShader, {
atmosphere: {
bruneton: {
common,
definitions,
runtime
}
},
types
}),
fragmentShader: unrollLoops(
resolveIncludes(fragmentShader, {
core: {
depth,
math,
turbo,
generators,
raySphereIntersection,
cascadedShadowMaps,
interleavedGradientNoise,
vogelDisk
},
atmosphere: {
bruneton: {
common,
definitions,
runtime
}
},
types,
parameters,
clouds
})
),
// prettier-ignore
uniforms: {
...parameterUniforms,
...layerUniforms,
...atmosphereUniforms,
depthBuffer: new Uniform(null),
viewMatrix: new Uniform(new Matrix4()),
inverseProjectionMatrix: new Uniform(new Matrix4()),
inverseViewMatrix: new Uniform(new Matrix4()),
reprojectionMatrix: new Uniform(new Matrix4()),
viewReprojectionMatrix: new Uniform(new Matrix4()),
resolution: new Uniform(new Vector2()),
cameraNear: new Uniform(0),
cameraFar: new Uniform(0),
cameraHeight: new Uniform(0),
frame: new Uniform(0),
temporalJitter: new Uniform(new Vector2()),
targetUvScale: new Uniform(new Vector2()),
mipLevelScale: new Uniform(1),
stbnTexture: new Uniform(null),
// Scattering
skyLightScale: new Uniform(1),
groundBounceScale: new Uniform(1),
powderScale: new Uniform(0.8),
powderExponent: new Uniform(150),
// Primary raymarch
maxIterationCount: new Uniform(defaults.clouds.maxIterationCount),
minStepSize: new Uniform(defaults.clouds.minStepSize),
maxStepSize: new Uniform(defaults.clouds.maxStepSize),
maxRayDistance: new Uniform(defaults.clouds.maxRayDistance),
perspectiveStepScale: new Uniform(defaults.clouds.perspectiveStepScale),
minDensity: new Uniform(defaults.clouds.minDensity),
minExtinction: new Uniform(defaults.clouds.minExtinction),
minTransmittance: new Uniform(defaults.clouds.minTransmittance),
// Secondary raymarch
maxIterationCountToSun: new Uniform(defaults.clouds.maxIterationCountToSun),
maxIterationCountToGround: new Uniform(defaults.clouds.maxIterationCountToGround),
minSecondaryStepSize: new Uniform(defaults.clouds.minSecondaryStepSize),
secondaryStepScale: new Uniform(defaults.clouds.secondaryStepScale),
// Beer shadow map
shadowBuffer: new Uniform(null),
shadowTexelSize: new Uniform(new Vector2()),
shadowIntervals: new Uniform(
Array.from({ length: 4 }, () => new Vector2()) // Populate the max number of elements
),
shadowMatrices: new Uniform(
Array.from({ length: 4 }, () => new Matrix4()) // Populate the max number of elements
),
shadowFar: new Uniform(0),
maxShadowFilterRadius: new Uniform(6),
shadowLayerMask: new Uniform(new Vector4().setScalar(1)), // Disable mask
// Shadow length
maxShadowLengthIterationCount: new Uniform(defaults.clouds.maxShadowLengthIterationCount),
minShadowLengthStepSize: new Uniform(defaults.clouds.minShadowLengthStepSize),
maxShadowLengthRayDistance: new Uniform(defaults.clouds.maxShadowLengthRayDistance),
// Haze
hazeDensityScale: new Uniform(3e-5),
hazeExponent: new Uniform(1e-3),
hazeScatteringCoefficient: new Uniform(0.9),
hazeAbsorptionCoefficient: new Uniform(0.5),
} satisfies Partial<AtmosphereMaterialBaseUniforms> &
CloudsMaterialUniforms
},
atmosphere
)
}
override onBeforeRender(
renderer: WebGLRenderer,
scene: Scene,
camera: Camera,
geometry: BufferGeometry,
object: Object3D,
group: Group
): void {
// Disable onBeforeRender in AtmosphereMaterialBase because we're rendering
// into fullscreen quad with another camera for the scene projection.
const prevLogarithmicDepthBuffer =
this.defines.USE_LOGARITHMIC_DEPTH_BUFFER != null
const nextLogarithmicDepthBuffer =
renderer.capabilities.logarithmicDepthBuffer
if (nextLogarithmicDepthBuffer !== prevLogarithmicDepthBuffer) {
if (nextLogarithmicDepthBuffer) {
this.defines.USE_LOGARITHMIC_DEPTH_BUFFER = '1'
} else {
delete this.defines.USE_LOGARITHMIC_DEPTH_BUFFER
}
}
const prevPowder = this.defines.POWDER != null
const nextPowder = this.uniforms.powderScale.value > 0
if (nextPowder !== prevPowder) {
if (nextPowder) {
this.defines.POWDER = '1'
} else {
delete this.defines.POWDER
}
this.needsUpdate = true
}
const prevGroundIrradiance = this.defines.GROUND_BOUNCE != null
const nextGroundIrradiance =
this.uniforms.groundBounceScale.value > 0 &&
this.uniforms.maxIterationCountToGround.value > 0
if (nextGroundIrradiance !== prevGroundIrradiance) {
if (nextPowder) {
this.defines.GROUND_BOUNCE = '1'
} else {
delete this.defines.GROUND_BOUNCE
}
this.needsUpdate = true
}
}
override copyCameraSettings(camera: Camera): void {
// Intentionally omit the call to super.
if (camera.isPerspectiveCamera === true) {
if (this.defines.PERSPECTIVE_CAMERA !== '1') {
this.defines.PERSPECTIVE_CAMERA = '1'
this.needsUpdate = true
}
} else {
if (this.defines.PERSPECTIVE_CAMERA != null) {
delete this.defines.PERSPECTIVE_CAMERA
this.needsUpdate = true
}
}
const uniforms = this.uniforms
uniforms.viewMatrix.value.copy(camera.matrixWorldInverse)
uniforms.inverseViewMatrix.value.copy(camera.matrixWorld)
const previousProjectionMatrix =
this.previousProjectionMatrix ?? camera.projectionMatrix
const previousViewMatrix =
this.previousViewMatrix ?? camera.matrixWorldInverse
const inverseProjectionMatrix = uniforms.inverseProjectionMatrix.value
const inverseViewMatrix = uniforms.inverseViewMatrix.value
const reprojectionMatrix = uniforms.reprojectionMatrix.value
const viewReprojectionMatrix = uniforms.viewReprojectionMatrix.value
if (this.temporalUpscale) {
const frame = uniforms.frame.value % 16
const resolution = uniforms.resolution.value
const offset = bayerOffsets[frame]
const dx = ((offset.x - 0.5) / resolution.x) * 4
const dy = ((offset.y - 0.5) / resolution.y) * 4
uniforms.temporalJitter.value.set(dx, dy)
uniforms.mipLevelScale.value = 0.25 // NOTE: Not exactly
inverseProjectionMatrix.copy(camera.projectionMatrix)
inverseProjectionMatrix.elements[8] += dx * 2
inverseProjectionMatrix.elements[9] += dy * 2
inverseProjectionMatrix.invert()
// Jitter the previous projection matrix with the current jitter.
reprojectionMatrix.copy(previousProjectionMatrix)
reprojectionMatrix.elements[8] += dx * 2
reprojectionMatrix.elements[9] += dy * 2
reprojectionMatrix.multiply(previousViewMatrix)
viewReprojectionMatrix
.copy(reprojectionMatrix)
.multiply(inverseViewMatrix)
} else {
uniforms.temporalJitter.value.setScalar(0)
uniforms.mipLevelScale.value = 1
inverseProjectionMatrix.copy(camera.projectionMatrixInverse)
reprojectionMatrix
.copy(previousProjectionMatrix)
.multiply(previousViewMatrix)
viewReprojectionMatrix
.copy(reprojectionMatrix)
.multiply(inverseViewMatrix)
}
reinterpretType<PerspectiveCamera | OrthographicCamera>(camera)
uniforms.cameraNear.value = camera.near
uniforms.cameraFar.value = camera.far
const cameraPosition = camera.getWorldPosition(
uniforms.cameraPosition.value
)
const cameraPositionECEF = vectorScratch
.copy(cameraPosition)
.applyMatrix4(uniforms.worldToECEFMatrix.value)
try {
uniforms.cameraHeight.value =
geodeticScratch.setFromECEF(cameraPositionECEF).height
} catch (error) {
// Abort when unable to project position to the ellipsoid surface.
}
}
// copyCameraSettings can be called multiple times within a frame. Only
// reliable way is to explicitly store the matrices.
copyReprojectionMatrix(camera: Camera): void {
this.previousProjectionMatrix ??= new Matrix4()
this.previousViewMatrix ??= new Matrix4()
this.previousProjectionMatrix.copy(camera.projectionMatrix)
this.previousViewMatrix.copy(camera.matrixWorldInverse)
}
setSize(
width: number,
height: number,
targetWidth?: number,
targetHeight?: number
): void {
this.uniforms.resolution.value.set(width, height)
if (targetWidth != null && targetHeight != null) {
// The size of the high-resolution target buffer differs from the upscaled
// resolution, which is a multiple of 4. This must be corrected when
// reading from the depth buffer.
this.uniforms.targetUvScale.value.set(
width / targetWidth,
height / targetHeight
)
} else {
this.uniforms.targetUvScale.value.setScalar(1)
}
// Invalidate reprojection.
this.previousProjectionMatrix = undefined
this.previousViewMatrix = undefined
}
setShadowSize(width: number, height: number): void {
this.uniforms.shadowTexelSize.value.set(1 / width, 1 / height)
}
get depthBuffer(): Texture | null {
return this.uniforms.depthBuffer.value
}
set depthBuffer(value: Texture | null) {
this.uniforms.depthBuffer.value = value
}
@defineInt('DEPTH_PACKING')
depthPacking = 0
@defineExpression('LOCAL_WEATHER_CHANNELS', {
validate: value => /^[rgba]{4}$/.test(value)
})
localWeatherChannels = 'rgba'
@define('SHAPE_DETAIL')
shapeDetail: boolean = defaults.shapeDetail
@define('TURBULENCE')
turbulence: boolean = defaults.turbulence
@define('SHADOW_LENGTH')
shadowLength: boolean = defaults.lightShafts
@define('HAZE')
haze: boolean = defaults.haze
@defineInt('MULTI_SCATTERING_OCTAVES', { min: 1, max: 12 })
multiScatteringOctaves: number = defaults.clouds.multiScatteringOctaves
/** @deprecated Use accurateSunSkyLight instead. */
get accurateSunSkyIrradiance(): boolean {
return this.accurateSunSkyLight
}
/** @deprecated Use accurateSunSkyLight instead. */
set accurateSunSkyIrradiance(value: boolean) {
this.accurateSunSkyLight = value
}
@define('ACCURATE_SUN_SKY_LIGHT')
accurateSunSkyLight: boolean = defaults.clouds.accurateSunSkyLight
@define('ACCURATE_PHASE_FUNCTION')
accuratePhaseFunction: boolean = defaults.clouds.accuratePhaseFunction
@defineInt('SHADOW_CASCADE_COUNT', { min: 1, max: 4 })
shadowCascadeCount: number = defaults.shadow.cascadeCount
@defineInt('SHADOW_SAMPLE_COUNT', { min: 1, max: 16 })
shadowSampleCount = 8
// Ideally these should be uniforms, but perhaps due to the phase function
// is highly optimizable and used many times, defining them as macros
// improves fps by around 2-4, depending on the condition, though.
@defineFloat('SCATTER_ANISOTROPY_1')
scatterAnisotropy1 = 0.7
@defineFloat('SCATTER_ANISOTROPY_2')
scatterAnisotropy2 = -0.2
@defineFloat('SCATTER_ANISOTROPY_MIX')
scatterAnisotropyMix = 0.5
}