Skip to content

PolyScene

The scene is the root of every PolyCSS render tree. It applies scene-level lighting and atlas options, then renders its children (typically meshes or individual polygons) in 3D space. PolyScene (React/Vue) must be nested inside a camera component (PolyCamera, PolyPerspectiveCamera, or PolyOrthographicCamera) — it throws otherwise — and createPolyScene() takes a required camera handle in its options. The <poly-scene> custom element prefers an ancestor camera element, but can stand alone: without one it builds an implicit camera from its own perspective, rot-x, rot-y, zoom, distance, and target attributes.

It’s available as a custom element (<poly-scene>), via the imperative createPolyScene(host, opts) API, and as React / Vue components (<PolyScene>).

(React / Vue prop names use camelCase; the <poly-scene> custom element accepts the kebab-case form, e.g. textureQualitytexture-quality.)

The React/Vue components and createPolyScene() support the full table except rows marked otherwise (polygons, children, and centerPolygons are framework-only; shadow.dragDefinition — and the imperative-only debugShadowAttrs option, not listed here — are vanilla-only). The <poly-scene> custom element supports directional-*, ambient-*, texture-lighting, texture-quality, texture-leaf-sizing, texture-image-rendering, texture-backend, texture-projection, auto-center, and — when no ancestor camera element is present — the implicit camera attributes perspective, rot-x, rot-y, zoom, distance, and target. Only perspective, rot-x, rot-y, and zoom are observed: mutating distance or target alone does not update the implicit camera — their current values are read at connect time and re-applied only when a rot-x, rot-y, or zoom mutation next fires. (perspective likewise only selects the camera type at connect.) Use the imperative API for options such as shadow, seamBleed, and strategies in vanilla.

PropTypeDefaultDescription
directionalLightPolyDirectionalLightNoneDirectional light source.
pointLightsPolyPointLight[]NonePositional lights (direction-only, no falloff). Baked mode only; set castShadow: true per light for radial shadows.
ambientLightPolyAmbientLightNoneAmbient fill light.
textureLighting"baked" | "dynamic""baked"Whether texture lighting is rasterized into atlases or computed with CSS variables.
textureQualitynumber | "auto""auto"Atlas bitmap budget and compositor sprite size. Auto caps large runtime bitmaps and uses a larger desktop sprite to avoid Safari/Firefox flattening artifacts; lower numeric values reduce texture memory and detail.
textureLeafSizing"canonical" | "local" | "raster""canonical"Texture leaf CSS primitive sizing.
textureImageRendering"auto" | "pixelated""auto"Default image filtering for atlas and direct-image texture leaves.
textureBackend"auto" | "atlas" | "image""auto"Default texture backend request per polygon. "auto" currently always resolves to the atlas — direct image leaves must be requested explicitly with "image".
textureProjection"affine" | "projective""affine"Default texture projection request for textured quads.
seamBleednumber | "auto"1.5Overscan on detected shared solid seam edges. Every request is additionally fitted per edge to what the polygon plan can safely absorb; 0 disables it. Semantics currently differ by renderer. Numbers: React/Vue treat the number as the requested CSS-pixel amount, while vanilla createPolyScene clamps it to 0..1 and multiplies the built-in 1.5 px default (so vanilla values above 1 behave like 1, capping at 1.5 px). "auto": vanilla resolves it to the full 1.5 px default, but React/Vue forward it into atlas planning, which accepts numeric amounts only — so an explicit "auto" in React/Vue produces no shared-edge overscan (per-strategy primitive bleeds are unaffected). Only the numeric default 1.5 behaves identically everywhere.
strategies{ disable?: ("b" | "i" | "u")[] }NoneDiagnostic override for render strategy selection. Disabled solid strategies fall through to <s> atlas slices; <s> cannot be disabled.
autoCenterbooleanfalseRotate around the content bbox center instead of world origin. Polygon data is not mutated.
centerPolygonsPolygon[]None(Framework only.) Bbox source for autoCenter when renderable polygons live inside child meshes.
shadow{ color?, opacity?, lift?, maxExtend?, parametric?, definition?, style?, dragDefinition?, followAnimation? }{ color:"#000000", opacity:0.25, lift:0.05, maxExtend:2000 }Appearance + SVG extent cap for cast shadows. parametric swaps to a cheap low-res silhouette (definition = detail, style: "vector" | "pixel"); dragDefinition (vanilla) and followAnimation cover light-drag and animated meshes. See Parametric shadows.
polygonsPolygon[]None(Framework only.) Flat array of polygon objects rendered as direct children. Composes with JSX/slot children.
childrenNoneNoneMeshes, polygons, controls, helpers, selection wrappers, and transform controls.

