Script: 3D Slide With Physics

3D Slide With Physics picture
Type
Typescript logo indicatortypescript
Author
matas
Date Created
Nov 18, 2023, 6:35:43 AM
Last Edit Date
Oct 2, 2025, 7:09:49 PM

Project Information

This is an example project that shows how you can create a 3D slide to roll things through by using wire offsets

View Full Project

Script Code

const points = [
    [0, 42, -20],
    [-10, 40, -10],
    [0, 35, 0],
    [0, 32, 10],
    [20, 30, 16],
    [40, 27, 40],
    [-20, 24, 16],
    [-20, 20, -16],
    [10, 15, 16],
    [-10, 10, 16],
    [-10, 5, -16],
    [-25, 0, -26],
] as Bit.Inputs.Base.Point3[];

const scene = bitbybit.babylon.scene.getScene();
const cam = scene.activeCamera as BABYLON.ArcRotateCamera;
cam.position = new BABYLON.Vector3(20, 50, 50);
cam.target = new BABYLON.Vector3(0, 20, 0);
cam.panningSensibility = 40;

type Model = {
    dashboard: BABYLON.GUI.AdvancedDynamicTexture | undefined,
    nrBalls: number,
    sphereMeshes: BABYLON.Mesh[],
    sphereAggregates: BABYLON.PhysicsAggregate[],
    pointsForSpheres: Bit.Inputs.Base.Point3[] | undefined,
    sphereBase: BABYLON.Mesh | undefined,
    wireForSpherePositions: Bit.Inputs.OCCT.TopoDSShapePointer | undefined,
}

const model: Model = {
    dashboard: undefined,
    nrBalls: 25,
    sphereMeshes: [],
    sphereAggregates: [],
    pointsForSpheres: undefined,
    sphereBase: undefined,
    wireForSpherePositions: undefined
}

const guiControlNames = {
    button: "RunSimButton",
    nrBallsHeader: "NrBallsHeader",
    nrBallsSlider: "NrBallsSlider",
    loadingIndicator: "LoadingHeader",
}

const start = async () => {
    createGUI();
    showLoadingIndicator();
    const bawlMesh = await createBawl(50, 4, [0, 10, 0]);
    const bawlMeshCollision = bawlMesh.getChildMeshes()[0] as BABYLON.Mesh;

    const res = await createSlide(points);
    const slideMeshCollision = res.slide.getChildMeshes()[0] as BABYLON.Mesh;

    model.sphereBase = await createCompleteSphereMesh();

    bitbybit.babylon.scene.enablePhysics({
        vector: [0, -9.81, 0]
    })

    new BABYLON.PhysicsAggregate(slideMeshCollision, BABYLON.PhysicsShapeType.MESH, { mesh: slideMeshCollision, mass: 0, restitution: 0.75 });
    new BABYLON.PhysicsAggregate(bawlMeshCollision, BABYLON.PhysicsShapeType.MESH, { mesh: bawlMeshCollision, mass: 0, restitution: 0.75 });

    prepareEnvironment();

    hideLoadingIndicator();
}

start();

async function startSimulation() {

    if (model.sphereMeshes && model.sphereMeshes.length) {
        model.sphereMeshes.forEach(m => m.dispose());
        model.sphereMeshes = [];
    }

    if (model.sphereAggregates && model.sphereAggregates.length) {
        model.sphereAggregates.forEach(a => a.dispose());
        model.sphereAggregates = [];
    }

    const radius = 1;
    model.sphereBase.scaling = new BABYLON.Vector3(radius, radius, radius);

    const points = await getPointsForSpheres();
    points.forEach(pt => {
        const sphere = model.sphereBase.clone(`${pt}`);
        sphere.getChildMeshes().forEach(m => m.isVisible = true);
        sphere.position = new BABYLON.Vector3(pt[0], pt[1], pt[2]);
        const sphereAggregate = new BABYLON.PhysicsAggregate(sphere, BABYLON.PhysicsShapeType.SPHERE, { radius, mass: 0.1, restitution: 0.75 });
        model.sphereMeshes.push(sphere);
        const shadowGenerators: BABYLON.ShadowGenerator[] = scene.metadata?.shadowGenerators;
        if (shadowGenerators && shadowGenerators.length) {
            shadowGenerators.forEach(sg => {
                sg.addShadowCaster(sphere);
            })
        }
        model.sphereAggregates.push(sphereAggregate);
    })
}

