Script: Focus With 3D Alien Ring

Focus With 3D Alien Ring picture
Type
Typescript logo indicatortypescript
Date Created
Mar 23, 2025, 4:16:22 PM
Last Edit Date
Jul 2, 2025, 10:07:55 AM

Project Information

This project was co-developed with Grok 3 LLM and contains mesmerising zero gravity fluid simulation. Alien ring reacts to mouse and makes sounds. It's hypnotising aesthetics can be used as therapy to calm down and focus.

View Full Project

Script Code

createGUI();

const start = () => {

    const scene = bitbybit.babylon.scene.getScene();
    const engine = scene.getEngine();
    const canvas = engine.getRenderingCanvas();
    const skyboxOpt = new Bit.Inputs.BabylonScene.SkyboxDto();
    skyboxOpt.skybox = "city" as Bit.Inputs.Base.skyboxEnum;
    skyboxOpt.blur = 0.6;
    bitbybit.babylon.scene.enableSkybox(skyboxOpt);
    const dtoLight = new Bit.Inputs.BabylonScene.DirectionalLightDto();
    dtoLight.intensity = 10;
    dtoLight.shadowBias = 0.002;
    dtoLight.shadowGeneratorMapSize = 4000;
    bitbybit.babylon.scene.drawDirectionalLight(dtoLight);
    const cam = bitbybit.babylon.scene.getActiveCamera() as BABYLON.ArcRotateCamera;
    cam.position = new BABYLON.Vector3(0, 20, 70);
    cam.target = new BABYLON.Vector3(0, -5, 0);
    cam.upperRadiusLimit = 100;
    cam.lowerRadiusLimit = 20;

    const boxOpt = new Bit.Inputs.BabylonMeshBuilder.CreateCubeDto();
    boxOpt.size = 600;
    boxOpt.enableShadows = false;
    const bx = bitbybit.babylon.meshBuilder.createCube(boxOpt);
    const material1 = new BABYLON.StandardMaterial("x", scene);
    material1.backFaceCulling = false;
    material1.diffuseColor = new BABYLON.Color3(0, 0, 0);
    bx.material = material1;

    // Torus parameters
    const R = 14,  // Major radius
        r = 4,   // Minor radius
        step = 3; // Angular step
    const num_theta = 360 / step;
    const num_phi = 360 / step;

    // Generate initial vertex positions
    let base_positions = [];
    for (let theta = 0; theta < 360; theta += step) {
        for (let phi = 0; phi < 360; phi += step) {
            let tRad = theta * Math.PI / 180;
            let pRad = phi * Math.PI / 180;
            let r1 = r * (1 + 0.5 * Math.sin(3 * tRad + 2 * pRad));
            let x = (R + r1 * Math.cos(pRad)) * Math.cos(tRad);
            let y = (R + r1 * Math.cos(pRad)) * Math.sin(tRad);
            let z = r1 * Math.sin(pRad) + 2 * Math.sin(5 * tRad) * Math.cos(3 * pRad);
            base_positions.push(x, y, z);
        }
    }

    // Generate indices
    let indices = [];
    for (let theta_idx = 0; theta_idx < num_theta; theta_idx++) {
        for (let phi_idx = 0; phi_idx < num_phi; phi_idx++) {
            let idx00 = phi_idx * num_theta + theta_idx;
            let idx10 = phi_idx * num_theta + (theta_idx + 1) % num_theta;
            let idx01 = ((phi_idx + 1) % num_phi) * num_theta + theta_idx;
            let idx11 = ((phi_idx + 1) % num_phi) * num_theta + (theta_idx + 1) % num_theta;
            indices.push(idx00, idx10, idx01);
            indices.push(idx10, idx11, idx01);
        }
    }

    // Create mesh
    let vertexData = new BABYLON.VertexData();
    vertexData.positions = base_positions.slice();
    vertexData.indices = indices;
    BABYLON.VertexData.ComputeNormals(vertexData.positions, vertexData.indices, vertexData.normals = []);
    let mesh = new BABYLON.Mesh("custom", scene);
    vertexData.applyToMesh(mesh, true);

    // Apply material
    let material = new BABYLON.PBRMaterial("pbr", scene);
    material.albedoColor = new BABYLON.Color3(0.1, 0.1, 1);
    material.metallic = 0.86;
    material.roughness = 0.14;
    mesh.material = material;
    mesh.receiveShadows = true;

    const sgs = scene.metadata.shadowGenerators;
    sgs.forEach((s: BABYLON.ShadowGenerator) => {
        s.addShadowCaster(mesh);
    });

    // Animation parameters
    const a = 0.2, b = 0.2, c = 0.2; // Wobble speeds
    const ripple_amplitude = 0.1;
    const k = 2 * Math.PI / 1;
    const w = 2 * Math.PI * 2;
    const d = 0.5;
    const lifetime = 3;
    const maxWaveSources = 10;
    const fadeInTime = 0.3;
    const fadeOutTime = 1.0;

    let waveSources = [];
    let t = 0;
    let lastWaveTime = 0;

    // Audio setup
    const audioContext = new AudioContext();
    const oscillator = audioContext.createOscillator();
    const gainNode = audioContext.createGain();
    const filter = audioContext.createBiquadFilter();
    const lfo = audioContext.createOscillator();
    const lfoGain = audioContext.createGain();

    // Configure audio for mysterious sound
    oscillator.type = 'sine';
    oscillator.frequency.value = 60; // Deep base tone
    filter.type = 'lowpass';
    filter.frequency.value = 200; // Muted initial state
    filter.Q.value = 3;
    lfo.type = 'sine';
    lfo.frequency.value = 0.5; // Slow eerie modulation
    lfoGain.gain.value = 50; // Modulation depth
    gainNode.gain.value = 0.1; // Initial volume

    // Connect audio nodes
    lfo.connect(lfoGain);
    lfoGain.connect(filter.frequency);
    oscillator.connect(filter);
    filter.connect(gainNode);
    gainNode.connect(audioContext.destination);

    // Start audio
    oscillator.start();
    lfo.start();

    // Interaction intensity tracking
    let interactionIntensity = 0.2;
    const maxIntensity = 0.1;
    const intensityDecay = 0.98;

    // Function to update sound based on interaction
    function updateSound() {
        const intensityFactor = interactionIntensity / maxIntensity;

        oscillator.frequency.value = 60 + intensityFactor * 20; // Slight pitch rise
        filter.frequency.value = 200 + intensityFactor * 800; // Open filter
        gainNode.gain.value = 0.1 + intensityFactor * 0.2; // Volume boost
        lfo.frequency.value = 0.1 + intensityFactor * 0.2 + Math.random() * 0.05; // Chaotic modulation
    }

    // Helper function to get initial position of a vertex
    function getInitialPosition(index) {
        return new BABYLON.Vector3(
            base_positions[index * 3],
            base_positions[index * 3 + 1],
            base_positions[index * 3 + 2]
        );
    }

    function getEnvelope(age) {
        if (age < fadeInTime) {
            return age / fadeInTime;
        } else if (age < lifetime - fadeOutTime) {
            return 1;
        } else {
            let fadeOutProgress = (lifetime - age) / fadeOutTime;
            return fadeOutProgress * Math.exp(1 - 1 / fadeOutProgress);
        }
    }

    // Mouse move handler with sound integration
    canvas.addEventListener("pointermove", (event) => {
        let pickResult = scene.pick(event.clientX, event.clientY);
        if (pickResult.hit && pickResult.pickedMesh === mesh) {
            if (t - lastWaveTime > 0.2 && waveSources.length < maxWaveSources) {
                let faceId = pickResult.faceId;
                let vertexIndex = indices[faceId * 3];
                waveSources.push({ vertexIndex: vertexIndex, startTime: t });
                lastWaveTime = t;
                interactionIntensity = Math.min(interactionIntensity + 1, maxIntensity); // Increase sound intensity
            }
        }
    });

    // Animation loop

    let rotationAngle = 0;

    scene.onBeforeRenderObservable.add(() => {
        t += scene.getEngine().getDeltaTime() / 1000;
        waveSources = waveSources.filter(ws => t - ws.startTime < lifetime);

        // Decay interaction intensity
        interactionIntensity *= intensityDecay;
        interactionIntensity = Math.max(interactionIntensity, 0);

        // Update sound
        updateSound();

        // Compute wobbled positions with z-scaling
        let current_base_positions = [];
        const zScale = 1.5;
        for (let theta = 0; theta < 360; theta += step) {
            for (let phi = 0; phi < 360; phi += step) {
                let tRad = theta * Math.PI / 180;
                let pRad = phi * Math.PI / 180;
                let r1 = r * (1 + 0.2 * Math.sin(3 * tRad + 2 * pRad + c * t));
                let x = (R + r1 * Math.cos(pRad)) * Math.cos(tRad);
                let y = (R + r1 * Math.cos(pRad)) * Math.sin(tRad);
                let z_base = r1 * Math.sin(pRad) + 0.5 * Math.sin(5 * tRad + a * t) * Math.cos(3 * pRad + b * t);
                z_base *= zScale;
                current_base_positions.push(x, y, z_base);
            }
        }

        // Compute normals for wobbled positions
        let base_normals = [];
        BABYLON.VertexData.ComputeNormals(current_base_positions, indices, base_normals);

        // Apply ripple effect with phasing
        let final_positions = [];
        for (let i = 0; i < num_theta * num_phi; i++) {
            let base_pos = new BABYLON.Vector3(
                current_base_positions[i * 3],
                current_base_positions[i * 3 + 1],
                current_base_positions[i * 3 + 2]
            );
            let normal = new BABYLON.Vector3(
                base_normals[i * 3],
                base_normals[i * 3 + 1],
                base_normals[i * 3 + 2]
            );

            let ripple_term = 0;
            for (let ws of waveSources) {
                let pos_i = getInitialPosition(i);
                let pos_ws = getInitialPosition(ws.vertexIndex);
                let distance = BABYLON.Vector3.Distance(pos_i, pos_ws);
                let age = t - ws.startTime;
                let phase = k * distance - w * age;
                let envelope = getEnvelope(age);
                ripple_term += ripple_amplitude * envelope * Math.sin(phase) * Math.exp(-d * distance);
            }

            let final_pos = base_pos.add(normal.scale(ripple_term));
            final_positions.push(final_pos.x, final_pos.y, final_pos.z);
        }

        // Update mesh
        mesh.updateVerticesData(BABYLON.VertexBuffer.PositionKind, final_positions);
        let final_normals = [];
        BABYLON.VertexData.ComputeNormals(final_positions, indices, final_normals);
        mesh.updateVerticesData(BABYLON.VertexBuffer.NormalKind, final_normals);


        // Rotate the plane
        rotationAngle += 0.001;
        mesh.rotation.y = rotationAngle;
    });
}
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);
}