Camera state and input are normally set on the wrapping camera element (<poly-camera> / PolyCamera): rot-x, rot-y, zoom, distance. Without an ancestor camera element, <poly-scene> falls back to an implicit camera driven by its own perspective, rot-x, rot-y, zoom, distance, and target attributes (distance and target are read at connect and on the next rot-x / rot-y / zoom change — they are not live-observed on their own). Add a child <poly-orbit-controls> / <PolyOrbitControls> to enable drag, wheel, or autorotate: see PolyOrbitControls.

React/Vue <PolyMesh> supports the full table. The <poly-mesh> custom element supports src, mtl, mesh-resolution, position, scale, rotation, auto-center, cast-shadow, receive-shadow, plus the OBJ parse attributes target-size, default-color, palette, include-objects, and exclude-objects (these five affect .obj sources only). position, scale, rotation, cast-shadow, and receive-shadow update live; changing src, mtl, mesh-resolution, or any of the OBJ parse attributes tears the mesh down and reloads it; auto-center is read at load only, so changing it after mount does nothing. Use scene.add(result, opts) for vanilla options the element doesn’t expose, such as merge, stableDom, and shadowDefinition.

PropTypeDescription
idstringStable mesh identifier. Reflected as data-poly-mesh-id and exposed on mesh handles for selection / transform tools.
srcstringURL to .obj, .stl, .glb, .gltf, or .vox.
polygonsPolygon[]Pre-parsed polygons (alternative to src). Framework only.
positionVec3[x, y, z] offset in scene space.
scalenumber | Vec3Uniform or per-axis scale.
rotationVec3Euler rotation in degrees [x, y, z].
textureLighting"baked" | "dynamic"Per-mesh lighting mode override. React / Vue only; vanilla meshes inherit the scene value.
textureQualitynumber | "auto"Atlas bitmap budget and compositor sprite size. React / Vue only; vanilla meshes inherit the scene’s texture-quality.
textureLeafSizing"canonical" | "local" | "raster"Per-mesh override of the scene default. React / Vue only; vanilla meshes inherit the scene value.
textureImageRendering"auto" | "pixelated"Per-mesh override of the scene default. React / Vue only; vanilla meshes inherit the scene value.
textureBackend"auto" | "atlas" | "image"Per-mesh override of the scene default. React / Vue only; vanilla meshes inherit the scene value.
textureProjection"affine" | "projective"Per-mesh override of the scene default. React / Vue only; vanilla meshes inherit the scene value.
seamBleednumber | "auto"Per-mesh solid seam overscan. React / Vue only; vanilla meshes inherit the scene setting.
atomicAtlasbooleanHold the previous atlas frame until the next frame is decoded, then swap atomically. React / Vue only.
onFrameReady() => voidFires when an atomic atlas frame swaps to a ready one. React / Vue only.
autoCenterbooleanShift the loaded mesh so its bounding-box center sits at the local origin before applying position. Useful when assets aren’t centered in their file coordinates.
mtlstringCompanion .mtl URL for OBJ models.
parseOptionsUseMeshOptionsParser options forwarded to loadMesh; meshResolution defaults to "lossy".
meshResolution"lossless" | "lossy"Top-level optimizer intent. Wins over parseOptions.meshResolution; defaults to "lossy".
castShadowbooleanEmit SVG cast shadows in both lighting modes; projections update when light, ground, or mesh geometry changes.
receiveShadowbooleanCasters project per-coplanar-face SVG shadows onto this mesh’s visible surfaces (Three.js mesh.receiveShadow semantics). Defaults to false. In React/Vue, any receiver in the scene disables the casters’ ground-shadow fallback; vanilla has no such fallback, so a receiver is required for any shadow to appear.
shadowDefinitionnumberPer-mesh parametric-shadow detail, overriding the scene’s shadow.definition (only when shadow.parametric).
mergebooleanRun the polygon optimizer (dedupe, interior cull, coplanar/lossy merge). Defaults to true. Set false to render the polygon array entering the renderer exactly as given. It cannot restore source-file geometry: with src, loadMesh has already optimized the parse result before merge is consulted.
fallbackReactNodeRendered while src is loading. React prop / Vue #fallback slot.
errorFallback(error: Error) => ReactNodeRendered if parse fails. React prop / Vue #error slot.
children(polygon, index) => ReactNodePer-polygon render prop / scoped slot. (React / Vue only.)

