GIF equalizer in Babylon.js! (Makarov version)

In one of the topics, a person decided to make a gif in babylon.js It looks like no one has done this before, and so I decided to continue the theme and make an equalizer from a gif, which no one has done before, and I warn you it may lag a lot.

gif equalizer Makarov version (Generation using cubes) | Babylon.js Playground

Maybe someone will continue this topic and develop the project if they need to, but I don’t need to develop it.

It runs at ~10fps on my laptop, have a try with thin instance and per-instance color

But you can also do it the way you want - using thin per-instance.

gif equalizer Makarov version (Version 1 - Generation using cubes but using per-instance) | Babylon.js Playground

1 Like

Although then you need to add an input field.

gif equalizer Makarov version (Version 2 - Generation using cubes but using per-instance with an input field) | Babylon.js Playground

Although I think I’ve seen it before… But I don’t remember where exactly, it was definitely done by someone before me… Or it just seems to me…

Now all that remains is to make a color version.

gif equalizer Makarov version (Version 3 - Generation using cubes but using per-instance but color version) | Babylon.js Playground

We’ve done it, now all that’s left is we have done this, now all that remains is to make a version with a surface fluctuation.

In general, I wrote in the comments under one video 4 years ago, one person had the idea to overlay the radio frequency spectrum on a wireframe grid

In general, he still hasn’t done it, even though 4 years have passed.

In general, it is not possible to make a version with a gradient, and there is no desire to continue it, maybe someone will come up with a solution to this problem here is the code:

