Skip to content

Loading Meshes

PolyCSS loads 3D mesh files (OBJ, STL, glTF, GLB, and MagicaVoxel VOX) and renders them as DOM elements. UV textures are extracted from OBJ/glTF/GLB files and packed into generated atlas pages; STL imports are triangle meshes with optional binary Magics face colors, but no standard units, textures, UVs, or hierarchy.

The broken stone slab below uses stone.obj with a companion stone.mtl file that points at stone_low_Material_Diffuse.png. Each UV-mapped face is a DOM sprite whose background is a generated atlas page.

Loading…

The simplest way to render a mesh is the mesh element with a src. It fetches the file, parses it, and renders one polygon element per polygon automatically.

<script type="module" src="https://esm.sh/@layoutit/polycss/elements"></script>
<poly-camera rot-x="65" rot-y="45">
<poly-scene>
<poly-mesh
src="https://polycss.com/gallery/obj/cottage.obj"
mtl="https://polycss.com/gallery/obj/cottage.mtl"
></poly-mesh>
</poly-scene>
</poly-camera>
FormatExtensionNotes
OBJ + MTL.obj + .mtlText format. UV maps via vt. Material textures from map_Kd.
STL.stlASCII or binary triangle mesh. Supports binary Magics face colors; STL has no standard units, textures, UVs, or hierarchy.
glTF.gltfJSON format. Embedded or external buffers. TEXCOORD_0 UVs.
GLB.glbBinary glTF. Embedded textures extracted as blob URLs.
MagicaVoxel.voxVoxel format. Exposed faces become colored polygon quads; eligible vanilla, React, and Vue meshes use a baked direct-voxel fast path.

When your OBJ has a companion MTL file with textures, PolyCSS reads map_Kd entries and applies them as UV-mapped textures. Pass the mtl attribute / prop:

<!-- Vanilla -->
<poly-mesh src="/rock.obj" mtl="/rock.mtl"></poly-mesh>
// React
<PolyMesh
src="/rock.obj"
mtl="/rock.mtl"
parseOptions={{ objOptions: { targetSize: 40 } }}
/>

Override material colors or textures without modifying the source files (React / Vue prop form):

<PolyMesh
src="/character.obj"
parseOptions={{
objOptions: {
materialColors: { Skin: "#f4c2a1" },
materialTextures: { Body: "/body-diffuse.png" },
includeObjects: ["Body", "Head"], // only these objects
},
}}
/>

For glTF/GLB files that preserved UVs but lost an external image reference, use the same material-name texture override under gltfOptions:

<PolyMesh
src="/character.glb"
parseOptions={{
gltfOptions: {
materialTextures: { Texture: "/character-atlas.png" },
},
}}
/>

For programmatic loading with explicit lifecycle, use loadMesh from the core parser. This is the universal vanilla path; React adds a usePolyMesh hook on top:

// Vanilla: works anywhere, no framework
import { createPolyCamera, loadMesh, createPolyScene } from "@layoutit/polycss";
const result = await loadMesh("https://polycss.com/gallery/obj/cottage.obj", {
mtlUrl: "https://polycss.com/gallery/obj/cottage.mtl",
gltfOptions: { targetSize: 60 },
});
const camera = createPolyCamera({ rotX: 65, rotY: 45 });
const scene = createPolyScene(document.getElementById("host")!, { camera });
scene.add(result);
// later:
scene.destroy(); // removes the scene and disposes registered meshes
// React: usePolyMesh wraps loadMesh + dispose() on unmount
import { PolyCamera, PolyScene, Poly, usePolyMesh } from "@layoutit/polycss-react";
function Viewer() {
const { polygons, loading, error } = usePolyMesh("https://polycss.com/gallery/obj/cottage.obj", {
mtlUrl: "https://polycss.com/gallery/obj/cottage.mtl",
});
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<PolyCamera>
<PolyScene>
{polygons.map((p, i) => <Poly key={i} {...p} />)}
</PolyScene>
</PolyCamera>
);
}

For textured polygons, PolyCSS runs a one-time atlas canvas pass at mount. Flat-color polygons can bypass the atlas when they can render as CSS solids or border-shape polygons.

  1. Extract the texture image from the file (or fetch it by URL) when the polygon has a texture.
  2. Solve a 6-DOF affine transform from the polygon’s UV coordinates to its 2D screen footprint when UVs are available.
  3. Pack polygon footprints into one or more atlas pages.
  4. Clip, draw texture pixels or shaded color fills, and export atlas pages to blob URLs via canvas.toBlob(). textureQuality="auto" can rasterize these pages below full CSS resolution when packed pages would create oversized runtime bitmaps, and also selects the atlas leaf sprite size used for CSS compositing.
  5. Repair antialiased atlas pixels along shared textured edges, then render each polygon as an <s> with background-image, background-size, and background-position.

Generated atlas blob URLs are revoked on unmount (call dispose() or let PolyMesh / usePolyMesh handle it).

  • targetSize: scale the model so its longest axis fits this many world units (default: 60). .vox models snap to the nearest integer voxel CSS cell size, so the final size may differ slightly to keep voxel fast-path coordinates integral.
  • paletteMergeDistance / colorRegionMergeDistance: for .vox files with noisy palettes, fold nearby opaque, hue-compatible colors before greedy meshing and optionally clean up small local color islands/streaks. These are lossy and change authored colors, but can reduce material count and split-quad output. In the gallery and builder, the Mesh resolution control applies them only in Lossy mode; Lossless keeps the authored palette exact.
  • solidTextureSamples: when enabled through loadMesh, texture-backed faces whose sampled UV region is effectively one color are converted to solid-color polygons before optimization. This avoids atlas slices for assets that use texture images as color swatches.
  • In the gallery and builder, Mesh resolution Lossy also collapses nearby colors produced by solid texture sampling on OBJ/GLB assets before mesh optimization. Lossless keeps those sampled colors exact.
  • textureQuality: leave at "auto" for workload-based bitmap caps and browser/device sprite sizing, or set a numeric raster scale for explicit quality. 0.5 uses about one quarter of the atlas bitmap memory of 1.
  • Shared textured edges are repaired automatically during atlas generation. Geometry stays unchanged; only low-alpha atlas pixels at those shared edges are filled from nearby opaque texels.
  • baseUrl: for OBJ/glTF files with external texture paths, pass the file’s URL so relative paths resolve correctly.
  • For large meshes: blob URLs for embedded textures and generated atlases are revoked when dispose() is called. Always let PolyCSS manage this: don’t hold references to blob URLs across remounts.