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.05
});
const projection = await bitbybit.occt.shapes.wire.project({
wire: fillet,
shape: sphere,
direction: [1, 0, 0]
});
const split = await bitbybit.occt.operations.splitShapeWithShapes({
shape: sphere,
shapes: [projection]
})
const faces = await bitbybit.occt.shapes.face.getFaces({
shape: split,
});
const face = faces[7];
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;
}
}