utils/NormalMapCreator.tstypescript
/*
 * Copyright 2026 Michael Gevlich
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import * as quadTree from "../quadTree/quadTree";
import { Framebuffer } from "../webgl/Framebuffer";
import { Lock, Key } from "../Lock";
import { Planet } from "../scene/Planet";
import { ShaderProgram } from "../webgl/ShaderProgram";
import { QueueArray } from "../QueueArray";
import { Segment } from "../segment/Segment";
import { Handler } from "../webgl/Handler";
import type { WebGLBufferExt, WebGLTextureExt } from "../webgl/Handler";

interface INormalMapCreatorParams {
    minTableSize?: number;
    maxTableSize?: number;
    width?: number;
    height?: number;
}

export class NormalMapCreator {
    protected _minTabelSize: number;
    protected _maxTableSize: number;

    protected _planet: Planet;
    protected _handler: Handler | null;
    protected _verticesBufferArray: WebGLBufferExt[];
    protected _indexBufferArray: WebGLBufferExt[];
    protected _positionBuffer: WebGLBufferExt | null;
    protected _framebuffer: Framebuffer | null;
    protected _normalMapVerticesTexture: WebGLTextureExt | null;

    protected _width: number;
    protected _height: number;

    protected _queue: QueueArray<Segment>;

    protected _lock: Lock;

    constructor(planet: Planet, options: INormalMapCreatorParams = {}) {
        this._minTabelSize = options.minTableSize || 1;
        this._maxTableSize = options.maxTableSize || 8;

        this._planet = planet;
        this._handler = null;
        this._verticesBufferArray = [];
        this._indexBufferArray = [];
        this._positionBuffer = null;
        this._framebuffer = null;
        this._normalMapVerticesTexture = null;

        this._width = options.width || 128;
        this._height = options.height || 128;

        this._queue = new QueueArray(1024);

        this._lock = new Lock();
    }

    public get width(): number {
        return this._width;
    }

    public get height(): number {
        return this._height;
    }

    public init() {
        this._maxTableSize = this._planet.maxGridSize || 8;

        this._handler = this._planet.renderer!.handler;

        let isWebkit = false; //('WebkitAppearance' in document.documentElement.style) && !/^((?!chrome).)*safari/i.test(navigator.userAgent);

        const normalMapBlur = new ShaderProgram("normalMapBlur", {
            attributes: {
                a_position: "vec2"
            },
            uniforms: {
                s_texture: "sampler2d"
            },
            vertexShader: `#version 300 es
                        in vec2 a_position;

                        out vec2 blurCoordinates[5];

                        void main() {
                            vec2 vt = a_position * 0.5 + 0.5;
                            ${isWebkit ? "vt.y = 1.0 - vt.y; " : " "}
                            gl_Position = vec4(a_position, 0.0, 1.0);
                            blurCoordinates[0] = vt;
                            blurCoordinates[1] = vt + vec2(${(1.0 / this._width) * 1.407333}, 0.0);
                            blurCoordinates[2] = vt - vec2(${(1.0 / this._width) * 1.407333}, 0.0);
                            blurCoordinates[3] = vt + vec2(0.0, ${(1.0 / this._height) * 3.294215});
                            blurCoordinates[4] = vt - vec2(0.0, ${(1.0 / this._height) * 3.294215});
                        }`,
            fragmentShader: `#version 300 es
                        precision highp float;
                        uniform sampler2D s_texture;
                        in vec2 blurCoordinates[5];
                        out vec4 fragColor;

                        vec3 decodeNormal(vec3 c) {
                            return c * 2.0 - 1.0;
                        }

                        void main() {
                            vec2 uv = blurCoordinates[0];
                            vec4 center = texture(s_texture, uv);

                            // Keep border intact to avoid seams between adjacent tiles.
                            if(uv.x <= ${3.294215 / this._width} || uv.x >= ${1.0 - 3.294215 / this._width} ||
                               uv.y <= ${3.294215 / this._height} || uv.y >= ${1.0 - 3.294215 / this._height}) {
                                fragColor = center;
                                return;
                            }

                            vec3 n = vec3(0.0);
                            n += decodeNormal(center.rgb) * 0.204164;
                            n += decodeNormal(texture(s_texture, blurCoordinates[1]).rgb) * 0.304005;
                            n += decodeNormal(texture(s_texture, blurCoordinates[2]).rgb) * 0.304005;
                            n += decodeNormal(texture(s_texture, blurCoordinates[3]).rgb) * 0.093913;
                            n += decodeNormal(texture(s_texture, blurCoordinates[4]).rgb) * 0.093913;

                            n = normalize(n);
                            fragColor = vec4(n * 0.5 + 0.5, center.a);
                        }`
        });

        const normalMap = new ShaderProgram("normalMap", {
            attributes: {
                a_position: "vec2",
                a_normal: "vec3"
            },
            uniforms: {},
            vertexShader: `#version 300 es
                        in vec2 a_position;
                        in vec3 a_normal;

                        out vec3 v_color;

                        void main() {
                            gl_Position = vec4(a_position, 0.0, 1.0);
                            v_color = normalize(a_normal) * 0.5 + 0.5;
                        }`,
            fragmentShader: `#version 300 es
                        precision highp float;

                        in vec3 v_color;
                        out vec4 fragColor;

                        void main () {
                            fragColor = vec4(v_color, 1.0);
                        }`
        });

        this._handler.addProgram(normalMapBlur);
        this._handler.addProgram(normalMap);

        //create hidden handler buffer
        this._framebuffer = new Framebuffer(this._handler, {
            width: this._width,
            height: this._height,
            useDepth: false
        });

        this._framebuffer.init();

        this._normalMapVerticesTexture = this._handler.createEmptyTexture_l(this._width, this._height);

        //create vertices hasharray for different grid size segments from 2^4(16) to 2^7(128)
        for (let p = this._minTabelSize; p <= this._maxTableSize; p++) {
            const gs = 1 << p; //Math.pow(2, p);
            const gs2 = gs / 2;

            let vertices = new Float32Array((gs + 1) * (gs + 1) * 2);

            for (let i = 0; i <= gs; i++) {
                for (let j = 0; j <= gs; j++) {
                    let ind = (i * (gs + 1) + j) * 2;
                    vertices[ind] = -1 + j / gs2;
                    vertices[ind + 1] = -1 + i / gs2;
                }
            }

            this._verticesBufferArray[gs] = this._handler.createArrayBuffer(vertices, 2, vertices.length / 2);

            this._indexBufferArray[gs] =
                this._planet._indexesCache[Math.log2(gs)][Math.log2(gs)][Math.log2(gs)][Math.log2(gs)][
                    Math.log2(gs)
                ].buffer!;
        }

        //create 2d screen square buffer
        const positions = new Float32Array([-1.0, -1.0, 1.0, -1.0, -1.0, 1.0, 1.0, 1.0]);

        this._positionBuffer = this._handler.createArrayBuffer(positions, 2, positions.length / 2);
    }

    protected _drawNormalMapBlur(segment: Segment): boolean {
        let normals = segment.normalMapNormals;
        if (segment.node && segment.node.getState() !== quadTree.NOTRENDERING && normals && normals.length) {
            const size = normals.length / 3;
            const gridSize = Math.sqrt(size) - 1;

            let indBuf = this._verticesBufferArray[gridSize];

            if (indBuf) {
                if (segment.planet.terrain!.equalizeNormals) {
                    segment._normalMapEdgeEqualize(quadTree.N);
                    segment._normalMapEdgeEqualize(quadTree.S);
                    segment._normalMapEdgeEqualize(quadTree.W);
                    segment._normalMapEdgeEqualize(quadTree.E);
                }

                let outTexture = segment.normalMapTexturePtr;

                const h = this._handler!;
                const gl = h.gl!;

                let _normalsBuffer = h.createArrayBuffer(normals, 3, size, gl.DYNAMIC_DRAW);

                const f = this._framebuffer!;
                let p = h.programs.normalMap;
                let sha = p.attributes;

                f.bindOutputTexture(this._normalMapVerticesTexture!);

                p.activate();

                gl.bindBuffer(gl.ARRAY_BUFFER, indBuf);
                gl.vertexAttribPointer(sha.a_position, indBuf.itemSize, gl.FLOAT, false, 0, 0);

                gl.bindBuffer(gl.ARRAY_BUFFER, _normalsBuffer);
                gl.vertexAttribPointer(sha.a_normal, _normalsBuffer.itemSize, gl.FLOAT, false, 0, 0);

                gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this._indexBufferArray[gridSize]);
                gl.drawElements(gl.TRIANGLE_STRIP, this._indexBufferArray[gridSize].numItems, gl.UNSIGNED_INT, 0);

                gl.deleteBuffer(_normalsBuffer);

                //
                // blur pass
                //
                f.bindOutputTexture(outTexture!);

                p = h.programs.normalMapBlur;

                p.activate();
                gl.bindBuffer(gl.ARRAY_BUFFER, this._positionBuffer!);
                gl.vertexAttribPointer(p.attributes.a_position, this._positionBuffer!.itemSize, gl.FLOAT, false, 0, 0);
                gl.activeTexture(gl.TEXTURE0);
                gl.bindTexture(gl.TEXTURE_2D, this._normalMapVerticesTexture!);
                gl.uniform1i(p.uniforms.s_texture, 0);
                gl.drawArrays(gl.TRIANGLE_STRIP, 0, this._positionBuffer!.numItems);
                return true;
            } else {
                return true;
            }
        }
        return false;
    }

    protected _drawNormalMapNoBlur(segment: Segment): boolean {
        let normals = segment.normalMapNormals;
        if (segment.node && segment.node.getState() !== quadTree.NOTRENDERING && normals && normals.length) {
            const size = normals.length / 3;
            const gridSize = Math.sqrt(size) - 1;

            let indBuf = this._verticesBufferArray[gridSize];

            if (indBuf) {
                if (segment.planet.terrain!.equalizeNormals) {
                    segment._normalMapEdgeEqualize(quadTree.N);
                    segment._normalMapEdgeEqualize(quadTree.S);
                    segment._normalMapEdgeEqualize(quadTree.W);
                    segment._normalMapEdgeEqualize(quadTree.E);
                }

                let outTexture = segment.normalMapTexturePtr;

                const h = this._handler!;
                const gl = h.gl!;

                let _normalsBuffer = h.createArrayBuffer(normals, 3, size, gl.DYNAMIC_DRAW);

                const f = this._framebuffer!;
                const p = h.programs.normalMap;
                const sha = p.attributes;

                f.bindOutputTexture(outTexture!);

                p.activate();

                gl.bindBuffer(gl.ARRAY_BUFFER, indBuf);
                gl.vertexAttribPointer(sha.a_position, indBuf.itemSize, gl.FLOAT, false, 0, 0);

                gl.bindBuffer(gl.ARRAY_BUFFER, _normalsBuffer);
                gl.vertexAttribPointer(sha.a_normal, _normalsBuffer.itemSize, gl.FLOAT, false, 0, 0);

                gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this._indexBufferArray[gridSize]);
                gl.drawElements(gl.TRIANGLE_STRIP, this._indexBufferArray[gridSize].numItems, gl.UNSIGNED_INT, 0);

                gl.deleteBuffer(_normalsBuffer);

                return true;
            } else {
                return true;
            }
        }
        return false;
    }

    protected _drawNormalMap(segment: Segment) {
        if (segment.planet.terrain!.isBlur(segment)) {
            return this._drawNormalMapBlur(segment);
        } else {
            return this._drawNormalMapNoBlur(segment);
        }
    }

    public drawSingle(segment: Segment) {
        const h = this._handler!;
        const gl = h.gl!;

        this._framebuffer!.activate();

        gl.disable(gl.CULL_FACE);
        gl.disable(gl.DEPTH_TEST);
        gl.disable(gl.BLEND);

        if (segment.terrainReady && this._drawNormalMap(segment)) {
            segment.normalMapReady = true;
            segment.normalMapTexture = segment.normalMapTexturePtr;
            segment.normalMapTextureBias[0] = 0;
            segment.normalMapTextureBias[1] = 0;
            segment.normalMapTextureBias[2] = 1;
        }
        segment._inTheQueue = false;

        gl.enable(gl.DEPTH_TEST);
        gl.enable(gl.CULL_FACE);
        gl.enable(gl.BLEND);

        this._framebuffer!.deactivate();
    }

    /**
     * Returns true when there is nothing left to rasterize.
     * @public
     * @returns {boolean}
     */
    public get isIdle(): boolean {
        return this._queue.length === 0;
    }

    public frame() {
        if (this._queue.length) {
            const h = this._handler!;
            const gl = h.gl!;

            this._framebuffer!.activate();

            gl.disable(gl.CULL_FACE);
            gl.disable(gl.DEPTH_TEST);
            gl.disable(gl.BLEND);

            let deltaTime = 0,
                startTime = window.performance.now();

            while (this._lock.isFree() && this._queue.length && deltaTime < 0.25) {
                const segment = this._queue.shift()!;
                if (segment.terrainReady && this._drawNormalMap(segment)) {
                    segment.normalMapReady = true;
                    segment.normalMapTexture = segment.normalMapTexturePtr;
                    segment.normalMapTextureBias[0] = 0;
                    segment.normalMapTextureBias[1] = 0;
                    segment.normalMapTextureBias[2] = 1;
                }
                segment._inTheQueue = false;
                deltaTime = window.performance.now() - startTime;
            }

            gl.enable(gl.BLEND);
            gl.enable(gl.DEPTH_TEST);
            gl.enable(gl.CULL_FACE);

            this._framebuffer!.deactivate();
        }
    }

    public get queueSize(): number {
        return this._queue.length;
    }

    public queue(segment: Segment) {
        segment._inTheQueue = true;
        this._queue.push(segment);
    }

    public unshift(segment: Segment) {
        segment._inTheQueue = true;
        this._queue.unshift(segment);
    }

    public remove(segment: Segment) {
        //...
    }

    public clear() {
        while (this._queue.length) {
            let s = this._queue.pop()!;
            s._inTheQueue = false;
        }
    }

    /**
     * Set activity off
     * @public
     */
    public lock(key: Key) {
        this._lock.lock(key);
    }

    /**
     * Set activity on
     * @public
     */
    public free(key: Key) {
        this._lock.free(key);
    }
}