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 = Bit.Inputs.Base.skyboxEnum.city;
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);
}