@takram/three-atmosphere
Version:
A Three.js and R3F implementation of Precomputed Atmospheric Scattering
247 lines (230 loc) • 7 kB
text/typescript
import {
Data3DTexture,
DataTexture,
FloatType,
HalfFloatType,
LinearFilter,
Loader,
type DataTextureImageData,
type LoadingManager,
type Texture,
type WebGLRenderer
} from 'three'
import join from 'url-join'
import {
DataTextureLoader,
EXR3DTextureLoader,
EXRTextureLoader,
Float16Array,
isFloatLinearSupported,
parseFloat16Array,
reinterpretType,
type AnyFloatType
} from '@takram/three-geospatial'
import {
IRRADIANCE_TEXTURE_HEIGHT,
IRRADIANCE_TEXTURE_WIDTH,
SCATTERING_TEXTURE_DEPTH,
SCATTERING_TEXTURE_HEIGHT,
SCATTERING_TEXTURE_WIDTH,
TRANSMITTANCE_TEXTURE_HEIGHT,
TRANSMITTANCE_TEXTURE_WIDTH
} from './constants'
import type { PrecomputedTextures } from './types'
interface LoaderLike<T> extends Loader<T> {
load: (
url: string,
onLoad?: (data: T) => void,
onProgress?: (event: ProgressEvent) => void,
onError?: (err: unknown) => void
) => T
}
interface LoadTextureOptions<T> {
key: string
loader: LoaderLike<T>
path: string
}
const TRANSMITTANCE_SIZE = {
width: TRANSMITTANCE_TEXTURE_WIDTH,
height: TRANSMITTANCE_TEXTURE_HEIGHT
}
const SCATTERING_SIZE = {
width: SCATTERING_TEXTURE_WIDTH,
height: SCATTERING_TEXTURE_HEIGHT,
depth: SCATTERING_TEXTURE_DEPTH
}
const IRRADIANCE_SIZE = {
width: IRRADIANCE_TEXTURE_WIDTH,
height: IRRADIANCE_TEXTURE_HEIGHT
}
export type PrecomputedTexturesFormat = 'binary' | 'exr'
export interface PrecomputedTexturesLoaderOptions {
format?: PrecomputedTexturesFormat
type?: AnyFloatType
combinedScattering?: boolean
higherOrderScattering?: boolean
}
export class PrecomputedTexturesLoader extends Loader<PrecomputedTextures> {
format: PrecomputedTexturesFormat
type: AnyFloatType
combinedScattering: boolean
higherOrderScattering: boolean
constructor(
{
format = 'exr',
type = HalfFloatType,
combinedScattering = true,
higherOrderScattering = true
}: PrecomputedTexturesLoaderOptions = {},
manager?: LoadingManager
) {
super(manager)
this.format = format
this.type = type
this.combinedScattering = combinedScattering
this.higherOrderScattering = higherOrderScattering
}
setType(renderer: WebGLRenderer): this {
this.type = isFloatLinearSupported(renderer) ? FloatType : HalfFloatType
return this
}
override load(
url: string,
onLoad?: (data: PrecomputedTextures) => void,
onProgress?: (event: ProgressEvent) => void,
onError?: (error: unknown) => void
): PrecomputedTextures {
const textures: Record<string, Texture | undefined> = {}
const loadTexture = <T extends Texture>({
key,
loader,
path
}: LoadTextureOptions<T>): T => {
loader.setRequestHeader(this.requestHeader)
loader.setPath(this.path)
loader.setWithCredentials(this.withCredentials)
return loader.load(
join(url, path),
texture => {
texture.type = this.type
// EXR and binary data are parsed to Uint16Array, which must be
// converted to Float32Array when FloatType is used.
if (this.type === FloatType) {
reinterpretType<DataTextureImageData>(texture.image)
if (texture.image.data != null) {
texture.image.data = new Float32Array(
new Float16Array(texture.image.data?.buffer)
)
}
}
texture.minFilter = LinearFilter
texture.magFilter = LinearFilter
textures[`${key}Texture`] = texture
if (
textures.irradianceTexture != null &&
textures.scatteringTexture != null &&
textures.transmittanceTexture != null &&
(this.combinedScattering ||
textures.singleMieScatteringTexture != null) &&
(!this.higherOrderScattering ||
textures.higherOrderScatteringTexture != null)
) {
onLoad?.(textures as unknown as PrecomputedTextures)
}
},
onProgress,
onError
)
}
if (this.format === 'exr') {
return {
transmittanceTexture: loadTexture({
key: 'transmittance',
loader: new EXRTextureLoader(TRANSMITTANCE_SIZE, this.manager),
path: 'transmittance.exr'
}),
scatteringTexture: loadTexture({
key: 'scattering',
loader: new EXR3DTextureLoader(SCATTERING_SIZE, this.manager),
path: 'scattering.exr'
}),
irradianceTexture: loadTexture({
key: 'irradiance',
loader: new EXRTextureLoader(IRRADIANCE_SIZE, this.manager),
path: 'irradiance.exr'
}),
singleMieScatteringTexture: !this.combinedScattering
? loadTexture({
key: 'singleMieScattering',
loader: new EXR3DTextureLoader(SCATTERING_SIZE, this.manager),
path: 'single_mie_scattering.exr'
})
: undefined,
higherOrderScatteringTexture: this.higherOrderScattering
? loadTexture({
key: 'higherOrderScattering',
loader: new EXR3DTextureLoader(SCATTERING_SIZE, this.manager),
path: 'higher_order_scattering.exr'
})
: undefined
}
} else {
return {
transmittanceTexture: loadTexture({
key: 'transmittance',
loader: new DataTextureLoader(
DataTexture,
parseFloat16Array,
TRANSMITTANCE_SIZE,
this.manager
),
path: 'transmittance.bin'
}),
scatteringTexture: loadTexture({
key: 'scattering',
loader: new DataTextureLoader(
Data3DTexture,
parseFloat16Array,
SCATTERING_SIZE,
this.manager
),
path: 'scattering.bin'
}),
irradianceTexture: loadTexture({
key: 'irradiance',
loader: new DataTextureLoader(
DataTexture,
parseFloat16Array,
IRRADIANCE_SIZE,
this.manager
),
path: 'irradiance.bin'
}),
singleMieScatteringTexture: !this.combinedScattering
? loadTexture({
key: 'singleMieScattering',
loader: new DataTextureLoader(
Data3DTexture,
parseFloat16Array,
SCATTERING_SIZE,
this.manager
),
path: 'single_mie_scattering.bin'
})
: undefined,
higherOrderScatteringTexture: this.higherOrderScattering
? loadTexture({
key: 'higherOrderScattering',
loader: new DataTextureLoader(
Data3DTexture,
parseFloat16Array,
SCATTERING_SIZE,
this.manager
),
path: 'higher_order_scattering.bin'
})
: undefined
}
}
}
}