var createScene = function() {
    var scene = new BABYLON.Scene(engine);

    // Камера
    var camera = new BABYLON.ArcRotateCamera("camera", -Math.PI/2.5, Math.PI/3, 50, BABYLON.Vector3.Zero(), scene);
    camera.attachControl(canvas, true);

    // Освещение
    var light = new BABYLON.HemisphericLight("light", new BABYLON.Vector3(0, 1, 0), scene);
    light.intensity = 0.7;

    // DOM элементы (поле и кнопка)
    const input = document.createElement('input');
    input.type = 'text';
    input.placeholder = 'Введите URL изображения GIF';
    input.style.position = 'fixed';
    input.style.bottom = '40px';
    input.style.right = '80px';
    input.style.width = '250px';
    input.style.zIndex = 1000;

    const button = document.createElement('button');
    button.innerHTML = 'Запуск';
    button.style.position = 'fixed';
    button.style.bottom = '40px';
    button.style.right = '340px';
    button.style.zIndex = 1000;

    document.body.appendChild(input);
    document.body.appendChild(button);

    // переменная для меша
    var mapMesh = null;

    // переменные для обработки GIF
    let gifFrames = [];
    let currentFrameIdx = 0;

    let lastFrameTime = 0;
    const frameDuration = 300; // ms

    function startProcessing(url) {
        if (mapMesh) {
            mapMesh.dispose();
            mapMesh = null;
        }
        // Загружаем и обрабатываем изображение
        loadGIF(url);
    }

    // Обработчик кнопки
    button.onclick = () => {
        const url = input.value.trim();
        if (url) startProcessing(url);
    };

    // Обработка на Enter
    input.onkeydown = (e) => {
        if (e.key === 'Enter') {
            const url = input.value.trim();
            if (url) startProcessing(url);
        }
    };

    // Функция загрузки GIF и разбор кадров
    function loadGIF(url) {
        gifFrames = [];
        fetchGifFrames(url);
    }

    // Fetch и декодер GIF (работать только если API поддержки есть)
    function fetchGifFrames(url) {
        fetch(url)
            .then(res => res.arrayBuffer())
            .then(buf => {
                const decoder = new ImageDecoder({ data: buf, type: 'image/gif' });
                return decoder.completed.then(() => decoder.tracks.ready).then(() => decoder);
            })
            .then(async decoder => {
                const frameCount = decoder.tracks.selectedTrack.frameCount;
                let width = 0, height = 0;
                for (let i = 0; i < frameCount; i++) {
                    const frameInfo = await decoder.decode({ frameIndex: i });
                    const imageBitmap = frameInfo.image;
                    if (i === 0) {
                        width = imageBitmap.codedWidth;
                        height = imageBitmap.codedHeight;
                    }
                    const tex = new BABYLON.DynamicTexture("gifFrame_" + i, { width, height }, scene, false);
                    tex.getContext().drawImage(imageBitmap, 0, 0);
                    tex.update();
                    gifFrames.push({ texture: tex, width, height });
                }
            });
    }

    // Здесь главный цикл обновления через registerBeforeRender
    scene.registerBeforeRender(() => {
        const now = Date.now();
        if (gifFrames.length === 0 || !mapMesh) return;

        if (now - lastFrameTime > frameDuration) {
            currentFrameIdx = (currentFrameIdx + 1) % gifFrames.length;
            updateVerticesByFrame(gifFrames[currentFrameIdx]);
            lastFrameTime = now;
        }
    });

    // Обновление высот вершин
    function updateVerticesByFrame(frame) {
        const { texture, width: texW, height: texH } = frame;
        const ctx = texture.getContext();
        const imageData = ctx.getImageData(0, 0, texW, texH);
        const data = imageData.data;

        for (let i = 0; i < vertices.length / 3; i++) {
            const vx = vertices[i * 3]; // -size/2 ... +size/2
            const vy = vertices[i * 3 + 1];

            const u = Math.floor(((vx + 10) / 20) * texW);
            const v = Math.floor(((vy + 10) / 20) * texH);

            const pixelIdx = (v * texW + u) * 4;
            const r = data[pixelIdx];
            const g = data[pixelIdx + 1];
            const b = data[pixelIdx + 2];

            const brightness = (0.21 * r + 0.72 * g + 0.07 * b) / 255;
            const heightVal = brightness * 10; // масштаб
            vertices[i * 3 + 1] = heightVal;
        }
        ground.updateVerticesData(BABYLON.VertexBuffer.PositionKind, vertices);
    }

    // Создаем и запускаем
    var vertices = null;
    var ground = null;

    // Изначально стартуем
    // Можно сразу вызвать старт с каким-нибудь URL
    startProcessing('https://i.postimg.cc/D0drgZv5/image.png');

    // Вытаскиваем вершины после создания меша
    scene.onReadyObservable.add(() => {
        if (!ground) {
            ground = BABYLON.MeshBuilder.CreateGround("ground", {
                width: 20,
                height: 20,
                subdivisions: 50
            });
            vertices = ground.getVerticesData(BABYLON.VertexBuffer.PositionKind);
        }
    });
    // ваши вспомогательные функции — пусть останутся без изменений
    function convertToGrayscale(imageUrl, callback) {
        const canvas = document.createElement('canvas');
        const ctx = canvas.getContext('2d');
        const img = new Image();
        img.crossOrigin = "Anonymous";

        img.onload = function () {
            canvas.width = img.width;
            canvas.height = img.height;
            ctx.drawImage(img, 0, 0);
            const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
            const data = imageData.data;

            for (let i = 0; i < data.length; i += 4) {
                const r = data[i];
                const g = data[i + 1];
                const b = data[i + 2];
                const gray = 0.21 * r + 0.72 * g + 0.07 * b;
                data[i] = data[i + 1] = data[i + 2] = gray;
            }
            ctx.putImageData(imageData, 0, 0);
            callback(canvas.toDataURL());
        };
        img.src = imageUrl;
    }
        function enhanceContrast(imageUrl, contrast, callback) {
        const canvas = document.createElement('canvas');
        const ctx = canvas.getContext('2d');
        const img = new Image();

        img.onload = function () {
            canvas.width = img.width;
            canvas.height = img.height;
            ctx.drawImage(img, 0, 0);
            const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
            const data = imageData.data;
            const factor = (259 * (contrast + 255)) / (255 * (259 - contrast));
            for (let i = 0; i < data.length; i += 4) {
                data[i] = factor * (data[i] - 128) + 128;
                data[i + 1] = factor * (data[i + 1] - 128) + 128;
                data[i + 2] = factor * (data[i + 2] - 128) + 128;
            }
            ctx.putImageData(imageData, 0, 0);
            callback(canvas.toDataURL());
        }
        img.src = imageUrl;
    }

    function createTerracedHeightmap(imageUrl, steps, callback) {
        const canvas = document.createElement('canvas');
        const ctx = canvas.getContext('2d');
        const img = new Image();

        img.onload = function () {
            canvas.width = img.width;
            canvas.height = img.height;
            ctx.drawImage(img, 0, 0);
            const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
            const data = imageData.data;
            for (let i = 0; i < data.length; i += 4) {
                const gray = data[i];
                const steppedValue = Math.round(gray / (255 / steps)) * (255 / steps);
                data[i] = data[i + 1] = data[i + 2] = steppedValue;
            }
            ctx.putImageData(imageData, 0, 0);
            callback(canvas.toDataURL());
        };
        img.src = imageUrl;
    }
    
    return scene;
};

Offer your own equalizer options… Maybe someone will have something interesting…

I was still able to solve the problem, but for some reason it doesn’t work with a lot of pictures.

gif equalizer Makarov version (Version 4 - Version with gradient fields on the plane) | Babylon.js Playground

And also an older version without superimposing the image on the plane.

gif equalizer Makarov version (Version 5 - Version with gradient fields on the plane without image overlay) | Babylon.js Playground