PolyGround is a React/Vue convenience component for a flat shadow-receiving plane.

PropTypeDefaultDescription
sizenumber6Side length in world units.
znumber0World-space floor height.
center[number, number][0,0]Ground center in world X/Y.
colorstring"#7d848e"Ground fill color.
className / classstringNoneAdditional CSS class.
interface PolyDirectionalLight {
direction: [number, number, number]; // Surface-to-light source direction
color?: string; // Light color (default: "#ffffff")
intensity?: number; // Directional intensity (default: 1)
}
interface PolyAmbientLight {
color?: string; // Ambient tint (default: "#ffffff")
intensity?: number; // Ambient intensity (default: 0.4)
}

Helpers render as ordinary scene children and are available in vanilla custom elements plus React/Vue components.

HelperPropsDescription
<poly-axes-helper> / PolyAxesHelpersize, thickness, negative, xColor, yColor, zColorDraws red/green/blue world axes from the origin.
<poly-directional-light-helper> / PolyDirectionalLightHelperReact/Vue: light, target, distance, size, color. Vanilla: direction, target, distance, size, color.Draws a small marker along a directional light vector.

A scene with a dodecahedron at default camera angle.

<script type="module" src="https://esm.sh/@layoutit/polycss/elements"></script>
<poly-camera rot-x="65" rot-y="45">
<poly-scene>
<poly-dodecahedron size="100" color="#a78bfa"></poly-dodecahedron>
</poly-scene>
</poly-camera>
import { PolyPerspectiveCamera, PolyScene, PolyTorus, PolyBox } from "@layoutit/polycss-react";
<PolyPerspectiveCamera perspective={1000} rotX={65} rotY={45}>
<PolyScene
directionalLight={{ direction: [0.5, -0.7, 0.6], color: "#ffe4a8" }}
ambientLight={{ intensity: 0.4 }}
>
<PolyTorus color="#4ecdc4" position={[0, 0, 0]} />
<PolyBox size={60} color="#ffd166" position={[8, 0, 0]} />
</PolyScene>
</PolyPerspectiveCamera>

Shadows are SVG-projected surfaces that reproject when the light or scene geometry changes. Directional-light shadows work in both lighting modes. Point-light shadows are baked mode only — like point-light shading, they’re omitted in dynamic mode (a colored point shadow over a floor those lights never lit would look broken), so dynamic-mode shadows are directional-only. Where multiple lights overlap on a face, the shadows composite to the correct both-blocked color (each light shows the others’ color where it alone is blocked).

import { PolyCamera, PolyScene, PolyGround, PolyMesh } from "@layoutit/polycss-react";
<PolyCamera rotX={65} rotY={45}>
<PolyScene
textureLighting="dynamic"
directionalLight={{ direction: [0.4, -0.6, 1], intensity: 1 }}
shadow={{ opacity: 0.28, lift: 0.04 }}
>
<PolyGround size={8} color="#d8d2c7" />
<PolyMesh src="/model.glb" castShadow />
</PolyScene>
</PolyCamera>

Use strategies.disable when you need to compare paths or isolate browser rendering issues.

<PolyScene strategies={{ disable: ["b", "i", "u"] }}>
<PolyMesh src="/model.glb" />
</PolyScene>
<PolyCamera rotX={65} rotY={45}>
<PolyScene>
<PolyIcosahedron size={80} color="#ff6644" position={[0, 0, 0]} />
<PolyBox size={60} color="#7dd3fc" position={[10, 0, 0]} />
</PolyScene>
</PolyCamera>

Pass a polygons array directly to render static geometry without a file loader.

const polygons: Polygon[] = [
{ vertices: [[0,0,0],[1,0,0],[0,1,0]], color: "#f00" },
{ vertices: [[2,0,0],[3,0,0],[2,1,0]], color: "#00f" },
];
<PolyCamera>
<PolyScene polygons={polygons} />
</PolyCamera>