Script: 3D Field Of AI Flowers

3D Field Of AI Flowers picture
Type
Typescript logo indicatortypescript
Date Created
Mar 7, 2025, 3:39:22 PM
Last Edit Date
Oct 9, 2025, 10:05:15 PM

Project Information

This script was completely generated by using Artificial Intelligence. We prompted Grok LLM multiple times in quite a few sessions. but the result is this beautiful creative field of flowers. Grok also made sure to create sounds, beautiful effects of opening and closing of flowers.

View Full Project

Script Code

createGUI();

const start = async () => {

    // Assuming BabylonJS is already included and scene is initialized
    const scene = bitbybit.babylon.scene.getScene();

    const cam = bitbybit.babylon.scene.getActiveCamera() as BABYLON.ArcRotateCamera;
    cam.position = new BABYLON.Vector3(10, 70, 10);

    // ### AudioContext Setup
    const audioCtx = new AudioContext();
    const oscillatorPool = [];
    for (let i = 0; i < 5; i++) {
        const osc = audioCtx.createOscillator();
        osc.type = 'sine';
        const gain = audioCtx.createGain();
        gain.gain.setValueAtTime(0, audioCtx.currentTime);
        osc.connect(gain);
        gain.connect(audioCtx.destination);
        const vibratoOsc = audioCtx.createOscillator();
        vibratoOsc.type = 'sine';
        vibratoOsc.frequency.setValueAtTime(5, audioCtx.currentTime);
        const vibratoGain = audioCtx.createGain();
        vibratoGain.gain.setValueAtTime(10, audioCtx.currentTime);
        vibratoOsc.connect(vibratoGain);
        vibratoGain.connect(osc.frequency);
        osc.start();
        vibratoOsc.start();
        oscillatorPool.push({ osc, gain, inUse: false });
    }

    function getAvailableOscillator() {
        return oscillatorPool.find(oscData => !oscData.inUse) || null;
    }

    function modulateSound(osc, gain, frequency, isOpening) {
        osc.frequency.setValueAtTime(frequency, audioCtx.currentTime);
        if (isOpening) {
            gain.gain.cancelScheduledValues(audioCtx.currentTime);
            gain.gain.setValueAtTime(0, audioCtx.currentTime);
            gain.gain.linearRampToValueAtTime(0.7, audioCtx.currentTime + 0.1);
            gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.8);
        } else {
            gain.gain.cancelScheduledValues(audioCtx.currentTime);
            gain.gain.setValueAtTime(0, audioCtx.currentTime);
            gain.gain.linearRampToValueAtTime(0.5, audioCtx.currentTime + 0.2);
            gain.gain.linearRampToValueAtTime(0, audioCtx.currentTime + 0.6);
        }
    }

    // ### Shader Definitions for Petal Gradients
    BABYLON.Effect.ShadersStore["petalVertexShader"] = `
    precision highp float;
    attribute vec3 position;
    attribute vec2 uv;
    uniform mat4 worldViewProjection;
    varying vec2 vUV;
    void main(void) {
        gl_Position = worldViewProjection * vec4(position, 1.0);
        vUV = uv;
    }
`;

    BABYLON.Effect.ShadersStore["petalFragmentShader"] = `
    precision highp float;
    varying vec2 vUV;
    uniform float invertGradient;
    void main(void) {
        float t = invertGradient > 0.5 ? 1.0 - vUV.y : vUV.y;
        vec3 color;
        if (t < 0.5) {
            color = mix(vec3(1, 0.0, 1), vec3(0.0, 0.0, 1.0), t * 2.0);
        } else {
            color = mix(vec3(0.0, 0.0, 1.0), vec3(1.0, 1.0, 1.0), (t - 0.5) * 2.0);
        }
        gl_FragColor = vec4(color, 1.0);
    }
`;

    // ### Create Shader Materials
    const petalMaterialNormal = new BABYLON.ShaderMaterial("petalShaderNormal", scene, {
        vertex: "petal",
        fragment: "petal"
    }, {
        attributes: ["position", "uv"],
        uniforms: ["worldViewProjection", "invertGradient"]
    });
    petalMaterialNormal.setFloat("invertGradient", 0.0);

    const petalMaterialInverted = new BABYLON.ShaderMaterial("petalShaderInverted", scene, {
        vertex: "petal",
        fragment: "petal"
    }, {
        attributes: ["position", "uv"],
        uniforms: ["worldViewProjection", "invertGradient"]
    });
    petalMaterialInverted.setFloat("invertGradient", 1.0);

    // ### Petal Creation Function
    function createPetal(scene) {
        const points = [
            new BABYLON.Vector3(0, 0, 0),
            new BABYLON.Vector3(0.15, 0.3, 0),
            new BABYLON.Vector3(0.3, 0.6, 0),
            new BABYLON.Vector3(0.4, 0.9, 0),
            new BABYLON.Vector3(0.5, 1.2, 0),
            new BABYLON.Vector3(0.6, 0.9, 0),
            new BABYLON.Vector3(0.7, 0.6, 0)
        ];

        const tube = BABYLON.MeshBuilder.CreateTube("petalTube", {
            path: points,
            radius: 0.03,
            tessellation: 3,
            cap: BABYLON.Mesh.CAP_ALL,
            sideOrientation: BABYLON.Mesh.DOUBLESIDE,
            updatable: false
        }, scene);

        return tube;
    }

    // ### Flower Creation Function
    function createFlower(scene) {
        const flower = new BABYLON.TransformNode("flower", scene);
        const scale = 0.5 + Math.random() * 5.5; // Scale between 0.5 and 6.0
        flower.scaling = new BABYLON.Vector3(scale, scale, scale);

        let position;
        let attempts = 0;
        do {
            position = new BABYLON.Vector3(Math.random() * 100 - 50, 0, Math.random() * 100 - 50); // Smaller 100x100 field
            attempts++;
            if (attempts > 10000) {
                console.warn("Could not find non-overlapping position after 10000 attempts");
                break;
            }
        } while (flowerDataArray.some(fd => BABYLON.Vector3.Distance(position, fd.position) < (scale + fd.scale) * 1.2)); // Adjusted overlap check

        const amp_x = 0.05 + Math.random() * 0.2;
        const amp_z = 0.05 + Math.random() * 0.2;
        const amp_y = 0.05 + Math.random() * 0.2;
        const phase_x = Math.random() * Math.PI * 2;
        const phase_z = Math.random() * Math.PI * 2;
        const phase_y = Math.random() * Math.PI * 2;
        const basePosition = position.clone();
        basePosition.y = amp_y;
        flower.position = basePosition.clone();

        const petalNodes = [];
        const petalCount = 24;
        const invertGradient = Math.random() < 0.5; // Per-flower gradient decision
        const petalMaterial = invertGradient ? petalMaterialInverted : petalMaterialNormal;

        for (let i = 0; i < petalCount; i++) {
            const petalTube = createPetal(scene);
            petalTube.material = petalMaterial; // Consistent material for all petals
            petalTube.metadata = { flower: flower };
            const petalNode = new BABYLON.TransformNode("petalNode", scene);
            petalNode.rotation.y = (i / petalCount) * Math.PI * 2;
            petalTube.parent = petalNode;
            petalNode.parent = flower;
            petalNodes.push(petalTube);
        }

        const flowerData = {
            flower,
            basePosition,
            amp_x,
            amp_z,
            amp_y,
            phase_x,
            phase_z,
            phase_y,
            freq: 0.5 + Math.random(),
            scale,
            petalNodes,
            position, // For overlap check
            isAnimating: false,
            animateFullCycle: function () {
                if (this.isAnimating) return;
                const oscData = getAvailableOscillator();
                if (!oscData) return;
                oscData.inUse = true;
                modulateSound(oscData.osc, oscData.gain, 440 / this.scale, true);
                this.isAnimating = true;
                const openAnim = createBloomAnimation(true);
                const closeAnim = createBloomAnimation(false);
                scene.beginDirectAnimation(this.petalNodes, [openAnim], 0, 60, false, 1, () => {
                    modulateSound(oscData.osc, oscData.gain, 330 / this.scale, false);
                    scene.beginDirectAnimation(this.petalNodes, [closeAnim], 0, 60, false, 1, () => {
                        this.isAnimating = false; // Reset flag for re-triggering
                        oscData.inUse = false;
                        oscData.gain.gain.linearRampToValueAtTime(0, audioCtx.currentTime + 0.2);
                    });
                });
            }
        };
        return flowerData;
    }

    // ### Bloom Animation Function
    function createBloomAnimation(isOpening) {
        const animation = new BABYLON.Animation(
            "bloom",
            "rotation.x",
            30,
            BABYLON.Animation.ANIMATIONTYPE_FLOAT,
            BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT
        );
        const keys = isOpening
            ? [{ frame: 0, value: 0 }, { frame: 60, value: -Math.PI / 4 }]
            : [{ frame: 0, value: -Math.PI / 4 }, { frame: 60, value: 0 }];
        animation.setKeys(keys);
        const easing = new BABYLON.SineEase();
        easing.setEasingMode(BABYLON.EasingFunction.EASINGMODE_EASEINOUT);
        animation.setEasingFunction(easing);
        return animation;
    }

    // ### Create Flowers and Wind Animation
    const flowerDataArray = [];
    for (let i = 0; i < 250; i++) {
        const flowerData = createFlower(scene);
        flowerDataArray.push(flowerData);
    }

    scene.registerBeforeRender(() => {
        const time = performance.now() / 1000;
        flowerDataArray.forEach(flowerData => {
            if (flowerData && flowerData.flower && flowerData.basePosition) {
                const { basePosition, amp_x, amp_z, amp_y, phase_x, phase_z, phase_y, freq } = flowerData;
                const x = basePosition.x + amp_x * Math.sin(freq * time + phase_x);
                const y = basePosition.y + amp_y * Math.sin(freq * time + phase_y);
                const z = basePosition.z + amp_z * Math.sin(freq * time + phase_z);
                flowerData.flower.position.set(x, y, z);
            } else {
                console.warn("Skipping invalid flower data:", flowerData);
            }
        });
    });

    // ### Event Listeners for Interaction
    const hoveredFlowers = new Set();
    scene.onPointerMove = () => {
        const pickResult = scene.pick(scene.pointerX, scene.pointerY);
        if (pickResult.hit && pickResult.pickedMesh.metadata && pickResult.pickedMesh.metadata.flower) {
            const flower = pickResult.pickedMesh.metadata.flower;
            const flowerData = flowerDataArray.find(fd => fd.flower === flower);
            if (flowerData && !flowerData.isAnimating && !hoveredFlowers.has(flower)) {
                hoveredFlowers.add(flower);
                flowerData.animateFullCycle();
            }
        } else {
            hoveredFlowers.clear(); // Clear if not hovering over a flower
        }
    };

    scene.onPointerUp = () => {
        if (hoveredFlowers.size === 0) {
            if (oscillatorPool) {
                oscillatorPool.forEach(oscData => {
                    oscData.gain.gain.linearRampToValueAtTime(0, audioCtx.currentTime + 0.2);
                });
            }
        }
    };
}

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();
    const stackPanel = gui.stackPanel.createStackPanel(stackPanelOpt);
    const stackPanelOpt2 = new Bit.Inputs.BabylonGui.CreateStackPanelDto();
    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.paddingBottomInPixels = 10;
    const txtBlockOptions = new Bit.Inputs.BabylonGui.CreateTextBlockDto();
    txtBlockOptions.text = "BITBYBIT.DEV";
    const txtBlock = gui.textBlock.createTextBlock(txtBlockOptions);
    txtBlock.height = "60px";
    txtBlock.paddingBottomInPixels = 30;

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