Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
360 changes: 360 additions & 0 deletions .github/copilot/skills/port-flow-graph-block.md

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions demos-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,13 @@
"tags": ["showcase"],
"mobile": true
},
{
"slug": "calculator",
"name": "Calculator (KHR_interactivity)",
"description": "Interactive glTF showcase: the Khronos \"Calculator\" sample driven by Babylon Lite's flow-graph runtime (glTF KHR_interactivity). On load the embedded interactivity graph runs — resetting the display to \"00\" by scrolling the digit materials' texture-transform offset, hiding the minus sign via KHR_node_visibility, and seeding scene variables — exercising the ported math, pointer and event blocks. KHR_interactivity is an unratified draft; the runtime tracks the current spec. Khronos glTF-Test-Assets-Interactivity (CC0).",
"tags": ["showcase", "interactivity", "gltf"],
"mobile": true
},
{
"slug": "littlest-tokyo",
"name": "Littlest Tokyo",
Expand Down
2 changes: 2 additions & 0 deletions docs/lite/architecture/00-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ Pages are ordered by how commonly Babylon Lite users reach for them — start wi
| [38-bundle-size-tooling.md](/lite/architecture/38-bundle-size-tooling) | Bundle Size Tooling | Per-scene bundle analysis, ceilings, treemaps |
| [39-animation-parity-testing.md](/lite/architecture/39-animation-parity-testing) | Animation Parity | Animated scene test methodology |
| [40-material-stencil.md](/lite/architecture/40-material-stencil) | Material Stencil | Opt-in `enableMaterialStencil` per-material stencil mask/test on Standard/PBR/Shader, zero-impact hook |
| [41-audio-engine.md](/lite/architecture/41-audio-engine) | Audio Engine | WebAudio engine, sound sources, spatial audio |
| [42-flow-graph.md](/lite/architecture/42-flow-graph) | Flow Graph | Pure-state visual-scripting runtime + glTF `KHR_interactivity` port plan (DESIGN) |

---

Expand Down
872 changes: 872 additions & 0 deletions docs/lite/architecture/42-flow-graph.md

Large diffs are not rendered by default.

140 changes: 140 additions & 0 deletions lab/lite/demo-calculator.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<!-- Disable the browser's pinch-to-zoom so two-finger gestures drive the camera (pinch zoom) instead. -->
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
<title>Babylon Lite — Demo: Calculator (KHR_interactivity)</title>
<style>
html, body { margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden; background: #000; }
/* touch-action: none lets the canvas receive raw touch gestures (pinch/drag) for camera control
instead of the browser consuming them for page scroll/zoom. */
canvas { width: 100%; height: 100%; display: block; touch-action: none; }
.loading-overlay {
position: fixed; inset: 0; z-index: 10000;
display: flex; align-items: center; justify-content: center;
background: #0b0809; color: #f4ece9;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
transition: opacity 0.5s ease;
}
.loading-overlay.is-hidden { opacity: 0; pointer-events: none; }
.loading-box { display: flex; flex-direction: column; align-items: center; gap: 1rem; text-align: center; padding: 0 1rem; }
.loading-spinner {
width: 54px; height: 54px; border-radius: 50%;
border: 4px solid rgba(224, 104, 75, 0.25); border-top-color: #e0684b;
animation: lite-spin 0.9s linear infinite;
}
.loading-label { font-size: 1.05rem; font-weight: 600; }
.loading-sub { font-size: 0.8rem; color: #b8a9a4; }
.loading-size { font-size: 0.78rem; color: #d8c9c4; margin-top: 0.4rem; min-height: 1em; letter-spacing: 0.01em; }
@keyframes lite-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .loading-spinner { animation-duration: 2.4s; } }

/* Always-visible attribution for the Khronos test asset. */
.credit {
position: fixed; right: 12px; bottom: 10px; z-index: 10001;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
font-size: 0.72rem; line-height: 1.3; color: #e9e2df;
background: rgba(0, 0, 0, 0.45); padding: 5px 9px; border-radius: 6px;
text-decoration: none; backdrop-filter: blur(3px);
transition: background 0.2s ease;
}
.credit:hover { background: rgba(0, 0, 0, 0.7); }
.credit b { font-weight: 600; }
/* Auto-rotate toggle button. */
.ctrl-btn {
position: fixed; left: 12px; bottom: 10px; z-index: 10001;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
font-size: 0.75rem; line-height: 1; color: #e9e2df;
background: rgba(0, 0, 0, 0.45); padding: 8px 12px; border-radius: 6px;
border: 1px solid rgba(255, 255, 255, 0.12); cursor: pointer;
backdrop-filter: blur(3px); transition: background 0.2s ease;
}
.ctrl-btn:hover { background: rgba(0, 0, 0, 0.7); }
.loading-bar {
width: 240px; max-width: 62vw; height: 4px; margin-top: 0.55rem;
border-radius: 999px; overflow: hidden; background: rgba(255, 255, 255, 0.14);
opacity: 0; transition: opacity 0.25s ease;
}
.loading-overlay.has-progress .loading-bar { opacity: 1; }
.loading-bar-fill {
width: 0%; height: 100%; border-radius: inherit; background: #e0684b;
transition: width 0.25s ease;
}
</style>
</head>
<body>
<!--
This page loads the production-BUNDLED demo (tree-shaken + minified) from
/bundle/demos/, NOT the raw TypeScript source. Run `pnpm build:bundle-demos`
(also part of `pnpm dev:lab`) to (re)generate the bundle this page references.
-->
<canvas id="renderCanvas"></canvas>
<button id="rotateToggle" class="ctrl-btn" type="button" aria-pressed="true">⏸ Auto-rotate</button>
<a class="credit" href="https://github.com/KhronosGroup/glTF-Test-Assets-Interactivity" target="_blank" rel="noopener noreferrer">
<b>&ldquo;Calculator&rdquo;</b> Khronos glTF KHR_interactivity test asset
</a>
<div id="loadingOverlay" class="loading-overlay" role="status" aria-live="polite">
<div class="loading-box">
<div class="loading-spinner" aria-hidden="true"></div>
<div class="loading-label">Loading Calculator…</div>
<div class="loading-sub">Fetching interactive glTF model & studio environment…</div>
<div class="loading-bar" aria-hidden="true"><div id="loadingBarFill" class="loading-bar-fill"></div></div>
<div class="loading-size" aria-hidden="true">Estimated demo assets: 1.7 MB</div>
</div>
</div>
<script>
// Loading screen: hide once the demo signals ready (or errors) via the canvas
// data attributes it sets when its assets are loaded and the engine starts.
(function () {
var overlay = document.getElementById("loadingOverlay");
var canvas = document.getElementById("renderCanvas");
if (!overlay || !canvas) return;
var done = false;
function hide() {
if (done) return;
done = true;
overlay.classList.add("is-hidden");
setTimeout(function () { if (overlay.parentNode) overlay.parentNode.removeChild(overlay); }, 600);
}
function check() {
if (canvas.dataset.ready === "true" || canvas.dataset.error) { hide(); return true; }
return false;
}
if (!check()) {
new MutationObserver(check).observe(canvas, { attributes: true, attributeFilter: ["data-ready", "data-error"] });
setTimeout(hide, 45000);
}
})();
</script>
<script>
// Determinate progress + asset-amount detail: mirror canvas data-progress /
// data-loading-detail (set by installFetchProgress) into the overlay. Runs
// independently of the data-ready/data-error hide logic above.
(function () {
var canvas = document.getElementById("renderCanvas");
var overlay = document.getElementById("loadingOverlay");
if (!canvas || !overlay) return;
var fill = document.getElementById("loadingBarFill");
var sub = overlay.querySelector(".loading-sub");
var size = overlay.querySelector(".loading-size");
function sync() {
var p = canvas.dataset.progress;
if (p !== undefined && p !== "") {
overlay.classList.add("has-progress");
if (fill) fill.style.width = Math.max(0, Math.min(100, +p)) + "%";
} else {
overlay.classList.remove("has-progress");
}
var d = canvas.dataset.loadingDetail;
if (d !== undefined && d !== "" && sub) sub.textContent = d;
var sz = canvas.dataset.loadingSize;
if (sz !== undefined && sz !== "" && size) size.textContent = sz;
}
sync();
new MutationObserver(sync).observe(canvas, { attributes: true, attributeFilter: ["data-progress", "data-loading-detail", "data-loading-size"] });
})();
</script>
<script type="module" src="/lite/bundle/demos/calculator.js"></script>
</body>
</html>
146 changes: 146 additions & 0 deletions lab/lite/src/demos/calculator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
// Demo — Calculator (glTF KHR_interactivity showcase)
//
// Loads the Khronos "Calculator" sample glb and runs its embedded
// KHR_interactivity graph with Babylon Lite's flow-graph runtime. On load the
// graph's `event/onStart` chain fires: it resets the two display digits to "00"
// by driving `pointer/set` on the digit materials' KHR_texture_transform offset
// (the digit atlas is scrolled so both wheels show "0"), hides the minus sign
// via KHR_node_visibility, and seeds the calculator's scene variables. The math
// blocks (add/sub/mul/div/rem/abs/floor/lt/clamp + combine2/extract2) and the
// pointer get/set accessors are all exercised by this graph.
//
// ⚠️ KHR_interactivity is an UNRATIFIED glTF draft; this demo (and the runtime
// behind it) tracks the current draft and will be re-synced when the spec and
// Babylon.js PR #18455 land.
//
// Model: "Calculator" from the Khronos glTF-Test-Assets-Interactivity repo
// https://github.com/KhronosGroup/glTF-Test-Assets-Interactivity (CC0 / public
// domain test asset). The directional light ships in the glb via
// KHR_lights_punctual; a studio HDR cube is loaded for PBR image-based lighting.

import {
addToScene,
attachControl,
createArcRotateCamera,
createEngine,
createSceneContext,
loadGltf,
onBeforeRender,
registerScene,
runFlowGraphs,
setCameraLimits,
startEngine,
} from "babylon-lite";
import { loadDdsEnvironment } from "babylon-lite/loader-env/load-dds-env";
import { configureDemoDecoderBases, demoAssetUrl } from "./demo-asset-url.js";
import { installFetchProgress } from "./loading-progress.js";

// Studio HDR cube used for image-based lighting only (no visible skybox — the
// calculator reads as a clean product shot on a neutral background).
const ENV_URL = "https://playground.babylonjs.com/textures/environment.dds";

// ArcRotate pose reconstructed from the glb's authored "Main Camera" (eye at
// roughly (-0.07, 1.59, 1.72) looking down on the calculator lying flat on the
// XZ plane, display at the far -Z edge, buttons toward +Z). Targets the body
// centre and looks down the authored ~44° tilt.
const CAM = {
alpha: 1.6115,
beta: 0.8736,
radius: 2.2443,
target: { x: 0, y: 0.15, z: 0 },
fov: 1.0471975, // glb Main Camera yfov (60°)
};

// Gentle auto-rotation so the showcase slowly turns; paused while the user
// interacts and for a short grace period afterwards.
const AUTO_ROTATE_SPEED = 0.0025; // radians per frame
const IDLE_DELAY_MS = 2500;

async function main(): Promise<void> {
const __initStart = performance.now();
const canvas = document.getElementById("renderCanvas") as HTMLCanvasElement;
const progress = installFetchProgress(canvas, { estimatedBytes: 1_900_000 });

const engine = await createEngine(canvas);
const scene = createSceneContext(engine);

const cam = createArcRotateCamera(CAM.alpha, CAM.beta, CAM.radius, CAM.target);
cam.fov = CAM.fov;
scene.camera = cam;
attachControl(cam, canvas, scene);
setCameraLimits(
cam,
{
lowerRadiusLimit: CAM.radius * 0.45,
upperRadiusLimit: CAM.radius * 2.5,
},
scene,
);

// The model is uncompressed, but configure the decoder bases anyway so the
// demo stays subpath-safe if the asset is ever re-exported compressed.
await configureDemoDecoderBases(import.meta.url);

const [asset] = await Promise.all([
loadGltf(engine, demoAssetUrl("./Calculator.glb", import.meta.url)),
loadDdsEnvironment(scene, ENV_URL, {
skipSkybox: true,
skipGround: true,
brdfUrl: demoAssetUrl("./brdf-lut.png", import.meta.url),
}),
]);
addToScene(scene, asset);

// Drive the embedded KHR_interactivity graph(s). The runtime starts on the
// first frame and runs the onStart chain → display resets to "00".
if (asset.flowGraphs?.length) {
await runFlowGraphs(scene, asset.flowGraphs, asset.animationGroups);
}

// Slow continuous orbit, paused during/after user interaction so it never
// fights a manual orbit or zoom.
let lastInteractionMs = -Infinity;
const markInteraction = (): void => {
lastInteractionMs = performance.now();
};
canvas.addEventListener("pointerdown", markInteraction);
canvas.addEventListener("wheel", markInteraction, { passive: true });
canvas.addEventListener("pointermove", (e) => {
if (e.buttons !== 0) {
markInteraction();
}
});
canvas.addEventListener("touchstart", markInteraction, { passive: true });
canvas.addEventListener("touchmove", markInteraction, { passive: true });

let autoRotateEnabled = true;
const rotateBtn = document.getElementById("rotateToggle");
if (rotateBtn) {
rotateBtn.addEventListener("click", () => {
autoRotateEnabled = !autoRotateEnabled;
rotateBtn.textContent = autoRotateEnabled ? "⏸ Auto-rotate" : "▶ Auto-rotate";
rotateBtn.setAttribute("aria-pressed", String(autoRotateEnabled));
});
}

onBeforeRender(scene, () => {
if (autoRotateEnabled && performance.now() - lastInteractionMs > IDLE_DELAY_MS) {
cam.alpha += AUTO_ROTATE_SPEED;
}
});

await registerScene(scene);
progress.done();
await startEngine(engine);

canvas.dataset.drawCalls = String(engine.drawCallCount);
canvas.dataset.camAlpha = String(cam.alpha);
canvas.dataset.camBeta = String(cam.beta);
canvas.dataset.camRadius = String(cam.radius);
canvas.dataset.camTarget = `${cam.target.x},${cam.target.y},${cam.target.z}`;
canvas.dataset.camFov = String(cam.fov);
canvas.dataset.initMs = String(performance.now() - __initStart);
canvas.dataset.ready = "true";
}

main().catch(console.error);
4 changes: 4 additions & 0 deletions lab/public/bundle/demos-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,9 @@
"platformer": {
"rawKB": 82.2,
"gzipKB": 29.1
},
"calculator": {
"rawKB": 131.6,
"gzipKB": 55.4
}
}
Loading