var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { BackSide, CustomBlending, DoubleSide, FrontSide, Group, MaxEquation, Mesh, MeshBasicMaterial, MeshDepthMaterial, Object3D, OrthographicCamera, PlaneGeometry, ShaderMaterial, Vector3, WebGLRenderTarget } from "three";
import { HorizontalBlurShader } from 'three/examples/jsm/shaders/HorizontalBlurShader.js';
import { VerticalBlurShader } from 'three/examples/jsm/shaders/VerticalBlurShader.js';
import { setAutoFitEnabled } from "../engine/engine_camera.js";
import { addComponent } from "../engine/engine_components.js";
import { Context } from "../engine/engine_context.js";
import { Gizmos } from "../engine/engine_gizmos.js";
import { onStart } from "../engine/engine_lifecycle_api.js";
import { serializable } from "../engine/engine_serialization_decorator.js";
import { getBoundingBox, getVisibleInCustomShadowRendering } from "../engine/engine_three_utils.js";
import { HideFlags } from "../engine/engine_types.js";
import { getParam } from "../engine/engine_utils.js";
import { setCustomVisibility } from "../engine/js-extensions/Layers.js";
import { Behaviour } from "./Component.js";
const debug = getParam("debugcontactshadows");
onStart(ctx => {
    const val = ctx.domElement.getAttribute("contactshadows") || ctx.domElement.getAttribute("contact-shadows");
    if (val != undefined && val != "0" && val != "false") {
        console.debug("Auto-creating ContactShadows because of `contactshadows` attribute");
        const shadows = ContactShadows.auto(ctx);
        const intensity = parseFloat(val);
        if (!isNaN(intensity)) {
            shadows.opacity = intensity;
            shadows.darkness = intensity;
        }
    }
});
// Adapted from https://github.com/mrdoob/three.js/blob/master/examples/webgl_shadow_contact.html.
// Improved with
// - ground occluder
// - backface shadowing (slightly less than front faces)
// - node can simply be scaled in Y to adjust max. ground height
/**
 * [ContactShadows](https://engine.needle.tools/docs/api/ContactShadows) renders proximity-based soft shadows on flat surfaces.
 * Ideal for products or objects that need visual grounding without real-time shadows.
 * Produces soft, blurred shadows that hug the ground, giving a sense of contact and depth.
 *
 * ![](https://cloud.needle.tools/-/media/87bPTNXHcsbV-An-oSEvHQ.gif)
 *
 * **Setup options:**
 * 1. `ContactShadows.auto(context)` - Auto-create and fit to scene
 * 2. Add component manually to control position and scale
 * 3. HTML attribute: `<needle-engine contactshadows="0.7">`
 *
 * **Properties:**
 * - `opacity` / `darkness` - Shadow intensity
 * - `blur` - Softness of shadow edges
 * - Object scale defines shadow area size
 *
 * **Debug:** Use `?debugcontactshadows` URL parameter.
 *
 *
 * @example Auto-create contact shadows
 * ```ts
 * const shadows = ContactShadows.auto(this.context);
 * shadows.opacity = 0.5;
 * shadows.darkness = 0.8;
 * ```
 *
 * @summary Display contact shadows on the ground
 * @category Rendering
 * @group Components
 * @see {@link ShadowCatcher} for real-time shadows from lights (more accurate, higher performance cost)
 * @see {@link Light} for real-time shadow casting
 * @see {@link Renderer} for material/rendering control
 * @link https://engine.needle.tools/samples/contact-shadows for a demo of contact shadows
 */
