尚未检测到更新。GitHub 发布版本后会显示在这里。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证制作者的介绍仅有上面的一句话。 更多详情请阅读仓库 README。 仓库 README ↗
正在加载完整 README。
查看完整 README ↗A library of custom components written with Lit to create 3D and VR scenes with BabylonJS.
This library has been inspired by A-Frame, but instead of ThreeJS we are using Babylon.
The name 'Garden' was chosen because of the Hanging Gardens of Babylon, we are using BabylonJS so this seems fitting.
I am also inspired by The Village Green Preservation Society, the main tutorial builds a village.
God save the village green!
Juel is the Jollify Unique Element Library which is another project I am working on.
I have developed this library by following the tutorials and taking some examples from the Babylon Playground and creating custom elements based on them.
Here is the Garden equivalent of the A-Frame basic scene:
<garden-scene>
<garden-camera></garden-camera>
<garden-light></garden-light>
<garden-element parent position="0 -1 0">
<garden-box position="-1 0.5 -3" rotation="0 45 0" colour="#4CC3D9"></garden-box>
<garden-sphere position="0 1.25 -5" diameter="2.50" colour="#EF2D5E"></garden-sphere>
<garden-cylinder position="1 0.75 -3" diameter="1" height="1.5" colour="#FFC65D"></garden-cylinder>
<garden-ground position="0 0 -4" width="4" height="4" colour="#7BC8A4"></garden-ground>
</garden-element>
</garden-scene>
To show one model in an ordinary page, one tag is enough (framed, turnable, its named animations playable: see Showing one model in a page):
<garden-model-viewer src="box.glb" style="width: 100%; aspect-ratio: 4 / 3"></garden-model-viewer>
<button onclick="document.querySelector('garden-model-viewer').playOnce('Open')">Open</button>
The Waypoints example shows how this library could be used to create educational VR experiences.
<garden-waypoint> moves you to a stop when you click it, or when you look at it and hold
(dwell, 1500 ms by default, 0 off: a white reticle, dark-edged so it reads on pale surfaces too, fills at the centre of view, for headsets
without controllers); the next stop in order has a glowing ring (highlight-next="false"
hides it); next(), previous() and go(index) fly along the order and fire
waypoint-changed ({ index }). For headsets keep the fly short (speed="2" or more).
Keys (keys, on by default): PageDown and Right arrow go to the next stop, PageUp and Left arrow
to the previous one, which is what presentation clickers send, so a published scene works with one
(in an iframe, while the iframe has focus); keys typed into a form field or editable text, or with
Ctrl/Alt/Meta held, are left alone, and keys="false" turns it off.
A stop that moves (one with orbit) is followed during the fly, so you land on it and ride its orbit.
Tests: node tests/waypoint-dwell.js, node tests/waypoint-orbit.js, node tests/waypoint-keys.js.
A stop can circle something: give it orbit (and orbit-angle, radians per frame) inside the
element it goes round. Its position stays world-space (height included), and a
<garden-mesh-model> parent is waited for until its file has loaded. Test: node tests/orbit.js.
My favourite example is the temple example which demonstrates how the library could be used to create experiences for museum exhibits:
<garden-scene gravity="0 -0.2 0" collisions>
<garden-camera type="free" collisions position="0 0 -40"></garden-camera>
<garden-light></garden-light>
<garden-room id="r1" collisions></garden-room>
<garden-opening between="r1 r2" position="0 1 -4" width="2" height="2" depth="0.4"></garden-opening>
<garden-room id="r2" position="0 0 -8" collisions></garden-room>
<garden-opening between="r2 s1" position="0 1 -12" width="2" height="2" depth="0.4"></garden-opening>
<garden-column position="-4 -5 -22" diameter="1" height="6" collisions>
<garden-particle effect="fire"></garden-particle>
</garden-column>
<garden-column position="4 -5 -22" diameter="1" height="6" collisions>
<garden-particle effect="fire"></garden-particle>
</garden-column>
<garden-stairs id="s1" position="0 -5 -32" height="5" depth="20" collisions>
<garden-sound loop autoplay maxdistance="20"
url="https://upload.wikimedia.org/wikipedia/commons...Epitaph_of_Seikilos.ogg">
</garden-sound>
</garden-stairs>
<garden-ground width="150" height="160" colour="green" position="0 -5 0" collisions></garden-ground>
</garden-scene>
A <garden-scene> takes over the window by default: the page's html and body lose
their margins and scrolling, and the canvas fills the body. To put a scene in the middle
of an ordinary page that scrolls, give it embedded and size it with CSS like any
block (width, height, aspect-ratio); the canvas follows the box as it changes size.
<p>Text above...</p>
<garden-scene embedded style="width: 100%; aspect-ratio: 16 / 9">
<garden-camera type="arc" radius="4"></garden-camera>
<garden-light></garden-light>
<garden-box position="0 0.5 0"></garden-box>
</garden-scene>
<p>...and below.</p>
Only a scene's own canvas is styled, so other canvases on the page keep their size. With
one full-window and one embedded scene on a page, each behaves as it asks; the full-window
page styles leave when the last full-window scene does. Test: node tests/embedded.js.
For one model, <garden-model-viewer> does all of this in one tag (below).
<garden-model-viewer><garden-model-viewer id="box" src="box.glb" style="width: 100%; aspect-ratio: 4 / 3"></garden-model-viewer>
<button onclick="document.getElementById('box').playOnce('Open')">Open</button>
It builds an embedded scene, an orbit camera, lights and a <garden-mesh-model> inside
itself. Once the model loads, the camera faces its front (glTF models face +z) and fits
the whole model in view for the viewer's shape; the clip planes and zoom limits follow the
model's size, so a 5 cm part and a 50 m building both fill it. It frames the model as it
loads: an animation that moves far outside that pose (a lid that opens) needs radius.
The page sizes it with CSS (300 px wide and 2:1 by default).
| Attribute | Default | What it does |
|---|---|---|
src | The model (glTF/GLB, or anything <garden-mesh-model> loads). | |
extension | The file type (glb, .gltf), for a src that doesn't end in one: a data: or blob: URL. | |
background | transparent | The page shows through; or a colour (#rrggbb or a name). |
alpha, beta | front, from a little above | The start angles, in radians, as <garden-camera>. A change after load turns the camera. |
radius | fits the model | The start distance from the model's centre, in metres. A change after load moves the camera (removed: fits again). |
wheel | ctrl | The wheel scrolls the page unless Ctrl (⌘) is held; zoom: it always zooms. |
auto-rotate | off | Degrees a second to turn on its own, until someone touches it. Not while the page prefers reduced motion. |
poster | An image shown until the model's first frame (and in its place, without WebGL). | |
materials | standard: passed to the model (see <garden-mesh-model>). | |
label | the file name | What screen readers hear (aria-label; the viewer has role="img"). |
Keyboard: the viewer is a tab stop with a focus ring; arrow keys turn the model 10° a
press and +/- zoom.
Deferred: deferred builds no scene and holds no WebGL context, for a page with more models than a browser
allows contexts (about 16), such as a slide deck. It shows poster, or the frame it last drew (still). Taking
the attribute away builds the scene and loads the model; adding it again keeps a still, disposes the scene and
lets the context go at once. loaded waits until the model has been asked for.
No WebGL: if the browser can't start WebGL, the viewer builds no scene. It shows poster
and the page's own content marked slot="fallback" inside it (hidden while WebGL works),
the poster above the content when there are both, or else a short message; it gets the
no-webgl attribute, loaded resolves false and it fires garden-model-error. With
fallback content the viewer drops role="img", so screen readers reach its links.
A model that can't load: the same slot="fallback" content takes the empty scene's place,
with a load-failed attribute, until a model does load. With no such content the scene
stays. A page opened straight from disk is the usual case, as a browser won't let it read
the model's file.
Script: loaded (a Promise, there from the start: true once the model has loaded, false
if it failed or there's no WebGL; a new src makes a new one), animationNames,
play(name, options), playOnce(name) (a Promise that resolves when the one-shot ends;
the model holds its last pose), stopAnimation(), model, scene, camera, frame().
The model's garden-model-load, garden-model-error and garden-model-progress events
bubble out of the viewer. A script placed after the viewer can run after its model has
loaded, too late to hear garden-model-load: await viewer.loaded instead (the example
does). No buttons are drawn: the page
makes its own. There's no loading screen, and nothing is fetched from a CDN (no default
environment map). Example: examples/ModelViewer/index.html. Test: node tests/model-viewer.js.
Every entry below except the last two registers every <garden-*> element. A page that only
shows a model doesn't need them all:
| Entry | For | Size in KB (minified; gzipped) |
|---|---|---|
dist/juel-garden.js (npm run build) | A <script> tag: every element, plus all of Babylon as window.BABYLON | 7,390; 1,665 |
import "juel-garden" (lib/juel-garden.mjs) | An app with a bundler: every element | 3,513 up front ¹ |
dist/juel-garden-viewer.js (npm run build:viewer) | A <script> tag on a page that shows only <garden-model-viewer> | 2,959; 719 |
import "juel-garden/elements/model-viewer" | An app with a bundler that shows only models | 2,800 up front ¹ |
¹ Built as an app's bundler does it (ESM, code splitting), counting what the page loads up
front: Babylon's WebGPU shaders and most glTF extensions become chunks fetched only when
needed. KB of 1,024 bytes, measured on 2026-10-29 (node Tools/bundle-report.mjs lists what's in each build).
The viewer bundle has <garden-scene>, <garden-camera>, <garden-light>,
<garden-mesh-model> and <garden-model-viewer>, and no window.BABYLON (the classes
are window.JuelGarden, as in the full bundle). It loads glTF/GLB, OBJ and STL, not
.babylon files, and leaves out what a viewer page can't use: the WebGPU shaders (scenes
use WebGL), texture="html", and the glTF interactivity and audio-emitter extensions (a
model that uses them still loads, without that behaviour). Tools/build-viewer.mjs says how.
The element entries are juel-garden/elements/scene, …/camera, …/light,
…/mesh-model and …/model-viewer. Each registers its element and the elements it
needs, and they share code with "juel-garden", so an app can import both. Their source
is src/elements/ (built to lib/src/elements/), and the API docs (npm run doc) list them
as the elements/* modules. Test: npm run test:elements-entries.
<garden-link> is the equivalent of A-Frame's link.
Its href is another page (the next scene, a website) or #id of an element
in this scene, which flies the camera there -- e.g. from a shopping centre's
concourse into one shop. Each #id jump is a browser history entry, so the
back button flies back out, and another page can link straight to
mall.html#shop-12. Where you put it decides how it looks:
<!-- On its own: a portal, turned to face the camera. -->
<garden-link href="food-court.html" title="Food court" image="food-court.jpg" position="-4 1.5 1"></garden-link>
<!-- Inside a mesh: the mesh is the link (outlined on hover). -->
<garden-plane position="0 2.9 4.85" width="4" height="1" texture="coffee-sign.png">
<garden-link href="#coffee" offset="0 1.6 -2.5" look-at="#coffee-counter"></garden-link>
</garden-plane>
<!-- Inside a <garden-info>: a button on the info card. -->
<garden-info title="Coffee">
<garden-link href="https://example.com/" target="_blank">Website</garden-link>
</garden-info>
On a touch screen, a free camera's walking stick takes fingers on the left half of the screen. A mouse or pen goes through to the scene, unless touch="always" has the mouse stand in for a finger (test:touch-move).
A <garden-webxr> asks another site for something only on entering VR: the list of
real controllers, from immersive-web.github.io. Refused, or offline, the session
carries on with Babylon's generic controllers. Nothing is requested on load.
In a headset #id jumps teleport instead of flying, and on browsers with WebXR
navigation (Meta Quest Browser) <garden-webxr> re-enters VR on the next
page, so visitors can walk from scene to scene without leaving VR. Every
link fires a cancelable garden-navigate event first, for host pages that
want to route it themselves. See
examples/Navigation/shopping-centre.html;
npm run test:link drives it in Chrome.
<garden-room> is a floor plus walls, merged into one mesh. Its type picks the footprint:
| Attribute | Applies to | Default | Meaning |
|---|---|---|---|
type | all | rect | rect: a width x depth box; rotunda: round, or oval with an explicit depth; polygon: any simple polygon from points |
width, depth | rect, rotunda | 8, 16 (a rotunda without depth is round: depth = width) | outer size in metres (ignored by polygon) |
points | polygon | none | the corners, "x z, x z, ..." (at least three; x y z also accepted, y ignored), in the room's own space (relative to position), tracing the walls' outer face. Concave shapes (an L, a U) work; either winding. A repeated corner (or a closing point equal to the first) is dropped; fewer than three distinct corners warns and builds a box. A <garden-roof> child (or a structure's roof) sizes to the points' bounds, and a flat one is trimmed to the outline unless clip="false". |
height, thickness | all | 6, 0.2 | wall height and wall/floor thickness |
wall-thickness | all | thickness | the walls' thickness alone, when it differs from the floor slab's (a venue's 1.5 m walls). The outer face stays on the outline, so a thicker wall grows inward; a <garden-structure> join cuts through both rooms' walls however thick. |
floor-color, north-color, east-color, south-color, west-color | all | floor #9c968c, walls #e0dcd3 | surface colours; a rotunda, a polygon room or one with <garden-wall slot> sides has one wall colour, north-color |
debug-colors | all | off | the per-face black/green/red/blue/yellow palette |
A <garden-material slot="floor"> child overrides the floor; slot="north" (and
east/south/west) a box room's walls, or slot="wall" the single wall of a rotunda,
polygon or curved-side room. A polygon room's walls are mitred, so they keep their
full thickness into every corner.
Inside a <garden-structure>, a polygon room joins its neighbours the way a curved
room does: along its actual wall line, not its bounding box, so a shared edge gets a
doorway at any angle (a diagonal wall between two triangles works) and against a box
room too. A hand-placed <garden-opening> is an axis-aligned box, so it cuts cleanly
only through an edge that runs along x or z; for a slanted edge, rely on the
structure's join. (Every structure doorway, box or polygon, starts 5 cm below the
higher room's floor top: a shallow groove at the threshold, never through the slab.)
<garden-room type="polygon" height="4" points="-6 -4, 6 -4, 6 0, 0 0, 0 4, -6 4"></garden-room>
See examples/Structures/polygon-room.html;
npm run test:polygon checks it in Chrome.
A <garden-structure> cuts a doorway (opening-width wide, 3 m by default) in the
middle of every wall two of its rooms share. When the doors come from a plan instead
(several doors in one wall, a wide arch, a wall with none), set doorways="none":
the structure then cuts no doorways of its own, only the floor openings its stairs
need, and each door is a <garden-opening>.
<garden-structure doorways="none">
<garden-room id="a" width="10" depth="10" position="-5 0 0"></garden-room>
<garden-room id="b" width="10" depth="10" position="5 0 0"></garden-room>
<garden-opening between="#a #b" position="0 0 2" width="0.6" depth="2" height="3"></garden-opening>
</garden-structure>
Building blocks for a third-person "follow the guide" game, and for museums and walk-throughs with guides and talking exhibits (the plan is DESIGN-characters-and-guides.md; examples in examples/Characters/):
<garden-scene fog-density="0.01">
<!-- free-look: the camera keeps its own angle; look round with a drag, Q/E, the gamepad's
right stick, or a second thumb pad bottom right on touch screens (look-pad). -->
<garden-camera type="follow" target="#player" free-look radius="7" height-offset="3.5"></garden-camera>
<!-- Touch screens: two thumbsticks, one to walk and one to look (floating: touch anywhere on that half). -->
<garden-thumbstick side="left" controls="#player"></garden-thumbstick>
<garden-thumbstick side="right" controls="camera"></garden-thumbstick>
<garden-sky colour="#d89a6a" colour-to="#1b2140" duration="120"></garden-sky>
<garden-height-map id="dunes" url="dunes.png" width="120" height="120" maxheight="6"></garden-height-map>
<!-- A walking player: WASD / gamepad / touch thumb pad, relative to the camera. -->
<!-- materials="standard": its PBR materials as StandardMaterials (no environment map here). -->
<garden-mesh-model id="player" src="robot.glb" height="1.8" walk terrain="#dunes" materials="standard"
anim-idle="Idle" anim-walk="Walking" anim-run="Running"></garden-mesh-model>
<!-- A guide on a path who waits (and turns round) when you fall 10 m behind. -->
<garden-line points="0 0 0, 0 0 40, 30 0 40">
<garden-mesh-model id="guide" src="robot.glb" track follow-wait="#player" follow-wait-distance="10" materials="standard"
anim-idle="Idle" anim-walk="Walking">
<garden-light pool="6" position="0 3 0" diffuse="1 0.8 0.5"></garden-light>
<garden-sound url="voice.mp3" loop ref-distance="3"></garden-sound>
</garden-mesh-model>
</garden-line>
<!-- Animals that trail the guide, keeping apart. -->
<garden-mesh-model src="fox.glb" follow="#guide" follow-spacing="1.5" anim-walk="Walk"></garden-mesh-model>
<garden-marker target="#guide" icon="★" from="#player" hide-within="5" show-distance></garden-marker>
<garden-particle preset="sand" wind="7 0 2" density="0.5"></garden-particle>
<garden-lightning interval="8 20"><garden-sound url="thunder.wav"></garden-sound></garden-lightning>
</garden-scene>
<script>
// A cutaway to the marker's target: when the guide stops to wait, the camera swings round
// to show where he is, then back to exactly where it was.
const guide = document.getElementById("guide");
guide.addEventListener("garden-wait", () => {
const camera = document.querySelector("garden-camera").camera;
camera.show(guide.mesh);
setTimeout(() => camera.unshow(), 3000);
});
</script>
| Piece | What it does |
|---|---|
walk | A walking player, camera-relative; Havok's character controller with <garden-physics>, else terrain or collisions. element.walker |
follow, follow-wait | Followers that keep apart; a leader that stops for stragglers (garden-wait / garden-resume) |
play(), anim-* | Named GLB animations with cross-fades; idle/walk/run picked by speed; one-shots fire garden-animation-end. play changed later cross-fades over blend seconds (default 0.25); a .babylon file's skeleton ranges are named animations too |
weights, sync | Several animations at once at their own weights, weights="walk 0.5, strafe 0.5" (live: fades to new weights over blend); sync="walk" keeps the others in step with one; setWeights({ ... }, { blend, sync }) from code (test:animation-blending) |
root-motion | The model's animations move it: each clip's travel is measured once (the root's own travel, else deduced from the planted feet of an in-place cycle), the pose plays in place and the model's node is moved by it, weighted as the mixer blends, so the feet don't skate. Live; straight clips only (a turn isn't taken out). rootMotionOf(name) gives a clip's cycleDistance, duration, speed, direction and source; anim-walk-speed="auto" (and anim-run-speed) take the speed from the clip. From code: RootMotionClip and RootMotionController, ported from Babylon 9's (test:root-motion) |
<garden-clip> | One more animation for the model it's in, by name: cut by frame (from/to, from group or a .babylon file's one take), or retargeted from another file (src) made for another rig with the same bone names, e.g. Mixamo's (ground-reference, fix-ground-reference, fix-root-position, root-node, ); live () |
For a script driving rigged characters (npm run test:characters; the classes are exported
by the ES module, and are window.JuelGarden.* with the <script> bundle):
| Class | What it does |
|---|---|
PoseLayer | Changes to bones laid over whatever animation plays: turn(bone, by), scale(bone, by), rest(bones) (back to the rig's rest pose); fades in and out with target. A child's proportions from an adult rig, legs spread to sit astride |
waveLayer() | A wave over any clip (arm up, forearm swaying, hand open); defaults for the Unreal-style rig of Quaternius's Universal Animation Library |
BoneSeat | Keeps one model's bone on another's as both move: a child on someone's shoulders |
HeldProp | A staff, lamp or flag in the hand, following it but staying upright; lift for a gesture |
Huddle | A group shelters behind someone from a danger (pausing their follow), turns to watch it, and says when each has settled; end() lets them go |
worldPosition(), boneNode() | A node's exact place this frame; a skeleton's bone by name |
All of them together, with the nature pieces, in examples/Characters/pieces.html.
Pieces for outdoor scenes built in code, light enough for a tablet (npm run test:nature):
| Piece | What it does |
|---|---|
splatMaterial() | Three ground textures with normal maps, blended by a mix map (a StandardMaterial plugin, so fog and lights work) |
imageFromFunction() | A heightmap or mix map drawn from a function, as a PNG data URL |
PanoramaSky | One or two sky photographs on domes, cross-fading (blend), hidden by thick fog, lit by flash() |
StillWater | A dark pool with moving ripples, and a mirror switched on by reflect() |
rockMesh(), stoneWall(), arcPath() | Lumpy rocks; a tapering dry-stone wall with capstones, round a circle with a gateway |
cardTrees(), leafTexture(), leafMaterial() | Gnarled low trees with leaf-card crowns, as templates to instance |
GlowRing | A glowing rim on the ground round a circle (where a pool of light ends), following the ground |
mottle(), noise(), texturedMaterial() | Brightness varied over a ground; smooth value noise; a texture with its normal map |
The same pieces as elements, for scenes written in HTML (npm run test:nature-elements; all of
them together in examples/Nature/nature.html):
| Element | Attributes |
|---|---|
<garden-water> | A still pool (StillWater): position (its centre; the surface's height), radius (default 5), colour, ripples (0–1, default 0.25), mirror (0–1, default 0.4), reflect (bare: mirror the whole scene once it has loaded; a selector: only those elements; set it from code to turn the mirror on later, change it to mirror something else, remove it to turn the mirror off). element.water |
<garden-rocks> | Rocks scattered and merged into one mesh: count (default 12), width/depth (the area round position, default 10 × 10), size (smallest and largest radius, default 0.3 0.9), seed, ground (a selector for a ground or height map: the rocks sit on it, and cover it unless width/depth say otherwise); texture, bump-texture, colour or a <garden-material> child as on any mesh |
<garden-trees> | Low gnarled trees (cardTrees) as instances: count (default 8), width/depth (default 20 × 20) or ground, seed, variants (shapes, default 3), size (scale range, default 0.85 1.15), spacing (least distance between trunks, default 2.5), bark-colour, bark-texture, bark-bump-texture, leaf-colours (shades separated by spaces). element.trees, element.shapes |
<garden-stone-wall> | A dry-stone wall (stoneWall): along points (relative to position), or a ring of radius round position with gate (the gateway's direction in degrees, 0 = +z; off for a closed ring) and gate-width (default 1.2); height (default 1.05), foot-width, top-width, cap-every (0 for no capstones), ground (follow it up and down); , , or a , shared by the capstones () |
The test characters are Quaternius's CC0 RobotExpressive and Mannequin (the Universal
Animation Library's, cut down by Tools/slim-glb.py) and the Khronos Fox
(see examples/assets/characters/LICENCES.md).
For scenes lit by an environment map (npm run test:precompile for the last two):
| Piece | What it does |
|---|---|
toPBRMaterials() | A model's materials tuned for image-based lighting (a believable roughness, metalness off, alpha cut-outs); StandardMaterials become PBRMaterials. vertexColours: false switches off the white vertex colours modelling tools export, which otherwise compile a second shader per material |
pbrSplatMaterial(), pbrTexturedMaterial() | A PBR ground of three photo-scanned layers blended by a mix map (height blending, anti-tiling, parallax), and one scanned surface with its normal, occlusion and roughness maps |
precompile() | Compiles the scene's shaders before play in two stages: what a first screen shows (first), then the rest behind it (all), instanced shaders included |
compiledEffectCount() | How many shader variants the engine has compiled: the same material compiles one for each set of mesh attributes, bones, instances and lights |
.ktx2 textures | GPU-compressed textures (Basis Universal, in KTX2 files or glTF's KHR_texture_basisu): any <garden-texture src>, material texture or model texture can be one. They stay compressed on the GPU (a 1024 px map takes about 1 MB instead of 5), upload in a moment, and ETC1S files are smaller than JPEGs. Decoded by Babylon's KTX2 decoder (WebAssembly, in workers) from decoders/ktx2/ beside the bundle, never the CDN. Made by node Tools/ktx2-encode.mjs in.png out.ktx2 [--uastc] [--linear] (ETC1S for colour maps; UASTC for normal and data maps, with --linear). npm run test:ktx2 |
Set everything that changes a shader's defines before a mesh first renders: the rendering
pipeline, every light (make it early and move it later rather than add it later), shadow
generators and receiveShadows, fog, the materials' features. A change afterwards compiles the
shader again, and with Direct3D that is hundreds of milliseconds each. A scene renders while it
loads, loading screen or not. A game that got this wrong was compiling every shader two or
three times and took 14 s to its title; done right, 7 s.
On top of <garden-physics> and a mesh's physics attribute (Havok), the pieces a physics game
needs, all declared (together in examples/Physics/joints.html):
<garden-physics debug="moving"></garden-physics>
<!-- A gate on a hinge, a ball on a rope, a wheel on a motor. -->
<garden-joint type="hinge" between="#post #gate" pivot-a="0.15 0 0" pivot-b="-1 0 0" axis-a="0 1 0" limits="angular-x -1.57 1.57"></garden-joint>
<garden-joint type="distance" between="#beam #ball" min-distance="0" max-distance="3"></garden-joint>
<garden-joint type="hinge" between="#axle #wheel" axis-a="0 0 1" axis-b="0 1 0" motor="1.5"></garden-joint>
<!-- 120 bricks as one mesh, each its own body; an explosion to knock them down. -->
<garden-box width="0.9" height="0.45" depth="0.45" grid="12 5 2" grid-spacing="1 0.45 0.5" physics mass="0.5"></garden-box>
<garden-force id="blast" type="explosion" position="0 1 3" radius="6" strength="4" falloff="linear"></garden-force>
<!-- A character that falls as a ragdoll when knocked over. -->
<garden-mesh-model src="guy.glb" play="Idle">
<garden-ragdoll><garden-ragdoll-bone bones="Hips" size="0.25"></garden-ragdoll-bone>...</garden-ragdoll>
</garden-mesh-model>
| Piece | What it does |
|---|---|
<garden-joint> | A joint between two bodies (between="#a #b"): ball, distance (a rod; a rope with min-distance), hinge, slider, prismatic, lock, spring, six-dof; pivot-a/-b, axis-a/-b, limits="angular-x 0 1.57, ...", motor (speed) with motor-axis and motor-force, or motor-type="position" (motor is then an angle, or metres, to drive to and hold: a servo, a self-closing door); all four live. instance-a/instance-b join copies of a physics grid; collisions. Joins rebuilt bodies again (test:joint, test:joint-motor) |
<garden-force> | explosion (once), gravity, updraft, vortex (fields): position, radius, strength, height, falloff; event="load" (once), delay, duration, show; fire()/stop(), garden-force event. A change keeps a field that's on on, with the new values, for the rest of its duration; it doesn't set off an explosion, or a field that has gone off, again (test:force) |
physics.raycast(from, to, { ignore, triggers, collideWith }) | The first body on a line: { element, point, normal, distance, instance, triangle }, or null. Aim lines, line of sight, what's under the pointer; collideWith (bits) sees only those physics-groups, triangle is set for a physics-shape="mesh" hit (test:raycast, test:physics-filter) |
physics-group, physics-collides | On any physics mesh: the collision groups it's in and the ones it collides with, as bits (0x... allowed; default all). A ball that passes through glass but not walls, a ghost that only the floor stops () |
Extra cameras (test:viewport): a second <garden-camera viewport="x y w h"> (fractions,
from the bottom-left) is drawn over the main one -- a picture-in-picture view, a rear-view mirror,
an overhead map. It takes no input unless it has controls. layer-mask on cameras and meshes
says which camera sees what (a roof hidden from the overhead camera). rotation (degrees) turns a
free camera, and the meshes inside a <garden-camera parent> ride with it.
Sprites (test:sprite, test:picking): <garden-sprite cells="0 40" cell-delay="100"> plays a sprite
sheet's cells in turn (cell-loop="false" once); cell="5" shows one, still, and
cell="0 18 44" gives each sprite its own (taken in turn). angle turns them (degrees, a
list taken in turn too); pickable lets the pointer pick them. Without capacity, the
sheet has room for every sprite placed.
Picking, rays and touching (test:picking, test:overlap): what's under the pointer, what a
line meets, what touches what -- the questions a game asks every frame -- without physics.
<garden-box id="turret" ray ray-length="50" ray-show="#ff0000" face-pointer></garden-box>
<garden-sphere diameter="0.3" at-pointer emissive-colour="#00ff00"></garden-sphere>
<garden-sphere overlap="#spikes" overlap-precise emissive-colour="#ffffff">
<garden-action trigger="garden-overlap" type="set" property="material.emissiveColor" value="#ff0000"></garden-action>
</garden-sphere>
<garden-sprite url="players.png" width="64" height="64" positions="0 0 0, 1 0 0" count="1" pickable>
<garden-action trigger="garden-pick" type="increment" property="angle" value="0.5"></garden-action>
</garden-sprite>
| Piece | What it does |
|---|---|
sceneEl.pick(x, y, { ignore, sprites }), sceneEl.pickRay(origin, direction, length, options) | The nearest mesh or pickable sprite under a screen point (default the pointer) or along a line: { element, mesh, sprite, index, point, normal, distance }; a model's parts give the model's element; index is the sprite (or grid copy) |
garden-pick | Fired (bubbling) on the element pressed, mesh or sprite; detail is the hit, with sprite and index for a sprite |
ray | A ray out of the mesh along its own "x y z" (bare: straight ahead), turning with it: element.rayHit, garden-ray-hit / garden-ray-miss when what it meets changes; ray-length (100), ray-show (a line; #rrggbb), ray-ignore |
face-pointer, at-pointer | Turn about the vertical to face where the pointer meets the scene; put the mesh there (at-pointer="0 0.5 0": an offset); pointer-ignore; element.pointerHit |
overlap | garden-overlap / garden-overlap-end when the mesh's box starts or stops touching another's (overlap="#a, #b"); overlap-precise uses their own turned boxes; overlap-point="x y z" a point; element.overlapping |
<garden-launcher> | A tap or click (in VR, a controller's trigger; hand="right" for one hand) throws a copy of its <template> along the pointer's ray: speed, offset, max; garden-launch, fire(origin, direction), clear() (test:throw) |
<garden-webxr physics-controllers physics-headset> | Bodies on the VR controllers or hands (the value is their size, default 0.1 m) and on the head: knock things over, bat a ball, headbutt it (test:throw checks it stays quiet without WebXR) |
<garden-action trigger="garden-..."> | On any DOM event from inside its parent: or a path (, , a sprite's ) of the element the event came from (the sprite picked, its light, its mesh), or of ; a colour, number, boolean or |
Materials and light (test:overlap): <garden-material> takes wireframe, back-faces
(both sides drawn), lighting="false" (unlit, only its emissive colour) and ambient-colour
(its share of the scene's ambient-colour, set on <garden-scene>, live).
<garden-light shadows shadow-filter="blur-exponential"> picks how the shadow's edges are
softened: pcf (default), poisson, exponential, blur-exponential, close-exponential,
blur-close-exponential, contact-hardening or none (live).
<garden-environment> (test:environment): Babylon's default environment, the lighting map
PBR models need plus an optional ground and sky (ground, sky, colour, ground-shadow,
ground-size, ground-y, texture).
GLSL written in the page, for game effects (a force field, a pulse, a hit flash, under water), and the pieces that keep a phone's frame rate up:
<!-- A shader material: a fragment shader alone gets a default vertex shader (vUV, vNormal, vPosition). -->
<garden-box>
<garden-material id="shield" uniform-colour="#33ccff">
<script type="x-shader/x-fragment">
precision highp float;
varying vec2 vUV;
uniform vec3 colour;
uniform float time; // seconds, filled in every frame
void main() { gl_FragColor = vec4(colour * (0.6 + 0.4 * sin(time * 4.0 + vUV.y * 20.0)), 1.0); }
</script>
</garden-material>
</garden-box>
<!-- A screen effect on the main camera, at a quarter size. -->
<garden-post-process ratio="0.25" uniform-threshold="0.3" fragment="#threshold-shader"></garden-post-process>
<!-- FSR 1 upscaling, and a HUD camera that sees only the crosshair's layer. -->
<garden-pipeline fsr="performance"></garden-pipeline>
<garden-camera type="free" position="0 0 -50" viewport="0 0 1 1" ortho layer-mask="0x20000000"></garden-camera>
<garden-box size="50" scale="0.05 1 1" layer-mask="0x20000000"><garden-material emissive-colour="#00ff00"></garden-material></garden-box>
| Piece | What it does |
|---|---|
<garden-material vertex fragment> | A shader material (Babylon's ShaderMaterial): each shader the #id of a <script>, a URL, or a child <script type="x-shader/x-vertex"> / "x-shader/x-fragment"; a fragment shader alone gets a default vertex shader passing vUV, vNormal and vPosition (world). Attributes and uniforms are read from the source; Babylon fills the matrices, uniform float time gets seconds. uniform-<name> sets the rest (numbers, #rrggbb, an image URL for a sampler, or a <garden-texture uniform="name"> child; uniform-main-colour is mainColour), live without a rebuild; setUniform(name, value) from a game every frame; opacity below 1 blends (test:shader-material) |
<garden-post-process> | A screen effect (Babylon's PostProcess) from a fragment shader (fragment or a child script) reading textureSampler at vUV: ratio (its size against the canvas), sampling, camera (default the main camera, so a HUD or inset is left alone), disabled (live, keeps its place in the chain); uniforms as above, screenSize and time filled in (test:post-process) |
<garden-pipeline fsr> | AMD's FidelityFX Super Resolution 1: the scene drawn smaller and upscaled with sharp edges, for phones and tablets: ultra-quality, quality (default), balanced, performance or a number; fsr-sharpness, fsr-samples; live; WebGL2 (test:fsr) |
<garden-scene scaling> | Babylon's hardware scaling level: 2 draws at half the canvas's size and lets the browser stretch it; live, for dropping resolution when frames are slow (test:fsr) |
<garden-scene performance> | Babylon's scene performance priority for thousands of meshes: intermediate stops picking as the pointer moves (hover actions; a press still picks) and clearing the canvas (the scene needs a sky or environment); also skips the view test and keeps render lists between frames (set it again after adding meshes); (default) is Babylon's usual. Live () |
npm run dev builds dist/juel-garden.js (with esbuild), the global bundle
for a plain <script> tag: every element, plus all of Babylon as
window.BABYLON. npm run build minifies it. Havok's WASM and the
Draco/meshopt decoders are copied next to it.
In an app with a bundler, install the package (a git dependency works:
npm install github:Jollify-Software/juel-garden; prepare builds it) and
import it:
import "juel-garden"; // registers the <garden-*> elements
import { GardenMeshModel } from "juel-garden"; // and the classes, with types
That is the ES-module build (npm run build:lib → lib/), with
@babylonjs/core imported per module, so the bundler leaves out the parts of
Babylon juel-garden doesn't use (about 4.4 MB minified, against 7.5 MB for the
global bundle). Copy HavokPhysics.wasm and decoders/ next to your page, or
point <garden-physics wasm="..."> at it.
npm run test:examples opens every example in Chrome; npm run test:lean
does the same against the npm build, which catches a Babylon side-effect
import missing from src/SideEffects.ts (it skips pages that script the
global bundle's BABYLON or JuelGarden, which the npm build doesn't
define). Each feature has its own
npm run test:*.
node tests/run-all.js runs the whole suite one test at a time (each browser test starts its
own Chrome, and several at once run a small PC out of memory), with a time limit on each
(--timeout=120, seconds) and a summary; build first. --unit runs only the unit tests in
tests/unit/ (pure maths on node:test, bundled by esbuild, no browser and no build);
--browser only the browser tests; words pick tests by name (node tests/run-all.js waypoint);
--list says what would run. --gpu=swiftshader draws WebGL in software, for a machine with no
GPU (CI); tests/gpu.js holds the Chrome switches every browser test uses. CI runs the
type-check, both builds and the unit tests, and the browser tests under SwiftShader as a job that
doesn't block yet.
Elements can be changed, added, moved and removed from script at any time:
Changing an attribute (or its property) after the element has built applies it.
position, rotation and scale alone move a mesh, a plain <garden-element> or a camera
in place (a physics body is teleported). On a mesh, a position, rotation (degrees, as the
attribute) or scale property set from code (box.position = new BABYLON.Vector3(0, 1, -2))
is written to its attribute, so it moves the mesh too, a parent element's share included;
call box.applyTransform() to move it at once rather than at the next update. Anything else, width, colour, texture, track,
physics and so on, builds the element again: its old mesh, own material and behaviours go,
and its child elements' meshes stay with the new one. A <garden-material> or
<garden-texture> that changes swaps itself into whatever wore the old one. A light, water,
sound, marker, thumbstick or model changes in place (a model reloads only for a new file).
A <garden-particle> builds its system again: the old one, with its texture, goes. A
<garden-gizmo> builds its manager again, with the gizmo that was on still on. A
<garden-action> replaces its parent mesh's action and its beforeRender loop (it doesn't
add a second one).
Changing id, class, style, slot, data-* or aria-* builds nothing.
Moving one (appendChild/insertBefore elsewhere, or anything else that removes it and
adds it back in the same task) keeps what it built: the same mesh, water, light or sound.
Under a different parent, a mesh follows a parent-tagged element's node (or leaves it), a
physics body is teleported to match, and <garden-rocks>/<garden-stone-wall>, which sit on
the ground where they are, are built again.
Removing one for good disposes what it built a microtask later: its mesh, physics body,
light, water, sky, sound, particles or model leave the scene, and its behaviours (track,
follow, walk, …) stop. A <garden-waypoint> takes its reticle, next-stop ring and click
handling with it, and added back it builds them again. A <garden-action> takes its parent
mesh's action and its beforeRender loop (its entry in the scene's actions stays), and a
stops its wobble and disposes its hidden reference sphere; the
wobble also moves to its parent's new mesh when the parent is built again. A
removed mid-flash ends the flash and puts the sky back, and a
removed before its first frame doesn't go off. A
stops handing new meshes to its shadow generator (so does a change that rebuilds it), and a
camera takes its look pad, keys and per-frame chase with it (also on a rebuild). A takes its walk pad, keys and character controller with it (a rebuild keeps them;
added back it gets new ones), and a its keys (also on a rebuild). A stops listening for lightning (so does
a rebuild, which listens afresh). A changed (new , ,
…) or removed takes its animation off its target, so the next uses the new
attributes; removed, it also stops what it was playing. Two s on one target
run side by side, each with its own //, and stops only its own. A rebuilt for a change draws its text where says, as
the first time, on a new texture in place of the old one (also on a mesh's material). Materials stay; their owns them.
node tests/live.js covers changes, node tests/moved.js moves and removals. A subclass
disposes its own objects by overriding dispose() (removed for good) and unbuild() (before a
rebuild for a change), each calling super; one whose updated() handles changes itself
returns true from handlesChanges. A script or behaviour ties per-frame work to an element's
build with element.observe(scene.onBeforeRenderObservable, fn) or element.whenDisposed(fn):
it stops when the element rebuilds or goes.
texture="html")A mesh with texture="html" wears its first child element, rasterised with html2canvas and
styled by the page's CSS like any other element (npm run test:html-texture):
<style>.sign { width: 100%; height: 100%; padding: 40px; border-radius: 32px; background: #fbfaf6; font: 32px sans-serif; }</style>
<garden-plane width="1.8" height="1.2" position="0 1.5 3" texture="html"
html-size="960 640" html-background="transparent" html-unlit>
<div class="sign"><h1>Welcome</h1><p>Some <em>styled</em> text.</p></div>
</garden-plane>
html-size="w h": the CSS-pixel box the content is laid out in (else its own size).html-resolution: texture pixels per CSS pixel (default 1.5); the texture is that rounded to a
power of two, at most html-max-size (default 2048) a side, mipmapped, so text stays sharp.html-background: the colour behind the content (default white); transparent lets rounded
corners and cut-outs show the scene through.html-unlit: the content shows in its own colours whatever the lights.The content moves into a hidden <div class="garden-html-host"> at the top of <body> (inside
the scene it would push the canvas down), so select it by its own classes, not through the
garden-* element. Editing it redraws the texture (debounced); after a stylesheet change call
element.renderHtml(). Each draw fires garden-html-rendered. Draws are queued one at a time.
html2canvas is not a browser: it supports most CSS (gradients, rounded corners, web fonts once
loaded), but not filters, blend modes or some newer layout features, and images must be
same-origin or CORS-enabled.
A <garden-waypoint> stop with look-at (a selector or "x y z") turns the camera to face it
while flying there (npm run test:waypoint-look); Parterre's slide platforms look at their panels.
Its stops are resolved once, after the document is parsed: an update that arrives meanwhile (a
property set by a script while the page is still loading) leaves them to the first, so they are
never set up twice and the selector is never read after it has been resolved (the n.trim is not a function race; npm run test:waypoint-race).
Parterre (@C:\Jollify\parterre) is a generator that targets
this library: annotated SVG floor plan in, a juel-garden scene out. It walks
the SVG with AngleSharp, maps each data-fixture element to a <garden-room> /
<garden-stairs> / <garden-ground> / wall, wraps the joinable ones in a
<garden-structure>, and drops the result into a <garden-scene> page with a
free camera and <garden-webxr>. The intended use is venues — museums,
galleries, shopping centres — publishing a walk-through 3D/VR map of their
space straight from a CAD/vector plan.
It is a separate C# repo (a sibling checkout, like jellyfish-cli is to
Jellyfish), not part of this build. It depends only on the authoring
contract for <garden-structure> and friends — the same markup a person
would write by hand — never on the juel-garden package itself. Practical
consequence for work in this repo: the attribute names and semantics of
garden-structure / garden-room / garden-stairs / garden-ground /
garden-opening are now a consumed API. If they change, update Parterre's
fixture handlers (Parterre/Fixtures/) and its README schema table to match.
This revives an idea first prototyped years ago as
Api/Services/Generators/VirtualLayout, which emitted A-Frame markup; the
target is now a first-party library.
Juel Garden is also the engine of games, not only venues and presentations:
a zero-gravity pool game built on its Havok physics (pocket cameras, an aim
guide, bodies coming to rest), and a third-person story game whose
characters, poses, held props, terrain, sky and water pieces now live here.
So physics, ragdolls, raycasts, extra cameras, shaders and post-processing
are in scope, and a change can break a game as well as Parterre: those games
build their scene markup from the same elements and call into the scene
directly (window.BABYLON, camera.show(), garden-rest).
Contribution would be greatly appreciated.
In order to contribute please open a new issue or a discussion topic and we can talk about it.
After you have created a issue, you should then fork the project to create your own repo, you can then make changes there.
Changes should be made on a separate branch, it is a good idea to use prefixes like feature/... or issue/... when creating your branch.
Pull requests will not be accepted unless they have a issue for them and reference that issue in the pull request.
I should also say that this is the first Open Source project I have created, so please be patient if I get things wrong.
position -- the default arc camera starts at radius 3 around the origin, far too close to actually frame that example's group of shapes. rotation (degrees) now turns a free camera; an orbiting arc camera still takes only a position.garden-structure currently detects joins by bounding-box overlap, so it only works for axis-aligned, box-ish children (rooms, stairs). Rotated children or curved shapes (a curvature stairway, a dome roof) need a hand-placed garden-opening instead -- would be good to support those too.<garden-webxr> component (a sibling of <garden-camera>, same pattern as <garden-light>) -- see the temple example or the dedicated webxr example. It shows an "enter VR/AR" button when the browser/device supports it and does nothing otherwise, so <garden-camera> stays a full mouse/keyboard fallback either way.
floor attribute (defaults to every <garden-ground>) -- a room's floor is fused into the same collision mesh as its walls/ceiling, so interior rooms aren't teleport-targets yet. Worth revisiting once rooms expose their floor as a separate mesh.map="from:to"test:animation-blendingplayOnce(), animation="none" | await model.playOnce("Open") plays a one-shot and resolves when it ends (false if another play took over, or the model was removed); with no looping or motion animation the model holds the last frame, so open-then-close works from buttons. Played again while it plays, it starts over (the first call resolves false). await model.loaded waits for the model (or its failure); it's replaced when src changes. With animation="none" every group's weight is 0 after load: use play()/playOnce(), or set group.weight = 1 before group.start(). Test: node tests/one-shot-hold.js (examples/assets/hinged-box.glb, made by Tools/make-hinged-box.py) |
near | garden-near / garden-far when something (or camera) comes close |
<garden-torus-knot> | radius, tube, p, q, radial-segments, tubular-segments |
grab | Pick a mesh or model up and move it in six directions (a controller or hand turns it over); grab="scale" pinches it bigger or smaller; grab-box draws a bounding box with handles; garden-grab / garden-release. A physics body is carried while held and thrown when let go, at the speed it was moving (grab-throw scales it, 0 drops it; test:throw) |
<garden-light pool> | Lights carried by meshes; a pool of light on the ground; live intensity/range; shadows (everything casts but grounds, height maps and <garden-environment>'s backdrop; a mesh's cast-shadows/receive-shadows overrides it); shadow-filter (pcf, poisson, exponential, blur-exponential, contact-hardening, ...), shadow-blur (soft blurred exponential shadows, a kernel size); a spot light's exponent; cascades (cascaded shadow maps for big outdoor scenes); specular |
| Weather | <garden-particle preset="rain/snow/sand/dust-devil">, <garden-lightning>, scene fog + fogTo(), <garden-sky colour> + fadeTo() |
<garden-camera type="arc"> | alpha/beta/radius (radians, as Babylon's ArcRotateCamera), target="x y z" or an element to follow, beta-lower/beta-upper/radius-lower/radius-upper; bounce: springs back a little from those limits (test:camera-bounce) |
<garden-camera type="free" keys> | keys="arrows" leaves W/A/S/D to the page (a game's own controls), keys="none" takes no keys |
<garden-action trigger="keydown"> | Keyboard actions: trigger="keydown"/"keyup", key="a ArrowLeft" (any case, space; empty for any key), e.g. type="increment" property="position.x" value="-0.1" inside a mesh; a held key repeats (test:key-action) |
<garden-camera near far> | Clip planes in metres, any camera type (minZ/maxZ; Babylon's defaults 1 and 10000 when unset): near="0.01" for a small model seen from close by. A change applies in place. Test: node tests/camera-clip.js |
<garden-camera type="free" collisions fall="5"> | Falls that many metres a second at any frame rate. The scene's gravity is a drop per frame, so on a fast screen it beats a slow walk's smaller step and the camera can't climb stairs. Walking off the edge of the world, it goes back to where it started. Test: node tests/camera-fall.js. |
<garden-camera type="free" hint> | Words on how to move at the bottom of the screen, until the visitor has walked a metre or for 15 seconds: keys with a keyboard, the floor on a touch screen, or hint="Your own words". A page can set camera.moveHint.visible. In a page that's only a scene, the camera also takes the focus as it attaches, so the keys work without a click first (not in a frame or an embedded scene). Test: node tests/camera-hint.js. |
<garden-camera type="free" collisions jump> | Space jumps, jump="1.2" metres high (bare: 1), for a visitor walking round a venue: it bumps its head on ceilings and lands on what's below; only while standing, never while typing in a form field. Opt-in, so games keep Space. jump-button adds a Jump button on the right for a visitor with no Space bar: bare or true on a touch screen only, always with a mouse too; a page can set camera.jumpButton.visible. Test: node tests/camera-jump.js |
<garden-camera type="device"> | Turns with the phone or tablet it's on (a phone in a viewer); a free camera on a desktop. target="x y z" aims a free or device camera too |
<garden-sky environment> | A prefiltered .env/.dds cube as the sky and the light PBR materials (glTF models) reflect; blur, lighting="false", backdrop="false" (light only: the background is colour), turn (degrees). environment="default": Babylon's default environment (backdrop, ground fitted under the models once they load, and light) |
<garden-material node> | A node material (the Node Material Editor's output) from a JSON file or #ID on Babylon's snippet server: shaders, water, anti-tiling grounds; <garden-texture block="name"> fills a texture block, input-<name> sets an input block |
<garden-node-geometry> | A mesh from a node geometry (JSON or #ID), input-<name> for its inputs |
<garden-pipeline> | The finish: samples, fxaa, bloom (+ bloom-threshold, bloom-kernel), sharpen, grain, vignette, chromatic-aberration, depth-of-field (+ focal-length, f-stop), tone-mapping="aces", exposure, contrast; each off unless given; live |
<garden-animation type="vector3"> | Keyframes with several parts: "0 (0 0 0), 60 ((Math.PI) 0 1.2)" or "0 0 0 0, 60 4 0 -2"; also vector2, quaternion, color3, color4 |
<garden-marker> | An icon over a target, or an edge-of-screen arrow to it |
<garden-sound> | Spatial audio on Babylon's audio engine: play()/stop(), play-on="garden-near", cones, roll-off. sync="name": a group that plays together, sample-accurately, autoplay waiting until all have loaded; play()/stop() on one act on the group (test:sound-sync) |
<garden-thumbstick> | On-screen thumbsticks for touch screens: one controls a walker, one the camera |
camera.show(what) | A cutaway: the follow camera swings round to look past the player at something happening, then unshow() swings it back to exactly where it was (swingTo(place); test:cutaway) |
materials="standard" | A GLB's PBR materials swapped for StandardMaterials, for scenes without an environment map (toStandardMaterials() from code) |
extension | A model's file type (glb, gltf, obj…) for a src that doesn't end in one, such as a blob: or data: URL; Babylon picks its loader by it (node tests/model-extension.js) |
texturebump-texturecolour<garden-material>element.caps<garden-sky panorama> | A photographed sky (PanoramaSky): panorama (a 2:1 image), panorama-to (a second behind it), blend (0 the first … 1 the second; live), duration (cross-fade to the second once built), tint/tint-to, haze-from/haze-range (thick fog hides it), size (the dome's diameter, default 900); a <garden-lightning> strike lights it up. blendTo(blend, seconds), flash(), element.panoramaSky. The colour and root skies work as before |
test:physics-filtergrid="nx ny nz" | Any mesh repeated as GPU thin instances, grid-spacing apart, instance-colours (random or a list), instance-rotations (random, or "x y z" degrees by commas, in turn: a heap, not a stack); with physics each copy is a body (always drawn: its bounds don't follow the copies; garden-collide only with collide-events), impulse(v, point, instance) pushes one; picking and raycasts say which copy (test:raycast, test:grid-physics) |
physics on <garden-mesh-model> | The model's body is shaped round the whole model, as scaled: a convex hull (dynamic, default), its triangles (static, default), or physics-shape="box" (sphere, cylinder, capsule) round it. A crate, a barrel, a statue from a glTF (test:model-physics) |
span="#a #b" | Keeps a mesh halfway between two elements, its y axis along them, every frame (after physics): a pendulum's rod, a rope, a tether, a laser; span-stretch scales it to reach (test:span) |
collide-events="false" | No garden-collide events for that body, so Havok needn't report its contacts: cheaper with many bodies |
<garden-ragdoll> | In a rigged <garden-mesh-model>, with a <garden-ragdoll-bone> per bone group (bones, size/width/height/depth, box-offset, rotation-axis, min/max, joint): boxes ride the animation until limp (or limp()), then it falls; impulse(v, bone); garden-ragdoll event. The skeleton's top bone must have a box (test:ragdoll) |
<garden-physics debug> | Wireframes of every body (debug="moving": not the static ones) |
type="set""increment"propertymaterial.emissiveColorposition.yangletargetvalue"x y z"aggressivecompatibletest:performance-priority<garden-camera ortho> | An orthographic camera: bare, one unit is a canvas pixel; ortho="5", 5 units above and below the centre. With viewport="0 0 1 1" and a layer-mask, a HUD drawn over the main camera: a crosshair, a gun sight (test:hud) |
<garden-particle texture-mask> | Babylon's textureMask ("r g b a", multiplies the image); a custom system directly in the scene plays with event="load"; a changed attribute rebuilds the system (test:particle-mask) |
<garden-texture> | sampling (nearest, bilinear, trilinear), wrap (repeat, clamp, mirror; two words for u and v), mipmaps="false", invert-y="false", turn (degrees about the picture's middle, on a plain mesh; a dome reads rotation instead, and unset changes nothing): crisp lookup tables for shaders, pixel art (test:texture-options) |
<garden-mesh-model> + <garden-material> | A material child dresses every part of the model: a node or shader material on a glTF's meshes; part="name" (names) dresses only those parts, so each part wears its own; getPart(name) finds a part (test:glow) |
<garden-particle part> / surface | Particles from a mesh's surface (Babylon's MeshParticleEmitter): surface from the parent mesh, part="name" from a model's named part once it has loaded; along the normals, or direction1/direction2 (use-normals to say outright) (test:mesh-emitter) |
<garden-particle colour-gradients> | Colour over a particle's life, "0 r g b a, 0.4 #rrggbbaa, 1 r g b a"; noise-strength (+ noise-speed, -octaves, -persistence, -brightness, -size) for Perlin turbulence; sprite-cell, sprite-cells, sprite-speed, sprite-random-start for a sprite sheet; blend (oneone, add, standard, multiply, multiplyadd), local, rendering-group, invert-y, min/max scale and initial rotation; cycle="on off" plays and stops it in turn (test:mesh-emitter) |
rendering-group on a mesh | Babylon's renderingGroupId (0 to 3), its parts' too: a later group draws over an earlier one (test:mesh-emitter) |
<garden-glow> | Babylon's GlowLayer: intensity, texture-size, blur. <garden-material glow> glows with its own material (a node material has no emissive colour); glow-input="name" sets that input block to 1 only while the glow draws (test:glow) |
<garden-animation> on a material, a part, a morph | target a <garden-material>: property="inputs.<name>" animates a node material's input block (a true/false one flips at 0.5), material.<property> the material; part="name" animates a model's part, morph (index or name) its morph target's influence; easing (sine, cubic, bounce... with -in/-out) (test:glow) |
<garden-duplicate-vertices><garden-lightning><garden-force event="load"><garden-light shadows>type="follow"walkdrive<garden-sky panorama><garden-animation>keyframespropertyspeedplay()<garden-animation>fromtoloopstop()<garden-canvas>point<garden-material>Adding it back after that builds it again from its attributes.
The <garden-scene> itself (or a <garden-model-viewer>, which has one inside) moved in the
page keeps its engine and goes on drawing; removed for good, its engine is disposed a
microtask later. Added back after that, it builds again (a new canvas, engine and scene),
and so do the elements inside it; a <garden-physics> among them starts its physics again
in the new scene, and the bodies join it. (A <garden-physics> removed on its own leaves
the physics on in its scene.) A <garden-webxr> removed for good takes its "enter VR"
button with it, so adding it, or its scene, back shows one button. Changed (a new mode,
teleportation or floor), it builds a new experience in place of the old one, still with
one button.
A <garden-info> card rebuilt for its own change (a new title) keeps its
<garden-text>, <garden-button> and <garden-link> rows, each once, and stays open if it
was open; a row removed for good leaves the card, and added back it is there once.
A <garden-link> inside a mesh keeps the mesh clickable when the mesh is built again (for
a change, or added back after removal), with its click and hover once. A portal rebuilt for
a change replaces its ring and title along with their materials and the title's texture, and
leaves no unused material behind.