async function start() {
const scene = bitbybit.babylon.scene.getScene();
const engine = bitbybit.babylon.engine.getEngine();
// **Constants**
// **Constants**
const numWeeds = 3000; // Number of weeds
const numSegments = 5; // Segments per weed
const segmentLength = 0.4; // Length of each segment
const weedHeight = numSegments * segmentLength; // Total height = 2 units
const mouseEffectRadius = 3; // Radius of mouse wind influence
const maxMouseInfluence = 1; // Maximum bending strength
const recoveryAlpha = 0.1; // Recovery speed (lower = slower)
const velocityInfluenceFactor = 2; // Mouse speed effect
// **Create invisible ground at the top of the weeds**
const ground = BABYLON.MeshBuilder.CreateGround("ground", { width: 100, height: 100 }, scene);
ground.position.y = weedHeight; // Ground at weed tops (y = 2)
ground.visibility = 0; // Invisible but pickable
// **Weed creation**
const weeds = [];
const baseColor = new BABYLON.Color4(0.5, 0, 1, 1); // Purple at base
const midColor = new BABYLON.Color4(0, 0, 1, 1); // Blue in middle
const topColor = new BABYLON.Color4(1, 1, 1, 1); // White at top
for (let i = 0; i < numWeeds; i++) {
const x = (Math.random() - 0.5) * 12;
const z = (Math.random() - 0.5) * 30;
const basePosition = new BABYLON.Vector3(x, 0, z);
const points = [];
const colors = [];
for (let k = 0; k <= numSegments; k++) {
points.push(new BABYLON.Vector3(x, k * segmentLength, z));
const t = k / numSegments;
let color;
if (t < 0.5) {
color = BABYLON.Color4.Lerp(baseColor, midColor, t * 2);
} else {
color = BABYLON.Color4.Lerp(midColor, topColor, (t - 0.5) * 2);
}
colors.push(color);
}
const linesMesh = BABYLON.MeshBuilder.CreateLines("weed" + i, {
points: points,
colors: colors,
updatable: true
}, scene);
linesMesh.isPickable = false;
weeds.push({
basePosition,
linesMesh,
randomOffset: Math.random() * Math.PI * 2,
smoothedMouseInfluence: 0,
smoothedMouseDirection: new BABYLON.Vector3(0, 0, 0)
});
}
// **Mouse and time tracking**
let time = 0;
let mouseWorldPos = new BABYLON.Vector3(0, weedHeight, 0); // Start at weed top height
let lastMouseWorldPos = mouseWorldPos.clone();
let mouseVelocity = new BABYLON.Vector3(0, 0, 0);
// **Mouse movement handler**
scene.onPointerObservable.add((pointerInfo) => {
if (pointerInfo.type === BABYLON.PointerEventTypes.POINTERMOVE && pointerInfo.event.buttons === 0) {
const pickInfo = scene.pick(pointerInfo.event.clientX, pointerInfo.event.clientY);
if (pickInfo.hit && pickInfo.pickedMesh === ground) {
lastMouseWorldPos = mouseWorldPos.clone();
mouseWorldPos = pickInfo.pickedPoint;
mouseVelocity = mouseWorldPos.subtract(lastMouseWorldPos);
}
}
});
// **Render loop**
engine.runRenderLoop(() => {
time += 0.02;
weeds.forEach(weed => {
// **Mouse influence as radial wind**
const weedTop = weed.basePosition.clone();
weedTop.y = weedHeight;
const fromWeedTopToMouse = mouseWorldPos.subtract(weedTop); // Direction away from weed top
const distance = fromWeedTopToMouse.length();
const falloff = (distance < mouseEffectRadius) ? (1 - distance / mouseEffectRadius) : 0;
// Directional influence from mouse velocity
const velocityNorm = mouseVelocity.length() > 0 ? mouseVelocity.normalize() : new BABYLON.Vector3(0, 0, 0);
const toMouseNorm = fromWeedTopToMouse.length() > 0 ? fromWeedTopToMouse.normalize() : new BABYLON.Vector3(0, 0, 0);
const velocityDot = BABYLON.Vector3.Dot(velocityNorm, toMouseNorm);
const velocityInfluence = Math.max(0, velocityDot) * mouseVelocity.length() * velocityInfluenceFactor;
// Total target influence
const targetMouseInfluence = falloff * maxMouseInfluence + velocityInfluence;
// **Smooth recovery**
weed.smoothedMouseInfluence = BABYLON.Scalar.Lerp(
weed.smoothedMouseInfluence,
targetMouseInfluence,
recoveryAlpha
);
const targetDirection = fromWeedTopToMouse.length() > 0 ? fromWeedTopToMouse.normalize() : new BABYLON.Vector3(0, 0, 0);
weed.smoothedMouseDirection = BABYLON.Vector3.Lerp(
weed.smoothedMouseDirection,
targetDirection,
recoveryAlpha
);
// **Build weed segments**
const points = [];
let currentPoint = weed.basePosition.clone();
points.push(currentPoint.clone());
const windVariation = Math.sin(time + weed.randomOffset) * 0.1;
const windStrength = 0.05 + windVariation;
const windAxis = new BABYLON.Vector3(1, 0, 0);
let mouseAxis = null;
if (weed.smoothedMouseInfluence > 0 && weed.smoothedMouseDirection.length() > 0) {
mouseAxis = BABYLON.Vector3.Cross(BABYLON.Axis.Y, weed.smoothedMouseDirection);
}
// Accumulate angles to ensure bottom leans less
let accumulatedMouseAngle = 0;
for (let k = 1; k <= numSegments; k++) {
const windAngle = windStrength * k;
const windMatrix = BABYLON.Matrix.RotationAxis(windAxis, windAngle);
let segmentDir = new BABYLON.Vector3(0, segmentLength, 0);
if (mouseAxis && weed.smoothedMouseInfluence > 0) {
// Incremental angle increase from base to top, leaning away
const mouseInfluenceFactor = k / numSegments; // 0.2 at k=1, 1 at k=5
const mouseAngleIncrement = weed.smoothedMouseInfluence * mouseInfluenceFactor * 0.2;
accumulatedMouseAngle += mouseAngleIncrement; // Add to previous angle
const mouseMatrix = BABYLON.Matrix.RotationAxis(mouseAxis, accumulatedMouseAngle); // Positive for away
segmentDir = BABYLON.Vector3.TransformCoordinates(segmentDir, mouseMatrix);
}
// Apply wind after mouse bending
segmentDir = BABYLON.Vector3.TransformCoordinates(segmentDir, windMatrix);
currentPoint = currentPoint.add(segmentDir);
points.push(currentPoint.clone());
}
const positions = points.reduce((acc, p) => acc.concat([p.x, p.y, p.z]), []);
weed.linesMesh.updateVerticesData(BABYLON.VertexBuffer.PositionKind, positions);
});
scene.render();
});
}
start();