function prepareEnvironment() {

    const dirLightOpt = new Bit.Inputs.BabylonScene.DirectionalLightDto();
    dirLightOpt.direction = [20, -50, -20];
    dirLightOpt.intensity = 4;
    dirLightOpt.shadowGeneratorMapSize = 4056;
    bitbybit.babylon.scene.drawDirectionalLight(dirLightOpt);

    const skyboxOpt = new Bit.Inputs.BabylonScene.SkyboxDto();
    skyboxOpt.skybox = Bit.Inputs.Base.skyboxEnum.clearSky;
    bitbybit.babylon.scene.enableSkybox(skyboxOpt)
}

async function createSlide(points: Bit.Inputs.Base.Point3[]): Promise<{ slide: BABYLON.Mesh }> {
    const wir1 = await bitbybit.occt.shapes.wire.createPolylineWire({
        points,
    });

    const depth = 1;
    const offsetSteps = 1.1;

    const wire1 = await bitbybit.occt.fillets.fillet3DWire({
        shape: wir1,
        radius: 5,
        direction: [0, 100, 0],
    })

    const wire2 = await bitbybit.occt.operations.offset3DWire({
        shape: wire1,
        offset: offsetSteps,
        direction: [0, 1, 0]
    });

    const wire3 = await bitbybit.occt.operations.offset3DWire({
        shape: wire1,
        offset: 2 * offsetSteps,
        direction: [0, 20, 0]
    });

    const wire4 = await bitbybit.occt.operations.offset3DWire({
        shape: wire1,
        offset: 3 * offsetSteps,
        direction: [0, 20, 0]
    });

    const wire5 = await bitbybit.occt.operations.offset3DWire({
        shape: wire1,
        offset: 4 * offsetSteps,
        direction: [0, 20, 0]
    });

    const wireT1 = await bitbybit.occt.transforms.translate({
        shape: wire1,
        translation: [0, -depth, 0],
    });

    const wireT3 = await bitbybit.occt.transforms.translate({
        shape: wire3,
        translation: [0, -depth * 2, 0],
    });

    const wireT5 = await bitbybit.occt.transforms.translate({
        shape: wire5,
        translation: [0, -depth, 0],
    });

    model.wireForSpherePositions = await bitbybit.occt.transforms.translate({
        shape: wireT3,
        translation: [0, depth * 5, 0],
    });

    const loft = await bitbybit.occt.operations.loft({
        shapes: [wireT1, wire2, wireT3, wire4, wireT5],
        makeSolid: false
    });

    const loftThick = await bitbybit.occt.operations.makeThickSolidSimple({
        shape: loft,
        offset: -0.1,
    });

    const opt2 = new Bit.Inputs.Draw.DrawOcctShapeOptions();
    opt2.precision = 0.01;
    opt2.faceColour = "#5555ff";
    const slide = await bitbybit.draw.drawAnyAsync({
        entity: loftThick,
        options: opt2
    });

    await bitbybit.occt.deleteShapes({
        shapes: [wir1, wire1, wire2, wire3, wire4, wire5, wireT1, wireT3, wireT5, loft, loftThick]
    })

    return { slide };
}

async function getPointsForSpheres(): Promise<Bit.Inputs.Base.Point3[]> {

    const optionsDivision = new Bit.Inputs.OCCT.DivideDto<Bit.Inputs.OCCT.TopoDSWirePointer>(model.wireForSpherePositions);
    optionsDivision.nrOfDivisions = model.nrBalls * 2;
    const pointsForSpheres = await bitbybit.occt.shapes.wire.divideWireByEqualDistanceToPoints(optionsDivision);

    const half = Math.ceil(pointsForSpheres.length / 2);

    return pointsForSpheres.slice(1, half)
}

