Script: Artificial Intelligence Generated 3D Sound Waves

Artificial Intelligence Generated 3D Sound Waves picture
Type
Typescript logo indicatortypescript
Date Created
Mar 20, 2025, 11:57:50 AM
Last Edit Date
Mar 23, 2025, 10:25:50 PM

Project Information

This creative coding reactive application uses microphone to capture sound waves on 3D circular plane. Application was developed by prompting artificial intelligence in multiple separate chats.

View Full Project

Script Code

createGUI();

const start = () => {
    // Get the Babylon.js scene (assuming a BitByBit context or similar setup)
    const scene = bitbybit.babylon.scene.getScene();

    // Create the plane with increased subdivisions for even smoother waves
    const plane = BABYLON.MeshBuilder.CreateGround(
        "wavePlane",
        { width: 40, height: 40, subdivisions: 300, updatable: true }, // Increased from 300 to 400
        scene
    );

    // Plane Vertex Shader
    BABYLON.Effect.ShadersStore["neonVertexShader"] = `
    precision highp float;
    attribute vec3 position;
    uniform mat4 worldViewProjection;
    varying vec2 vPos;
    void main() {
        gl_Position = worldViewProjection * vec4(position, 1.0);
        vPos = position.xz;
    }
`;

    // Plane Fragment Shader
    BABYLON.Effect.ShadersStore["neonFragmentShader"] = `
    precision highp float;
    varying vec2 vPos;
    void main() {
        float distance = length(vPos);
        if (distance > 20.0) {
            discard;
        } else {
            float normalizedDistance = distance / 20.0;
            vec3 blue = vec3(0.0, 0.0, 1.0);
            vec3 white = vec3(1.0, 1.0, 1.0);
            vec3 color = mix(blue, white, normalizedDistance);
            gl_FragColor = vec4(color, 1.0);
        }
    }
`;

    // Create shader material for the plane
    const shaderMaterial = new BABYLON.ShaderMaterial(
        "neonShader",
        scene,
        { vertex: "neon", fragment: "neon" },
        {
            attributes: ["position"],
            uniforms: ["worldViewProjection"]
        }
    );
    shaderMaterial.backFaceCulling = false;
    plane.material = shaderMaterial;

    // Add glow layer
    const glowLayer = new BABYLON.GlowLayer("glow", scene);
    glowLayer.intensity = 1.5;

    bitbybit.babylon.scene.backgroundColour({
        colour: "#ffffff",
    });

    // Audio Setup
    const audioContext = new AudioContext();
    let analyser, dataArray, gainNode;

    navigator.mediaDevices.getUserMedia({ audio: true })
        .then((stream) => {
            const source = audioContext.createMediaStreamSource(stream);
            gainNode = audioContext.createGain();
            analyser = audioContext.createAnalyser();
            gainNode.gain.value = 5.0;
            analyser.fftSize = 2048;
            const bufferLength = analyser.frequencyBinCount;
            dataArray = new Uint8Array(bufferLength);
            source.connect(gainNode);
            gainNode.connect(analyser);
            console.log("Microphone connected!");
        })
        .catch((err) => {
            console.error("Microphone error:", err);
            alert("Please allow microphone access.");
        });

    // Animation Parameters
    const numBands = 10;
    const amplitudeFactor = 6.0; // Increased from 4.0 for taller waves
    const baseFreq = 0.5; // Decreased from 1.0 for larger waves
    const maxRadius = 20;
    let rotationAngle = 0;

    // Random phases for waves
    const angles = [];
    for (let j = 0; j < numBands; j++) {
        angles[j] = Math.random() * Math.PI * 2;
    }

    scene.onBeforeRenderObservable.add(() => {
        if (!analyser || !dataArray) return;

        analyser.getByteFrequencyData(dataArray);

        // Compute amplitudes from audio
        const bandSize = Math.floor(dataArray.length / numBands);
        const amplitudes = [];
        for (let i = 0; i < numBands; i++) {
            let sum = 0;
            for (let j = 0; j < bandSize; j++) {
                sum += dataArray[i * bandSize + j];
            }
            amplitudes[i] = (sum / bandSize / 255) * amplitudeFactor;
        }

        // Update plane vertices for wave effect
        const positions = plane.getVerticesData(BABYLON.VertexBuffer.PositionKind);
        if (!positions) return;

        const numVertices = positions.length / 3;
        const time = performance.now() / 1000;

        for (let i = 0; i < numVertices; i++) {
            const x = positions[i * 3];
            const z = positions[i * 3 + 2];
            const r = Math.sqrt(x * x + z * z);
            let yDisp = 0;

            if (r <= maxRadius) {
                const theta = Math.atan2(z, x);
                const scale = Math.pow(1 - r / maxRadius, 2);

                for (let j = 0; j < numBands; j++) {
                    const freq = (j + 1) * baseFreq;
                    const wave = Math.sin(freq * r + time * 0.5 + angles[j]) * Math.cos((j + 1) * theta);
                    yDisp += amplitudes[j] * wave;
                }
                yDisp *= scale;
            }

            positions[i * 3 + 1] = yDisp;
        }

        // Rotate the plane
        rotationAngle += 0.005;
        plane.rotation.y = rotationAngle;

        plane.updateVerticesData(BABYLON.VertexBuffer.PositionKind, positions);
        plane.refreshBoundingInfo();
    });
}

