UNPKG

@takram/three-atmosphere

Version:
144 lines (123 loc) 4.13 kB
import { CubeCamera, WebGLCubeRenderTarget as CubeRenderTarget, HalfFloatType, Mesh, RGBAFormat, Vector3 } from 'three' import { pmremTexture, positionGeometry, vec4 } from 'three/tsl' import { NodeMaterial, NodeUpdateType, TempNode, type NodeBuilder, type NodeFrame } from 'three/webgpu' import { QuadGeometry, radians } from '@takram/three-geospatial' import { OnBeforeFrameUpdate } from '@takram/three-geospatial/webgpu' import { getAtmosphereContext } from './AtmosphereContext' import { sky, type SkyNode } from './SkyNode' export class SkyEnvironmentNode extends TempNode { static override get type(): string { return 'SkyEnvironmentNode' } skyNode: SkyNode distanceThreshold = 1000 angularThreshold = radians(0.1) private readonly renderTarget: CubeRenderTarget private readonly cubeCamera: CubeCamera private readonly material = new NodeMaterial() private readonly mesh = new Mesh(new QuadGeometry(), this.material) private currentVersion?: number private removeLUTUpdateListener?: () => void constructor(size = 64) { super('vec3') this.updateBeforeType = NodeUpdateType.FRAME this.material.name = 'SkyEnvironment' this.skyNode = sky() this.skyNode.showSun = false this.skyNode.showMoon = false this.skyNode.showStars = false this.skyNode.useContextCamera = false this.renderTarget = new CubeRenderTarget(size, { depthBuffer: false, type: HalfFloatType, format: RGBAFormat }) this.cubeCamera = new CubeCamera(0.1, 1000, this.renderTarget) } override updateBefore({ renderer }: NodeFrame): void { if (renderer == null || this.version === this.currentVersion) { return } this.currentVersion = this.version this.cubeCamera.update(renderer, this.mesh) } override setup(builder: NodeBuilder): unknown { const context = getAtmosphereContext(builder) const { camera } = context if (camera != null) { const nextPosition = new Vector3() const prevPosition = new Vector3() OnBeforeFrameUpdate(() => { // TODO: Ideally, this should be compared against the parameterization // values of the LUT. (i.e. radius, angle between view and sun, etc.) nextPosition .copy(camera.position) .divideScalar(this.distanceThreshold) .round() if (!prevPosition.equals(nextPosition)) { prevPosition.copy(nextPosition) this.needsUpdate = true } }) } const sunDirection = context.sunDirectionECEF.value.clone() OnBeforeFrameUpdate(() => { const { value } = context.sunDirectionECEF if (sunDirection.angleTo(value) > this.angularThreshold) { sunDirection.copy(value) this.needsUpdate = true } }) const moonDirection = context.moonDirectionECEF.value.clone() OnBeforeFrameUpdate(() => { const { value } = context.moonDirectionECEF if (moonDirection.angleTo(value) > this.angularThreshold) { moonDirection.copy(value) this.needsUpdate = true } }) const handleLUTUpdate = (): void => { this.needsUpdate = true } context.lutNode.addEventListener( // @ts-expect-error Cannot specify the events map 'update', handleLUTUpdate ) this.removeLUTUpdateListener?.() this.removeLUTUpdateListener = () => { context.lutNode.removeEventListener( // @ts-expect-error Cannot specify the events map 'update', handleLUTUpdate ) } this.material.vertexNode = vec4(positionGeometry.xy, 0, 1) this.material.fragmentNode = this.skyNode return pmremTexture(this.renderTarget.texture) } override dispose(): void { this.removeLUTUpdateListener?.() this.renderTarget.dispose() this.skyNode.dispose() // TODO: Conditionally depending on the owner. this.material.dispose() this.mesh.geometry.dispose() super.dispose() } } export const skyEnvironment = ( ...args: ConstructorParameters<typeof SkyEnvironmentNode> ): SkyEnvironmentNode => new SkyEnvironmentNode(...args)