export class ContactShadows extends Behaviour {
    static _instances = new Map();
    /**
     * Create contact shadows for the scene. Automatically fits the shadows to the scene.
     * The instance of contact shadows will be created only once.
     * @param context The context to create the contact shadows in.
     * @returns The instance of the contact shadows.
     */
    static auto(context, params) {
        if (!context)
            context = Context.Current;
        if (!context) {
            throw new Error("No context provided and no current context set.");
        }
        let instance = this._instances.get(context);
        if (!instance || instance.destroyed) {
            const obj = new Object3D();
            obj.name = "ContactShadows";
            instance = addComponent(obj, ContactShadows, {
                autoFit: false,
                occludeBelowGround: false
            });
            this._instances.set(context, instance);
        }
        context.scene.add(instance.gameObject);
        instance.fitShadows(params);
        return instance;
    }
    /**
     * When enabled the contact shadows component will be created to fit the whole scene.
     * @default false
     */
    autoFit = false;
    /**
     * Darkness of the shadows.
     * @default 0.5
     */
    darkness = 0.5;
    /**
     * Opacity of the shadows.
     * @default 0.5
     */
    opacity = 0.5;
    /**
     * Blur of the shadows.
     * @default 4.0
     */
    blur = 4.0;
    /**
     * When enabled objects will not be visible below the shadow plane
     * @default false
     */
    occludeBelowGround = false;
    /**
     * When enabled the backfaces of objects will cast shadows as well.
     * @default true
     */
    backfaceShadows = true;
    /**
     * The minimum size of the shadows box
     * @default undefined
     */
    minSize;
    /**
     * When enabled the shadows will not be updated automatically. Use `needsUpdate()` to update the shadows manually.
     * This is useful when you want to update the shadows only when the scene changes.
     * @default false
     */
    manualUpdate = false;
    /**
     * Call this method to update the shadows manually. The update will be done in the next frame.
     */
    set needsUpdate(val) {
        this._needsUpdate = val;
    }
    get needsUpdate() {
        return this._needsUpdate;
    }
    _needsUpdate = false;
    /** All shadow objects are parented to this object.
     * The gameObject itself should not be transformed because we want the ContactShadows object e.g. also have a GroundProjectedEnv component
     * in which case ContactShadows scale would affect the projection
     **/
    shadowsRoot = new Object3D();
    shadowCamera;
    shadowGroup = new Group();
    renderTarget;
    renderTargetBlur;
    plane;
    occluderMesh;
    blurPlane;
    planeMaterial;
    depthMaterial;
    horizontalBlurMaterial;
    verticalBlurMaterial;
    textureSize = 512;
    /**
     * Call to fit the shadows to the scene.
     */
    fitShadows(params = {}) {
        if (debug)
            console.warn("Fitting shadows to scene");
        setAutoFitEnabled(this.shadowsRoot, false);
        const objectToFit = params.object || this.context.scene;
        const box = getBoundingBox(objectToFit, [this.shadowsRoot]);
        // expand box in all directions (except below ground)
        // 0.75 expands by 75% in each direction
        // The "32" is pretty much heuristically determined – adjusting the value until we don't get a visible border anymore.
        const expandFactor = Math.max(1, this.blur / 32);
        const sizeX = box.max.x - box.min.x;
        const sizeZ = box.max.z - box.min.z;
        box.expandByVector(new Vector3(expandFactor * sizeX, 0, expandFactor * sizeZ));
        if (debug)
            Gizmos.DrawWireBox3(box, 0xffff00, 60);
        if (this.gameObject.parent) {
            // transform box from world space into parent space
            box.applyMatrix4(this.gameObject.parent.matrixWorld.clone().invert());
        }
        const min = box.min;
        const offset = Math.max(0.00001, (box.max.y - min.y) * .002);
        box.max.y += offset;
        // This is for cases where GroundProjection with autoFit is used
        // Since contact shadows can currently not ignore certain objects from rendering
        // we need to make sure the GroundProjection is not exactly on the same level as ContactShadows
        // We can't move GroundProjection down because of immersive-ar mesh/plane tracking where occlusion would otherwise hide GroundProjection
        this.shadowsRoot.position.set((min.x + box.max.x) / 2, min.y - offset, (min.z + box.max.z) / 2);
        this.shadowsRoot.scale.set(box.max.x - min.x, box.max.y - min.y, box.max.z - min.z);
        if (params.positionOffset) {
            if (params.positionOffset.x !== undefined)
                this.shadowsRoot.position.x += params.positionOffset.x;
            if (params.positionOffset.y !== undefined)
                this.shadowsRoot.position.y += params.positionOffset.y;
            if (params.positionOffset.z !== undefined)
                this.shadowsRoot.position.z += params.positionOffset.z;
        }
        if (params.scaleFactor) {
            if (params.scaleFactor.x !== undefined)
                this.shadowsRoot.scale.x *= params.scaleFactor.x;
            if (params.scaleFactor.y !== undefined)
                this.shadowsRoot.scale.y *= params.scaleFactor.y;
            if (params.scaleFactor.z !== undefined)
                this.shadowsRoot.scale.z *= params.scaleFactor.z;
        }
        this.applyMinSize();
        this.shadowsRoot.matrixWorldNeedsUpdate = true;
        if (debug)
            console.log("Fitted shadows to scene", this.shadowsRoot.scale.clone());
    }
    /** @internal */
    awake() {
        ContactShadows._instances.set(this.context, this);
        this.shadowsRoot.hideFlags = HideFlags.DontExport;
        // ignore self for autofitting
        setAutoFitEnabled(this.shadowsRoot, false);
    }
    /** @internal */
    start() {
        if (debug)
            console.log("Create ContactShadows on " + this.gameObject.name, this);
        this.gameObject.add(this.shadowsRoot);
        this.shadowsRoot.add(this.shadowGroup);
        // the render target that will show the shadows in the plane texture
        this.renderTarget = new WebGLRenderTarget(this.textureSize, this.textureSize);
        this.renderTarget.texture.generateMipmaps = false;
        // the render target that we will use to blur the first render target
        this.renderTargetBlur = new WebGLRenderTarget(this.textureSize, this.textureSize);
        this.renderTargetBlur.texture.generateMipmaps = false;
        // make a plane and make it face up
        const planeGeometry = new PlaneGeometry(1, 1).rotateX(Math.PI / 2);
        if (this.gameObject instanceof Mesh) {
            console.warn("ContactShadows can not be added to a Mesh. Please add it to a Group or an empty Object");
            // this.enabled = false;
            setCustomVisibility(this.gameObject, false);
            // this.plane = this.gameObject as any as Mesh;
            // // Make sure we clone the material once because it might be used on another object as well
            // const mat = this.plane.material = (this.plane.material as MeshBasicMaterial).clone();
            // mat.map = this.renderTarget.texture;
            // mat.opacity = this.opacity;
            // mat.transparent = true;
            // mat.depthWrite = false;
            // mat.needsUpdate = true;
            // When someone makes a custom mesh, they can set these values right on the material.
            // mat.opacity = this.state.plane.opacity;
            // mat.transparent = true;
            // mat.depthWrite = false;
        }
        const planeMaterial = this.planeMaterial = new MeshBasicMaterial({
            map: this.renderTarget.texture,
            opacity: this.opacity,
            color: 0x000000,
            transparent: true,
            depthWrite: false,
            side: FrontSide,
        });
        this.plane = new Mesh(planeGeometry, planeMaterial);
        this.plane.scale.y = -1;
        this.plane.layers.set(2);
        this.shadowsRoot.add(this.plane);
        if (this.plane)
            this.plane.renderOrder = 1;
        this.occluderMesh = new Mesh(this.plane.geometry, new MeshBasicMaterial({
            depthWrite: true,
            stencilWrite: true,
            colorWrite: false,
            side: BackSide,
        }))
            // .rotateX(Math.PI)
            .translateY(-0.0001);
        this.occluderMesh.renderOrder = -100;
        this.occluderMesh.layers.set(2);
        this.shadowsRoot.add(this.occluderMesh);
        // the plane onto which to blur the texture
        this.blurPlane = new Mesh(planeGeometry);
        this.blurPlane.visible = false;
        this.shadowGroup.add(this.blurPlane);
        // max. ground distance is controlled via object scale
        const near = 0;
        const far = 1.0;
        this.shadowCamera = new OrthographicCamera(-1 / 2, 1 / 2, 1 / 2, -1 / 2, near, far);
        this.shadowCamera.layers.enableAll();
        this.shadowCamera.rotation.x = Math.PI / 2; // get the camera to look up
        // Disable automatic matrix world updates because Camera.updateMatrixWorld
        // resets scale to 1 which would break the shadow camera transform
        this.shadowCamera.matrixWorldAutoUpdate = false;
        this.shadowGroup.add(this.shadowCamera);
        // Update the local matrix after setting position/rotation
        this.shadowCamera.updateMatrix();
        // like MeshDepthMaterial, but goes from black to transparent
        this.depthMaterial = new MeshDepthMaterial();
        this.depthMaterial.userData.darkness = { value: this.darkness };
        // this will properly overlap calculated shadows
        this.depthMaterial.blending = CustomBlending;
        this.depthMaterial.blendEquation = MaxEquation;
        // this.depthMaterial.blendEquation = MinEquation;
        this.depthMaterial.onBeforeCompile = shader => {
            if (!this.depthMaterial)
                return;
            shader.uniforms.darkness = this.depthMaterial.userData.darkness;
            shader.fragmentShader = /* glsl */ `
                uniform float darkness;
                ${shader.fragmentShader.replace('gl_FragColor = vec4( vec3( 1.0 - fragCoordZ ), opacity );', 
            // we're scaling the shadow value down a bit when it's a backface (looks better)
            'gl_FragColor = vec4( vec3( 1.0 ), ( 1.0 - fragCoordZ ) * darkness * opacity * (gl_FrontFacing ? 1.0 : 0.66) );')}
            `;
        };
        this.depthMaterial.depthTest = false;
        this.depthMaterial.depthWrite = false;
        this.horizontalBlurMaterial = new ShaderMaterial(HorizontalBlurShader);
        this.horizontalBlurMaterial.depthTest = false;
        this.verticalBlurMaterial = new ShaderMaterial(VerticalBlurShader);
        this.verticalBlurMaterial.depthTest = false;
        this.shadowGroup.visible = false;
        if (this.autoFit)
            this.fitShadows();
        else
            this.applyMinSize();
    }
    onEnable() {
        this._needsUpdate = true;
    }
    /** @internal */
    onDestroy() {
        const instance = ContactShadows._instances.get(this.context);
        if (instance === this) {
            ContactShadows._instances.delete(this.context);
        }
        // dispose the render targets
        this.renderTarget?.dispose();
        this.renderTargetBlur?.dispose();
        // dispose the materials
        this.depthMaterial?.dispose();
        this.horizontalBlurMaterial?.dispose();
        this.verticalBlurMaterial?.dispose();
        // dispose the geometries
        this.blurPlane?.geometry.dispose();
        this.plane?.geometry.dispose();
        this.occluderMesh?.geometry.dispose();
    }
    /** @internal */
    onBeforeRender(_frame) {
        if (this.manualUpdate) {
            if (!this._needsUpdate)
                return;
        }
        this._needsUpdate = false;
        if (!this.renderTarget || !this.renderTargetBlur ||
            !this.depthMaterial || !this.shadowCamera ||
            !this.blurPlane || !this.shadowGroup || !this.plane ||
            !this.horizontalBlurMaterial || !this.verticalBlurMaterial || !this.planeMaterial) {
            if (debug)
                console.error("ContactShadows: not initialized yet");
            return;
        }
        // Update properties that might have been changed
        this.depthMaterial.userData.darkness.value = this.darkness;
        this.planeMaterial.opacity = this.opacity;
        const scene = this.context.scene;
        const renderer = this.context.renderer;
        const initialRenderTarget = renderer.getRenderTarget();
        // Idea: shear the shadowCamera matrix to add some light direction to the ground shadows
        /*
        const mat = this.shadowCamera.projectionMatrix.clone();
        this.shadowCamera.projectionMatrix.multiply(new Matrix4().makeShear(0, 0, 0, 0, 0, 0));
        */
        this.shadowGroup.visible = true;
        if (this.occluderMesh)
            this.occluderMesh.visible = false;
        const planeWasVisible = this.plane.visible;
        this.plane.visible = false;
        if (this.gameObject instanceof Mesh) {
            // this.gameObject.visible = false;
            setCustomVisibility(this.gameObject, false);
        }
        // remove the background
        const initialBackground = scene.background;
        scene.background = null;
        // force the depthMaterial to everything
        scene.overrideMaterial = this.depthMaterial;
        if (this.backfaceShadows)
            this.depthMaterial.side = DoubleSide;
        else {
            this.depthMaterial.side = FrontSide;
        }
        // set renderer clear alpha
        const initialClearAlpha = renderer.getClearAlpha();
        renderer.setClearAlpha(0);
        const prevXRState = renderer.xr.enabled;
        renderer.xr.enabled = false;
        const prevSceneMatrixAutoUpdate = this.context.scene.matrixWorldAutoUpdate;
        this.context.scene.matrixWorldAutoUpdate = false;
        const list = renderer.renderLists.get(scene, 0);
        const prevTransparent = list.transparent;
        empty_buffer.length = 0;
        list.transparent = empty_buffer;
        // we need to hide objects that don't render color or that are wireframes
        objects_hidden.length = 0;
        for (const entry of list.opaque) {
            if (!entry.object.visible)
                continue;
            const mat = entry.material;
            // Ignore objects that don't render color
            let hide = entry.material.colorWrite == false || mat.wireframe === true || getVisibleInCustomShadowRendering(entry.object) === false;
            // Ignore line materials (e.g. GridHelper)
            if (!hide && (entry.material["isLineMaterial"]))
                hide = true;
            // Ignore point materials
            if (!hide && (entry.material["isPointsMaterial"]))
                hide = true;
            if (hide) {
                objects_hidden.push(entry.object);
                entry.object["needle:visible"] = entry.object.visible;
                entry.object.visible = false;
            }
        }
        // Manually update shadow camera's matrix world without calling Camera.updateMatrixWorld
        // (which would reset scale to 1 for glTF conformance)
        if (this.shadowCamera.parent) {
            this.shadowCamera.matrixWorld.multiplyMatrices(this.shadowCamera.parent.matrixWorld, this.shadowCamera.matrix);
        }
        else {
            this.shadowCamera.matrixWorld.copy(this.shadowCamera.matrix);
        }
        this.shadowCamera.matrixWorldInverse.copy(this.shadowCamera.matrixWorld).invert();
        // render to the render target to get the depths
        renderer.setRenderTarget(this.renderTarget);
        renderer.clear();
        renderer.render(scene, this.shadowCamera);
        list.transparent = prevTransparent;
        // reset previously hidden objects
        for (const object of objects_hidden) {
            if (object["needle:visible"] != undefined) {
                object.visible = object["needle:visible"];
            }
        }
        // for the shearing idea
        // this.shadowCamera.projectionMatrix.copy(mat);
        // and reset the override material
        scene.overrideMaterial = null;
        const blurAmount = Math.max(this.blur, 0.05);
        // two-pass blur to reduce the artifacts
        this.blurShadow(blurAmount * 2);
        this.blurShadow(blurAmount * 0.5);
        this.shadowGroup.visible = false;
        if (this.occluderMesh)
            this.occluderMesh.visible = this.occludeBelowGround;
        this.plane.visible = planeWasVisible;
        // reset and render the normal scene
        renderer.setRenderTarget(initialRenderTarget);
        renderer.setClearAlpha(initialClearAlpha);
        scene.background = initialBackground;
        renderer.xr.enabled = prevXRState;
        this.context.scene.matrixWorldAutoUpdate = prevSceneMatrixAutoUpdate;
    }
    // renderTarget --> blurPlane (horizontalBlur) --> renderTargetBlur --> blurPlane (verticalBlur) --> renderTarget
    blurShadow(amount) {
        if (!this.blurPlane || !this.shadowCamera ||
            !this.renderTarget || !this.renderTargetBlur ||
            !this.horizontalBlurMaterial || !this.verticalBlurMaterial)
            return;
        this.blurPlane.visible = true;
        // Correct for contact shadow plane aspect ratio.
        // since we have a separable blur, we can just adjust the blur amount for X and Z individually
        const ws = this.shadowsRoot.worldScale;
        const avg = (ws.x + ws.z) / 2;
        const aspectX = ws.z / avg;
        const aspectZ = ws.x / avg;
        // blur horizontally and draw in the renderTargetBlur
        this.blurPlane.material = this.horizontalBlurMaterial;
        this.blurPlane.material.uniforms.tDiffuse.value = this.renderTarget.texture;
        this.horizontalBlurMaterial.uniforms.h.value = amount * 1 / this.textureSize * aspectX;
        const renderer = this.context.renderer;
        const currentRt = renderer.getRenderTarget();
        renderer.setRenderTarget(this.renderTargetBlur);
        renderer.render(this.blurPlane, this.shadowCamera);
        // blur vertically and draw in the main renderTarget
        this.blurPlane.material = this.verticalBlurMaterial;
        this.blurPlane.material.uniforms.tDiffuse.value = this.renderTargetBlur.texture;
        this.verticalBlurMaterial.uniforms.v.value = amount * 1 / this.textureSize * aspectZ;
        renderer.setRenderTarget(this.renderTarget);
        renderer.render(this.blurPlane, this.shadowCamera);
        this.blurPlane.visible = false;
        renderer.setRenderTarget(currentRt);
    }
    applyMinSize() {
        if (this.minSize) {
            this.shadowsRoot.scale.set(Math.max(this.minSize.x || 0, this.shadowsRoot.scale.x), Math.max(this.minSize.y || 0, this.shadowsRoot.scale.y), Math.max(this.minSize.z || 0, this.shadowsRoot.scale.z));
        }
    }
}
__decorate([
    serializable()
], ContactShadows.prototype, "autoFit", void 0);
__decorate([
    serializable()
], ContactShadows.prototype, "darkness", void 0);
__decorate([
    serializable()
], ContactShadows.prototype, "opacity", void 0);
__decorate([
    serializable()
], ContactShadows.prototype, "blur", void 0);
__decorate([
    serializable()
], ContactShadows.prototype, "occludeBelowGround", void 0);
__decorate([
    serializable()
], ContactShadows.prototype, "backfaceShadows", void 0);
const empty_buffer = [];
const objects_hidden = new Array();
//# sourceMappingURL=ContactShadows.js.map