function createGUI() {
    const gui = bitbybit.babylon.gui;
    const textOpt = new Bit.Inputs.BabylonGui.CreateFullScreenUIDto();
    const fullScreenUI = gui.advancedDynamicTexture.createFullScreenUI(textOpt);
    const stackPanelOpt = new Bit.Inputs.BabylonGui.CreateStackPanelDto();
    stackPanelOpt.background = "#00000000";
    const stackPanel = gui.stackPanel.createStackPanel(stackPanelOpt);
    const stackPanelOpt2 = new Bit.Inputs.BabylonGui.CreateStackPanelDto();
    stackPanelOpt2.background = "#00000000";
    const stackPanel2 = gui.stackPanel.createStackPanel(stackPanelOpt2);
    stackPanel2.verticalAlignment = BABYLON.GUI.Control.VERTICAL_ALIGNMENT_BOTTOM;
    fullScreenUI.addControl(stackPanel2);
    stackPanel.paddingTopInPixels = 10;
    stackPanel2.addControl(stackPanel);

    const buttonOptions = new Bit.Inputs.BabylonGui.CreateButtonDto();
    buttonOptions.width = "400px";
    buttonOptions.height = "150px";
    buttonOptions.label = "START EXPERIENCE";
    buttonOptions.color = "#ffffff";
    buttonOptions.background = "#00000000";
    const button = gui.button.createSimpleButton(buttonOptions);
    button.paddingTopInPixels = 20;
    button.paddingBottomInPixels = 20;

    button.onPointerClickObservable.add(() => {
        start();
    })

    const imageOpt = new Bit.Inputs.BabylonGui.CreateImageDto();
    imageOpt.url = "assets/logo-gold-small.png";
    const image = gui.image.createImage(imageOpt);
    const padding = 30;
    image.paddingTopInPixels = padding;
    image.paddingBottomInPixels = padding;
    image.paddingLeftInPixels = padding;
    image.paddingRightInPixels = padding;

    image.onPointerClickObservable.add(() => {
        window.open("https://bitbybit.dev", '_blank').focus();
    });
    const txtBlockOptions1 = new Bit.Inputs.BabylonGui.CreateTextBlockDto();
    txtBlockOptions1.text = "Powered By";
    const txtBlock1 = gui.textBlock.createTextBlock(txtBlockOptions1);
    txtBlock1.height = "40px";
    txtBlock1.color = "#ffffff";
    txtBlock1.paddingBottomInPixels = 10;
    const txtBlockOptions = new Bit.Inputs.BabylonGui.CreateTextBlockDto();
    txtBlockOptions.text = "BITBYBIT.DEV";
    const txtBlock = gui.textBlock.createTextBlock(txtBlockOptions);
    txtBlock.height = "60px";
    txtBlock.color = "#ffffff";
    txtBlock.paddingBottomInPixels = 30;

    stackPanel2.addControl(button);
    stackPanel2.addControl(image);
    stackPanel2.addControl(txtBlock1);
    stackPanel2.addControl(txtBlock);
}