async function createCompleteSphereMesh(): Promise<BABYLON.Mesh> {
    const res1 = await createSphere(0.25, 1.8, 1, 0.1)
    const res2 = await createSphere(0.5, 1.5, 0.9, 0.15)
    const res3 = await createSphere(0.5, 1.55, 0.75, 0.15)

    const compound = await bitbybit.occt.shapes.compound.makeCompound({
        shapes: [res1.sphere, res2.sphere, res3.sphere],
    });

    const drawOptions = new Bit.Inputs.Draw.DrawOcctShapeOptions();
    drawOptions.faceColour = "#0000ff";
    drawOptions.edgeWidth = 1;
    drawOptions.drawEdges = false;
    drawOptions.precision = 0.05;

    const sphereMesh = await bitbybit.draw.drawAnyAsync({
        entity: compound,
        options: drawOptions
    });

    const mat = sphereMesh.getChildMeshes()[0].material as BABYLON.PBRMetallicRoughnessMaterial;
    mat.metallic = 0.9;
    mat.roughness = 0.1;
    sphereMesh.getChildMeshes().forEach(m => m.isVisible = false);
    await bitbybit.occt.deleteShapes({ shapes: [res1.sphere, res2.sphere, res3.sphere, compound, ...res1.shapesToDelete, ...res2.shapesToDelete, ...res3.shapesToDelete] });
    return sphereMesh;
}

async function createSphere(starInnerRadius: number, starOuterRadius: number, sphereRadius: number, thickness: number): Promise<{ sphere: Bit.Inputs.OCCT.TopoDSShapePointer, shapesToDelete: Bit.Inputs.OCCT.TopoDSShapePointer[] }> {

    const star = await bitbybit.occt.shapes.wire.createStarWire({
        direction: [1, 0, 0],
        center: [5, 0, 0],
        innerRadius: starInnerRadius,
        outerRadius: starOuterRadius,
        numRays: 8,
        half: false
    });

    const sphere = await bitbybit.occt.shapes.solid.createSphere({
        radius: sphereRadius,
        center: [0, 0, 0]
    });

    const fillet = await bitbybit.occt.fillets.fillet2d({
        shape: star,
        radius: 0.03
    });

    const projection = await bitbybit.occt.shapes.wire.project({
        wire: fillet,
        shape: sphere,
        direction: [1, 0, 0]
    });

    const projectionWires = await bitbybit.occt.shapes.wire.getWires({ shape: projection });

    const split = await bitbybit.occt.operations.splitShapeWithShapes({
        shape: sphere,
        shapes: projectionWires,
        nonDestructive: true,
        localFuzzyTolerance: 0.00001,
    });

    const faces = await bitbybit.occt.shapes.face.getFaces({ shape: split[8] });
    const face = faces.shift();

    const thicken = await bitbybit.occt.operations.makeThickSolidSimple({
        shape: face,
        offset: -thickness,
    })


    bitbybit.occt.deleteShapes({
        shapes: [fillet]
    })

    return { sphere: thicken, shapesToDelete: [star, sphere, fillet, projection, ...split, ...faces] };
}

async function createBawl(radius: number, thickness: number, center: Bit.Inputs.Base.Vector3): Promise<BABYLON.Mesh> {
    const sphere = await bitbybit.occt.shapes.solid.createSphere({
        radius,
        center: center
    });
    const sphere2 = await bitbybit.occt.shapes.solid.createSphere({
        radius: radius - thickness,
        center: center
    });
    const box = await bitbybit.occt.shapes.solid.createCube({ size: radius * 2, center: [center[0], center[1] + radius, center[2]] });

    const halfSphere = await bitbybit.occt.booleans.difference({
        shape: sphere, shapes: [box, sphere2],
        keepEdges: false
    });

    const drawOpt = new Bit.Inputs.Draw.DrawOcctShapeOptions();
    drawOpt.faceColour = "#2222ff";
    drawOpt.edgeColour = "#000000";
    return bitbybit.draw.drawAnyAsync({
        entity: halfSphere,
        options: drawOpt
    })
}

