import { Color, Mesh, ColorRepresentation, Scene, WebGPURenderer } from 'three/webgpu'; import { DIVEComponent } from '../../engine/component/Component.ts'; import { DIVENode } from '../../engine/node/Node.ts'; /** * A soft contact shadow under the node it is attached to, baked once. * * What it draws is not a shadow map and does not come from any light: a camera * under the node looks up and records how close the geometry above it comes to * the ground, and that gets blurred and put on a plane. So it costs one * transparent quad per frame however complicated the model is, the softness is a * number rather than a consequence of a light's size, and it does not flicker or * crawl the way a shadow map does at grazing angles. * * The trade is that it is baked. It holds for a model that stands still -- * which is what it is for -- and anything that moves, turns or reshapes the node * needs another bake. The same events {@link BoundingBoxComponent} watches mark * it stale, and {@link tick} re-bakes it by itself; {@link setAutoBake} turns * that off for a caller that would rather decide, such as an editor dragging a * gizmo. * * The bake needs a renderer and a scene, which a component has no way to ask * for. Both arrive through `onBeforeRender` on the contributed plane -- the * sanctioned route, and the same one {@link GridComponent} uses for its camera. * The first frame therefore draws nothing, and the shadow appears in the second. * {@link bake} is there for a caller that has a renderer and does not want to * wait for that. * * The plane sits on {@link SHADOW_LAYER_MASK}, which keeps it out of bounding * boxes, exports and picking -- and, since the bake camera renders * {@link PRODUCT_LAYER_MASK}, out of its own shadow. Everything else on the * product layer within the frustum is captured, so a model standing next to * another one is shaded by it too. * * @module */ export declare class SoftShadowComponent extends DIVEComponent { readonly isSoftShadowComponent: true; private _mesh; private _material; /** Where the bake ends up, and what the plane samples. Never swapped. */ private _target; /** The other half of the blur ping-pong. */ private _scratch; private _quad; private _blurToScratch; private _blurToTarget; private _horizontal; private _vertical; private _camera; private _depthMaterial; private _opacity; private _darkness; /** How high the bake frustum reaches, so the shader can normalise depth. */ private _reach; private _bounds; private _stale; private _needsBake; private _resolution; private _softness; private _autoBake; /** World-space extent of the plane, which is what a blur radius is in UV of. */ private _width; private _depth; private _renderer; private _scene; private _invalidate; constructor(); /** The plane the shadow is drawn on. */ get mesh(): Mesh; /** Whether the shadow is drawn. */ get visible(): boolean; /** The shadow colour. */ get color(): Color; /** How opaque the shadow is drawn, 0 to 1. */ get opacity(): number; /** How dark the shadow goes where something touches the ground, 0 to 1. */ get darkness(): number; /** How far the shadow spreads past what casts it, in meters. */ get softness(): number; /** Edge length of the shadow texture, in pixels. */ get resolution(): number; /** Whether {@link tick} re-bakes the shadow when it goes stale. */ get autoBake(): boolean; /** Whether the shadow on screen is behind what the node looks like now. */ get needsBake(): boolean; /** * @param visible - Whether the shadow is drawn. */ setVisibility(visible: boolean): this; /** * Costs no bake: the bake writes alpha, and this is the colour under it. * * @param color - The shadow colour. */ setColor(color: ColorRepresentation): this; /** * Costs no bake either, which makes it the one to animate or tie to a slider. * * @param opacity - How opaque the shadow is drawn, 0 to 1. */ setOpacity(opacity: number): this; /** * How dark the shadow goes directly under the model. * * Baked into the texture, unlike {@link setOpacity}, because it is a curve * over height rather than one value over the whole plane. * * @param darkness - The darkness at ground contact, 0 to 1. */ setDarkness(darkness: number): this; /** * How far the shadow spreads past what casts it. * * In meters, so it stays put when the model is swapped for a bigger one. * The plane grows with it -- {@link PADDING_FACTOR} radii past the bounds -- * so raising this never clips the falloff. * * @param softness - The spread in meters. */ setSoftness(softness: number): this; /** * @param resolution - Edge length of the shadow texture in pixels. */ setResolution(resolution: number): this; /** * Whether the component re-bakes itself when the node changes. * * On by default. Worth turning off while something drags the node around, * where every frame would otherwise pay for a bake nobody sees. * * @param autoBake - Whether to re-bake on tick. */ setAutoBake(autoBake: boolean): this; /** * Marks the shadow stale, so it is baked again. * * Only needed for changes nothing announces -- a geometry swapped in place * on a mesh that is already in the node. Everything that goes through the * node's own API arrives here as an event. */ refresh(): this; /** * Renders the shadow. * * Four passes: the silhouette from below, then the blur twice over, each * horizontal and vertical. All of it on the GPU, nothing read back, so this * is synchronous -- but it does render the scene, so it is not something to * call every frame by choice. * * Puts back everything it borrows from the renderer and the scene, including * on the way out of a failure, because the live view renders from the same * two. * * @param renderer - An initialised renderer. * @param scene - The scene the node stands in. */ bake(renderer: WebGPURenderer, scene: Scene): this; /** * Keeps the plane under the node, and bakes when something has changed. * * Enrolled only while there is something to do: a bake turns the tick off * again, and an event turns it back on. */ tick(): void; protected onAttach(owner: DIVENode): void; protected onDetach(previousOwner: DIVENode): void; copy(source: this): this; dispose(): void; /** Re-encloses the owner if anything has happened since the last read. */ private _measure; /** * Sizes the plane to the bounds and puts it under them. * * World-aligned and flat on the lowest point of the node, whatever the * owner's own transform is -- which is why the owner's world matrix is * multiplied back out: a contributed object would otherwise be transformed * by it a second time. */ private _place; /** Points the bake camera up through the bounds from the plane. */ private _aim; /** Matches both targets to the configured resolution. */ private _resize; /** * One separable blur: horizontal into the scratch target, vertical back. * * The radius is in meters and the taps step in UV, so each direction is * divided by the extent of the plane along it -- which is what keeps a * shadow on a wide, shallow plane from being blurred into an oval. * * @param renderer - The renderer to draw with. * @param scale - The radius for this pass, relative to {@link softness}. */ private _blur; /** Marks the shadow current, and stops the tick until something changes. */ private _settle; }