function createGUI() {

    model.dashboard = BABYLON.GUI.AdvancedDynamicTexture.CreateFullscreenUI("UI");

    var panel = new BABYLON.GUI.StackPanel();
    panel.width = "700px";
    panel.background = "#00000055";
    panel.paddingLeftInPixels = 40;
    panel.paddingTopInPixels = 40;
    panel.horizontalAlignment = BABYLON.GUI.Control.HORIZONTAL_ALIGNMENT_LEFT;
    panel.verticalAlignment = BABYLON.GUI.Control.VERTICAL_ALIGNMENT_TOP;
    panel.adaptHeightToChildren = true;
    model.dashboard.addControl(panel);

    var header1 = new BABYLON.GUI.TextBlock("Header");
    header1.text = "3D Slide Example";
    header1.height = "80px";
    header1.color = "#f0b89d";
    header1.fontSize = "40px";

    panel.addControl(header1);

    var header = new BABYLON.GUI.TextBlock(guiControlNames.loadingIndicator);
    header.paddingTopInPixels = 10;
    header.text = "Loading...";
    header.height = "60px";
    header.color = "white";
    header.fontSize = "30px"

    panel.addControl(header);

    const labelSlider = "Number Of Balls:";
    createSliderWithLabel(panel, guiControlNames.nrBallsSlider, guiControlNames.nrBallsHeader, labelSlider, 25, 10, 50, 1,
        (slider: BABYLON.GUI.Slider, header: BABYLON.GUI.TextBlock) => {
            header.text = labelSlider + " " + slider.value;
            model.nrBalls = slider.value;
        }
    );

    var button1 = BABYLON.GUI.Button.CreateSimpleButton(guiControlNames.button, "Run Simulation!");
    button1.width = "350px"
    button1.thickness = 0;
    button1.height = "120px";
    button1.paddingTop = 30;
    button1.color = "white";
    button1.fontSize = 30;
    button1.cornerRadius = 10;
    button1.background = "black";
    button1.onPointerUpObservable.add(() => {
        startSimulation();
    });
    panel.addControl(button1);

    var header2 = new BABYLON.GUI.TextBlock("bitbybit.dev");
    header2.verticalAlignment = BABYLON.GUI.Control.VERTICAL_ALIGNMENT_BOTTOM;
    header2.text = "bitbybit.dev";
    header2.height = "120px";
    header2.color = "#f0b89d";
    header2.fontSize = "40px";

    panel.addControl(header2);
}

function createSliderWithLabel(
    panel: BABYLON.GUI.StackPanel,
    nameSlider: string,
    nameHeader: string,
    label: string,
    defaultVal: number,
    min: number,
    max: number,
    step: number,
    funcToRun: (slider: BABYLON.GUI.Slider, label: BABYLON.GUI.TextBlock) => void
) {
    var header = new BABYLON.GUI.TextBlock(nameHeader);
    header.paddingTopInPixels = 10;
    header.text = label + " " + defaultVal;
    header.height = "60px";
    header.color = "white";
    header.fontSize = "30px"

    panel.addControl(header);

    var slider = new BABYLON.GUI.Slider(nameSlider);
    slider.thumbColor = "#f0b89d";
    slider.isThumbCircle = true;

    slider.borderColor = "#f0b89d"
    slider.minimum = min;
    slider.maximum = max;
    slider.step = step;
    slider.value = defaultVal;
    slider.paddingLeftInPixels = 10;
    slider.paddingRightInPixels = 10;
    slider.isVertical = false;
    slider.alpha = 1;

    slider.height = "25px";
    slider.onPointerUpObservable.add(() => {
        funcToRun(slider, header);
    });

    panel.addControl(slider);
}

function showLoadingIndicator() {
    if (model.dashboard) {
        const loadingIndicator = model.dashboard.getControlByName(guiControlNames.loadingIndicator);
        const button = model.dashboard.getControlByName(guiControlNames.button);
        const ballsSlider = model.dashboard.getControlByName(guiControlNames.nrBallsSlider);
        const ballsHeader = model.dashboard.getControlByName(guiControlNames.nrBallsHeader);
        loadingIndicator.isVisible = true;
        button.isVisible = false;
        ballsSlider.isVisible = false;
        ballsHeader.isVisible = false;
    }
}

function hideLoadingIndicator() {
    if (model.dashboard) {
        const loadingIndicator = model.dashboard.getControlByName(guiControlNames.loadingIndicator);
        const button = model.dashboard.getControlByName(guiControlNames.button);
        const ballsSlider = model.dashboard.getControlByName(guiControlNames.nrBallsSlider);
        const ballsHeader = model.dashboard.getControlByName(guiControlNames.nrBallsHeader);
        loadingIndicator.isVisible = false;
        button.isVisible = true;
        ballsSlider.isVisible = true;
        ballsHeader.isVisible = true;
    }
}
Plans & Pricing

Choose Your Plan

Editor plans for 3D development, API keys for server-side CAD algorithms

B2B

ENTERPRISE

Custom pricing

Custom software development, dedicated servers & CAD automation at scale.

CAD Automation & Software
  • Custom software development
  • Cloud CAD automation pipelines
  • 3D configurators (STEP & GLTF)
  • Batch export jobs
  • Custom algorithms & deployment
Infrastructure & Support
  • Custom compute allocation
  • Dedicated / VPS server tenants
  • Long-running computation jobs
  • Custom upload limits & overage
  • SLA & premium support